From 68bdc632315dc5a34034de8b2dcc774fcfbfea5f Mon Sep 17 00:00:00 2001 From: 2910410219 <2910410219@qq.com> Date: Wed, 19 Aug 2026 19:53:06 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A6=96=E9=A1=B5=E5=B7=A5=E4=BD=9C=E6=B5=81?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E5=BC=95=E7=94=A8=E5=A4=9A=E9=80=89=E4=B8=8E?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E5=9B=9E=E6=98=BE=E5=AE=8C=E5=96=84=EF=BC=9A?= =?UTF-8?q?isMultiParameter=20=E5=85=A8=E9=93=BE=E8=B7=AF=E9=80=8F?= =?UTF-8?q?=E4=BC=A0=20+=20valueSource=20=E6=95=B0=E7=BB=84=E5=8C=96/label?= =?UTF-8?q?=20+=20=E6=97=A7=20DSL=20=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isMultiParameter 节点级开关从节点库透传至引用下拉,支持多选勾选/去重/逐条删除/清空 - valueSource 契约统一为数组并补 label(节点名.字段中文名),覆盖用户引用、表单展示标记与旧 DSL 单对象 - ModelField 递归透传 multi,object/array 嵌套子字段同样支持多选 - 保存/回显兼容旧 DSL 单对象 valueSource,重开自动升级为数组 - modelRequestParamsPath 每项新增 label 字段,后端按 path 重组请求参数 Co-Authored-By: Claude --- src/api/settings/workflow/index.ts | 7 +- .../home/components/WorkflowFormCard.vue | 14 ++- .../workflow/component/MfReferenceButton.vue | 57 +++++++-- .../workflow/component/ModelField.vue | 109 ++++++++++++------ .../workflow/component/ModelParamsForm.vue | 3 +- .../workflow/component/NodeConfigPanel.vue | 78 ++++++++++++- .../workflow/component/SubFlowParams.vue | 2 + .../workflow/component/modelParamUtils.ts | 61 +++++++--- .../workflow/component/subFlowTypes.ts | 4 +- src/views/settings/workflow/index.vue | 43 +++++-- 10 files changed, 298 insertions(+), 80 deletions(-) diff --git a/src/api/settings/workflow/index.ts b/src/api/settings/workflow/index.ts index a2f7d00..7c0c9a9 100644 --- a/src/api/settings/workflow/index.ts +++ b/src/api/settings/workflow/index.ts @@ -25,8 +25,11 @@ export interface NodeLibraryItem { sort: number; desc?: string; batchExecOption: boolean; - preToolOption: boolean; - postToolOption: boolean; + // 是否允许该节点内「引用上级节点输出」多选(true 时所有引用下拉可多选;节点库返回,仅 model 节点 true) + isMultiParameter: boolean; + // 前置/后置方法配置定义(结构同 presetOption,值为数组;仅 model 节点提供) + preToolOption: NodeLibraryPresetOption[] | null; + postToolOption: NodeLibraryPresetOption[] | null; skillOption: boolean; promptOption: boolean; negativePromptOption: boolean; diff --git a/src/views/home/components/WorkflowFormCard.vue b/src/views/home/components/WorkflowFormCard.vue index 0f4a1bb..0194e2c 100644 --- a/src/views/home/components/WorkflowFormCard.vue +++ b/src/views/home/components/WorkflowFormCard.vue @@ -229,14 +229,20 @@ const getVisibleFields = (node: any): any[] => { return collectHomeFormFields(node); }; -// 引用来源展示:valueSource.nodeId 定位上游节点名 + 字段 +// 引用来源展示:valueSource 统一数组 [{ nodeId, field }](兼容旧 DSL 单对象),逐项定位上游节点名 + 字段 const getSourceDisplay = (field: any): string => { const vs = field?.valueSource; if (!vs || typeof vs !== 'object') return ''; + const arr = Array.isArray(vs) ? vs : [vs]; const nodes = props.detail?.nodeInputParams || []; - const srcNode = nodes.find((n: any) => String(n?.id) === String(vs.nodeId)); - const nodeName = srcNode?.name || srcNode?.nodeName || (vs.nodeId ? `节点 ${vs.nodeId}` : '上游节点'); - return vs.field ? `${nodeName} · ${vs.field}` : nodeName; + const parts = arr + .filter((r: any) => r && typeof r === 'object') + .map((r: any) => { + const srcNode = nodes.find((n: any) => String(n?.id) === String(r.nodeId)); + const nodeName = srcNode?.name || srcNode?.nodeName || (r.nodeId ? `节点 ${r.nodeId}` : '上游节点'); + return r.field ? `${nodeName} · ${r.field}` : nodeName; + }); + return parts.join(','); }; const isFileField = (field: any): boolean => { diff --git a/src/views/settings/workflow/component/MfReferenceButton.vue b/src/views/settings/workflow/component/MfReferenceButton.vue index 3f21868..98d6302 100644 --- a/src/views/settings/workflow/component/MfReferenceButton.vue +++ b/src/views/settings/workflow/component/MfReferenceButton.vue @@ -1,5 +1,5 @@ @@ -71,12 +73,22 @@ class="mf-leaf-expose" >表单展示 - - + +
- 已引用:{{ valueSourceLabel }} - 清除 +
+ {{ ref.label }} +
+ 清空
(); +const props = defineProps<{ fieldDef: any; path?: string; upstreamNodes?: UpstreamNodeInfo[]; multi?: boolean }>(); // 用 computed 实时取当前 props,避免 internalSync 替换对象树后仍持有旧引用 const def = computed(() => props.fieldDef); @@ -167,37 +179,56 @@ const isUpload = computed(() => isLeaf.value && fieldType.value === 'upload'); // ===== 引用上级节点输出 ===== // 引用按钮与候选面板在 MfReferenceButton 中统一渲染(叶子与对象/数组整体均可引用) -const hasValueSource = computed(() => !!def.value?.valueSource); -// 已引用时的展示文案:{节点label}.{字段label} -const valueSourceLabel = computed(() => { - const ref = def.value?.valueSource; - if (!ref) return ''; - const node = props.upstreamNodes?.find((n) => n.id === ref.nodeId); - const nodeName = node?.label || ref.nodeId; - const fieldLabel = node?.outputFields.find((f) => f.field === ref.field)?.label || ref.field; - return `${nodeName}.${fieldLabel}`; +// valueSource 契约统一数组 [{ nodeId, field }];multi(节点 isMultiParameter)时支持多选 +const valueSourceList = computed(() => { + const vs = def.value?.valueSource; + // 兼容旧 DSL 单对象 { nodeId, field }:统一按数组读取 + if (!vs || typeof vs !== 'object') return []; + const arr = Array.isArray(vs) ? vs : [vs]; + return arr.map((r: any) => { + const node = props.upstreamNodes?.find((n) => n.id === r?.nodeId); + const nodeName = node?.label || r?.nodeId || ''; + const fieldLabel = node?.outputFields.find((f) => f.field === r?.field)?.label || r?.field || ''; + const invalid = !node || !node.outputFields.some((f) => f.field === r?.field); + return { nodeId: r?.nodeId, field: r?.field, label: `${nodeName}.${fieldLabel}`, invalid }; + }); }); +const hasValueSource = computed(() => valueSourceList.value.length > 0); +// 任一引用失效即整体标红(失效项逐条展示,由失效清理 watch 自动移除) +const valueSourceInvalid = computed(() => valueSourceList.value.some((r) => r.invalid)); -// 引用是否已失效:上游节点被删除,或其输出字段已不存在(换模型/换配置导致 schema 变化) -const valueSourceInvalid = computed(() => { - const ref = def.value?.valueSource; - if (!ref) return false; - const node = props.upstreamNodes?.find((n) => n.id === ref.nodeId); - if (!node) return true; - return !node.outputFields.some((f) => f.field === ref.field); -}); - -// 失效引用自动清理,避免保存脏数据(就地删除 valueSource,复用深 watch 上传链路) +// 失效引用自动清理,避免保存脏数据(过滤失效项;全部失效则删除整个 valueSource,复用深 watch 上传链路) watch( - valueSourceInvalid, - (invalid) => { - if (invalid && def.value) { - delete def.value.valueSource; - } + () => valueSourceList.value, + (list) => { + if (!def.value) return; + const vs = def.value.valueSource; + if (!vs || typeof vs !== 'object') return; + // 兼容旧 DSL 单对象:按数组处理,清理后统一写回数组 + const arr = Array.isArray(vs) ? vs : [vs]; + if (!list.some((r) => r.invalid)) return; + const keep = arr.filter((r: any) => { + const node = props.upstreamNodes?.find((n) => n.id === r?.nodeId); + return node && node.outputFields.some((f) => f.field === r?.field); + }); + if (keep.length === 0) delete def.value.valueSource; + else def.value.valueSource = keep; }, { immediate: true } ); +// 删除单个引用(标签 closable) +const removeValueSource = (ref: any) => { + if (!def.value) return; + const vs = def.value.valueSource; + if (!vs || typeof vs !== 'object') return; + const arr = Array.isArray(vs) ? vs : [vs]; + const keep = arr.filter((r: any) => !(r?.nodeId === ref.nodeId && r?.field === ref.field)); + if (keep.length === 0) delete def.value.valueSource; + else def.value.valueSource = keep; +}; + +// 清空全部引用 const clearValueSource = () => { if (!def.value) return; delete def.value.valueSource; @@ -446,20 +477,26 @@ const variantLabel = (idx: number): string => templateLabel(def.value?.enumValue .mf-ref-bound { display: flex; - align-items: center; - gap: 8px; + align-items: flex-start; + gap: 6px; font-size: 12px; color: #3b82f6; background: #eff6ff; border-radius: 4px; - padding: 4px 8px; + padding: 6px 8px; - .mf-ref-bound-text { + .mf-ref-tags { flex: 1; min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + display: flex; + flex-wrap: wrap; + gap: 4px; + + .mf-ref-tag { + :deep(.el-tag__content) { + font-size: 12px; + } + } } &.is-invalid { diff --git a/src/views/settings/workflow/component/ModelParamsForm.vue b/src/views/settings/workflow/component/ModelParamsForm.vue index 9907b22..2b90608 100644 --- a/src/views/settings/workflow/component/ModelParamsForm.vue +++ b/src/views/settings/workflow/component/ModelParamsForm.vue @@ -14,6 +14,7 @@ :field-def="def" :path="key" :upstream-nodes="upstreamNodes" + :multi="multi" />
@@ -33,7 +34,7 @@ interface UpstreamNodeInfo { outputFields: { field: string; label: string }[]; } -const props = defineProps<{ modelRequestParams: Record | null; upstreamNodes?: UpstreamNodeInfo[] }>(); +const props = defineProps<{ modelRequestParams: Record | null; upstreamNodes?: UpstreamNodeInfo[]; multi?: boolean }>(); const emit = defineEmits<{ 'update:modelRequestParams': [Record | null] }>(); const localParams = ref>({}); diff --git a/src/views/settings/workflow/component/NodeConfigPanel.vue b/src/views/settings/workflow/component/NodeConfigPanel.vue index c37e0f1..347e381 100644 --- a/src/views/settings/workflow/component/NodeConfigPanel.vue +++ b/src/views/settings/workflow/component/NodeConfigPanel.vue @@ -13,8 +13,39 @@ @update:model-value="updateNodeDesc" /> - - {{ selectedNode.data?.nodeCode }} + + + + + + + + + + + @@ -219,6 +250,7 @@ @@ -245,6 +277,7 @@ @@ -291,6 +324,8 @@ interface NodeData { negativePrompt?: string; patchLayout?: boolean; isSaveFile?: boolean; + preTool?: string | null; + postTool?: string | null; runFormFields?: any[]; // 子流程节点(sub_flow):引入的工作流配置(workflowId + 引入参数) subFlowConfig?: any; @@ -298,6 +333,9 @@ interface NodeData { interface NodeConfig { formConfig: any[]; + preToolOption: any[]; + postToolOption: any[]; + isMultiParameter: boolean; modelConfigOption: boolean; formConfigOption: boolean; skillOption: boolean; @@ -465,6 +503,42 @@ const updateModelRequestParams = (params: Record | null) => { emit('update:selectedNode', updatedNode); }; +// 前置/后置方法配置定义(节点库 preToolOption / postToolOption,仅 model 节点提供;取首个配置项渲染) +const preToolDef = computed(() => { + const defs = props.nodeConfig?.preToolOption; + return Array.isArray(defs) && defs.length > 0 ? defs[0] : null; +}); +const postToolDef = computed(() => { + const defs = props.nodeConfig?.postToolOption; + return Array.isArray(defs) && defs.length > 0 ? defs[0] : null; +}); + +// 前置方法值写节点顶层 preTool(与 outputConfig 平级) +const updatePreTool = (value: string) => { + if (!props.selectedNode?.data) return; + const updatedNode = { + ...props.selectedNode, + data: { + ...props.selectedNode.data, + preTool: value, + }, + }; + emit('update:selectedNode', updatedNode); +}; + +// 后置方法值写节点顶层 postTool(与 outputConfig 平级) +const updatePostTool = (value: string) => { + if (!props.selectedNode?.data) return; + const updatedNode = { + ...props.selectedNode, + data: { + ...props.selectedNode.data, + postTool: value, + }, + }; + emit('update:selectedNode', updatedNode); +}; + // 子流程引入参数就地变更(ModelField 已就地写共享引用): // 仅刷新节点引用同步 VueFlow / 父组件状态;fields 为同一数组引用,避免与 SubFlowParams 深 watch 形成循环 const updateSubFlowFields = (fields: SubFlowField[]) => { diff --git a/src/views/settings/workflow/component/SubFlowParams.vue b/src/views/settings/workflow/component/SubFlowParams.vue index 1350bcd..7959d2d 100644 --- a/src/views/settings/workflow/component/SubFlowParams.vue +++ b/src/views/settings/workflow/component/SubFlowParams.vue @@ -10,6 +10,7 @@ :field-def="f" :path="f.field" :upstream-nodes="upstreamNodes" + :multi="multi" /> @@ -26,6 +27,7 @@ defineOptions({ name: 'SubFlowParams' }); interface Props { fields: SubFlowField[]; upstreamNodes?: any[]; + multi?: boolean; } interface Emits { diff --git a/src/views/settings/workflow/component/modelParamUtils.ts b/src/views/settings/workflow/component/modelParamUtils.ts index abebb6e..807c243 100644 --- a/src/views/settings/workflow/component/modelParamUtils.ts +++ b/src/views/settings/workflow/component/modelParamUtils.ts @@ -188,7 +188,7 @@ export interface ExposedField { value?: any; defaultValue?: any; // 默认值:首页初始化/回显用 fieldConstraint?: any; // 字段约束:上传格式/大小/数量、数字 min/max 等 - valueSource?: any; // 引用上游节点输出({ nodeId, field });有则首页只读展示 + valueSource?: any; // 引用上游节点输出([{ nodeId, field }] 数组,支持多选);有则首页只读展示 multiple?: boolean; // 多文件上传标记 refNodeId?: string; // 预留:引用其他节点功能(后续启用) } @@ -232,7 +232,7 @@ export function collectExposedFields(params: any, prefix = ''): ExposedField[] { value: def.value, defaultValue: def.defaultValue, fieldConstraint: def.fieldConstraint && typeof def.fieldConstraint === 'object' ? def.fieldConstraint : undefined, - valueSource: def.valueSource && typeof def.valueSource === 'object' ? def.valueSource : undefined, + valueSource: Array.isArray(def.valueSource) ? def.valueSource : undefined, multiple: def.multiple || def.fieldType === 'uploadMultiple' || undefined, }); } @@ -243,12 +243,14 @@ export function collectExposedFields(params: any, prefix = ''): ExposedField[] { // ===== 模型请求参数扁平清单(modelRequestParamsPath)===== // 后端契约:保存时与 modelRequestParams 平级新增 modelRequestParamsPath, // 为嵌套模型参数树的全部叶子字段扁平清单(path 文法与 collectExposedFields 一致), -// 后端按 path 重组请求参数。value 保留当前值;valueSource 规则: -// - 已有引用配置(用户在设计器配置「引用上游」)→ 原样保留 { nodeId, field } -// - 无引用但勾选「表单展示」(runtimeShow=true)→ 补 { nodeId: 开始节点id, field: path } +// 后端按 path 重组请求参数。value 保留当前值;valueSource 规则(统一数组 [{ nodeId, field }]): +// - 已有引用配置(用户在设计器配置「引用上游」)→ 原样保留数组 +// - 无引用但勾选「表单展示」(runtimeShow=true)→ 补 [{ nodeId: 开始节点id, field: path }] // - 其余 → 不带 valueSource export interface ModelRequestParamsPathItem { path: string; + // 叶子字段中文名(回显/展示用;无 label 时兜底 path 最后一段字段名) + label: string; type: string; required: boolean; value: any; @@ -290,17 +292,44 @@ function collectAllLeafFields(params: any, prefix = ''): { path: string; def: an // 生成 modelRequestParamsPath 扁平清单 // 所有叶子字段(含对象/数组内部)都支持「引用上游」,valueSource 原样保留; // 与编辑器「引用上级输出与表单展示同级」语义一致 -export function buildModelRequestParamsPath(params: any, startNodeId = ''): ModelRequestParamsPathItem[] { +export function buildModelRequestParamsPath( + params: any, + startNodeId = '', + // 节点输出索引(id → 节点名 + 字段 label),用于补全 valueSource 的引用来源 label + outputIndex?: Map }> +): ModelRequestParamsPathItem[] { const fields = collectAllLeafFields(params); + // 引用来源 label:节点名.字段label(无索引/未命中时兜底 field) + const resolveRefLabel = (nodeId: string, field: string): string => { + const info = outputIndex?.get(nodeId); + if (!info) return field; + const fieldLabel = info.fields.get(field) || field; + return info.name ? `${info.name}.${fieldLabel}` : fieldLabel; + }; return fields.map(({ path, def }) => { - const valueSource = - def.valueSource && typeof def.valueSource === 'object' - ? def.valueSource // 引用上游:前端编辑态 { nodeId, field } 原样保留 - : def.runtimeShow === true && startNodeId - ? { nodeId: startNodeId, field: path } // 勾选「表单展示」:补后端契约 { nodeId, field } - : undefined; + let valueSource: any; + if (Array.isArray(def.valueSource)) { + // 引用上游:前端编辑态 [{ nodeId, field }] 数组,逐项补 label + valueSource = def.valueSource.map((r: any) => + r && r.nodeId && r.field + ? { nodeId: r.nodeId, field: r.field, label: resolveRefLabel(r.nodeId, r.field) } + : r + ); + } else if (def.valueSource && typeof def.valueSource === 'object') { + // 兼容旧 DSL 单对象 { nodeId, field } + const r = def.valueSource as any; + valueSource = [ + r && r.nodeId && r.field + ? { nodeId: r.nodeId, field: r.field, label: resolveRefLabel(r.nodeId, r.field) } + : r, + ]; + } else if (def.runtimeShow === true && startNodeId) { + // 勾选「表单展示」:补后端契约数组(指向开始节点,label 取开始节点字段名) + valueSource = [{ nodeId: startNodeId, field: path, label: resolveRefLabel(startNodeId, path) }]; + } return { path, + label: def.label || path.split('.').pop() || '', type: def.type || 'string', required: Boolean(def.required), value: def.value ?? '', @@ -319,8 +348,10 @@ export function restoreRuntimeShow(params: any, fields: ExposedField[] | null | 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; + if (startNodeId && Array.isArray(cur.valueSource)) { + // 移除指向开始节点(表单展示标记)的引用;全部移除后删除整个 valueSource + cur.valueSource = cur.valueSource.filter((r: any) => !(r && r.nodeId === startNodeId)); + if (cur.valueSource.length === 0) delete cur.valueSource; } } } @@ -334,7 +365,7 @@ export function attachFormFieldValueSources(params: any, fields: ExposedField[] 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, field: f.path }; + cur.valueSource = [{ nodeId: startNodeId, field: f.path }]; } } } diff --git a/src/views/settings/workflow/component/subFlowTypes.ts b/src/views/settings/workflow/component/subFlowTypes.ts index 2a528c7..4dc4a23 100644 --- a/src/views/settings/workflow/component/subFlowTypes.ts +++ b/src/views/settings/workflow/component/subFlowTypes.ts @@ -19,7 +19,7 @@ export interface SubFlowField { // 多文件上传标记 multiple?: boolean; // 引用上级节点输出(主工作流上游);有值则编辑器内只读引用展示 - valueSource?: { nodeId: string; field: string } | null; + valueSource?: { nodeId: string; field: string }[] | null; // 编辑器内「表单展示」勾选(ModelField 用 runtimeShow) runtimeShow?: boolean; } @@ -45,7 +45,7 @@ export interface SubFlowDslField { fieldConstraint?: any; options?: any[] | null; multiple?: boolean; - valueSource?: { nodeId: string; field: string } | null; + valueSource?: { nodeId: string; field: string }[] | null; // 勾选表单展示 → 聚合进主工作流开始节点(首页表单可填) isFormField?: boolean; } diff --git a/src/views/settings/workflow/index.vue b/src/views/settings/workflow/index.vue index d511ef0..239264d 100644 --- a/src/views/settings/workflow/index.vue +++ b/src/views/settings/workflow/index.vue @@ -179,6 +179,7 @@ interface NodeData { patchLayout?: boolean; isSaveFile?: boolean; preTool?: string | null; + postTool?: string | null; runFormFields?: RunFormField[]; // 仅开始节点使用 // 子流程节点(sub_flow):引入的工作流配置(workflowId + 引入参数) subFlowConfig?: SubFlowConfig | null; @@ -205,6 +206,9 @@ const nodeConfigMap = computed(() => { string, { formConfig: any[]; + preToolOption: any[]; + postToolOption: any[]; + isMultiParameter: boolean; modelConfigOption: boolean; formConfigOption: boolean; skillOption: boolean; @@ -217,6 +221,10 @@ const nodeConfigMap = computed(() => { group.nodes.forEach((item) => { map.set(item.key, { formConfig: item.presetOption || [], + // 前置/后置方法配置定义(后端新增,仅 model 节点提供;值保存到节点顶层 preTool/postTool 字段) + preToolOption: item.preToolOption || [], + postToolOption: item.postToolOption || [], + isMultiParameter: item.isMultiParameter || false, modelConfigOption: item.modelConfigOption || false, formConfigOption: item.formConfigOption || false, skillOption: item.skillOption || false, @@ -656,7 +664,7 @@ const syncRunFormFields = () => { value: f.value ?? f.defaultValue ?? '', defaultValue: f.defaultValue, fieldConstraint: f.fieldConstraint && typeof f.fieldConstraint === 'object' ? f.fieldConstraint : undefined, - valueSource: f.valueSource && typeof f.valueSource === 'object' ? f.valueSource : undefined, + valueSource: Array.isArray(f.valueSource) ? f.valueSource : undefined, options: Array.isArray(f.options) ? f.options : undefined, multiple: isUploadMultiple || f.multiple || undefined, nodeId: n.id, @@ -1102,7 +1110,7 @@ const buildOutputConfig = (node: Node, startNodeId = '') => { const entry = formConfig.find((f: any) => f.field === def.field); const value = entry?.value ?? ''; // 勾选「表单展示」的字段:值来自开始节点表单,补 valueSource 标记(后端契约) - const valueSource = def.isFormField === true ? { nodeId: startNodeId, field: def.field } : undefined; + const valueSource = def.isFormField === true ? [{ nodeId: startNodeId, field: def.field }] : undefined; if (def.field === 'responseType') { const expand = entry?.expand || []; return { @@ -1131,7 +1139,7 @@ const buildOutputConfig = (node: Node, startNodeId = '') => { label: f.label || f.field || '', value: f.value ?? '', required: Boolean(f.required), - ...(fieldName ? { valueSource: { nodeId: startNodeId, field: fieldName } } : {}), + ...(fieldName ? { valueSource: [{ nodeId: startNodeId, field: fieldName }] } : {}), ...(f.type === 'uploadMultiple' ? { fieldConstraint: { @@ -1169,7 +1177,7 @@ const buildOutputConfig = (node: Node, startNodeId = '') => { label: def.label || def.field, // isFormField 字段值在开始节点运行表单,不重复保存(回显从开始节点反查) ...(isForm ? {} : { value: entry?.value ?? '' }), - ...(isForm ? { valueSource: { nodeId: startNodeId, field: def.field } } : {}), + ...(isForm ? { valueSource: [{ nodeId: startNodeId, field: def.field }] } : {}), }; }); }; @@ -1203,9 +1211,9 @@ const serializeSubFlowConfig = (config: SubFlowConfig | null | undefined, node?: // - 引用上级输出:值来自主工作流上游节点,统一转 field // - 其余:无引用(静态默认值) valueSource: f.runtimeShow - ? { nodeId: startNodeId, field: f.field } - : f.valueSource && typeof f.valueSource === 'object' - ? { nodeId: f.valueSource.nodeId, field: f.valueSource.field } + ? [{ nodeId: startNodeId, field: f.field }] + : Array.isArray(f.valueSource) + ? f.valueSource.map((vs: any) => ({ nodeId: vs.nodeId, field: vs.field })) : null, isFormField: Boolean(f.runtimeShow), })), @@ -1232,11 +1240,12 @@ const buildSubFlowConfigFromDsl = (subConfig: any, startNodeId = '') => { multiple: Boolean(f.multiple) || f.fieldType === 'uploadMultiple', valueSource: (() => { const vs = f.valueSource; + // 统一数组:兼容旧 DSL 单对象 + const arr = Array.isArray(vs) ? vs : vs && typeof vs === 'object' ? [vs] : []; // 指向开始节点:值为表单展示(首页可填),编辑器不显示为引用(勾选由 isFormField 恢复) - if (vs && typeof vs === 'object' && vs.nodeId && vs.nodeId === startNodeId) return null; + const refs = arr.filter((r: any) => !(r && r.nodeId && r.nodeId === startNodeId)); // 引用上级:后端 field(兼容旧 DSL 的 fieldName)转回前端 field 结构 - if (vs && typeof vs === 'object') return { nodeId: vs.nodeId, field: vs.fieldName ?? vs.field }; - return null; + return refs.length ? refs.map((r: any) => ({ nodeId: r.nodeId, field: r.fieldName ?? r.field })) : null; })(), runtimeShow: Boolean(f.isFormField), })), @@ -1317,6 +1326,7 @@ const loadWorkflowFromDsl = (dsl: any) => { patchLayout: n.patchLayout || false, isSaveFile: Boolean(n.isSaveFile), preTool: n.preTool ?? null, + postTool: n.postTool ?? null, // 子流程节点:恢复引入的工作流配置(无 subConfig 时返回 null,兼容旧 DSL) ...(n.nodeCode === 'sub_flow' ? { subFlowConfig: buildSubFlowConfigFromDsl(n.subConfig, startNodeId) } : {}), // 开始节点运行表单字段:新格式存 outputConfig;旧 DSL 顶层 runFormFields 兜底兼容 @@ -1403,6 +1413,16 @@ const confirmSaveWorkflow = async () => { ElMessage.warning(`存在重名运行字段:${dupNames.join('、')},首页表单可能出现冲突`); } + // 节点 id → 名称与输出字段(field → label)索引,供 modelRequestParamsPath 的 valueSource label 补全 + const outputFieldsIndex = new Map }>(); + nodes.value.forEach((n) => { + const fields = getNodeOutputFields(n); + outputFieldsIndex.set(n.id, { + name: n.data?.label || n.id, + fields: new Map(fields.map((f: any) => [f.field, f.label || f.field])), + }); + }); + const workflowDsl = { version: '1.0.0', startNodeId: startNode?.id || '', @@ -1432,6 +1452,7 @@ const confirmSaveWorkflow = async () => { height: gNode.dimensions?.height || gNode.height || 80, }, ...(n.data?.preTool ? { preTool: n.data.preTool } : {}), + ...(n.data?.postTool ? { postTool: n.data.postTool } : {}), isSaveFile: Boolean(n.data?.isSaveFile), // 子流程节点:序列化引入的工作流配置(含 workflowId 与 isFormField 标记);其余节点保持 null subConfig: nodeCode === 'sub_flow' ? serializeSubFlowConfig(n.data?.subFlowConfig, n, startNode?.id || '') : null, @@ -1444,7 +1465,7 @@ const confirmSaveWorkflow = async () => { : {}), modelRequestParams: savedModelRequestParams, // 全部叶子字段扁平清单(path + 值 + 值来源),后端按 path 重组请求参数;基于编辑器原始嵌套树生成,保留用户配置的引用 valueSource - modelRequestParamsPath: rawModelParams ? buildModelRequestParamsPath(rawModelParams, startNode?.id || '') : null, + modelRequestParamsPath: rawModelParams ? buildModelRequestParamsPath(rawModelParams, startNode?.id || '', outputFieldsIndex) : null, // 勾选的「表单展示」字段(路径带实例索引),已在上方基于完整结构收集 ...(savedModelFormFields && savedModelFormFields.length > 0 ? { modelFormFields: savedModelFormFields } : {}), // 模型返回参数随工作流保存,保证重开后仍可被下游引用