首页工作流:输出字段中文展示 + 引用面板滚动修复

- valueSource 契约统一为 { nodeId, field },替换 modelParamUtils 中 fieldName 旧用法
- 数组模板压缩时记录 __originIndex,叶子字段扁平清单按后端原始索引产出 enumValues
- 模型节点输出项保存时用 responseMapping 中文填充 value,引用展示显示中文
- http 节点输出字段识别 JsonEditor 包裹结构,修正真实数据路径并提取中文 label
- 引用上级节点输出面板超高时内部滚动,避免超出视口被截掉

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-19 17:07:21 +08:00
co-authored by Claude
parent c8f457dada
commit f7b3531392
5 changed files with 235 additions and 42 deletions
@@ -548,6 +548,9 @@ function handleClosed() { resetForm(); }
<style lang="scss"> <style lang="scss">
.fcd-ref-popover { .fcd-ref-popover {
.fcd-ref-panel { .fcd-ref-panel {
// 面板超高时内部滚动,避免超出浏览器视口被截掉显示不全
max-height: 40vh;
overflow-y: auto;
.fcd-ref-empty { .fcd-ref-empty {
color: #94a3b8; color: #94a3b8;
font-size: 12px; font-size: 12px;
@@ -76,7 +76,7 @@
import { ref, computed, watch } from 'vue'; import { ref, computed, watch } from 'vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import ModelSelector from '/@/views/settings/workflow/component/ModelSelector.vue'; import ModelSelector from '/@/views/settings/workflow/component/ModelSelector.vue';
import { stripReadonlyFields, deepClone } from '/@/views/settings/workflow/component/modelParamUtils'; import { stripReadonlyFields, deepClone, buildModelRequestParamsPath, enrichResponseBodyMapping } from '/@/views/settings/workflow/component/modelParamUtils';
import { getWorkflowDetail, saveWorkflow } from '/@/api/settings/creation'; import { getWorkflowDetail, saveWorkflow } from '/@/api/settings/creation';
import { getModelManage, updateModelManage } from '/@/api/settings/modelConfigV2'; import { getModelManage, updateModelManage } from '/@/api/settings/modelConfigV2';
@@ -109,6 +109,13 @@ const systemModelNodeIds = ref<Record<string, boolean>>({});
const modelNodes = computed(() => nodes.value.filter((n) => String(n?.nodeCode || '').toLowerCase() === 'model')); const modelNodes = computed(() => nodes.value.filter((n) => String(n?.nodeCode || '').toLowerCase() === 'model'));
// 开始节点 id:模板 DSL 与工作流管理 DSL 结构一致(nodeCode === '__start__'),
// 供 buildModelRequestParamsPath 为「表单展示」字段补 valueSource 指向开始节点
const getStartNodeId = () => {
const sn = nodes.value.find((n) => String(n?.nodeCode || '').toLowerCase() === '__start__');
return sn?.id || '';
};
const canSave = computed(() => { const canSave = computed(() => {
if (!flowName.value.trim()) return false; if (!flowName.value.trim()) return false;
// 所有模型节点都必须已绑定模型 // 所有模型节点都必须已绑定模型
@@ -162,9 +169,13 @@ const handleModelConfirm = (model: any) => {
node.modelConfig.modelId = model.id || ''; node.modelConfig.modelId = model.id || '';
node.modelConfig.modelName = model.modelName; node.modelConfig.modelName = model.modelName;
node.modelConfig.modelType = model.modelType; node.modelConfig.modelType = model.modelType;
// 深拷贝模型 requestBodyMapping 作为参数模板,剔除只读字段 // 深拷贝模型 requestBodyMapping 作为参数模板,剔除只读字段(strip 会给被压缩的数组模板记 __originIndex
node.modelConfig.modelRequestParams = stripReadonlyFields(deepClone(model.requestBodyMapping ?? null)); const params = stripReadonlyFields(deepClone(model.requestBodyMapping ?? null));
node.modelConfig.modelResponseBodyMapping = model.responseBodyMapping ?? null; node.modelConfig.modelRequestParams = params;
// 后端契约:基于编辑器参数树(含 __originIndex 原始索引)生成全部叶子字段扁平清单,与工作流管理保存一致
node.modelConfig.modelRequestParamsPath = buildModelRequestParamsPath(params, getStartNodeId());
// 用模型 responseMapping 的中文描述填充 value,供工作流管理「引用上级节点输出」展示中文
node.modelConfig.modelResponseBodyMapping = enrichResponseBodyMapping(model.responseBodyMapping, model.responseMapping);
// 选中为用户模型,无需再补 API Key // 选中为用户模型,无需再补 API Key
systemModelNodeIds.value[node.id] = false; systemModelNodeIds.value[node.id] = false;
apiKeyInputs.value[node.id] = ''; apiKeyInputs.value[node.id] = '';
@@ -249,6 +260,14 @@ const handleSave = async () => {
for (const node of modelNodes.value.filter((n) => systemModelNodeIds.value[n.id])) { for (const node of modelNodes.value.filter((n) => systemModelNodeIds.value[n.id])) {
await convertSystemModel(node); await convertSystemModel(node);
} }
// 模板 DSL 若缺 modelRequestParamsPath(旧版模板),用现有参数树兜底生成,保证后端拿到契约字段
const startNodeId = getStartNodeId();
for (const node of modelNodes.value) {
const mc = node?.modelConfig;
if (mc?.modelRequestParams && !mc.modelRequestParamsPath) {
mc.modelRequestParamsPath = buildModelRequestParamsPath(mc.modelRequestParams, startNodeId);
}
}
const flowContent = { const flowContent = {
...(templateFlowContent.value || {}), ...(templateFlowContent.value || {}),
nodes: nodes.value, nodes: nodes.value,
@@ -113,7 +113,7 @@
</el-popover> </el-popover>
</div> </div>
<div v-if="hasValueSource" class="mf-ref-bound" :class="{ 'is-invalid': valueSourceInvalid }"> <div v-if="hasValueSource" class="mf-ref-bound" :class="{ 'is-invalid': valueSourceInvalid }">
<span class="mf-ref-bound-text">已引用{{ valueSourceLabel }}</span> <span class="mf-ref-bound-text" :title="valueSourceLabel">已引用{{ valueSourceLabel }}</span>
<el-button size="small" text type="danger" @click="clearValueSource">清除</el-button> <el-button size="small" text type="danger" @click="clearValueSource">清除</el-button>
</div> </div>
<div v-else-if="!isUpload" class="mf-leaf-ctrl"> <div v-else-if="!isUpload" class="mf-leaf-ctrl">
@@ -525,6 +525,9 @@ const variantLabel = (idx: number): string => templateLabel(def.value?.enumValue
<style lang="scss"> <style lang="scss">
.mf-ref-popover { .mf-ref-popover {
.mf-ref-panel { .mf-ref-panel {
// 面板超高时内部滚动,避免超出浏览器视口被截掉显示不全
max-height: 40vh;
overflow-y: auto;
.mf-ref-empty { .mf-ref-empty {
color: #94a3b8; color: #94a3b8;
font-size: 12px; font-size: 12px;
@@ -102,6 +102,15 @@ export function stripReadonlyFields(params: any): any {
} }
return true; return true;
}); });
// 有模板被剔除(子字段全只读)→ 数组索引被压缩;给保留的模板补记原始位置 __originIndex
// 供 buildModelRequestParamsPath 生成后端契约的 enumValues 原始索引(幂等:已带则不覆盖)
if (keptTemplates.length !== templates.length) {
templates.forEach((t: any, originIdx: number) => {
if (t && typeof t === 'object' && keptTemplates.includes(t) && t.__originIndex === undefined) {
t.__originIndex = originIdx;
}
});
}
if (isEnumValues) def.enumValues = keptTemplates; if (isEnumValues) def.enumValues = keptTemplates;
if (isAttrsArray) def.attrs = keptTemplates; if (isAttrsArray) def.attrs = keptTemplates;
if (keptTemplates.length > 0) hasEditable = true; if (keptTemplates.length > 0) hasEditable = true;
@@ -231,6 +240,73 @@ export function collectExposedFields(params: any, prefix = ''): ExposedField[] {
return result; return result;
} }
// ===== 模型请求参数扁平清单(modelRequestParamsPath=====
// 后端契约:保存时与 modelRequestParams 平级新增 modelRequestParamsPath
// 为嵌套模型参数树的全部叶子字段扁平清单(path 文法与 collectExposedFields 一致),
// 后端按 path 重组请求参数。value 保留当前值;valueSource 规则:
// - 已有引用配置(用户在设计器配置「引用上游」)→ 原样保留 { nodeId, field }
// - 无引用但勾选「表单展示」(runtimeShow=true)→ 补 { nodeId: 开始节点id, field: path }
// - 其余 → 不带 valueSource
export interface ModelRequestParamsPathItem {
path: string;
type: string;
required: boolean;
value: any;
valueSource?: any;
}
// 收集嵌套树的全部叶子字段(含数组模板元素,不过滤 runtimeShow),返回 { path, def }
function collectAllLeafFields(params: any, prefix = ''): { path: string; def: any }[] {
if (!params || typeof params !== 'object' || Array.isArray(params)) return [];
const result: { path: string; def: any }[] = [];
for (const key of Object.keys(params)) {
const def = params[key];
if (!def || typeof def !== 'object') continue;
const path = prefix ? `${prefix}.${key}` : key;
const t = def.type;
if (t === 'object') {
const attrs = def.attrs;
if (attrs && typeof attrs === 'object' && !Array.isArray(attrs)) {
result.push(...collectAllLeafFields(attrs, `${path}.attrs`));
}
} else if (t === 'array') {
// 数组字段遍历模板(enumValues 优先,其次 attrs 数组),path 用 enumValues[i] 定位模板,
// 与 ModelField 渲染 / collectExposedFields 文法一致(值/勾选均落在模板上,不依赖 value 实例)。
// 模板带 __originIndexstrip 时记录的原始位置)时用原始索引,保证与后端模型完整定义的 enumValues 编号一致
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)) {
const originIdx = typeof tpl.__originIndex === 'number' ? tpl.__originIndex : i;
result.push(...collectAllLeafFields(tpl.attrs, `${path}.enumValues[${originIdx}].attrs`));
}
});
} else {
result.push({ path, def });
}
}
return result;
}
// 生成 modelRequestParamsPath 扁平清单
export function buildModelRequestParamsPath(params: any, startNodeId = ''): ModelRequestParamsPathItem[] {
const fields = collectAllLeafFields(params);
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;
return {
path,
type: def.type || 'string',
required: Boolean(def.required),
value: def.value ?? '',
...(valueSource ? { valueSource } : {}),
};
});
}
// 按暴露清单的 path 反解,把对应叶子 def 的 runtimeShow 置 true(加载时还原勾选) // 按暴露清单的 path 反解,把对应叶子 def 的 runtimeShow 置 true(加载时还原勾选)
// startNodeId:勾选「表单展示」的字段保存时带 valueSource 指向开始节点(后端契约); // startNodeId:勾选「表单展示」的字段保存时带 valueSource 指向开始节点(后端契约);
// 回显时该 valueSource 无编辑器引用语义,清除避免 ModelField 误判引用/误删 // 回显时该 valueSource 无编辑器引用语义,清除避免 ModelField 误判引用/误删
@@ -248,7 +324,7 @@ export function restoreRuntimeShow(params: any, fields: ExposedField[] | null |
} }
} }
// 勾选「表单展示」的字段:保存前按后端契约补 valueSource={nodeId:开始节点id, fieldName:path} // 勾选「表单展示」的字段:保存前按后端契约补 valueSource={nodeId:开始节点id, field:path}
// 标记字段值来自主工作流开始节点(首页表单)。startNodeId 为空(无开始节点)时跳过。 // 标记字段值来自主工作流开始节点(首页表单)。startNodeId 为空(无开始节点)时跳过。
export function attachFormFieldValueSources(params: any, fields: ExposedField[] | null | undefined, startNodeId = ''): void { export function attachFormFieldValueSources(params: any, fields: ExposedField[] | null | undefined, startNodeId = ''): void {
if (!params || typeof params !== 'object' || !Array.isArray(fields) || fields.length === 0 || !startNodeId) return; if (!params || typeof params !== 'object' || !Array.isArray(fields) || fields.length === 0 || !startNodeId) return;
@@ -256,7 +332,7 @@ export function attachFormFieldValueSources(params: any, fields: ExposedField[]
if (!f || typeof f.path !== 'string') continue; if (!f || typeof f.path !== 'string') continue;
const cur = resolvePath(params, f.path); const cur = resolvePath(params, f.path);
if (cur && typeof cur === 'object' && !Array.isArray(cur)) { if (cur && typeof cur === 'object' && !Array.isArray(cur)) {
cur.valueSource = { nodeId: startNodeId, fieldName: f.path }; cur.valueSource = { nodeId: startNodeId, field: f.path };
} }
} }
} }
@@ -297,3 +373,53 @@ function resolvePath(params: any, path: string): any {
export function isEqual(a: any, b: any): boolean { export function isEqual(a: any, b: any): boolean {
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null); return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
} }
// ===== 模型返回参数中文 label(引用展示用)=====
// 模型 responseMapping(响应映射:路径→中文)支持两种结构:普通对象 {路径: 中文} 与
// { type, attrs/value, label } 包裹格式。独立实现平铺(不依赖 settings/modelConfigV2),
// 供保存 modelResponseBodyMapping 时把中文描述填入 value,引用上级节点输出展示中文而非英文 key。
function flattenResponseLabelMap(mapping: any, basePath = '', out: Record<string, string>): void {
if (!mapping || typeof mapping !== 'object') return;
// { type, attrs/value } 包裹格式:叶子节点带 label
if (typeof mapping.type === 'string' && ['string', 'number', 'boolean', 'null', 'object', 'array'].includes(mapping.type)) {
const jtype = mapping.type;
const dataKey = jtype === 'object' || jtype === 'array' ? 'attrs' : 'value';
if (jtype === 'object' && dataKey in mapping && mapping[dataKey] && typeof mapping[dataKey] === 'object' && !Array.isArray(mapping[dataKey])) {
for (const [key, val] of Object.entries(mapping[dataKey])) {
flattenResponseLabelMap(val, basePath ? `${basePath}.attrs.${key}` : key, out);
}
} else if (jtype === 'array' && dataKey in mapping && Array.isArray(mapping[dataKey])) {
mapping[dataKey].forEach((item: any, idx: number) => {
flattenResponseLabelMap(item, `${basePath}.attrs[${idx}]`, out);
});
} else if (['string', 'number', 'boolean', 'null'].includes(jtype)) {
if (mapping.label) out[basePath] = String(mapping.label);
}
return;
}
// 普通对象(非包裹格式)
if (!Array.isArray(mapping)) {
for (const [key, val] of Object.entries(mapping)) {
const childPath = basePath ? `${basePath}.${key}` : key;
if (val && typeof val === 'object' && !Array.isArray(val)) {
flattenResponseLabelMap(val, childPath, out);
} else if (typeof val === 'string' && val) {
out[childPath] = val;
}
}
}
}
// 用模型 responseMapping 的中文描述填充 modelResponseBodyMapping 的 value
// { key: '' } → { key: '中文' }。结构不变(对象 key→string),无对应中文时保留原 value(兜底 key)。
export function enrichResponseBodyMapping(body: any, mapping: any): any {
if (!body || typeof body !== 'object' || Array.isArray(body)) return body ?? null;
const labelMap: Record<string, string> = {};
flattenResponseLabelMap(mapping, '', labelMap);
const out: Record<string, string> = {};
for (const k of Object.keys(body)) {
const raw = typeof body[k] === 'string' ? body[k] : '';
out[k] = labelMap[k] || raw || k;
}
return out;
}
+77 -35
View File
@@ -124,7 +124,10 @@ import {
isEqual, isEqual,
deepClone, deepClone,
removeArrayValueInstances, removeArrayValueInstances,
buildModelRequestParamsPath,
enrichResponseBodyMapping,
type ExposedField, type ExposedField,
type ModelRequestParamsPathItem,
} from './component/modelParamUtils'; } from './component/modelParamUtils';
import NodeLibraryPanel from './component/NodeLibraryPanel.vue'; import NodeLibraryPanel from './component/NodeLibraryPanel.vue';
import WorkflowListPanel from './component/WorkflowListPanel.vue'; import WorkflowListPanel from './component/WorkflowListPanel.vue';
@@ -164,6 +167,7 @@ interface NodeData {
modelName?: string; modelName?: string;
modelType?: string | number; modelType?: string | number;
modelRequestParams?: any; modelRequestParams?: any;
modelRequestParamsPath?: ModelRequestParamsPathItem[] | null;
modelFormFields?: ExposedField[] | null; modelFormFields?: ExposedField[] | null;
modelResponseBodyMapping?: any; // 模型返回参数(数组或对象两种结构),供下游引用 modelResponseBodyMapping?: any; // 模型返回参数(数组或对象两种结构),供下游引用
} | null; } | null;
@@ -355,8 +359,9 @@ const handleModelConfirm = (model: any) => {
// 深拷贝模型 requestBodyMapping 作为参数模板(重选模型时覆盖旧参数) // 深拷贝模型 requestBodyMapping 作为参数模板(重选模型时覆盖旧参数)
// 剔除 isForm=false 的只读字段,使其不显示也不随工作流保存 // 剔除 isForm=false 的只读字段,使其不显示也不随工作流保存
modelRequestParams: stripReadonlyFields(JSON.parse(JSON.stringify(model.requestBodyMapping ?? null))), modelRequestParams: stripReadonlyFields(JSON.parse(JSON.stringify(model.requestBodyMapping ?? null))),
// 保存模型返回参数(responseBodyMapping),作为该节点可被下游引用的输出项 // 保存模型返回参数(responseBodyMapping),作为该节点可被下游引用的输出项
modelResponseBodyMapping: model.responseBodyMapping ?? null, // 用模型 responseMapping 的中文描述填充 value,引用展示显示中文而非英文 key
modelResponseBodyMapping: enrichResponseBodyMapping(model.responseBodyMapping, model.responseMapping),
}, },
}, },
}; };
@@ -724,27 +729,50 @@ const parseSchema = (value: any): any => {
return null; return null;
}; };
// 递归收集 JSON schema 的所有叶子字段(数组取首元素对象作为结构样本) // 递归收集 http 节点 response schema 的叶子输出字段(识别 JsonEditor 的 { type, attrs/value, label } 包裹结构):
const collectLeafFieldsFromJson = (obj: any, prefix = ''): UpstreamField[] => { // field 用实际返回数据路径(不含 attrs 元数据,如 data.url 而非 data.attrs.url.value),
const result: UpstreamField[] = []; // label 优先用字段配置的中文名(label),无则兜底路径 key
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return result; const collectHttpLeafFields = (node: any, prefix = ''): UpstreamField[] => {
for (const key of Object.keys(obj)) { // JsonEditor 包裹结构:{ type: 'object'|'array'|'string'|..., attrs/value, label }
if (!key || key.startsWith('_temp_')) continue; if (node && typeof node === 'object' && !Array.isArray(node) && typeof node.type === 'string') {
const path = prefix ? `${prefix}.${key}` : key; const jtype = node.type;
const val = obj[key]; if (['object', 'array', 'string', 'number', 'boolean', 'null'].includes(jtype)) {
if (val && typeof val === 'object') { if (jtype === 'object') {
if (Array.isArray(val)) { // 对象容器:子字段在 attrs 里,field 路径直接拼子字段名,不拼 attrs
const sample = val.find((it: any) => it && typeof it === 'object' && !Array.isArray(it)); const attrs = node.attrs;
if (sample) result.push(...collectLeafFieldsFromJson(sample, path)); if (attrs && typeof attrs === 'object' && !Array.isArray(attrs)) {
else result.push({ field: path, label: path }); const out: UpstreamField[] = [];
} else { for (const k of Object.keys(attrs)) {
result.push(...collectLeafFieldsFromJson(val, path)); out.push(...collectHttpLeafFields(attrs[k], prefix ? `${prefix}.${k}` : k));
}
return out;
}
return [];
} }
} else { if (jtype === 'array') {
result.push({ field: path, label: path }); // 数组:取首元素样本作为结构(实际值为数组整体,field 不带索引)
const arr = node.attrs;
if (Array.isArray(arr) && arr.length > 0) return collectHttpLeafFields(arr[0], prefix);
return [];
}
// 标量叶子:label 为字段配置的中文名,无则兜底路径
return [{ field: prefix, label: (typeof node.label === 'string' && node.label) || prefix }];
} }
} }
return result; // 普通对象(根/未包裹):递归子字段;子字段为原始标量时以路径兜底产出
if (node && typeof node === 'object' && !Array.isArray(node)) {
const out: UpstreamField[] = [];
for (const k of Object.keys(node)) {
const child = node[k];
const path = prefix ? `${prefix}.${k}` : k;
if (child && typeof child === 'object') out.push(...collectHttpLeafFields(child, path));
else out.push({ field: path, label: path });
}
return out;
}
// 数组根:取首元素结构样本
if (Array.isArray(node)) return node.length > 0 ? collectHttpLeafFields(node[0], prefix) : [];
return [];
}; };
// 取节点在设计时的可引用输出字段(运行时这些字段会产出实际值) // 取节点在设计时的可引用输出字段(运行时这些字段会产出实际值)
@@ -767,16 +795,26 @@ const getNodeOutputFields = (node: Node<NodeData, any, string>): UpstreamField[]
? (responseTypeEntry?.expand || []).find((e: any) => e.field === 'response') ? (responseTypeEntry?.expand || []).find((e: any) => e.field === 'response')
: formConfig.find((f: any) => f.field === 'response'); : formConfig.find((f: any) => f.field === 'response');
const schema = parseSchema(responseField?.value); const schema = parseSchema(responseField?.value);
if (schema) return collectLeafFieldsFromJson(schema); if (schema) return collectHttpLeafFields(schema);
return []; return [];
} }
if (nodeCode === 'model') { if (nodeCode === 'model') {
// 模型节点:模型的返回参数(responseBodyMapping)即可被下游引用的输出 // 模型节点:模型的返回参数(responseBodyMapping)即可被下游引用的输出
// value 为保存时从模型 responseMapping 填充的中文描述,展示优先用中文,无则兜底 key
const resp = node.data?.modelConfig?.modelResponseBodyMapping; const resp = node.data?.modelConfig?.modelResponseBodyMapping;
const keys = Array.isArray(resp) ? resp : resp && typeof resp === 'object' ? Object.keys(resp) : []; // 数组结构:元素带 field/label
return keys if (Array.isArray(resp)) {
.filter((k: any) => typeof k === 'string' && k.trim() !== '') return resp
.map((k: string) => ({ field: k, label: k })); .filter((it: any) => it && typeof it === 'object' && (it.field || it.label || it.key))
.map((it: any) => ({ field: it.field || it.label || it.key || '', label: it.label || it.field || it.key || '' }));
}
// 对象结构:{ key: 中文描述 }
if (resp && typeof resp === 'object') {
return Object.keys(resp)
.filter((k: any) => typeof k === 'string' && k.trim() !== '')
.map((k: string) => ({ field: k, label: (typeof resp[k] === 'string' && resp[k]) || k }));
}
return [];
} }
if (nodeCode === START_NODE_CODE) { if (nodeCode === START_NODE_CODE) {
// 开始节点:运行表单字段(被勾选的模型参数 + form 自定义字段) // 开始节点:运行表单字段(被勾选的模型参数 + form 自定义字段)
@@ -1051,7 +1089,7 @@ const buildOutputConfig = (node: Node<NodeData>, startNodeId = '') => {
const entry = formConfig.find((f: any) => f.field === def.field); const entry = formConfig.find((f: any) => f.field === def.field);
const value = entry?.value ?? ''; const value = entry?.value ?? '';
// 勾选「表单展示」的字段:值来自开始节点表单,补 valueSource 标记(后端契约) // 勾选「表单展示」的字段:值来自开始节点表单,补 valueSource 标记(后端契约)
const valueSource = def.isFormField === true ? { nodeId: startNodeId, fieldName: def.field } : undefined; const valueSource = def.isFormField === true ? { nodeId: startNodeId, field: def.field } : undefined;
if (def.field === 'responseType') { if (def.field === 'responseType') {
const expand = entry?.expand || []; const expand = entry?.expand || [];
return { return {
@@ -1080,7 +1118,7 @@ const buildOutputConfig = (node: Node<NodeData>, startNodeId = '') => {
label: f.label || f.field || '', label: f.label || f.field || '',
value: f.value ?? '', value: f.value ?? '',
required: Boolean(f.required), required: Boolean(f.required),
...(fieldName ? { valueSource: { nodeId: startNodeId, fieldName } } : {}), ...(fieldName ? { valueSource: { nodeId: startNodeId, field: fieldName } } : {}),
...(f.type === 'uploadMultiple' ...(f.type === 'uploadMultiple'
? { ? {
fieldConstraint: { fieldConstraint: {
@@ -1118,7 +1156,7 @@ const buildOutputConfig = (node: Node<NodeData>, startNodeId = '') => {
label: def.label || def.field, label: def.label || def.field,
// isFormField 字段值在开始节点运行表单,不重复保存(回显从开始节点反查) // isFormField 字段值在开始节点运行表单,不重复保存(回显从开始节点反查)
...(isForm ? {} : { value: entry?.value ?? '' }), ...(isForm ? {} : { value: entry?.value ?? '' }),
...(isForm ? { valueSource: { nodeId: startNodeId, fieldName: def.field } } : {}), ...(isForm ? { valueSource: { nodeId: startNodeId, field: def.field } } : {}),
}; };
}); });
}; };
@@ -1147,14 +1185,14 @@ const serializeSubFlowConfig = (config: SubFlowConfig | null | undefined, node?:
fieldConstraint: f.fieldConstraint ?? null, fieldConstraint: f.fieldConstraint ?? null,
options: Array.isArray(f.options) ? f.options : null, options: Array.isArray(f.options) ? f.options : null,
multiple: Boolean(f.multiple), multiple: Boolean(f.multiple),
// 值来源契约(后端统一 { nodeId, fieldName }): // 值来源契约(后端统一 { nodeId, field }):
// - 勾选表单展示:值来自主工作流开始节点(首页表单),指向开始节点 // - 勾选表单展示:值来自主工作流开始节点(首页表单),指向开始节点
// - 引用上级输出:值来自主工作流上游节点,统一转 fieldName // - 引用上级输出:值来自主工作流上游节点,统一转 field
// - 其余:无引用(静态默认值) // - 其余:无引用(静态默认值)
valueSource: f.runtimeShow valueSource: f.runtimeShow
? { nodeId: startNodeId, fieldName: f.field } ? { nodeId: startNodeId, field: f.field }
: f.valueSource && typeof f.valueSource === 'object' : f.valueSource && typeof f.valueSource === 'object'
? { nodeId: f.valueSource.nodeId, fieldName: f.valueSource.field } ? { nodeId: f.valueSource.nodeId, field: f.valueSource.field }
: null, : null,
isFormField: Boolean(f.runtimeShow), isFormField: Boolean(f.runtimeShow),
})), })),
@@ -1183,7 +1221,7 @@ const buildSubFlowConfigFromDsl = (subConfig: any, startNodeId = '') => {
const vs = f.valueSource; const vs = f.valueSource;
// 指向开始节点:值为表单展示(首页可填),编辑器不显示为引用(勾选由 isFormField 恢复) // 指向开始节点:值为表单展示(首页可填),编辑器不显示为引用(勾选由 isFormField 恢复)
if (vs && typeof vs === 'object' && vs.nodeId && vs.nodeId === startNodeId) return null; if (vs && typeof vs === 'object' && vs.nodeId && vs.nodeId === startNodeId) return null;
// 引用上级:后端 fieldName 转回前端 field 结构 // 引用上级:后端 field(兼容旧 DSL 的 fieldName转回前端 field 结构
if (vs && typeof vs === 'object') return { nodeId: vs.nodeId, field: vs.fieldName ?? vs.field }; if (vs && typeof vs === 'object') return { nodeId: vs.nodeId, field: vs.fieldName ?? vs.field };
return null; return null;
})(), })(),
@@ -1205,6 +1243,8 @@ const buildModelConfigFromDsl = (n: any, startNodeId = '') => {
modelName: n.modelConfig?.modelName || '', modelName: n.modelConfig?.modelName || '',
modelType: n.modelConfig?.modelType ?? undefined, modelType: n.modelConfig?.modelType ?? undefined,
modelRequestParams, modelRequestParams,
// 全部叶子字段扁平清单随 DSL 透传,编辑过程不消费,仅保证重开保存不丢
modelRequestParamsPath: n.modelConfig?.modelRequestParamsPath ?? null,
modelFormFields, modelFormFields,
modelResponseBodyMapping: n.modelConfig?.modelResponseBodyMapping ?? null, modelResponseBodyMapping: n.modelConfig?.modelResponseBodyMapping ?? null,
}; };
@@ -1362,7 +1402,7 @@ const confirmSaveWorkflow = async () => {
// 数组字段的实例值(value)由前端从模板复制而来,与 enumValues 模板重复, // 数组字段的实例值(value)由前端从模板复制而来,与 enumValues 模板重复,
// 保存时不提交;deepClone 避免污染节点运行时状态 // 保存时不提交;deepClone 避免污染节点运行时状态
const savedModelRequestParams = rawModelParams ? removeArrayValueInstances(deepClone(rawModelParams)) : null; const savedModelRequestParams = rawModelParams ? removeArrayValueInstances(deepClone(rawModelParams)) : null;
// 勾选「表单展示」的字段:补 valueSource={nodeId:开始节点id, fieldName:path}(后端契约, // 勾选「表单展示」的字段:补 valueSource={nodeId:开始节点id, field:path}(后端契约,
// 标记字段值来自主工作流开始节点首页表单) // 标记字段值来自主工作流开始节点首页表单)
if (savedModelRequestParams && startNode?.id) { if (savedModelRequestParams && startNode?.id) {
attachFormFieldValueSources(savedModelRequestParams, savedModelFormFields, startNode.id); attachFormFieldValueSources(savedModelRequestParams, savedModelFormFields, startNode.id);
@@ -1390,6 +1430,8 @@ const confirmSaveWorkflow = async () => {
? { modelType: n.data.modelConfig.modelType } ? { modelType: n.data.modelConfig.modelType }
: {}), : {}),
modelRequestParams: savedModelRequestParams, modelRequestParams: savedModelRequestParams,
// 全部叶子字段扁平清单(path + 值 + 值来源),后端按 path 重组请求参数;基于编辑器原始嵌套树生成,保留用户配置的引用 valueSource
modelRequestParamsPath: rawModelParams ? buildModelRequestParamsPath(rawModelParams, startNode?.id || '') : null,
// 勾选的「表单展示」字段(路径带实例索引),已在上方基于完整结构收集 // 勾选的「表单展示」字段(路径带实例索引),已在上方基于完整结构收集
...(savedModelFormFields && savedModelFormFields.length > 0 ? { modelFormFields: savedModelFormFields } : {}), ...(savedModelFormFields && savedModelFormFields.length > 0 ? { modelFormFields: savedModelFormFields } : {}),
// 模型返回参数随工作流保存,保证重开后仍可被下游引用 // 模型返回参数随工作流保存,保证重开后仍可被下游引用