diff --git a/src/views/settings/workflow/component/modelParamUtils.ts b/src/views/settings/workflow/component/modelParamUtils.ts index ea27e37..2822074 100644 --- a/src/views/settings/workflow/component/modelParamUtils.ts +++ b/src/views/settings/workflow/component/modelParamUtils.ts @@ -232,13 +232,31 @@ export function collectExposedFields(params: any, prefix = ''): ExposedField[] { } // 按暴露清单的 path 反解,把对应叶子 def 的 runtimeShow 置 true(加载时还原勾选) -export function restoreRuntimeShow(params: any, fields: ExposedField[] | null | undefined): void { +// startNodeId:勾选「表单展示」的字段保存时带 valueSource 指向开始节点(后端契约); +// 回显时该 valueSource 无编辑器引用语义,清除避免 ModelField 误判引用/误删 +export function restoreRuntimeShow(params: any, fields: ExposedField[] | null | undefined, startNodeId = ''): void { if (!params || typeof params !== 'object' || !Array.isArray(fields) || fields.length === 0) return; for (const f of fields) { if (!f || typeof f.path !== 'string') continue; const cur = resolvePath(params, f.path); if (cur && typeof cur === 'object' && !Array.isArray(cur)) { cur.runtimeShow = true; + if (startNodeId && cur.valueSource && typeof cur.valueSource === 'object' && cur.valueSource.nodeId === startNodeId) { + delete cur.valueSource; + } + } + } +} + +// 勾选「表单展示」的字段:保存前按后端契约补 valueSource={nodeId:开始节点id, fieldName:path}, +// 标记字段值来自主工作流开始节点(首页表单)。startNodeId 为空(无开始节点)时跳过。 +export function attachFormFieldValueSources(params: any, fields: ExposedField[] | null | undefined, startNodeId = ''): void { + if (!params || typeof params !== 'object' || !Array.isArray(fields) || fields.length === 0 || !startNodeId) return; + for (const f of fields) { + if (!f || typeof f.path !== 'string') continue; + const cur = resolvePath(params, f.path); + if (cur && typeof cur === 'object' && !Array.isArray(cur)) { + cur.valueSource = { nodeId: startNodeId, fieldName: f.path }; } } } diff --git a/src/views/settings/workflow/index.vue b/src/views/settings/workflow/index.vue index de3abb1..abebada 100644 --- a/src/views/settings/workflow/index.vue +++ b/src/views/settings/workflow/index.vue @@ -120,6 +120,7 @@ import { stripReadonlyFields, collectExposedFields, restoreRuntimeShow, + attachFormFieldValueSources, isEqual, deepClone, removeArrayValueInstances, @@ -1006,30 +1007,34 @@ const buildNodeFormConfigFromDsl = (n: any, startRunFields: any[] = []) => { }; }); } - // 新格式:HTTP 节点 outputConfig 为 {type,field,label,value} 数组 - if (Array.isArray(n.outputConfig) && n.outputConfig.length > 0 && n.nodeCode !== '__start__') { + // 非开始节点:优先按节点库 presetOption 定义回显参数。 + // outputConfig 可能为 null/空数组(如 sub_flow 勾选参数全部聚合进开始节点,outputConfig 被过滤), + // 只要节点有 presetOption 定义就按定义构建,值从 outputConfig 或开始节点运行表单反查 + if (n.nodeCode !== '__start__') { const defs = nodeConfigMap.value.get(n.nodeCode)?.formConfig || []; - // 无 presetOption 定义(如 form 节点为命名值数组)时直接透传 - if (defs.length === 0) return n.outputConfig; - - return defs.map((def: any) => { - const out = n.outputConfig.find((o: any) => o.field === def.field); - if (def.field === 'responseType') { - // 恢复嵌套配置:定义取自 presetOption 选中选项的 config,值取自 options[0].config - const savedConfig = out?.options?.[0]?.config || []; - const selectedOpt = (def.options || []).find((o: any) => o.key === out?.value || o.value === out?.value); - const expandDefs = selectedOpt?.config || []; - const expand = expandDefs.map((cd: any) => { - const saved = savedConfig.find((c: any) => c.field === cd.field); - return { ...cd, value: saved?.value ?? '' }; - }); - return { ...def, value: out?.value ?? '', expand }; - } - // isFormField:true 字段不随节点 outputConfig 保存(值在开始节点运行表单), - // 从开始节点反查恢复,保证编辑器参数面板回显与运行表单一致 - const startValue = startRunFields.find((r: any) => r && r.field === def.field)?.value; - return { ...def, value: out?.value ?? startValue ?? '' }; - }); + if (defs.length > 0) { + const outList = Array.isArray(n.outputConfig) ? n.outputConfig : []; + return defs.map((def: any) => { + const out = outList.find((o: any) => o.field === def.field); + if (def.field === 'responseType') { + // 恢复嵌套配置:定义取自 presetOption 选中选项的 config,值取自 options[0].config + const savedConfig = out?.options?.[0]?.config || []; + const selectedOpt = (def.options || []).find((o: any) => o.key === out?.value || o.value === out?.value); + const expandDefs = selectedOpt?.config || []; + const expand = expandDefs.map((cd: any) => { + const saved = savedConfig.find((c: any) => c.field === cd.field); + return { ...cd, value: saved?.value ?? '' }; + }); + return { ...def, value: out?.value ?? '', expand }; + } + // isFormField:true 字段不随节点 outputConfig 保存(值在开始节点运行表单), + // 从开始节点反查恢复,保证编辑器参数面板回显与运行表单一致 + const startValue = startRunFields.find((r: any) => r && r.field === def.field)?.value; + return { ...def, value: out?.value ?? startValue ?? '' }; + }); + } + // 无 presetOption 定义:outputConfig 为数组时透传(form 节点已提前处理) + if (Array.isArray(n.outputConfig) && n.outputConfig.length > 0) return n.outputConfig; } // 旧格式兜底 if (Array.isArray(n.formConfig)) return n.formConfig; @@ -1037,7 +1042,7 @@ const buildNodeFormConfigFromDsl = (n: any, startRunFields: any[] = []) => { }; // 构建保存用的 outputConfig(HTTP/form 节点),其余返回 null(开始节点的运行表单字段由保存处单独写入) -const buildOutputConfig = (node: Node) => { +const buildOutputConfig = (node: Node, startNodeId = '') => { const nodeCode = node.data?.nodeCode || ''; if (nodeCode === 'http') { const defs = nodeConfigMap.value.get(nodeCode)?.formConfig || []; @@ -1045,6 +1050,8 @@ const buildOutputConfig = (node: Node) => { return defs.map((def: any) => { const entry = formConfig.find((f: any) => f.field === def.field); const value = entry?.value ?? ''; + // 勾选「表单展示」的字段:值来自开始节点表单,补 valueSource 标记(后端契约) + const valueSource = def.isFormField === true ? { nodeId: startNodeId, fieldName: def.field } : undefined; if (def.field === 'responseType') { const expand = entry?.expand || []; return { @@ -1052,44 +1059,67 @@ const buildOutputConfig = (node: Node) => { field: 'responseType', label: def.label, value, + ...(valueSource ? { valueSource } : {}), options: expand.length ? [{ config: expand.map((e: any) => ({ type: e.type, field: e.field, label: e.label, value: e.value })) }] : [], }; } - return { type: def.type, field: def.field, label: def.label, value }; + return { type: def.type, field: def.field, label: def.label, value, ...(valueSource ? { valueSource } : {}) }; }); } if (nodeCode === 'form') { // form 节点 outputConfig 为自定义字段完整结构(含 type/required,便于完整回显) + // 全部字段均为运行表单字段(首页可填),保存时补 valueSource 指向开始节点(后端契约) const fields = Array.isArray(node.data?.formConfig) ? node.data.formConfig : []; - return fields.map((f: any) => ({ - type: f.type || 'input', - field: f.label || f.field || '', - label: f.label || f.field || '', - value: f.value ?? '', - required: Boolean(f.required), - ...(f.type === 'uploadMultiple' - ? { - fieldConstraint: { - ...(f.fileTypes ? { fileTypes: f.fileTypes } : {}), - ...(f.maxFileSize !== undefined && f.maxFileSize !== null ? { maxFileSize: f.maxFileSize } : {}), - ...(f.maxFileCount !== undefined && f.maxFileCount !== null ? { maxFileCount: f.maxFileCount } : {}), - }, - } - : {}), - })); + return fields.map((f: any) => { + const fieldName = f.field || f.label || ''; + return { + type: f.type || 'input', + field: f.label || f.field || '', + label: f.label || f.field || '', + value: f.value ?? '', + required: Boolean(f.required), + ...(fieldName ? { valueSource: { nodeId: startNodeId, fieldName } } : {}), + ...(f.type === 'uploadMultiple' + ? { + fieldConstraint: { + ...(f.fileTypes ? { fileTypes: f.fileTypes } : {}), + ...(f.maxFileSize !== undefined && f.maxFileSize !== null ? { maxFileSize: f.maxFileSize } : {}), + ...(f.maxFileCount !== undefined && f.maxFileCount !== null ? { maxFileCount: f.maxFileCount } : {}), + }, + } + : {}), + }; + }); } - // 其它带 presetOption 的节点(如 sub_flow):isFormField:true 的字段已聚合进开始节点运行表单, - // 此处仅序列化 isFormField !== true 的节点配置参数,保证重开后编辑器参数面板回显 + // 其它带 presetOption 的节点: + // - sub_flow:isFormField:true 字段已聚合进开始节点并存入 subConfig(如 maxConcurrency),保持原过滤 + // - 其它节点(数字人等):勾选「表单展示」的字段值来自开始节点表单, + // 序列化时补 valueSource 标记(后端契约);值不重复保存(回显从开始节点反查) const defs = nodeConfigMap.value.get(nodeCode)?.formConfig || []; if (defs.length === 0) return null; const formConfig = Array.isArray(node.data?.formConfig) ? node.data.formConfig : []; + if (nodeCode === 'sub_flow') { + return defs + .filter((def: any) => def && typeof def === 'object' && def.isFormField !== true) + .map((def: any) => { + const entry = formConfig.find((f: any) => f && f.field === def.field); + return { type: def.type || 'input', field: def.field, label: def.label || def.field, value: entry?.value ?? '' }; + }); + } return defs - .filter((def: any) => def && typeof def === 'object' && def.isFormField !== true) .map((def: any) => { const entry = formConfig.find((f: any) => f && f.field === def.field); - return { type: def.type || 'input', field: def.field, label: def.label || def.field, value: entry?.value ?? '' }; + const isForm = Boolean(def && typeof def === 'object' && def.isFormField === true); + return { + type: def.type || 'input', + field: def.field, + label: def.label || def.field, + // isFormField 字段值在开始节点运行表单,不重复保存(回显从开始节点反查) + ...(isForm ? {} : { value: entry?.value ?? '' }), + ...(isForm ? { valueSource: { nodeId: startNodeId, fieldName: def.field } } : {}), + }; }); }; @@ -1163,11 +1193,11 @@ const buildSubFlowConfigFromDsl = (subConfig: any, startNodeId = '') => { }; // 从 DSL 构建模型配置:剔除只读字段 + 用 modelFormFields 还原勾选(幂等,兼容旧 DSL) -const buildModelConfigFromDsl = (n: any) => { +const buildModelConfigFromDsl = (n: any, startNodeId = '') => { const modelRequestParams = stripReadonlyFields(n.modelConfig?.modelRequestParams ?? null); const modelFormFields = n.modelConfig?.modelFormFields ?? null; if (modelRequestParams && typeof modelRequestParams === 'object' && Array.isArray(modelFormFields)) { - restoreRuntimeShow(modelRequestParams, modelFormFields); + restoreRuntimeShow(modelRequestParams, modelFormFields, startNodeId); } return { modelId: n.modelConfig?.modelId || '', @@ -1227,7 +1257,7 @@ const loadWorkflowFromDsl = (dsl: any) => { nodeCode: n.nodeCode, desc: n.desc || '', formConfig, - modelConfig: buildModelConfigFromDsl(n), + modelConfig: buildModelConfigFromDsl(n, startNodeId), skillName: n.skillName || null, prompt: n.prompt || '', negativePrompt: n.negativePrompt || '', @@ -1332,6 +1362,11 @@ const confirmSaveWorkflow = async () => { // 数组字段的实例值(value)由前端从模板复制而来,与 enumValues 模板重复, // 保存时不提交;deepClone 避免污染节点运行时状态 const savedModelRequestParams = rawModelParams ? removeArrayValueInstances(deepClone(rawModelParams)) : null; + // 勾选「表单展示」的字段:补 valueSource={nodeId:开始节点id, fieldName:path}(后端契约, + // 标记字段值来自主工作流开始节点首页表单) + if (savedModelRequestParams && startNode?.id) { + attachFormFieldValueSources(savedModelRequestParams, savedModelFormFields, startNode.id); + } return { id: n.id, nodeCode, @@ -1361,7 +1396,7 @@ const confirmSaveWorkflow = async () => { modelResponseBodyMapping: n.data?.modelConfig?.modelResponseBodyMapping ?? null, }, // 开始节点:运行表单字段存入 outputConfig(不再单独存顶层 runFormFields 字段) - outputConfig: isStartNode(n) ? (n.data?.runFormFields ?? []) : buildOutputConfig(n), + outputConfig: isStartNode(n) ? (n.data?.runFormFields ?? []) : buildOutputConfig(n, startNode?.id || ''), ...(n.data?.skillName ? { skillName: n.data.skillName } : {}), ...(n.data?.prompt ? { prompt: n.data.prompt } : {}), ...(n.data?.negativePrompt ? { negativePrompt: n.data.negativePrompt } : {}),