首页工作流节点引用多选与保存回显完善:isMultiParameter 全链路透传 + valueSource 数组化/label + 旧 DSL 兼容

- isMultiParameter 节点级开关从节点库透传至引用下拉,支持多选勾选/去重/逐条删除/清空
- valueSource 契约统一为数组并补 label(节点名.字段中文名),覆盖用户引用、表单展示标记与旧 DSL 单对象
- ModelField 递归透传 multi,object/array 嵌套子字段同样支持多选
- 保存/回显兼容旧 DSL 单对象 valueSource,重开自动升级为数组
- modelRequestParamsPath 每项新增 label 字段,后端按 path 重组请求参数

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-19 19:53:06 +08:00
co-authored by Claude
parent b6208b37e4
commit 68bdc63231
10 changed files with 298 additions and 80 deletions
+5 -2
View File
@@ -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;
+10 -4
View File
@@ -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 => {
@@ -1,5 +1,5 @@
<template>
<!-- 引用上级节点输出按钮 + 候选面板叶子字段 + 对象/数组整体 -->
<!-- 引用上级节点输出按钮 + 候选面板叶子字段 + 对象/数组整体multi 时支持多选 -->
<el-popover
v-if="canValueSource"
placement="bottom"
@@ -32,8 +32,12 @@
v-for="of in node.outputFields"
:key="`${of.field}-${of.fieldType || 'scalar'}`"
class="mf-ref-field"
@click="setValueSource(node, of)"
:class="{ 'is-selected': isSelected(node, of) }"
@click="toggleValueSource(node, of)"
>
<span class="mf-ref-check">
<el-icon v-if="isSelected(node, of)"><Check /></el-icon>
</span>
<span v-if="of.fieldType === 'object'" class="mf-ref-type" title="引用对象整体">{} 对象</span>
<span v-else-if="of.fieldType === 'array'" class="mf-ref-type" title="引用数组整体">[] 数组</span>
<span class="mf-ref-field-text">{{ of.label || of.field }}</span>
@@ -46,7 +50,7 @@
<script setup lang="ts">
import { computed } from 'vue';
import { Link } from '@element-plus/icons-vue';
import { Check, Link } from '@element-plus/icons-vue';
defineOptions({ name: 'MfReferenceButton' });
@@ -63,7 +67,7 @@ interface UpstreamNodeInfo {
outputFields: UpstreamField[];
}
const props = defineProps<{ fieldDef: any; upstreamNodes?: UpstreamNodeInfo[] }>();
const props = defineProps<{ fieldDef: any; upstreamNodes?: UpstreamNodeInfo[]; multi?: boolean }>();
// 可引用:非开关、非固定选项下拉(开关/下拉值来自模型);叶子与对象/数组整体均可引用
const canValueSource = computed(() => {
@@ -73,10 +77,35 @@ const canValueSource = computed(() => {
});
const hasUpstream = computed(() => (props.upstreamNodes?.length ?? 0) > 0);
// 选中前驱节点输出字段:就地写 fieldDef.valueSource,复用父组件深 watch 上传链路
const setValueSource = (node: UpstreamNodeInfo, of: UpstreamField) => {
// 当前引用集合(契约统一数组 [{ nodeId, field }];兼容旧 DSL 单对象)
const valueSource = computed<any[]>(() => {
const vs = props.fieldDef?.valueSource;
if (Array.isArray(vs)) return vs;
return vs && typeof vs === 'object' && !Array.isArray(vs) ? [vs] : [];
});
const isSelected = (node: UpstreamNodeInfo, of: UpstreamField): boolean =>
valueSource.value.some((r) => r && r.nodeId === node.id && r.field === of.field);
// 点选切换:就地写 fieldDef.valueSource 数组,复用父组件深 watch 上传链路
const toggleValueSource = (node: UpstreamNodeInfo, of: UpstreamField) => {
if (!props.fieldDef) return;
props.fieldDef.valueSource = { nodeId: node.id, field: of.field };
const cur = valueSource.value;
if (props.multi) {
// 多选:勾选/取消切换,同字段去重
if (isSelected(node, of)) {
props.fieldDef.valueSource = cur.filter((r) => !(r && r.nodeId === node.id && r.field === of.field));
} else {
props.fieldDef.valueSource = [...cur, { nodeId: node.id, field: of.field }];
}
} else {
// 单选:点已选字段取消,否则替换为单元素数组
if (cur.length === 1 && isSelected(node, of)) {
props.fieldDef.valueSource = [];
} else {
props.fieldDef.valueSource = [{ nodeId: node.id, field: of.field }];
}
}
};
</script>
@@ -141,11 +170,25 @@ const setValueSource = (node: UpstreamNodeInfo, of: UpstreamField) => {
border-radius: 4px;
cursor: pointer;
.mf-ref-check {
flex-shrink: 0;
width: 14px;
font-size: 12px;
color: #3b82f6;
display: inline-flex;
align-items: center;
}
&:hover {
background: #eff6ff;
color: #3b82f6;
}
&.is-selected {
background: #eff6ff;
color: #3b82f6;
}
.mf-ref-type {
flex-shrink: 0;
font-size: 11px;
@@ -16,6 +16,7 @@
:field-def="childDef"
:path="path ? `${path}.attrs.${childKey}` : childKey"
:upstream-nodes="upstreamNodes"
:multi="multi"
/>
</div>
</div>
@@ -46,6 +47,7 @@
:field-def="subDef"
:path="path ? `${path}.enumValues[${idx}].attrs.${subKey}` : String(subKey)"
:upstream-nodes="upstreamNodes"
:multi="multi"
/>
</template>
<el-input v-else :model-value="getTemplatePrimitive(item)" @input="(v: any) => setTemplatePrimitive(item, v)" size="small" />
@@ -71,12 +73,22 @@
class="mf-leaf-expose"
>表单展示</el-checkbox>
</el-tooltip>
<!-- 引用上级输出与表单展示勾选同级所有叶子参数含数组元素内字段都可引用 -->
<MfReferenceButton :field-def="def" :upstream-nodes="upstreamNodes" />
<!-- 引用上级输出与表单展示勾选同级所有叶子参数含数组元素内字段都可引用multi 时支持多选 -->
<MfReferenceButton :field-def="def" :upstream-nodes="upstreamNodes" :multi="multi" />
</div>
<div v-if="hasValueSource" class="mf-ref-bound" :class="{ 'is-invalid': valueSourceInvalid }">
<span class="mf-ref-bound-text" :title="valueSourceLabel">已引用{{ valueSourceLabel }}</span>
<el-button size="small" text type="danger" @click="clearValueSource">清除</el-button>
<div class="mf-ref-tags">
<el-tag
v-for="(ref, i) in valueSourceList"
:key="`${ref.nodeId}-${ref.field}-${i}`"
size="small"
class="mf-ref-tag"
:type="ref.invalid ? 'danger' : ''"
closable
@close="removeValueSource(ref)"
>{{ ref.label }}</el-tag>
</div>
<el-button size="small" text type="danger" @click="clearValueSource">清空</el-button>
</div>
<div v-if="!hasValueSource && !isUpload" class="mf-leaf-ctrl">
<el-input-number
@@ -135,7 +147,7 @@ interface UpstreamNodeInfo {
outputFields: { field: string; label: string; fieldType?: 'scalar' | 'object' | 'array' }[];
}
const props = defineProps<{ fieldDef: any; path?: string; upstreamNodes?: UpstreamNodeInfo[] }>();
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 {
@@ -14,6 +14,7 @@
:field-def="def"
:path="key"
:upstream-nodes="upstreamNodes"
:multi="multi"
/>
</div>
</div>
@@ -33,7 +34,7 @@ interface UpstreamNodeInfo {
outputFields: { field: string; label: string }[];
}
const props = defineProps<{ modelRequestParams: Record<string, any> | null; upstreamNodes?: UpstreamNodeInfo[] }>();
const props = defineProps<{ modelRequestParams: Record<string, any> | null; upstreamNodes?: UpstreamNodeInfo[]; multi?: boolean }>();
const emit = defineEmits<{ 'update:modelRequestParams': [Record<string, any> | null] }>();
const localParams = ref<Record<string, any>>({});
@@ -13,8 +13,39 @@
@update:model-value="updateNodeDesc"
/>
</el-form-item>
<el-form-item label="节点类型">
<el-tag>{{ selectedNode.data?.nodeCode }}</el-tag>
<!-- 前置方法 / 后置方法节点库 preToolOption / postToolOption 配置定义值保存到节点顶层 preTool / postTool outputConfig 平级 -->
<el-form-item v-if="preToolDef" :label="preToolDef.label || '前置方法'">
<el-select
:model-value="selectedNode.data?.preTool ?? ''"
@update:model-value="updatePreTool"
clearable
class="w100"
:placeholder="preToolDef.required ? '必选' : '选填'"
>
<el-option
v-for="opt in preToolDef.options || []"
:key="opt.key || opt.value"
:label="opt.value ?? opt.key"
:value="opt.key ?? opt.value"
/>
</el-select>
</el-form-item>
<el-form-item v-if="postToolDef" :label="postToolDef.label || '后置方法'">
<el-select
:model-value="selectedNode.data?.postTool ?? ''"
@update:model-value="updatePostTool"
clearable
class="w100"
:placeholder="postToolDef.required ? '必选' : '选填'"
>
<el-option
v-for="opt in postToolDef.options || []"
:key="opt.key || opt.value"
:label="opt.value ?? opt.key"
:value="opt.key ?? opt.value"
/>
</el-select>
</el-form-item>
<!-- 开始节点运行表单字段摘要来自各模型节点勾选的表单展示字段 -->
@@ -219,6 +250,7 @@
<SubFlowParams
:fields="subFlowConfig.fields"
:upstream-nodes="upstreamNodes"
:multi="!!nodeConfig?.isMultiParameter"
@update:fields="updateSubFlowFields"
/>
</template>
@@ -245,6 +277,7 @@
<ModelParamsForm
:model-request-params="selectedNode.data.modelConfig.modelRequestParams"
:upstream-nodes="upstreamNodes"
:multi="!!nodeConfig?.isMultiParameter"
@update:model-request-params="updateModelRequestParams"
/>
</template>
@@ -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<string, any> | 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[]) => {
@@ -10,6 +10,7 @@
:field-def="f"
:path="f.field"
:upstream-nodes="upstreamNodes"
:multi="multi"
/>
</template>
<el-empty v-else description="该工作流无可引入的开始参数" :image-size="60" />
@@ -26,6 +27,7 @@ defineOptions({ name: 'SubFlowParams' });
interface Props {
fields: SubFlowField[];
upstreamNodes?: any[];
multi?: boolean;
}
interface Emits {
@@ -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<string, { name: string; fields: Map<string, string> }>
): 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 }];
}
}
}
@@ -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;
}
+32 -11
View File
@@ -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<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 }] : undefined;
if (def.field === 'responseType') {
const expand = entry?.expand || [];
return {
@@ -1131,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 }] } : {}),
...(f.type === 'uploadMultiple'
? {
fieldConstraint: {
@@ -1169,7 +1177,7 @@ 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 }] } : {}),
};
});
};
@@ -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<string, { name: string; fields: Map<string, string> }>();
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 } : {}),
// 模型返回参数随工作流保存,保证重开后仍可被下游引用