From 8892c30b557f785940614fd29017a245f5190f3b Mon Sep 17 00:00:00 2001 From: Ethan-Zhang Date: Tue, 5 Aug 2025 12:27:51 +0800 Subject: [PATCH 1/2] =?UTF-8?q?Feat:=20=E5=A2=9E=E5=8A=A0=E5=BE=AA?= =?UTF-8?q?=E7=8E=AF=E8=8A=82=E7=82=B9&=E6=8F=92=E5=85=A5=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=8A=9F=E8=83=BD=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main/common/platform.ts | 2 +- src/apis/paths/knowledge.ts | 2 +- src/assets/svgs/Refresh.svg | 1 + src/assets/svgs/StopFilled.svg | 1 + .../dialoguePanel/hooks/useMarkdownParser.ts | 2 +- .../createapp/components/CustomControl.vue | 15 +- src/views/createapp/components/types.ts | 22 + src/views/createapp/components/workFlow.vue | 1064 +++- .../workFlowConfig/ChoiceBranchCard.vue | 55 +- .../workFlowConfig/ChoiceBranchDrawer.vue | 223 +- .../workFlowConfig/ChoiceBranchNode.vue | 175 +- .../components/workFlowConfig/CustomEdge.vue | 39 +- .../components/workFlowConfig/CustomNode.vue | 117 +- .../workFlowConfig/CustomSaENode.vue | 114 +- .../components/workFlowConfig/LoopNode.vue | 4507 +++++++++++++++++ .../workFlowConfig/LoopNodeDrawer.vue | 1145 +++++ .../workFlowConfig/NodeListPanel.vue | 58 +- .../workFlowConfig/insertNodeMenu.vue | 10 +- .../components/workFlowConfig/useDnD.js | 40 +- src/views/styles/createApp.scss | 2 +- 20 files changed, 7257 insertions(+), 337 deletions(-) create mode 100644 src/assets/svgs/Refresh.svg create mode 100644 src/assets/svgs/StopFilled.svg create mode 100644 src/views/createapp/components/workFlowConfig/LoopNode.vue create mode 100644 src/views/createapp/components/workFlowConfig/LoopNodeDrawer.vue diff --git a/electron/main/common/platform.ts b/electron/main/common/platform.ts index 1d499b4..1cd6acc 100644 --- a/electron/main/common/platform.ts +++ b/electron/main/common/platform.ts @@ -42,7 +42,7 @@ export interface INodeProcess { cwd: () => string; } -let nodeProcess: INodeProcess | undefined = process; +const nodeProcess: INodeProcess | undefined = process; const isElectronProcess = typeof nodeProcess?.versions?.electron === 'string'; diff --git a/src/apis/paths/knowledge.ts b/src/apis/paths/knowledge.ts index 14d9320..b5ccbe2 100644 --- a/src/apis/paths/knowledge.ts +++ b/src/apis/paths/knowledge.ts @@ -21,7 +21,7 @@ export const updateKnowledgeList = ({ kb_ids: string[]; conversationId: string; }): Promise<[any, FcResponse<{}> | undefined]> => { - let kbIds = kb_ids; + const kbIds = kb_ids; return put('/api/knowledge', { kbIds }, { conversationId }); }; diff --git a/src/assets/svgs/Refresh.svg b/src/assets/svgs/Refresh.svg new file mode 100644 index 0000000..092d34d --- /dev/null +++ b/src/assets/svgs/Refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/svgs/StopFilled.svg b/src/assets/svgs/StopFilled.svg new file mode 100644 index 0000000..9533802 --- /dev/null +++ b/src/assets/svgs/StopFilled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/dialoguePanel/hooks/useMarkdownParser.ts b/src/components/dialoguePanel/hooks/useMarkdownParser.ts index a444b93..9bb8158 100644 --- a/src/components/dialoguePanel/hooks/useMarkdownParser.ts +++ b/src/components/dialoguePanel/hooks/useMarkdownParser.ts @@ -22,7 +22,7 @@ export function useMarkdownParser( }, ); //将table提取出来中加一个
父节点控制溢出 - let tableStart = str.indexOf(''); + const tableStart = str.indexOf('
'); if (tableStart !== -1) { str = str.slice(0, tableStart) + diff --git a/src/views/createapp/components/CustomControl.vue b/src/views/createapp/components/CustomControl.vue index 06fd8ba..89dc8a5 100644 --- a/src/views/createapp/components/CustomControl.vue +++ b/src/views/createapp/components/CustomControl.vue @@ -92,8 +92,21 @@ const handleVisibleChange = (visible) => { watch( () => props.flowZoom, - () => { + (newZoom, oldZoom) => { + // 添加调试信息 + console.log('[CustomControl] flowZoom变化:', { + oldZoom: oldZoom, + newZoom: newZoom, + oldDisplayValue: zommChangeValue.value, + timestamp: Date.now() + }); + zommChangeValue.value = props.flowZoom; + + console.log('[CustomControl] 显示值更新后:', { + displayValue: zommChangeValue.value, + displayPercent: `${Number((zommChangeValue.value * 100).toFixed(0))}%` + }); }, ); diff --git a/src/views/createapp/components/types.ts b/src/views/createapp/components/types.ts index 425721a..c15c6c3 100644 --- a/src/views/createapp/components/types.ts +++ b/src/views/createapp/components/types.ts @@ -31,6 +31,9 @@ import CODE from '@/assets/svgs/userCode.svg'; import USER_CODE from '@/assets/svgs/userCode.svg'; import USER_DATABASE_CLASS from '@/assets/svgs/userDatabaseClass.svg'; import USER_DOCUMENT_CLASS from '@/assets/svgs/userDatabaseClass.svg'; +// 引入图片--循环控制相关图标 +import REFRESH from '@/assets/svgs/Refresh.svg'; +import STOP_FILLED from '@/assets/svgs/StopFilled.svg'; // 工具类型 export type LinkType = 'redirect' | 'action'; @@ -75,6 +78,12 @@ export const nodeTypeToIcon = { USER_CODE, USER_DATABASE_CLASS, USER_DOCUMENT_CLASS, + + // 循环控制相关图标 + REFRESH, + STOP_FILLED, + continue: REFRESH, + break: STOP_FILLED, }; // 这里是对应的图标 @@ -92,6 +101,19 @@ export const iconTypeList = [ icon: get_CVE_DETAIL, class: 'aposNode', }, + // 循环控制节点 + { + name: '跳过本轮', + value: 'continue', + icon: REFRESH, + class: 'systemNode', + }, + { + name: '退出循环', + value: 'break', + icon: STOP_FILLED, + class: 'systemNode', + }, ]; // 根据类型获取类名 diff --git a/src/views/createapp/components/workFlow.vue b/src/views/createapp/components/workFlow.vue index f4a9d9c..7b9a547 100644 --- a/src/views/createapp/components/workFlow.vue +++ b/src/views/createapp/components/workFlow.vue @@ -8,6 +8,8 @@ import { Background } from '@vue-flow/background'; import { MiniMap } from '@vue-flow/minimap'; import BranchNode from './workFlowConfig/BranchNode.vue'; import ChoiceBranchNode from './workFlowConfig/ChoiceBranchNode.vue'; +import LoopNode from './workFlowConfig/LoopNode.vue'; +import LoopNodeDrawer from './workFlowConfig/LoopNodeDrawer.vue'; import CustomEdge from './workFlowConfig/CustomEdge.vue'; import CustomNode from './workFlowConfig/CustomNode.vue'; import CustomControl from './CustomControl.vue'; @@ -58,17 +60,23 @@ const isEditStartNode = ref(false); const isEditCodeNode = ref(false); const isEditDirectReplyNode = ref(false); const isEditChoiceBranchNode = ref(false); +const isEditLoopNode = ref(false); const isEditEnvironmentVariables = ref(false); const nodeName = ref(''); const nodeDesc = ref(''); const currentCodeNodeData = ref({}); const currentDirectReplyNodeData = ref({}); const currentChoiceBranchNodeData = ref({}); +const currentLoopNodeData = ref({}); const flowZoom = ref(1); const debugDialogVisible = ref(false); const apiServiceList = ref([]); const allApiServiceList = ref([]); const yamlContent = ref(); + +// LoopNode InsertNodeMenu相关状态 +const isLoopInsertNodeMenuVisible = ref(false); +const loopInsertMenuData = ref(null); const nodeYamlId = ref(); const emits = defineEmits(['updateFlowsDebug']); const route = useRoute(); @@ -211,7 +219,6 @@ const handleVariablesUpdated = async () => { // 加载对话变量用于开始节点展示 const loadConversationVariablesForDisplay = async () => { if (!flowObj.value?.flowId) { - console.warn('没有flowId,跳过变量加载'); return; } @@ -244,7 +251,6 @@ const loadConversationVariablesForDisplay = async () => { } } catch (error) { - console.error('❌ 加载对话变量失败:', error); ElMessage.error('加载对话变量失败'); } finally { variablesLoading.value = false; @@ -290,6 +296,13 @@ const nodeAndLineConnection = () => { }; // 编辑yaml const editYamlDrawer = (name, desc, yamlCode, nodeId) => { + // 先重置所有抽屉状态,确保不会同时显示多个抽屉 + isEditYaml.value = false; + isEditCodeNode.value = false; + isEditDirectReplyNode.value = false; + isEditChoiceBranchNode.value = false; + isEditLoopNode.value = false; + // 查找当前节点 const currentNode = findNode(nodeId); @@ -316,6 +329,10 @@ const editYamlDrawer = (name, desc, yamlCode, nodeId) => { nodeYamlId.value = nodeId; selectedNodeId.value = nodeId; isEditCodeNode.value = true; + + // 编辑时,需要debug 后才可发布 + emits('updateFlowsDebug', false); + return; } else if (currentNode && currentNode.data.callId === 'DirectReply') { // 打开直接回复节点编辑器 currentDirectReplyNodeData.value = { @@ -332,6 +349,10 @@ const editYamlDrawer = (name, desc, yamlCode, nodeId) => { nodeYamlId.value = nodeId; selectedNodeId.value = nodeId; isEditDirectReplyNode.value = true; + + // 编辑时,需要debug 后才可发布 + emits('updateFlowsDebug', false); + return; } else if (currentNode && currentNode.data.callId === 'Choice') { // 打开条件分支节点编辑器 currentChoiceBranchNodeData.value = { @@ -351,6 +372,14 @@ const editYamlDrawer = (name, desc, yamlCode, nodeId) => { nodeYamlId.value = nodeId; selectedNodeId.value = nodeId; isEditChoiceBranchNode.value = true; + + // 编辑时,需要debug 后才可发布 + emits('updateFlowsDebug', false); + return; // 重要:直接返回,避免继续执行else分支 + } else if (currentNode && currentNode.data.callId === 'Loop') { + // 打开Loop节点编辑器 - 直接调用editLoopNode方法 + editLoopNode(name, desc, currentNode.data.parameters, nodeId); + return; // 直接返回,不需要继续执行 } else { // 打开YAML编辑器(其他节点类型) yamlContent.value = yamlCode; @@ -443,6 +472,216 @@ const saveChoiceBranchNode = (nodeData, nodeId) => { ElMessage.success('条件分支节点保存成功'); }; +// 编辑Loop节点 +const editLoopNode = (name, desc, parameters, nodeId) => { + // 查找当前节点 + const currentNode = findNode(nodeId); + + if (currentNode && currentNode.data.callId === 'Loop') { + // 打开Loop节点编辑器 + currentLoopNodeData.value = { + name: currentNode.data.name, + description: currentNode.data.description, + callId: currentNode.data.callId, + parameters: currentNode.data.parameters || { + input_parameters: { + variables: {}, + stop_condition: { + logic: 'and', + conditions: [] + }, + max_iteration: 10, + sub_flow_id: '' + }, + output_parameters: {} + } + }; + nodeYamlId.value = nodeId; + selectedNodeId.value = nodeId; + isEditLoopNode.value = true; + } + + // 编辑时,需要debug 后才可发布 + emits('updateFlowsDebug', false); +}; + +// 关闭Loop节点抽屉 +const closeLoopNodeDrawer = () => { + isEditLoopNode.value = false; + selectedNodeId.value = ''; + currentLoopNodeData.value = {}; +}; + +// 保存Loop节点 +const saveLoopNode = (nodeData) => { + // 更新节点数据 + const updateNodeParameter = { + id: nodeYamlId.value, + ...nodeData, + }; + + // 调用保存接口 + saveFlow(updateNodeParameter); + + // 关闭抽屉 + closeLoopNodeDrawer(); + + ElMessage.success('循环节点保存成功'); +}; + +// 编辑子工作流节点 - 处理来自LoopNode的子节点编辑事件 +const editSubFlowNode = (nodeName, nodeDesc, nodeParameters, nodeId, loopNodeId) => { + // 根据nodeParameters中的callId来确定节点类型并打开对应的drawer + const callId = nodeParameters?.callId; + + // 先重置所有抽屉状态 + isEditYaml.value = false; + isEditCodeNode.value = false; + isEditDirectReplyNode.value = false; + isEditChoiceBranchNode.value = false; + isEditLoopNode.value = false; + + switch (callId) { + case 'Code': + // 打开代码节点编辑器 + currentCodeNodeData.value = { + name: nodeName, + description: nodeDesc, + callId: callId, + + // 代码节点特有属性 + code: nodeParameters?.code || '', + codeType: nodeParameters?.codeType || 'python', + securityLevel: nodeParameters?.securityLevel || 'low', + timeoutSeconds: nodeParameters?.timeoutSeconds || 30, + memoryLimitMb: nodeParameters?.memoryLimitMb || 128, + cpuLimit: nodeParameters?.cpuLimit || 0.5, + + // 用户定义的输入输出参数 + input_parameters: nodeParameters?.input_parameters || {}, + output_parameters: nodeParameters?.output_parameters || {}, + }; + nodeYamlId.value = nodeId; + selectedNodeId.value = nodeId; + isEditCodeNode.value = true; + break; + + case 'DirectReply': + // 打开直接回复节点编辑器 + currentDirectReplyNodeData.value = { + name: nodeName, + description: nodeDesc, + callId: callId, + parameters: { + input_parameters: { + answer: nodeParameters?.input_parameters?.answer || '' + }, + output_parameters: nodeParameters?.output_parameters || {} + } + }; + nodeYamlId.value = nodeId; + selectedNodeId.value = nodeId; + isEditDirectReplyNode.value = true; + break; + + case 'Choice': + // 打开条件分支节点编辑器 + currentChoiceBranchNodeData.value = { + name: nodeName, + description: nodeDesc, + callId: callId, + parameters: nodeParameters || { + input_parameters: { choices: [] }, + output_parameters: { + branch_id: { + type: 'string', + description: '选中的分支ID' + } + } + } + }; + nodeYamlId.value = nodeId; + selectedNodeId.value = nodeId; + isEditChoiceBranchNode.value = true; + break; + + case 'Loop': + // 打开循环节点编辑器 + currentLoopNodeData.value = { + name: nodeName, + description: nodeDesc, + callId: callId, + parameters: nodeParameters || { + input_parameters: { + variables: {}, + stop_condition: { + logic: 'and', + conditions: [] + }, + max_iteration: 10, + sub_flow_id: '' + }, + output_parameters: {} + } + }; + nodeYamlId.value = nodeId; + selectedNodeId.value = nodeId; + isEditLoopNode.value = true; + break; + + default: + // 其他节点类型使用YAML编辑器 + editYamlDrawer(nodeName, nodeDesc, nodeParameters, nodeId); + return; + } + + // 编辑时,需要debug 后才可发布 + emits('updateFlowsDebug', false); +}; + +// 处理来自LoopNode的InsertNodeMenu显示请求 +const handleShowLoopInsertNodeMenu = (insertMenuData) => { + // 保存菜单数据 + loopInsertMenuData.value = insertMenuData; + + // 显示菜单 + isLoopInsertNodeMenuVisible.value = true; +}; + +// 关闭LoopNode InsertNodeMenu +const closeLoopInsertNodeMenu = () => { + isLoopInsertNodeMenuVisible.value = false; + loopInsertMenuData.value = null; +}; + +// 处理LoopNode InsertNodeMenu选择节点 +const handleLoopInsertNodeSelect = (nodeData) => { + + if (!loopInsertMenuData.value) { + return; + } + + // 通过LoopNode组件引用调用节点插入方法 + const loopComponentKey = `loopNode_${loopInsertMenuData.value.loopNodeId}`; + const loopRef = (window as any).loopNodeRefs?.[loopComponentKey]; + + console.log('[workFlow] 调用LoopNode插入方法:', { + loopComponentKey: loopComponentKey, + loopRef: !!loopRef, + edgeInfo: loopInsertMenuData.value.edgeInfo, + nodeData: nodeData + }); + + if (loopRef && loopRef.insertNodeIntoSubFlow) { + loopRef.insertNodeIntoSubFlow(nodeData, loopInsertMenuData.value.edgeInfo); + } else { + console.error('[workFlow] 未找到LoopNode引用或insertNodeIntoSubFlow方法'); + } + + // 关闭菜单 + closeLoopInsertNodeMenu(); +}; + // 打开环境变量配置 const openEnvironmentVariables = () => { if (debugDialogVisible.value) { @@ -500,7 +739,9 @@ const handleSaveNodeDescription = (nodeInfo) => { const handleZommOnScroll = () => { const zoomObj = getViewport(); - flowZoom.value = Number(zoomObj.zoom.toFixed(2)); + const newZoom = Number(zoomObj.zoom.toFixed(2)); + + flowZoom.value = newZoom; }; async function layoutGraph(direction) { @@ -525,6 +766,12 @@ const dropFunc = (e) => { onDrop(e); }; +// LoopNode滚轮事件处理函数 +const handleLoopNodeZoomUpdate = (event: CustomEvent) => { + // 手动调用handleZommOnScroll来更新zoom值 + handleZommOnScroll(); +}; + onMounted(() => { apiLoading.value = true; api @@ -534,18 +781,24 @@ onMounted(() => { }) .then((res) => { const services = res[1]?.result.services || []; - + apiServiceList.value = services; allApiServiceList.value = services; apiLoading.value = false; }); handleChangeZoom(DefaultViewPortZoom); + + // 添加LoopNode事件监听器作为备用方案 + document.addEventListener('loopNodeZoomUpdate', handleLoopNodeZoomUpdate); }); onUnmounted(() => { // 组件销毁时,清空sessionStorage的veiwport位置 sessionStorage.setItem('workflowViewPortX', ''); sessionStorage.setItem('workflowViewPortY', ''); + + // 清理LoopNode事件监听器 + document.removeEventListener('loopNodeZoomUpdate', handleLoopNodeZoomUpdate); }); @@ -681,7 +934,7 @@ const editFlow = async (item) => { await loadConversationVariablesForDisplay(); } } catch (error) { - console.error('加载工作流失败:', error); + // 处理加载错误 } finally { loading.value = false; } @@ -776,6 +1029,18 @@ const redrageFlow = (nodesList, edgesList) => { }); } + // 验证和清理重复的默认分支 + const defaultBranches = choices.filter(choice => choice.is_default === true); + if (defaultBranches.length > 1) { + console.warn(`发现Choice节点${node.stepId}有多个默认分支,将只保留第一个`); + // 只保留第一个默认分支,将其他的设为非默认 + choices.forEach((choice, index) => { + if (choice.is_default === true && index > 0) { + choice.is_default = false; + } + }); + } + newNode.data = { ...newNode.data, parameters: { @@ -790,6 +1055,14 @@ const redrageFlow = (nodesList, edgesList) => { } } }; + } else if (node.callId === 'Loop') { + // Loop节点特殊处理 + newNode.type = 'Loop'; + newNode.data = { + ...newNode.data, + nodeId: 'Loop', + callId: 'Loop', + }; } else if (node.callId === 'Code') { // Code节点特殊处理:从parameters中提取特有属性并添加到data中 newNode.type = 'custom'; @@ -884,37 +1157,242 @@ const handleInsertNode = (edgeInfo) => { const closeInsertNodeMenu = () => { insertMenuData.value.visible = false; insertMenuData.value.edgeInfo = null; + insertMenuData.value.handleInfo = null; insertMenuData.value.position = { x: 0, y: 0 }; }; +// 处理来自Handle位置的插入节点事件 +const handleInsertNodeFromHandle = (handleInfo) => { + if (debugDialogVisible.value) { + return; // 调试模式下不允许插入节点 + } + + // 获取Vue Flow画布的viewport信息 + const viewport = getViewport(); + + // 获取Vue Flow容器的位置信息 + const vueFlowElement = document.querySelector('.vue-flow__viewport'); + const containerRect = vueFlowElement?.getBoundingClientRect(); + + if (!containerRect) { + console.error('无法获取Vue Flow容器位置'); + return; + } + + // 根据Handle类型计算插入位置 + let insertX, insertY; + const baseX = handleInfo.nodePosition.x; + const baseY = handleInfo.nodePosition.y; + + if (handleInfo.handleType === 'source') { + // Source handle: 在节点右侧插入新节点(线性连接) + insertX = baseX + 250; // 节点间距 + insertY = baseY; + } else { + // Target handle: 在节点上方插入新节点(并行分支) + insertX = baseX; + insertY = baseY - 150; // 节点间距 + } + + // 将画布坐标转换为相对于Vue Flow容器的坐标 + const containerX = insertX * viewport.zoom + viewport.x; + const containerY = insertY * viewport.zoom + viewport.y; + + // 计算建议的菜单方向 + const menuWidth = 400; + let direction: 'left' | 'right' = 'right'; + + if (containerX + menuWidth + 20 > containerRect.width) { + direction = 'left'; + } else { + direction = 'right'; + } + + // 更新插入菜单数据,使用handleInfo而不是edgeInfo + insertMenuData.value = { + visible: true, + position: { + x: containerX, + y: containerY + }, + handleInfo, // 使用handleInfo代替edgeInfo + direction + }; +}; + // 执行插入节点操作 const executeInsertNode = (nodeMetaData) => { + try { - if (!insertMenuData.value.edgeInfo) { - console.error('没有边信息,无法插入节点'); + // 支持从边插入或从Handle插入两种模式 + if (!insertMenuData.value.edgeInfo && !insertMenuData.value.handleInfo) { return; } - const edgeInfo = insertMenuData.value.edgeInfo; - // 生成新节点ID const newNodeId = `node_${Date.now()}`; - // 找到源边 - const sourceEdge = getEdges.value.find(edge => edge.id === edgeInfo.edgeId); - if (!sourceEdge) { - console.error('找不到要插入的边'); - return; + let newNodePosition; + let newEdges = []; + const currentNodes = getNodes.value; + const currentEdges = getEdges.value; + + // 根据插入模式确定节点位置和连接逻辑 + if (insertMenuData.value.edgeInfo) { + // 从边插入模式(原有逻辑) + const edgeInfo = insertMenuData.value.edgeInfo; + const sourceEdge = currentEdges.find(edge => edge.id === edgeInfo.edgeId); + if (!sourceEdge) { + return; + } + + newNodePosition = { + x: edgeInfo.midX - 100, // 节点宽度的一半 + y: edgeInfo.midY - 40 // 节点高度的一半 + }; + + // 删除原边,创建新的边:源节点 -> 新节点 -> 目标节点 + const filteredEdges = currentEdges.filter(edge => edge.id !== edgeInfo.edgeId); + + // 检查源节点是否是Choice节点,如果是则需要使用默认分支的handle + const sourceNode = currentNodes.find(node => node.id === sourceEdge.source); + let newSourceHandle = sourceEdge.sourceHandle; + + if (sourceNode && sourceNode.type === 'Choice') { + // 对于Choice节点,找到默认分支(ELSE分支)的handle + const choices = sourceNode.data?.parameters?.input_parameters?.choices || []; + const defaultBranch = choices.find(choice => choice.is_default === true); + if (defaultBranch) { + newSourceHandle = defaultBranch.branch_id; + console.log('[插入节点] Choice节点使用默认分支handle:', newSourceHandle); + } + } + + // 检查新插入的节点是否是Choice节点,如果是则确定其输出连接的sourceHandle + let newNodeSourceHandle = undefined; + if (nodeMetaData.callId === 'Choice') { + // 对于新创建的Choice节点,使用ELSE分支的ID + newNodeSourceHandle = `else_${newNodeId}`; + console.log('[插入节点] 新Choice节点使用默认分支handle:', newNodeSourceHandle); + } + + newEdges = [ + ...filteredEdges, + { + id: `edge_${Date.now()}_1`, + source: sourceEdge.source, + target: newNodeId, + sourceHandle: newSourceHandle, // 使用修正后的sourceHandle + type: 'normal', + data: { sourceStatus: 'default', targetStatus: 'default' } + }, + { + id: `edge_${Date.now()}_2`, + source: newNodeId, + target: sourceEdge.target, + targetHandle: sourceEdge.targetHandle, + sourceHandle: newNodeSourceHandle, // 使用新节点的默认分支handle + type: 'normal', + data: { sourceStatus: 'default', targetStatus: 'default' } + } + ]; + } else { + // 从Handle插入模式 + const handleInfo = insertMenuData.value.handleInfo; + + if (handleInfo.handleType === 'source') { + // Source handle: 创建新分支,保留原有连接并添加新的连接 + const sourceNodeEdges = currentEdges.filter(edge => edge.source === handleInfo.nodeId); + + // 检查源节点是否是Choice节点,如果是则需要使用指定分支的handle + const sourceNode = currentNodes.find(node => node.id === handleInfo.nodeId); + let sourceHandle = undefined; // 默认不指定sourceHandle + + if (sourceNode && sourceNode.type === 'Choice') { + // 优先使用传递过来的特定分支ID + if (handleInfo.branchId && handleInfo.branchId !== 'default') { + sourceHandle = handleInfo.branchId; + console.log('[Handle插入节点] Choice节点使用指定分支handle:', sourceHandle, 'from branchId:', handleInfo.branchId); + } else { + // 如果没有指定分支ID,才使用默认分支(ELSE分支)的handle + const choices = sourceNode.data?.parameters?.input_parameters?.choices || []; + const defaultBranch = choices.find(choice => choice.is_default === true); + if (defaultBranch) { + sourceHandle = defaultBranch.branch_id; + console.log('[Handle插入节点] Choice节点使用默认分支handle:', sourceHandle); + } + } + } + + if (sourceNodeEdges.length > 0) { + // 如果已有输出连接,创建并行分支:保留原连接,添加新分支 + const firstTargetNode = getNodes.value.find(node => node.id === sourceNodeEdges[0].target); + + // 计算新节点位置:在第一个目标节点下方 + newNodePosition = { + x: firstTargetNode ? firstTargetNode.position.x : handleInfo.nodePosition.x + 350, + y: firstTargetNode ? firstTargetNode.position.y + 120 : handleInfo.nodePosition.y + 120 + }; + + // 保留所有原有边,只添加新的分支边 + newEdges = [ + ...currentEdges, // 保留所有现有连接 + { + id: `edge_${Date.now()}`, + source: handleInfo.nodeId, + target: newNodeId, + sourceHandle: sourceHandle, // 使用默认分支handle(如果是Choice节点) + type: 'normal', + data: { sourceStatus: 'default', targetStatus: 'default' } + } + ]; + } else { + // 如果没有输出连接,创建线性连接 + newNodePosition = { + x: handleInfo.nodePosition.x + 350, // 增加偏移距离,避免重叠 + y: handleInfo.nodePosition.y + }; + + // 创建从原节点到新节点的边 + newEdges = [ + ...currentEdges, + { + id: `edge_${Date.now()}`, + source: handleInfo.nodeId, + target: newNodeId, + sourceHandle: sourceHandle, // 使用默认分支handle(如果是Choice节点) + type: 'normal', + data: { sourceStatus: 'default', targetStatus: 'default' } + } + ]; + } + } else { + // Target handle: 并行分支,在当前节点前创建新的分支 + newNodePosition = { + x: handleInfo.nodePosition.x, + y: handleInfo.nodePosition.y - 150 + }; + + // 查找指向当前节点的边的源节点,为新节点创建分支连接 + const targetNodeEdges = currentEdges.filter(edge => edge.target === handleInfo.nodeId); + const newBranchEdges = targetNodeEdges.map(edge => ({ + id: `edge_${Date.now()}_branch_${edge.id}`, + source: edge.source, + target: newNodeId, + type: 'normal', + data: { sourceStatus: 'default', targetStatus: 'default' } + })); + + newEdges = [...currentEdges, ...newBranchEdges]; + } } // 创建新节点 const newNode = { id: newNodeId, - type: nodeMetaData.callId === 'Choice' ? 'Choice' : 'custom', - position: { - x: edgeInfo.midX - 100, // 节点宽度的一半 - y: edgeInfo.midY - 40 // 节点高度的一半 - }, + type: nodeMetaData.callId === 'Choice' ? 'Choice' : + nodeMetaData.callId === 'Loop' ? 'Loop' : 'custom', + position: newNodePosition, data: { name: nodeMetaData.name, description: nodeMetaData.description, @@ -924,6 +1402,28 @@ const executeInsertNode = (nodeMetaData) => { parameters: nodeMetaData.callId === 'Choice' ? { input_parameters: { choices: [ + { + branch_id: `if_${newNodeId}`, + name: 'IF', + is_default: false, + conditions: [ + { + id: `condition_${newNodeId}`, + left: { + type: 'reference', + value: '', + }, + right: { + type: 'string', + value: '', + }, + operate: 'string_equal', + dataType: 'string', + isRightReference: false, + } + ], + logic: 'and' + }, { branch_id: `else_${newNodeId}`, name: 'ELSE', @@ -944,6 +1444,17 @@ const executeInsertNode = (nodeMetaData) => { answer: '' // 确保新建的DirectReply节点内容为空 }, output_parameters: {} + } : nodeMetaData.callId === 'Loop' ? { + input_parameters: { + variables: {}, + stop_condition: { + logic: 'and', + conditions: [] + }, + max_iteration: 10, + sub_flow_id: '' + }, + output_parameters: {} } : { input_parameters: {}, output_parameters: {} @@ -952,40 +1463,9 @@ const executeInsertNode = (nodeMetaData) => { deletable: true }; - // 添加新节点 - const currentNodes = [...getNodes.value, newNode]; - setNodes(currentNodes); - - // 删除原来的边 - const currentEdges = getEdges.value.filter(edge => edge.id !== edgeInfo.edgeId); - - // 创建新的边:源节点 -> 新节点 -> 目标节点 - const newEdge1 = { - id: `edge_${Date.now()}_1`, - source: sourceEdge.source, - target: newNodeId, - sourceHandle: sourceEdge.sourceHandle, - type: 'normal', - data: { - sourceStatus: 'default', - targetStatus: 'default' - } - }; - - const newEdge2 = { - id: `edge_${Date.now()}_2`, - source: newNodeId, - target: sourceEdge.target, - targetHandle: sourceEdge.targetHandle, - type: 'normal', - data: { - sourceStatus: 'default', - targetStatus: 'default' - } - }; - - // 设置新的边 - setEdges([...currentEdges, newEdge1, newEdge2]); + // 添加新节点和更新边 + setNodes([...currentNodes, newNode]); + setEdges(newEdges); // 触发工作流状态更新 emits('updateFlowsDebug', false); @@ -998,7 +1478,6 @@ const executeInsertNode = (nodeMetaData) => { ElMessage.success(`${nodeMetaData.name} 节点插入成功`); } catch (error) { - console.error('插入节点失败:', error); ElMessage.error('插入节点失败'); } }; @@ -1115,195 +1594,257 @@ const cancelConnectStatus = () => { } }; -const saveFlow = (updateNodeParameter?, debug?) => { +const saveFlow = async (updateNodeParameter?, debug?) => { loading.value = true; const appId = route.query?.appId; if (!flowObj.value.flowId) { return; } - // 将对应的节点和边存储格式改造 - let updateNodes = getNodes.value.map((item) => { - const { ...otherItem } = item.data; - let newItem = { - enable: true, - editable: false, - position: item.position, - apiId: item.data.nodeId, - serviceId: item.data.serviceId, - stepId: item.id, - nodeId: item.data.nodeId, // 添加nodeId字段 - type: item.data.nodeId, - }; + + try { + // 第一步:保存所有Loop节点的子工作流 + const loopNodes = getNodes.value.filter(node => node.data.callId === 'Loop'); - // 对于Code节点,需要特殊处理parameters结构 - if (item.data.callId === 'Code') { - // Code节点:将所有配置放在parameters中 - newItem = { - ...newItem, - callId: item.data.callId, - name: item.data.name, - description: item.data.description, - parameters: { - input_parameters: item.data.parameters?.input_parameters || {}, - output_parameters: item.data.parameters?.output_parameters || {}, - code: item.data.code || '', - codeType: item.data.codeType || 'python', - securityLevel: item.data.securityLevel || 'low', - timeoutSeconds: item.data.timeoutSeconds || 30, - memoryLimitMb: item.data.memoryLimitMb || 128, - cpuLimit: item.data.cpuLimit || 0.5, + for (const loopNode of loopNodes) { + // 通过全局变量访问LoopNode组件实例(需要在LoopNode中设置) + const loopComponentKey = `loopNode_${loopNode.id}`; + const loopRef = (window as any).loopNodeRefs?.[loopComponentKey]; + + if (loopRef && typeof loopRef.hasUnsavedSubFlowChanges === 'function') { + const hasChanges = loopRef.hasUnsavedSubFlowChanges(); + + if (hasChanges) { + const subFlowId = await loopRef.saveSubFlow(); + + // 更新Loop节点的参数中的sub_flow_id + if (subFlowId) { + loopNode.data.parameters = loopNode.data.parameters || {}; + loopNode.data.parameters.input_parameters = loopNode.data.parameters.input_parameters || {}; + loopNode.data.parameters.input_parameters.sub_flow_id = subFlowId; + } + } else { + // 即使没有变更,也要确保sub_flow_id正确设置 + const subFlowId = loopRef.getSubFlowId(); + if (subFlowId) { + loopNode.data.parameters = loopNode.data.parameters || {}; + loopNode.data.parameters.input_parameters = loopNode.data.parameters.input_parameters || {}; + loopNode.data.parameters.input_parameters.sub_flow_id = subFlowId; + } } - }; - } else { - // 其他节点:使用原有逻辑 - newItem = { - ...newItem, - ...otherItem, - }; + } } - if (item.type === 'end' || item.type === 'start') { - // 更新开始结束节点结构 - newItem = { - ...newItem, - apiId: item.type, // 这两个id个应该没有-暂时随意复制 - serviceId: item.type, - nodeId: 'Empty', - callId: item.type, - type: 'startAndEnd', + + // 第二步:保存主工作流(原有逻辑) + // 将对应的节点和边存储格式改造 + let updateNodes = getNodes.value.map((item) => { + const { ...otherItem } = item.data; + let newItem = { + enable: true, + editable: false, + position: item.position, + apiId: item.data.nodeId, + serviceId: item.data.serviceId, + stepId: item.id, + nodeId: item.data.nodeId, // 添加nodeId字段 + type: item.data.nodeId, }; - } else if (item.type === 'branch') { - // 处理分支节点保存,确保包含ELSE分支 - const choices = item.data.parameters?.input_parameters?.choices || []; - // 检查是否已有默认分支 - const hasDefaultBranch = choices.some(choice => choice.is_default === true); - - // 如果没有默认分支,添加一个ELSE分支 - if (!hasDefaultBranch) { - choices.push({ - branch_id: `else_${item.id || Date.now()}`, - name: 'ELSE', - is_default: true, - conditions: [], - logic: 'and' - }); + // 对于Code节点,需要特殊处理parameters结构 + if (item.data.callId === 'Code') { + // Code节点:将所有配置放在parameters中 + newItem = { + ...newItem, + callId: item.data.callId, + name: item.data.name, + description: item.data.description, + parameters: { + input_parameters: item.data.parameters?.input_parameters || {}, + output_parameters: item.data.parameters?.output_parameters || {}, + code: item.data.code || '', + codeType: item.data.codeType || 'python', + securityLevel: item.data.securityLevel || 'low', + timeoutSeconds: item.data.timeoutSeconds || 30, + memoryLimitMb: item.data.memoryLimitMb || 128, + cpuLimit: item.data.cpuLimit || 0.5, + } + }; + } else { + // 其他节点:使用原有逻辑 + newItem = { + ...newItem, + ...otherItem, + }; } - newItem = { - ...newItem, - callId: 'Choice', - parameters: { - input_parameters: { choices: choices }, - output_parameters: item.data.parameters?.output_parameters || { - branch_id: { - type: 'string', - description: '选中的分支ID' + // 其余逻辑保持不变... + if (item.type === 'end' || item.type === 'start') { + newItem = { + ...newItem, + serviceId: item.type, + nodeId: 'Empty', + callId: item.type, + type: 'startAndEnd', + }; + } else if (item.type === 'branch') { + // 对于branch节点,处理choice数据 + let choices = []; + // 根据该节点在edge中作为source时的edge + const branchEdges = getEdges.value.filter((edge) => edge.source === item.id); + + // 如果有边连接,根据边的branchId创建choice + if (branchEdges.length > 0) { + choices = branchEdges.map((edge, index) => ({ + branch_id: edge.branchId, + name: edge.branchId || `choice_${index + 1}`, + is_default: edge.branchId === 'else', + conditions: [], + logic: 'and' + })); + } else { + // 如果没有边连接,使用现有的choices数据或创建默认的 + choices = item.data.parameters?.input_parameters?.choices || [ + { + branch_id: `else_${item.id}`, + name: 'ELSE', + is_default: true, + conditions: [], + logic: 'and' } - } - }, - }; - } else if (item.type === 'Choice') { - // 处理Choice节点保存,确保包含ELSE分支 - const choices = item.data.parameters?.input_parameters?.choices || []; - - // 检查是否已有默认分支 - const hasDefaultBranch = choices.some(choice => choice.is_default === true); - - // 如果没有默认分支,添加一个ELSE分支 - if (!hasDefaultBranch) { - choices.push({ - branch_id: `else_${item.id || Date.now()}`, - name: 'ELSE', - is_default: true, - conditions: [], - logic: 'and' - }); + ]; + } + + newItem = { + ...newItem, + callId: 'Choice', + parameters: { + input_parameters: { choices: choices }, + output_parameters: item.data.parameters?.output_parameters || { + branch_id: { + type: 'string', + description: '选中的分支ID' + } + } + }, + }; + } else if (item.type === 'Choice') { + // 处理条件分支节点 + let choices = []; + const branchEdges = getEdges.value.filter((edge) => edge.source === item.id); + + if (branchEdges.length > 0) { + choices = branchEdges.map((edge, index) => ({ + branch_id: edge.branchId, + name: edge.branchId || `choice_${index + 1}`, + is_default: edge.branchId === 'else', + conditions: [], + logic: 'and' + })); + } else { + choices = item.data.parameters?.input_parameters?.choices || [ + { + branch_id: `else_${item.id}`, + name: 'ELSE', + is_default: true, + conditions: [], + logic: 'and' + } + ]; + } + + newItem = { + ...newItem, + callId: 'Choice', + parameters: { + input_parameters: { choices: choices }, + output_parameters: item.data.parameters?.output_parameters || { + branch_id: { + type: 'string', + description: '选中的分支ID' + } + } + }, + }; + } else if (item.type === 'Loop') { + // 处理循环节点 + newItem = { + ...newItem, + callId: 'Loop', + parameters: { + input_parameters: item.data.parameters?.input_parameters || {}, + output_parameters: item.data.parameters?.output_parameters || {} + }, + }; } - newItem = { - ...newItem, - callId: 'Choice', - parameters: { - input_parameters: { choices: choices }, - output_parameters: item.data.parameters?.output_parameters || { - branch_id: { - type: 'string', - description: '选中的分支ID' - } - } - }, - }; - } - return newItem; - }); - // 更新对应的边的结构 - const updateEdges = getEdges.value.map((item) => { - let newEdge = { + return newItem; + }); + + // 处理边 + const updateEdges = getEdges.value.map((item) => ({ edgeId: item.id, - sourceNode: item.sourceNode.id, - targetNode: item.targetNode.id, - type: item.type, - branchId: item.sourceHandle || '', - }; - return newEdge; - }); - // 判断是否调用修改yaml文件,以确定是否修改对应的input_paramteters - if (updateNodeParameter) { - updateNodes.forEach((item) => { - if (item.stepId === updateNodeParameter.id) { - if (item.type === 'Code') { - item.parameters.input_parameters = updateNodeParameter.parameters.input_parameters; - item.parameters.output_parameters = updateNodeParameter.parameters.output_parameters; - item.parameters.code = updateNodeParameter.parameters.code; - item.parameters.codeType = updateNodeParameter.parameters.codeType; - item.parameters.securityLevel = updateNodeParameter.parameters.securityLevel; - item.parameters.timeoutSeconds = updateNodeParameter.parameters.timeoutSeconds; - item.parameters.memoryLimitMb = updateNodeParameter.parameters.memoryLimitMb; - item.parameters.cpuLimit = updateNodeParameter.parameters.cpuLimit; - } else if (item.callId === 'DirectReply') { - // 确保parameters对象存在 - if (!item.parameters) { - item.parameters = {}; - } - item.parameters.input_parameters = updateNodeParameter.parameters.input_parameters; - item.parameters.output_parameters = updateNodeParameter.parameters.output_parameters; - } else if (item.callId === 'Choice') { - // 条件分支节点 - if (!item.parameters) { - item.parameters = {}; - } - item.parameters.input_parameters = updateNodeParameter.parameters.input_parameters; - item.parameters.output_parameters = updateNodeParameter.parameters.output_parameters; - } else if (item.type === 'start') { - item.variables == updateNodeParameter.variables; - } else if (item.inputStream !== undefined) { - // 确保parameters对象存在 - if (!item.parameters) { - item.parameters = {}; - } - // 当Node以yaml编辑器形式修改了参数 - // 检查updateNodeParameter.inputStream是否包含新的数据结构 - if (updateNodeParameter.inputStream.input_parameters !== undefined && - updateNodeParameter.inputStream.output_parameters !== undefined) { - - item.parameters.input_parameters = updateNodeParameter.inputStream.input_parameters; - item.parameters.output_parameters = updateNodeParameter.inputStream.output_parameters; - } else { - // 旧格式:兼容处理 - item.parameters.input_parameters = updateNodeParameter.inputStream; + sourceNode: item.source, + targetNode: item.target, + branchId: item.sourceHandle, + })); + + // 处理节点参数更新 + if (updateNodeParameter) { + updateNodes.forEach((item) => { + if (item.stepId === updateNodeParameter.id) { + if (item.type === 'Code') { + item.parameters.input_parameters = updateNodeParameter.parameters.input_parameters; + item.parameters.output_parameters = updateNodeParameter.parameters.output_parameters; + item.parameters.code = updateNodeParameter.parameters.code; + item.parameters.codeType = updateNodeParameter.parameters.codeType; + item.parameters.securityLevel = updateNodeParameter.parameters.securityLevel; + item.parameters.timeoutSeconds = updateNodeParameter.parameters.timeoutSeconds; + item.parameters.memoryLimitMb = updateNodeParameter.parameters.memoryLimitMb; + item.parameters.cpuLimit = updateNodeParameter.parameters.cpuLimit; + } else if (item.callId === 'DirectReply') { + // 确保parameters对象存在 + if (!item.parameters) { + item.parameters = {}; + } + item.parameters.input_parameters = updateNodeParameter.parameters.input_parameters; + item.parameters.output_parameters = updateNodeParameter.parameters.output_parameters; + } else if (item.callId === 'Choice') { + // 条件分支节点 + if (!item.parameters) { + item.parameters = {}; + } + item.parameters.input_parameters = updateNodeParameter.parameters.input_parameters; + item.parameters.output_parameters = updateNodeParameter.parameters.output_parameters; + } else if (item.type === 'start') { + item.variables == updateNodeParameter.variables; + } else if (item.inputStream !== undefined) { + // 确保parameters对象存在 + if (!item.parameters) { + item.parameters = {}; + } + // 当Node以yaml编辑器形式修改了参数 + // 检查updateNodeParameter.inputStream是否包含新的数据结构 + if (updateNodeParameter.inputStream.input_parameters !== undefined && + updateNodeParameter.inputStream.output_parameters !== undefined) { + + item.parameters.input_parameters = updateNodeParameter.inputStream.input_parameters; + item.parameters.output_parameters = updateNodeParameter.inputStream.output_parameters; + } else { + // 旧格式:兼容处理 + item.parameters.input_parameters = updateNodeParameter.inputStream; + } } + item.name = updateNodeParameter.name; + item.description = updateNodeParameter.description; } - item.name = updateNodeParameter.name; - item.description = updateNodeParameter.description; - } - }); - } - if (debug) { - flowObj.value.debug = true; - } - // 更新最新的节点与边的数据 - api - .createOrUpdateFlowTopology( + }); + } + + if (debug) { + flowObj.value.debug = true; + } + + // 更新最新的节点与边的数据 + const response = await api.createOrUpdateFlowTopology( { appId: appId, flowId: flowObj.value.flowId, @@ -1319,16 +1860,20 @@ const saveFlow = (updateNodeParameter?, debug?) => { }, }, }, - ) - .then((res) => { - if (res[1]?.result) { - queryFlow('update'); - const updatedCurFlow = res[1].result.flow; - isNodeConnect.value = res[1].result.connectivity; - redrageFlow(updatedCurFlow?.nodes, updatedCurFlow?.edges); - } - loading.value = false; - }); + ); + + if (response[1]?.result) { + queryFlow('update'); + const updatedCurFlow = response[1].result.flow; + isNodeConnect.value = response[1].result.connectivity; + redrageFlow(updatedCurFlow?.nodes, updatedCurFlow?.edges); + } + + } catch (error) { + ElMessage.error('保存工作流失败'); + } finally { + loading.value = false; + } }; // TODO saveNode -> saveNodeYaml,仅当以yaml形式保存时才调用 @@ -1442,6 +1987,7 @@ defineExpose({ @delNode="delNode" @editYamlDrawer="editYamlDrawer" @updateConnectHandle="updateConnectHandle" + @insertNodeFromHandle="handleInsertNodeFromHandle" > @@ -1465,15 +2011,35 @@ defineExpose({ @delNode="delNode" @editYamlDrawer="editYamlDrawer" @updateConnectHandle="updateConnectHandle" + @insertNodeFromHandle="handleInsertNodeFromHandle" > + + +