首页调试工作流管理

This commit is contained in:
2026-08-13 18:02:35 +08:00
parent 2ae59c9252
commit 8e7948cff5
8 changed files with 967 additions and 135 deletions
+90 -33
View File
@@ -74,8 +74,13 @@
@confirm="confirmSaveWorkflow"
/>
<!-- 模型选择器 -->
<ModelSelector v-model="showModelSelector" :default-model="selectedModelData" @confirm="handleModelConfirm" />
<!-- 模型选择器管理员绘制模板时选系统模型无需填 API Key直接选中 -->
<ModelSelector
v-model="showModelSelector"
:default-model="selectedModelData"
:system-model-require-key="!isSuperAdmin"
@confirm="handleModelConfirm"
/>
<!-- 技能选择器 -->
<SkillSelector v-model="showSkillSelector" :default-skill="selectedSkillData" @confirm="handleSkillConfirm" />
@@ -97,6 +102,7 @@ import {
deleteWorkflow,
type WorkflowItem,
} from '/@/api/settings/creation';
import { checkIsSuperAdmin } from '/@/api/system/user';
import { getNodeLibraryList, type NodeLibraryGroup } from '/@/api/settings/workflow';
import NodeConfigPanel from './component/NodeConfigPanel.vue';
import {
@@ -226,6 +232,8 @@ let nodeId = 0;
// 模型选择器相关状态
const showModelSelector = ref(false);
const selectedModelData = ref<any>(null);
// 当前用户是否为超级管理员(管理员绘制/编辑模板工作流时选系统模型无需填 API Key)
const isSuperAdmin = ref(false);
// 技能选择器相关状态
const showSkillSelector = ref(false);
@@ -270,9 +278,10 @@ const onConnect = (connection: Connection) => {
return;
}
// id 含 source/target/handle,同一对节点间不同 handle 的连线也互不冲突
addEdges([
{
id: `edge-${connection.source}-${connection.target}`,
id: `edge-${connection.source}-${connection.sourceHandle || 'source-bottom'}-${connection.target}-${connection.targetHandle || 'target-top'}`,
source: connection.source,
target: connection.target,
sourceHandle: connection.sourceHandle,
@@ -695,28 +704,33 @@ const fetchWorkflowList = async () => {
}
};
// 添加默认开始节点
// 构造默认开始节点(固定 id,供新建/清空画布后恢复开始节点)
const createStartNode = () => ({
id: 'start-node',
type: 'input',
position: { x: 200, y: 200 },
data: { label: '开始', nodeCode: '__start__', runFormFields: [] },
});
// 添加默认开始节点(onMounted 首次初始化用 addNodes,此时画布为空、无 v-model watch 竞争)
const addDefaultStartNode = () => {
const startNode = {
id: 'start-node',
type: 'input',
position: { x: 200, y: 200 },
data: { label: '开始', nodeCode: '__start__', runFormFields: [] },
};
addNodes([startNode]);
nodes.value.push(startNode);
addNodes([createStartNode()]);
};
// 新建工作流
const createNewWorkflow = () => {
currentEditingWorkflowId.value = null;
nodes.value = [];
edges.value = [];
selectedNode.value = null;
nodeId = 0;
// 添加默认开始节点
addDefaultStartNode();
// 重置保存表单,避免新建后点保存还带着上次编辑/未保存的名称描述
saveForm.value.flowName = '';
saveForm.value.description = '';
// 直接整体赋值 v-modelnodes/edges),不再混用 removeNodes/addNodes 直接改 store
// 避免 VueFlow 双向 watch(外部↔store)竞争导致开始节点丢失
nodes.value = [createStartNode()];
edges.value = [];
ElMessage.success('已清空画布,可以开始创建新工作流');
};
@@ -773,7 +787,7 @@ const buildNodeFormConfigFromDsl = (n: any) => {
});
}
// 新格式:HTTP 节点 outputConfig 为 {type,field,label,value} 数组
if (Array.isArray(n.outputConfig) && n.outputConfig.length > 0) {
if (Array.isArray(n.outputConfig) && n.outputConfig.length > 0 && n.nodeCode !== '__start__') {
const defs = nodeConfigMap.value.get(n.nodeCode)?.formConfig || [];
// 无 presetOption 定义(如 form 节点为命名值数组)时直接透传
if (defs.length === 0) return n.outputConfig;
@@ -799,7 +813,7 @@ const buildNodeFormConfigFromDsl = (n: any) => {
return null;
};
// 构建保存用的 outputConfigHTTP/form 节点),其余返回 null
// 构建保存用的 outputConfigHTTP/form 节点),其余返回 null(开始节点的运行表单字段由保存处单独写入)
const buildOutputConfig = (node: Node<NodeData>) => {
const nodeCode = node.data?.nodeCode || '';
if (nodeCode === 'http') {
@@ -855,14 +869,26 @@ const buildModelConfigFromDsl = (n: any) => {
}
return {
modelId: n.modelConfig?.modelId || '',
modelName: '',
modelType: undefined,
// 回读 DSL 中保存的模型名称/类型(旧 DSL 无这些字段时为空,兼容)
modelName: n.modelConfig?.modelName || '',
modelType: n.modelConfig?.modelType ?? undefined,
modelRequestParams,
modelFormFields,
modelResponseBodyMapping: n.modelConfig?.modelResponseBodyMapping ?? null,
};
};
// 从 edge id 反向解析 handleedge id 形如 edge-{source}-{sourceHandle}-{target}-{targetHandle}
// 后端保存时可能丢弃 sourceHandle/targetHandle,用 id 兜底还原,保证连线回显位置正确。
const parseEdgeHandleFromId = (id: string) => {
const sourceMatch = id.match(/^edge-.+-source-(right|bottom)-.+$/);
const targetMatch = id.match(/-target-(top|left)$/);
return {
sourceHandle: sourceMatch ? `source-${sourceMatch[1]}` : undefined,
targetHandle: targetMatch ? `target-${targetMatch[1]}` : undefined,
};
};
// 从 DSL 加载工作流
const loadWorkflowFromDsl = (dsl: any) => {
if (!dsl) return;
@@ -886,21 +912,33 @@ const loadWorkflowFromDsl = (dsl: any) => {
patchLayout: n.patchLayout || false,
isSaveFile: Boolean(n.isSaveFile),
preTool: n.preTool ?? null,
...(isStart ? { runFormFields: Array.isArray(n.runFormFields) ? n.runFormFields : [] } : {}),
// 开始节点运行表单字段:新格式存 outputConfig;旧 DSL 顶层 runFormFields 兜底兼容
...(isStart
? {
runFormFields: Array.isArray(n.outputConfig)
? n.outputConfig
: Array.isArray(n.runFormFields)
? n.runFormFields
: [],
}
: {}),
},
};
});
const loadedEdges = (dsl.edges || []).map((e: any) => ({
id: e.id,
source: e.from,
target: e.to,
sourceHandle: e.sourceHandle || 'source-bottom',
targetHandle: e.targetHandle || 'target-top',
type: 'smoothstep',
animated: true,
style: { stroke: '#3b82f6', strokeWidth: 2 },
}));
const loadedEdges = (dsl.edges || []).map((e: any) => {
const fromId = parseEdgeHandleFromId(e.id || '');
return {
id: e.id,
source: e.from,
target: e.to,
sourceHandle: e.sourceHandle || fromId.sourceHandle || 'source-bottom',
targetHandle: e.targetHandle || fromId.targetHandle || 'target-top',
type: 'smoothstep',
animated: true,
style: { stroke: '#3b82f6', strokeWidth: 2 },
};
});
nodes.value = loadedNodes;
edges.value = loadedEdges;
@@ -965,6 +1003,11 @@ const confirmSaveWorkflow = async () => {
subConfig: null,
modelConfig: {
modelId: n.data?.modelConfig?.modelId || '',
// 模型名称/类型随工作流保存,供首页补全弹窗展示模型名与「同类型模型」过滤
...(n.data?.modelConfig?.modelName ? { modelName: n.data.modelConfig.modelName } : {}),
...(n.data?.modelConfig?.modelType !== undefined && n.data.modelConfig.modelType !== null && n.data.modelConfig.modelType !== ''
? { modelType: n.data.modelConfig.modelType }
: {}),
modelRequestParams: n.data?.modelConfig?.modelRequestParams ?? null,
// 保存时实时收集勾选的「表单展示」字段(路径带实例索引)
...(n.data?.modelConfig?.modelRequestParams
@@ -973,12 +1016,12 @@ const confirmSaveWorkflow = async () => {
// 模型返回参数随工作流保存,保证重开后仍可被下游引用
modelResponseBodyMapping: n.data?.modelConfig?.modelResponseBodyMapping ?? null,
},
outputConfig: buildOutputConfig(n),
// 开始节点:运行表单字段存入 outputConfig(不再单独存顶层 runFormFields 字段)
outputConfig: isStartNode(n) ? (n.data?.runFormFields ?? []) : 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,
};
}),
@@ -1037,6 +1080,12 @@ const deleteWorkflowAction = async (workflow: WorkflowItem) => {
currentEditingWorkflowId.value = null;
saveForm.value.flowName = '';
saveForm.value.description = '';
// 删除的是当前编辑中的工作流时,恢复画布到默认开始节点,避免旧节点/连线残留。
// 整体赋值 v-model,与 createNewWorkflow 一致,避免直接改 store 触发 watch 竞争
selectedNode.value = null;
nodeId = 0;
nodes.value = [createStartNode()];
edges.value = [];
}
await fetchWorkflowList();
@@ -1052,6 +1101,14 @@ onMounted(async () => {
await getNodeLibrary();
await fetchWorkflowList();
// 获取当前用户是否为超级管理员(管理员绘制模板时选系统模型无需填 API Key)
try {
const res: any = await checkIsSuperAdmin();
isSuperAdmin.value = res.data?.isSuperAdmin || false;
} catch {
isSuperAdmin.value = false;
}
// 添加默认开始节点(固定在页面中间偏左)
addDefaultStartNode();
nodeId = 0;