fix: 统一工作流空媒体参数处理

This commit is contained in:
2026-08-31 15:00:49 +08:00
parent 155af9989c
commit 0cedf85729
5 changed files with 146 additions and 22 deletions

View File

@@ -139,7 +139,7 @@ public class WorkflowRunningParameterResolver {
} }
/** /**
* 归一化工作流运行时变量,确保文件参数统一文件对象数组 * 归一化工作流运行时变量,统一文件结构并移除空图片值
* *
* @param content 工作流内容 * @param content 工作流内容
* @param variables 原始运行变量 * @param variables 原始运行变量
@@ -162,7 +162,13 @@ public class WorkflowRunningParameterResolver {
if (isFileParameter(parameter)) { if (isFileParameter(parameter)) {
normalized.put(name, normalizeFileVariableValue(normalized.get(name), name)); normalized.put(name, normalizeFileVariableValue(normalized.get(name), name));
} else if (isImageParameter(parameter)) { } else if (isImageParameter(parameter)) {
normalized.put(name, normalizeImageVariableValue(normalized.get(name), name)); Object imageValue = normalizeImageVariableValue(
normalized.get(name), name);
if (imageValue == null) {
normalized.remove(name);
} else {
normalized.put(name, imageValue);
}
} }
} }
return normalized; return normalized;
@@ -683,6 +689,10 @@ public class WorkflowRunningParameterResolver {
if (value == null) { if (value == null) {
return; return;
} }
if (value instanceof String stringValue
&& !StringUtils.hasText(stringValue)) {
return;
}
if (value instanceof Collection<?> collection) { if (value instanceof Collection<?> collection) {
for (Object item : collection) { for (Object item : collection) {
collectFileValues(item, result); collectFileValues(item, result);

View File

@@ -219,6 +219,38 @@ public class WorkflowRunningParameterResolverTest {
Assert.assertTrue(((List<?>) attachments).get(0) instanceof Map<?, ?>); Assert.assertTrue(((List<?>) attachments).get(0) instanceof Map<?, ?>);
} }
/**
* 空文件参数应统一归一化为空数组,避免旧客户端空字符串触发格式错误。
*
* @throws Exception 反射注入失败
*/
@Test
public void testNormalizeRuntimeVariablesShouldTreatBlankFileValuesAsEmptyList()
throws Exception {
WorkflowRunningParameterResolver resolver = newResolver();
Object[] emptyValues = {null, "", " ", List.of()};
for (Object emptyValue : emptyValues) {
Map<String, Object> variables = new LinkedHashMap<>();
variables.put("attachments", emptyValue);
Map<String, Object> normalized = resolver.normalizeRuntimeVariables(
workflowContentWithStartParameters(),
variables);
Assert.assertEquals(List.of(), normalized.get("attachments"));
}
Map<String, Object> variables = new LinkedHashMap<>();
variables.put("attachments", List.of(
" ",
"https://files.example.com/contracts/contract.docx"));
List<?> normalizedFiles = (List<?>) resolver.normalizeRuntimeVariables(
workflowContentWithStartParameters(),
variables).get("attachments");
Assert.assertEquals(1, normalizedFiles.size());
}
/** /**
* 文件参数应接受远程 URL 字符串数组并自动提取文件名。 * 文件参数应接受远程 URL 字符串数组并自动提取文件名。
* *
@@ -446,6 +478,30 @@ public class WorkflowRunningParameterResolverTest {
Assert.assertEquals("https://example.com/image.png", image.get("url")); Assert.assertEquals("https://example.com/image.png", image.get("url"));
} }
/**
* 空图片参数应从运行变量中移除,避免向执行引擎的并发 Map 写入 null。
*
* @throws Exception 反射注入失败
*/
@Test
public void testNormalizeRuntimeVariablesShouldRemoveBlankImageValues()
throws Exception {
WorkflowRunningParameterResolver resolver = newResolver();
Object[] emptyValues = {null, "", " "};
for (Object emptyValue : emptyValues) {
Map<String, Object> variables = new LinkedHashMap<>();
variables.put("image_input", emptyValue);
Map<String, Object> normalized = resolver.normalizeRuntimeVariables(
workflowContentWithImageStartParameter(),
variables);
Assert.assertFalse(normalized.containsKey("image_input"));
Assert.assertFalse(normalized.containsValue(null));
}
}
/** /**
* 运行入口不应接收 Data URI避免 Base64 写入工作流状态和审计参数。 * 运行入口不应接收 Data URI避免 Base64 写入工作流状态和审计参数。
* *

View File

@@ -67,7 +67,10 @@ import {
} from './workflowExecutionDetails'; } from './workflowExecutionDetails';
import WorkflowFinalOutput from './WorkflowFinalOutput.vue'; import WorkflowFinalOutput from './WorkflowFinalOutput.vue';
import WorkflowFormItem from './WorkflowFormItem.vue'; import WorkflowFormItem from './WorkflowFormItem.vue';
import { resolveWorkflowFormParameters } from './workflowFormParameters'; import {
buildWorkflowFormInitialValues,
resolveWorkflowFormParameters,
} from './workflowFormParameters';
import { import {
buildWorkflowFormParameterSummaries, buildWorkflowFormParameterSummaries,
buildWorkflowFormSubmissionImages, buildWorkflowFormSubmissionImages,
@@ -349,24 +352,7 @@ async function resolveSharedWorkflowId() {
} }
function initializeAdditionalValues() { function initializeAdditionalValues() {
const values: Record<string, any> = {}; const values = buildWorkflowFormInitialValues(additionalParameters.value);
for (const parameter of additionalParameters.value) {
const defaultValue = parameter.defaultValue;
if (
parameter.contentType === 'file' &&
(defaultValue === null ||
defaultValue === undefined ||
defaultValue === '')
) {
values[parameter.name] = [];
} else if (Array.isArray(defaultValue)) {
values[parameter.name] = [...defaultValue];
} else if (defaultValue && typeof defaultValue === 'object') {
values[parameter.name] = { ...defaultValue };
} else {
values[parameter.name] = defaultValue ?? '';
}
}
defaultExtraValues.value = values; defaultExtraValues.value = values;
extraValues.value = values; extraValues.value = values;
parametersLocked.value = false; parametersLocked.value = false;

View File

@@ -1,12 +1,13 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { import {
buildWorkflowFormInitialValues,
resolveWorkflowFormParameters, resolveWorkflowFormParameters,
resolveWorkflowParameterDisplayName, resolveWorkflowParameterDisplayName,
resolveWorkflowParameterLabel, resolveWorkflowParameterLabel,
} from '../workflowFormParameters'; } from '../workflowFormParameters';
describe('resolveWorkflowFormParameters', () => { describe('workflowFormParameters', () => {
it('uses the image parameter when a legacy schema still declares text', () => { it('uses the image parameter when a legacy schema still declares text', () => {
const parameters = resolveWorkflowFormParameters({ const parameters = resolveWorkflowFormParameters({
parameters: [ parameters: [
@@ -119,4 +120,33 @@ describe('resolveWorkflowFormParameters', () => {
'流程开始 > 用户问题123', '流程开始 > 用户问题123',
); );
}); });
it('builds initial values without submitting blank images', () => {
const imageDefault = {
sourceType: 'url',
url: 'https://example.com/default.png',
};
const choicesDefault = ['a'];
const values = buildWorkflowFormInitialValues([
{ name: 'files', contentType: 'file', defaultValue: ' ' },
{ name: 'image', contentType: 'image', defaultValue: '' },
{
name: 'defaultImage',
contentType: 'image',
defaultValue: imageDefault,
},
{ name: 'text', contentType: 'text', defaultValue: 'hello' },
{ name: 'choices', formType: 'checkbox', defaultValue: choicesDefault },
]);
expect(values).toEqual({
files: [],
defaultImage: imageDefault,
text: 'hello',
choices: ['a'],
});
expect(values).not.toHaveProperty('image');
expect(values.defaultImage).not.toBe(imageDefault);
expect(values.choices).not.toBe(choicesDefault);
});
}); });

View File

@@ -136,6 +136,48 @@ function resolveDataType(type: string, contentType: string) {
return 'String'; return 'String';
} }
function isBlankMediaValue(value: unknown) {
return (
value === null ||
value === undefined ||
(typeof value === 'string' && value.trim() === '')
);
}
/**
* 构建发布运行表单的初始值,确保空媒体参数使用稳定的运行时语义。
*
* @param parameters 工作流运行参数
* @returns 可直接绑定到发布运行表单的初始值
*/
export function buildWorkflowFormInitialValues(parameters: any[]) {
const values: Record<string, any> = {};
for (const parameter of parameters || []) {
const defaultValue = parameter?.defaultValue;
if (
parameter?.contentType === 'file' &&
isBlankMediaValue(defaultValue)
) {
values[parameter.name] = [];
continue;
}
if (
parameter?.contentType === 'image' &&
isBlankMediaValue(defaultValue)
) {
continue;
}
if (Array.isArray(defaultValue)) {
values[parameter.name] = [...defaultValue];
} else if (defaultValue && typeof defaultValue === 'object') {
values[parameter.name] = { ...defaultValue };
} else {
values[parameter.name] = defaultValue ?? '';
}
}
return values;
}
export function resolveWorkflowFormParameters(workflowParams: any) { export function resolveWorkflowFormParameters(workflowParams: any) {
const schema = Array.isArray(workflowParams?.startFormSchema) const schema = Array.isArray(workflowParams?.startFormSchema)
? workflowParams.startFormSchema ? workflowParams.startFormSchema