diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/WorkflowRunningParameterResolver.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/WorkflowRunningParameterResolver.java index 276ed97c..c30f9371 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/WorkflowRunningParameterResolver.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/WorkflowRunningParameterResolver.java @@ -139,7 +139,7 @@ public class WorkflowRunningParameterResolver { } /** - * 归一化工作流运行时变量,确保文件参数统一为文件对象数组。 + * 归一化工作流运行时变量,统一文件结构并移除空图片值。 * * @param content 工作流内容 * @param variables 原始运行变量 @@ -162,7 +162,13 @@ public class WorkflowRunningParameterResolver { if (isFileParameter(parameter)) { normalized.put(name, normalizeFileVariableValue(normalized.get(name), name)); } 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; @@ -683,6 +689,10 @@ public class WorkflowRunningParameterResolver { if (value == null) { return; } + if (value instanceof String stringValue + && !StringUtils.hasText(stringValue)) { + return; + } if (value instanceof Collection collection) { for (Object item : collection) { collectFileValues(item, result); diff --git a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/WorkflowRunningParameterResolverTest.java b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/WorkflowRunningParameterResolverTest.java index 0940d2c8..ea449154 100644 --- a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/WorkflowRunningParameterResolverTest.java +++ b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/WorkflowRunningParameterResolverTest.java @@ -219,6 +219,38 @@ public class WorkflowRunningParameterResolverTest { 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 variables = new LinkedHashMap<>(); + variables.put("attachments", emptyValue); + + Map normalized = resolver.normalizeRuntimeVariables( + workflowContentWithStartParameters(), + variables); + + Assert.assertEquals(List.of(), normalized.get("attachments")); + } + + Map 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 字符串数组并自动提取文件名。 * @@ -446,6 +478,30 @@ public class WorkflowRunningParameterResolverTest { 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 variables = new LinkedHashMap<>(); + variables.put("image_input", emptyValue); + + Map normalized = resolver.normalizeRuntimeVariables( + workflowContentWithImageStartParameter(), + variables); + + Assert.assertFalse(normalized.containsKey("image_input")); + Assert.assertFalse(normalized.containsValue(null)); + } + } + /** * 运行入口不应接收 Data URI,避免 Base64 写入工作流状态和审计参数。 * diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowChatPage.vue b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowChatPage.vue index bbee2085..274bcbdb 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowChatPage.vue +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowChatPage.vue @@ -67,7 +67,10 @@ import { } from './workflowExecutionDetails'; import WorkflowFinalOutput from './WorkflowFinalOutput.vue'; import WorkflowFormItem from './WorkflowFormItem.vue'; -import { resolveWorkflowFormParameters } from './workflowFormParameters'; +import { + buildWorkflowFormInitialValues, + resolveWorkflowFormParameters, +} from './workflowFormParameters'; import { buildWorkflowFormParameterSummaries, buildWorkflowFormSubmissionImages, @@ -349,24 +352,7 @@ async function resolveSharedWorkflowId() { } function initializeAdditionalValues() { - const values: Record = {}; - 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 ?? ''; - } - } + const values = buildWorkflowFormInitialValues(additionalParameters.value); defaultExtraValues.value = values; extraValues.value = values; parametersLocked.value = false; diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/__tests__/workflowFormParameters.test.ts b/easyflow-ui-admin/app/src/views/ai/workflow/components/__tests__/workflowFormParameters.test.ts index 53261006..b16d13d8 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/__tests__/workflowFormParameters.test.ts +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/__tests__/workflowFormParameters.test.ts @@ -1,12 +1,13 @@ import { describe, expect, it } from 'vitest'; import { + buildWorkflowFormInitialValues, resolveWorkflowFormParameters, resolveWorkflowParameterDisplayName, resolveWorkflowParameterLabel, } from '../workflowFormParameters'; -describe('resolveWorkflowFormParameters', () => { +describe('workflowFormParameters', () => { it('uses the image parameter when a legacy schema still declares text', () => { const parameters = resolveWorkflowFormParameters({ parameters: [ @@ -119,4 +120,33 @@ describe('resolveWorkflowFormParameters', () => { '流程开始 > 用户问题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); + }); }); diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowFormParameters.ts b/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowFormParameters.ts index e6b8a4d6..913ebb9a 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowFormParameters.ts +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowFormParameters.ts @@ -136,6 +136,48 @@ function resolveDataType(type: string, contentType: 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 = {}; + 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) { const schema = Array.isArray(workflowParams?.startFormSchema) ? workflowParams.startFormSchema