非子流程节点表单展示补 valueSource,修复子流程生成次数回显

- http/form/其它 presetOption 节点勾选表单展示的字段保存时补 valueSource={nodeId:开始节点id, fieldName},与子流程契约统一
- model 节点勾选字段按 path 补 valueSource,回显时清除指向开始节点的引用防误删
- 修复 sub_flow 节点 outputConfig 为空时参数面板(含生成次数)无法回显:回显逻辑改为节点库有 presetOption 定义即构建参数,值从开始节点运行表单反查
This commit is contained in:
2026-08-15 19:23:11 +08:00
parent 41655fa825
commit b8d29e31df
2 changed files with 103 additions and 50 deletions
@@ -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 };
}
}
}
+84 -49
View File
@@ -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[] = []) => {
};
// 构建保存用的 outputConfigHTTP/form 节点),其余返回 null(开始节点的运行表单字段由保存处单独写入)
const buildOutputConfig = (node: Node<NodeData>) => {
const buildOutputConfig = (node: Node<NodeData>, startNodeId = '') => {
const nodeCode = node.data?.nodeCode || '';
if (nodeCode === 'http') {
const defs = nodeConfigMap.value.get(nodeCode)?.formConfig || [];
@@ -1045,6 +1050,8 @@ const buildOutputConfig = (node: Node<NodeData>) => {
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<NodeData>) => {
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_flowisFormField: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 } : {}),