工作流 valueSource label 契约统一:去节点名前缀 + outputConfig/subConfig 补全

- resolveRefLabel 去掉「节点名.」前缀,valueSource label 只保留字段中文名
- ModelField 引用 tag 展示同步去前缀
- buildOutputConfig(http/form/其它节点)与 serializeSubFlowConfig(subConfig)的
  valueSource 补 label,subConfig 引用上游分支经 outputIndex 查上游字段中文名
- 取消引用改用 delete(避免写空数组),enumValues 按 __originIndex 统一索引

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-19 23:18:03 +08:00
co-authored by Claude
parent 68850c40fe
commit 998e13e6be
4 changed files with 42 additions and 22 deletions
@@ -94,14 +94,16 @@ const toggleValueSource = (node: UpstreamNodeInfo, of: UpstreamField) => {
if (props.multi) {
// 多选:勾选/取消切换,同字段去重
if (isSelected(node, of)) {
props.fieldDef.valueSource = cur.filter((r) => !(r && r.nodeId === node.id && r.field === of.field));
const keep = cur.filter((r) => !(r && r.nodeId === node.id && r.field === of.field));
if (keep.length === 0) delete props.fieldDef.valueSource;
else props.fieldDef.valueSource = keep;
} else {
props.fieldDef.valueSource = [...cur, { nodeId: node.id, field: of.field }];
}
} else {
// 单选:点已选字段取消,否则替换为单元素数组
if (cur.length === 1 && isSelected(node, of)) {
props.fieldDef.valueSource = [];
delete props.fieldDef.valueSource;
} else {
props.fieldDef.valueSource = [{ nodeId: node.id, field: of.field }];
}
@@ -187,10 +187,10 @@ const valueSourceList = computed(() => {
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 };
return { nodeId: r?.nodeId, field: r?.field, label: fieldLabel, invalid };
});
});
const hasValueSource = computed(() => valueSourceList.value.length > 0);
@@ -217,7 +217,10 @@ export function collectExposedFields(params: any, prefix = ''): ExposedField[] {
const templates = arrayTemplatesOf(def);
templates.forEach((tpl: any, i: number) => {
if (tpl && typeof tpl === 'object' && tpl.attrs && typeof tpl.attrs === 'object' && !Array.isArray(tpl.attrs)) {
result.push(...collectExposedFields(tpl.attrs, `${path}.enumValues[${i}].attrs`));
// 索引与 collectAllLeafFields 一致:优先用 strip 时记录的原始模板索引 __originIndex
// 保证 modelFormFields 与 modelRequestParamsPath 的 path 索引对齐后端模型完整定义
const originIdx = typeof tpl.__originIndex === 'number' ? tpl.__originIndex : i;
result.push(...collectExposedFields(tpl.attrs, `${path}.enumValues[${originIdx}].attrs`));
}
});
} else if (def.runtimeShow === true) {
@@ -232,7 +235,11 @@ export function collectExposedFields(params: any, prefix = ''): ExposedField[] {
value: def.value,
defaultValue: def.defaultValue,
fieldConstraint: def.fieldConstraint && typeof def.fieldConstraint === 'object' ? def.fieldConstraint : undefined,
valueSource: Array.isArray(def.valueSource) ? def.valueSource : undefined,
valueSource: (() => {
const vs = def.valueSource;
// 契约统一数组;兼容旧 DSL 单对象 { nodeId, field }
return Array.isArray(vs) ? vs : vs && typeof vs === 'object' ? [vs] : undefined;
})(),
multiple: def.multiple || def.fieldType === 'uploadMultiple' || undefined,
});
}
@@ -299,12 +306,11 @@ export function buildModelRequestParamsPath(
outputIndex?: Map<string, { name: string; fields: Map<string, string> }>
): ModelRequestParamsPathItem[] {
const fields = collectAllLeafFields(params);
// 引用来源 label节点名.字段label无索引/未命中时兜底 field
// 引用来源 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 info.fields.get(field) || field;
};
return fields.map(({ path, def }) => {
let valueSource: any;
@@ -348,10 +354,12 @@ 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 && Array.isArray(cur.valueSource)) {
// 移除指向开始节点(表单展示标记)的引用;全部移除后删除整个 valueSource
cur.valueSource = cur.valueSource.filter((r: any) => !(r && r.nodeId === startNodeId));
if (cur.valueSource.length === 0) delete cur.valueSource;
if (startNodeId && cur.valueSource && typeof cur.valueSource === 'object') {
// 移除指向开始节点(表单展示标记)的引用;兼容旧 DSL 单对象(统一数组化),全部移除后删除整个 valueSource
const arr = Array.isArray(cur.valueSource) ? cur.valueSource : [cur.valueSource];
const keep = arr.filter((r: any) => !(r && r.nodeId === startNodeId));
if (keep.length === 0) delete cur.valueSource;
else cur.valueSource = keep;
}
}
}
@@ -392,8 +400,12 @@ function resolvePath(params: any, path: string): any {
if (me) {
const idx = Number(me[1]);
const templates = arrayTemplatesOf(cur);
if (idx >= templates.length) return undefined;
cur = templates[idx];
// 路径索引可能为 strip 压缩前记录的原始索引(__originIndex),优先按原始索引匹配;
// 未带 __originIndex 时退化为按数组位置匹配(未压缩场景)
const byOrigin = templates.find((t: any) => t && typeof t.__originIndex === 'number' && t.__originIndex === idx);
if (byOrigin !== undefined) cur = byOrigin;
else if (idx < templates.length) cur = templates[idx];
else return undefined;
continue;
}
cur = cur[seg];
+13 -7
View File
@@ -1110,7 +1110,7 @@ const buildOutputConfig = (node: Node<NodeData>, 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, label: def.label || def.field }] : undefined;
if (def.field === 'responseType') {
const expand = entry?.expand || [];
return {
@@ -1139,7 +1139,7 @@ const buildOutputConfig = (node: Node<NodeData>, 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, label: f.label || f.field || fieldName }] } : {}),
...(f.type === 'uploadMultiple'
? {
fieldConstraint: {
@@ -1177,14 +1177,20 @@ const buildOutputConfig = (node: Node<NodeData>, 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, label: def.label || def.field }] } : {}),
};
});
};
// 子流程节点:编辑器态 subFlowConfig → DSL 保存态 subConfigruntimeShow → isFormField
const serializeSubFlowConfig = (config: SubFlowConfig | null | undefined, node?: Node<NodeData>, startNodeId = '') => {
const serializeSubFlowConfig = (config: SubFlowConfig | null | undefined, node?: Node<NodeData>, startNodeId = '', outputIndex?: Map<string, { name: string; fields: Map<string, string> }>) => {
if (!config || !config.workflowId) return null;
// 引用来源 label:字段中文名(不带「节点名.」前缀;无索引/未命中时兜底 field)
const resolveRefLabel = (nodeId: string, field: string): string => {
const info = outputIndex?.get(nodeId);
if (!info) return field;
return info.fields.get(field) || field;
};
// 生成次数(节点库 presetOption 的 maxConcurrency):勾选表单展示时聚合进开始节点表单,
// 此处同步保存到 subConfig,保证 sub_flow 节点自身也带该配置(后端执行时可读)
const formConfig = Array.isArray(node?.data?.formConfig) ? node.data.formConfig : [];
@@ -1211,9 +1217,9 @@ const serializeSubFlowConfig = (config: SubFlowConfig | null | undefined, node?:
// - 引用上级输出:值来自主工作流上游节点,统一转 field
// - 其余:无引用(静态默认值)
valueSource: f.runtimeShow
? [{ nodeId: startNodeId, field: f.field }]
? [{ nodeId: startNodeId, field: f.field, label: f.label || f.field }]
: Array.isArray(f.valueSource)
? f.valueSource.map((vs: any) => ({ nodeId: vs.nodeId, field: vs.field }))
? f.valueSource.map((vs: any) => ({ nodeId: vs.nodeId, field: vs.field, label: resolveRefLabel(vs?.nodeId, vs?.field) }))
: null,
isFormField: Boolean(f.runtimeShow),
})),
@@ -1455,7 +1461,7 @@ const confirmSaveWorkflow = async () => {
...(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,
subConfig: nodeCode === 'sub_flow' ? serializeSubFlowConfig(n.data?.subFlowConfig, n, startNode?.id || '', outputFieldsIndex) : null,
modelConfig: {
modelId: n.data?.modelConfig?.modelId || '',
// 模型名称/类型随工作流保存,供首页补全弹窗展示模型名与「同类型模型」过滤