1. 上游节点输出引用(核心功能)
- 新增 upstreamNodes computed:遍历当前选中节点的前驱链路,仅 model / http / form 三类节点被识别为有输出的节点
- 新增 getNodeOutputFields,按节点类型提取可引用字段:
- form → formConfig 的字段
- http → response schema 的叶子字段
- model → modelResponseBodyMapping(模型返回参数)的 key
- 开始节点 → runFormFields(运行表单字段),从而让下游节点能引用"用户填的运行表单"作为输出
2. 开始节点运行表单字段聚合
- syncRunFormFields + 深监听:开始节点 formConfig 变化时自动聚合字段,存为 runFormFields,作为开始节点自身可被下游引用的输出
3. 模型返回参数
- handleModelConfirm 用 stripReadonlyFields 清理 requestBodyMapping 冗余字段,同时保存 modelResponseBodyMapping 供下游引用
- 保存时 collectExposedFields 提取对外暴露字段存 modelFormFields
4. 提示词 / 反向提示词
- 保存 prompt、negativePrompt、runFormFields 到节点 DSL
5. 其他
- nodeTypes 用 markRaw 包裹 FlowNode,修复 Vue 警告
- buildNodeFormConfigFromDsl 的 responseType 匹配改为兼容 key/value 两种取值
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
<NodeConfigPanel
|
||||
:selected-node="selectedNode"
|
||||
:node-config="currentNodeConfig"
|
||||
:upstream-nodes="upstreamNodes"
|
||||
@update:selected-node="updateSelectedNode"
|
||||
@open-model-selector="showModelSelector = true"
|
||||
@remove-model="handleRemoveModel"
|
||||
@@ -82,7 +83,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { ref, computed, watch, onMounted, markRaw } from 'vue';
|
||||
import { VueFlow, useVueFlow } from '@vue-flow/core';
|
||||
import { Background } from '@vue-flow/background';
|
||||
import { Controls } from '@vue-flow/controls';
|
||||
@@ -98,6 +99,13 @@ import {
|
||||
} from '/@/api/settings/creation';
|
||||
import { getNodeLibraryList, type NodeLibraryGroup } from '/@/api/settings/workflow';
|
||||
import NodeConfigPanel from './component/NodeConfigPanel.vue';
|
||||
import {
|
||||
stripReadonlyFields,
|
||||
collectExposedFields,
|
||||
restoreRuntimeShow,
|
||||
isEqual,
|
||||
type ExposedField,
|
||||
} from './component/modelParamUtils';
|
||||
import NodeLibraryPanel from './component/NodeLibraryPanel.vue';
|
||||
import WorkflowListPanel from './component/WorkflowListPanel.vue';
|
||||
import SaveWorkflowDialog from './component/SaveWorkflowDialog.vue';
|
||||
@@ -105,26 +113,56 @@ import ModelSelector from './component/ModelSelector.vue';
|
||||
import SkillSelector from './component/SkillSelector.vue';
|
||||
import FlowNode from './component/FlowNode.vue';
|
||||
|
||||
// 开始节点「运行表单字段」:在暴露清单基础上标注来源节点
|
||||
interface RunFormField extends ExposedField {
|
||||
nodeId: string;
|
||||
nodeLabel: string;
|
||||
}
|
||||
|
||||
// 前驱节点的可引用输出字段(供模型参数「引用上级节点输出」)
|
||||
interface UpstreamField {
|
||||
field: string;
|
||||
label: string;
|
||||
}
|
||||
interface UpstreamNodeInfo {
|
||||
id: string;
|
||||
label: string;
|
||||
nodeCode: string;
|
||||
outputFields: UpstreamField[];
|
||||
}
|
||||
|
||||
interface NodeData {
|
||||
label?: string;
|
||||
nodeCode?: string;
|
||||
desc?: string;
|
||||
formConfig?: any[];
|
||||
modelConfig?: { modelId?: string; modelName?: string; modelType?: string | number; modelRequestParams?: any } | null;
|
||||
modelConfig?: {
|
||||
modelId?: string;
|
||||
modelName?: string;
|
||||
modelType?: string | number;
|
||||
modelRequestParams?: any;
|
||||
modelFormFields?: ExposedField[] | null;
|
||||
modelResponseBodyMapping?: any; // 模型返回参数(数组或对象两种结构),供下游引用
|
||||
} | null;
|
||||
skillName?: string;
|
||||
prompt?: string;
|
||||
negativePrompt?: string;
|
||||
patchLayout?: boolean;
|
||||
isSaveFile?: boolean;
|
||||
preTool?: string | null;
|
||||
runFormFields?: RunFormField[]; // 仅开始节点使用
|
||||
}
|
||||
|
||||
const { addNodes, addEdges, findNode, removeNodes, getNodes, updateNode } = useVueFlow();
|
||||
|
||||
// 常量定义
|
||||
const START_NODE_CODE = '__start__';
|
||||
// 有输出参数、可作为下游引用来源的节点类型(模型/HTTP/表单)
|
||||
const OUTPUT_NODE_CODES = ['model', 'http', 'form'];
|
||||
const JUDGE_KEYWORDS = ['判断', 'judge', 'condition', 'if', 'branch', 'gateway'];
|
||||
|
||||
// 自定义节点类型:默认节点内置 DefaultNode 只有上下两个 Handle,自定义组件支持左右连接
|
||||
const nodeTypes = { default: FlowNode, input: FlowNode };
|
||||
const nodeTypes = { default: markRaw(FlowNode), input: markRaw(FlowNode) };
|
||||
|
||||
// 节点库相关状态
|
||||
const nodeLibraryGroups = ref<NodeLibraryGroup[]>([]);
|
||||
@@ -140,6 +178,7 @@ const nodeConfigMap = computed(() => {
|
||||
formConfigOption: boolean;
|
||||
skillOption: boolean;
|
||||
promptOption: boolean;
|
||||
negativePromptOption: boolean;
|
||||
isSaveFileOption: boolean;
|
||||
}
|
||||
>();
|
||||
@@ -151,6 +190,7 @@ const nodeConfigMap = computed(() => {
|
||||
formConfigOption: item.formConfigOption || false,
|
||||
skillOption: item.skillOption || false,
|
||||
promptOption: item.promptOption || false,
|
||||
negativePromptOption: item.negativePromptOption || false,
|
||||
isSaveFileOption: item.isSaveFileOption || false,
|
||||
});
|
||||
});
|
||||
@@ -277,7 +317,11 @@ const handleModelConfirm = (model: any) => {
|
||||
modelId: model.id || '',
|
||||
modelName: model.modelName,
|
||||
modelType: model.modelType,
|
||||
modelRequestParams: null,
|
||||
// 深拷贝模型 requestBodyMapping 作为参数模板(重选模型时覆盖旧参数)
|
||||
// 剔除 isForm=false 的只读字段,使其不显示也不随工作流保存
|
||||
modelRequestParams: stripReadonlyFields(JSON.parse(JSON.stringify(model.requestBodyMapping ?? null))),
|
||||
// 保存模型返回参数(responseBodyMapping),作为该节点可被下游引用的输出项
|
||||
modelResponseBodyMapping: model.responseBodyMapping ?? null,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -396,6 +440,158 @@ const isStartNode = (node: Node<NodeData, any, string>) => {
|
||||
return node.data?.nodeCode === START_NODE_CODE;
|
||||
};
|
||||
|
||||
// 聚合所有非开始节点勾选的「表单展示」字段,同步到开始节点的运行表单字段
|
||||
const syncRunFormFields = () => {
|
||||
const startNode = nodes.value.find((n) => isStartNode(n));
|
||||
if (!startNode?.data) return;
|
||||
|
||||
const collected: RunFormField[] = [];
|
||||
for (const n of nodes.value) {
|
||||
if (isStartNode(n)) continue;
|
||||
const params = n.data?.modelConfig?.modelRequestParams;
|
||||
if (!params || typeof params !== 'object') continue;
|
||||
for (const f of collectExposedFields(params)) {
|
||||
collected.push({
|
||||
...f,
|
||||
nodeId: n.id,
|
||||
nodeLabel: n.data?.label || n.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 仅变更才写回,避免深监听死循环
|
||||
if (isEqual(collected, startNode.data.runFormFields)) return;
|
||||
startNode.data.runFormFields = collected;
|
||||
updateNode(startNode.id, startNode);
|
||||
const index = nodes.value.findIndex((n) => n.id === startNode.id);
|
||||
if (index >= 0) {
|
||||
nodes.value[index] = startNode;
|
||||
}
|
||||
};
|
||||
|
||||
// 节点变化(勾选/取消/删除/换模型)→ 自动重算开始节点运行表单字段
|
||||
watch(
|
||||
() => nodes.value,
|
||||
() => {
|
||||
syncRunFormFields();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 解析 schemaJson 值为 JSON 对象(兼容字符串/对象两种存储)
|
||||
const parseSchema = (value: any): any => {
|
||||
if (!value) return null;
|
||||
if (typeof value === 'object' && !Array.isArray(value)) return value;
|
||||
if (typeof value === 'string') {
|
||||
const t = value.trim();
|
||||
if (!t) return null;
|
||||
try {
|
||||
return JSON.parse(t);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 递归收集 JSON schema 的所有叶子字段(数组取首元素对象作为结构样本)
|
||||
const collectLeafFieldsFromJson = (obj: any, prefix = ''): UpstreamField[] => {
|
||||
const result: UpstreamField[] = [];
|
||||
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return result;
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (!key || key.startsWith('_temp_')) continue;
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
const val = obj[key];
|
||||
if (val && typeof val === 'object') {
|
||||
if (Array.isArray(val)) {
|
||||
const sample = val.find((it: any) => it && typeof it === 'object' && !Array.isArray(it));
|
||||
if (sample) result.push(...collectLeafFieldsFromJson(sample, path));
|
||||
else result.push({ field: path, label: path });
|
||||
} else {
|
||||
result.push(...collectLeafFieldsFromJson(val, path));
|
||||
}
|
||||
} else {
|
||||
result.push({ field: path, label: path });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// 取节点在设计时的可引用输出字段(运行时这些字段会产出实际值)
|
||||
const getNodeOutputFields = (node: Node<NodeData, any, string>): UpstreamField[] => {
|
||||
const nodeCode = node.data?.nodeCode || '';
|
||||
if (nodeCode === 'form') {
|
||||
// form 节点:自定义表单字段即输出
|
||||
return (node.data?.formConfig || []).map((f: any) => ({
|
||||
field: f.field || f.label || '',
|
||||
label: f.label || f.field || '',
|
||||
}));
|
||||
}
|
||||
if (nodeCode === 'http') {
|
||||
// http 节点:输出 = 结果返回结构(response schema)的所有叶子字段。
|
||||
// 结果返回方式为主动拉取(responseType === 'pull')时,取主动拉取分支下配置的结果返回结构。
|
||||
const formConfig = node.data?.formConfig || [];
|
||||
const responseTypeEntry = formConfig.find((f: any) => f.field === 'responseType');
|
||||
const isPull = !!responseTypeEntry && String(responseTypeEntry.value || '') === 'pull';
|
||||
const responseField = isPull
|
||||
? (responseTypeEntry?.expand || []).find((e: any) => e.field === 'response')
|
||||
: formConfig.find((f: any) => f.field === 'response');
|
||||
const schema = parseSchema(responseField?.value);
|
||||
if (schema) return collectLeafFieldsFromJson(schema);
|
||||
return [];
|
||||
}
|
||||
if (nodeCode === 'model') {
|
||||
// 模型节点:模型的返回参数(responseBodyMapping)即可被下游引用的输出
|
||||
const resp = node.data?.modelConfig?.modelResponseBodyMapping;
|
||||
const keys = Array.isArray(resp) ? resp : resp && typeof resp === 'object' ? Object.keys(resp) : [];
|
||||
return keys
|
||||
.filter((k: any) => typeof k === 'string' && k.trim() !== '')
|
||||
.map((k: string) => ({ field: k, label: k }));
|
||||
}
|
||||
if (nodeCode === START_NODE_CODE) {
|
||||
// 开始节点:运行表单字段(被勾选的模型参数)
|
||||
return (node.data?.runFormFields || []).map((f: any) => ({ field: f.path || '', label: f.label || f.path || '' }));
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// 当前选中节点的所有前驱链路节点(含各自可引用输出字段)
|
||||
const upstreamNodes = computed<UpstreamNodeInfo[]>(() => {
|
||||
const nodeId = selectedNode.value?.id;
|
||||
if (!nodeId) return [];
|
||||
|
||||
const predsByTarget = new Map<string, string[]>();
|
||||
edges.value.forEach((e) => {
|
||||
if (!predsByTarget.has(e.target)) predsByTarget.set(e.target, []);
|
||||
predsByTarget.get(e.target)!.push(e.source);
|
||||
});
|
||||
|
||||
const result: UpstreamNodeInfo[] = [];
|
||||
const visited = new Set<string>([nodeId]);
|
||||
const queue: string[] = [nodeId];
|
||||
while (queue.length) {
|
||||
const cur = queue.shift()!;
|
||||
for (const p of predsByTarget.get(cur) || []) {
|
||||
if (visited.has(p)) continue;
|
||||
visited.add(p);
|
||||
const n = nodes.value.find((x) => x.id === p);
|
||||
if (n) {
|
||||
// 只有模型/HTTP/表单节点有输出参数,可作为下游引用来源
|
||||
if (OUTPUT_NODE_CODES.includes(n.data?.nodeCode || '')) {
|
||||
result.push({
|
||||
id: n.id,
|
||||
label: n.data?.label || n.id,
|
||||
nodeCode: n.data?.nodeCode || '',
|
||||
outputFields: getNodeOutputFields(n),
|
||||
});
|
||||
}
|
||||
}
|
||||
queue.push(p);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
// 辅助函数:判断是否为判断节点
|
||||
const isJudgeNode = (node: Node<NodeData, any, string>) => {
|
||||
const nodeCode = (node.data?.nodeCode || '').toLowerCase();
|
||||
@@ -505,7 +701,7 @@ const addDefaultStartNode = () => {
|
||||
id: 'start-node',
|
||||
type: 'input',
|
||||
position: { x: 200, y: 200 },
|
||||
data: { label: '开始', nodeCode: '__start__' },
|
||||
data: { label: '开始', nodeCode: '__start__', runFormFields: [] },
|
||||
};
|
||||
addNodes([startNode]);
|
||||
nodes.value.push(startNode);
|
||||
@@ -587,7 +783,7 @@ const buildNodeFormConfigFromDsl = (n: any) => {
|
||||
if (def.field === 'responseType') {
|
||||
// 恢复嵌套配置:定义取自 presetOption 选中选项的 config,值取自 options[0].config
|
||||
const savedConfig = out?.options?.[0]?.config || [];
|
||||
const selectedOpt = (def.options || []).find((o: any) => o.value === out?.value);
|
||||
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);
|
||||
@@ -650,6 +846,23 @@ const buildOutputConfig = (node: Node<NodeData>) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
// 从 DSL 构建模型配置:剔除只读字段 + 用 modelFormFields 还原勾选(幂等,兼容旧 DSL)
|
||||
const buildModelConfigFromDsl = (n: any) => {
|
||||
const modelRequestParams = stripReadonlyFields(n.modelConfig?.modelRequestParams ?? null);
|
||||
const modelFormFields = n.modelConfig?.modelFormFields ?? null;
|
||||
if (modelRequestParams && typeof modelRequestParams === 'object' && Array.isArray(modelFormFields)) {
|
||||
restoreRuntimeShow(modelRequestParams, modelFormFields);
|
||||
}
|
||||
return {
|
||||
modelId: n.modelConfig?.modelId || '',
|
||||
modelName: '',
|
||||
modelType: undefined,
|
||||
modelRequestParams,
|
||||
modelFormFields,
|
||||
modelResponseBodyMapping: n.modelConfig?.modelResponseBodyMapping ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
// 从 DSL 加载工作流
|
||||
const loadWorkflowFromDsl = (dsl: any) => {
|
||||
if (!dsl) return;
|
||||
@@ -666,16 +879,14 @@ const loadWorkflowFromDsl = (dsl: any) => {
|
||||
nodeCode: n.nodeCode,
|
||||
desc: n.desc || '',
|
||||
formConfig: buildNodeFormConfigFromDsl(n),
|
||||
modelConfig: {
|
||||
modelId: n.modelConfig?.modelId || '',
|
||||
modelName: '',
|
||||
modelType: undefined,
|
||||
modelRequestParams: n.modelConfig?.modelRequestParams ?? null,
|
||||
},
|
||||
modelConfig: buildModelConfigFromDsl(n),
|
||||
skillName: n.skillName || null,
|
||||
prompt: n.prompt || '',
|
||||
negativePrompt: n.negativePrompt || '',
|
||||
patchLayout: n.patchLayout || false,
|
||||
isSaveFile: Boolean(n.isSaveFile),
|
||||
preTool: n.preTool ?? null,
|
||||
...(isStart ? { runFormFields: Array.isArray(n.runFormFields) ? n.runFormFields : [] } : {}),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -755,10 +966,19 @@ const confirmSaveWorkflow = async () => {
|
||||
modelConfig: {
|
||||
modelId: n.data?.modelConfig?.modelId || '',
|
||||
modelRequestParams: n.data?.modelConfig?.modelRequestParams ?? null,
|
||||
// 保存时实时收集勾选的「表单展示」字段(路径带实例索引)
|
||||
...(n.data?.modelConfig?.modelRequestParams
|
||||
? { modelFormFields: collectExposedFields(n.data.modelConfig.modelRequestParams) }
|
||||
: {}),
|
||||
// 模型返回参数随工作流保存,保证重开后仍可被下游引用
|
||||
modelResponseBodyMapping: n.data?.modelConfig?.modelResponseBodyMapping ?? null,
|
||||
},
|
||||
outputConfig: buildOutputConfig(n),
|
||||
...(n.data?.skillName ? { skillName: n.data.skillName } : {}),
|
||||
...(n.data?.prompt ? { prompt: n.data.prompt } : {}),
|
||||
...(n.data?.negativePrompt ? { negativePrompt: n.data.negativePrompt } : {}),
|
||||
...(n.data?.patchLayout ? { patchLayout: n.data.patchLayout } : {}),
|
||||
...(isStartNode(n) ? { runFormFields: n.data?.runFormFields ?? [] } : {}),
|
||||
outputResult: null,
|
||||
};
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user