From 73232babdcfeef797b70b07d9932fcd481809a3b Mon Sep 17 00:00:00 2001 From: liailing1026 <1815388873@qq.com> Date: Fri, 6 Mar 2026 11:22:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=89=A7=E8=A1=8C=E8=BF=87=E7=A8=8B?= =?UTF-8?q?=E7=BC=96=E6=8E=92=E6=96=B0=E5=BB=BA=E5=88=86=E6=94=AF=E5=8D=95?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E5=88=A0=E9=99=A4=E5=92=8C=E6=95=B4=E4=B8=AA?= =?UTF-8?q?=E5=88=86=E6=94=AF=E8=8A=82=E7=82=B9=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.py | 108 +++++++ frontend/src/api/index.ts | 37 +++ .../TaskProcess/components/PlanTask.vue | 276 ++++++++++++++++-- frontend/src/stores/modules/selection.ts | 84 +++++- 4 files changed, 475 insertions(+), 30 deletions(-) diff --git a/backend/server.py b/backend/server.py index 6efcc39..8cf2b91 100644 --- a/backend/server.py +++ b/backend/server.py @@ -2508,6 +2508,114 @@ def handle_delete_task_process_branch(data): }) +@socketio.on('delete_task_process_node') +def handle_delete_task_process_node(data): + """ + WebSocket版本:删除任务过程分支中的单个节点 + + 请求格式: + { + "id": "request-id", + "action": "delete_task_process_node", + "data": { + "task_id": "task-id", // 大任务ID(数据库主键) + "stepId": "step-id", // 小任务ID + "branchId": "branch-id", // 分支ID + "nodeId": "node-id" // 要删除的节点ID + } + } + """ + request_id = data.get('id') + incoming_data = data.get('data', {}) + task_id = incoming_data.get('task_id') + step_id = incoming_data.get('stepId') + branch_id = incoming_data.get('branchId') + node_id = incoming_data.get('nodeId') + edges = incoming_data.get('edges', []) # 更新后的 edges 数据 + + if not task_id or not step_id or not branch_id or not node_id: + emit('response', { + 'id': request_id, + 'status': 'error', + 'error': '缺少必要参数:task_id, stepId, branchId, nodeId' + }) + return + + try: + with get_db_context() as db: + # 获取现有的 branches 数据 + existing_task = MultiAgentTaskCRUD.get_by_id(db, task_id) + + if existing_task: + # 使用深拷贝避免修改共享引用 + existing_branches = copy.deepcopy(existing_task.branches) if existing_task.branches else {} + + if isinstance(existing_branches, dict): + # 获取现有的 task_process_branches + task_process_branches = existing_branches.get('task_process_branches', {}) + + if step_id in task_process_branches: + step_branches = task_process_branches[step_id] + + # 遍历所有 agentGroupKey 下的分支 + for agent_key, branches_list in step_branches.items(): + if isinstance(branches_list, list): + for branch in branches_list: + if branch.get('id') == branch_id: + # 找到目标分支,删除指定的节点 + nodes = branch.get('nodes', []) + tasks = branch.get('tasks', []) + + # 找到并删除节点 + for i, node in enumerate(nodes): + if node.get('id') == node_id: + nodes.pop(i) + if i < len(tasks): + tasks.pop(i) + break + + # 更新分支数据(包括 nodes, tasks, edges) + branch['nodes'] = nodes + branch['tasks'] = tasks + branch['edges'] = edges # 使用前端传入的更新后的 edges + break + + # 更新 branches 数据 + existing_branches['task_process_branches'] = task_process_branches + + # 直接更新数据库 + existing_task.branches = existing_branches + db.flush() + db.commit() + + print(f"[delete_task_process_node] 删除成功,task_id={task_id}, step_id={step_id}, branch_id={branch_id}, node_id={node_id}") + + emit('response', { + 'id': request_id, + 'status': 'success', + 'data': { + "message": "节点删除成功", + "deleted_node_id": node_id + } + }) + return + + # 如果找不到对应的节点 + print(f"[delete_task_process_node] 警告: 找不到要删除的节点,task_id={task_id}, step_id={step_id}, branch_id={branch_id}, node_id={node_id}") + emit('response', { + 'id': request_id, + 'status': 'error', + 'error': '未找到要删除的节点' + }) + + except Exception as e: + emit('response', { + 'id': request_id, + 'status': 'error', + 'error': str(e) + }) + + @socketio.on('save_task_outline') def handle_save_task_outline(data): """ diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index daf392b..233b18f 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -710,6 +710,43 @@ class Api { } } + /** + * 删除任务过程分支中的单个节点 + * @param TaskID 大任务ID + * @param stepId 小任务ID + * @param branchId 分支ID + * @param nodeId 要删除的节点ID + * @param edges 更新后的 edges 数据 + * @returns 是否删除成功 + */ + deleteTaskProcessNode = async ( + TaskID: string, + stepId: string, + branchId: string, + nodeId: string, + edges: any[] = [], + ): Promise => { + if (!websocket.connected) { + throw new Error('WebSocket未连接') + } + + try { + const rawResponse = await websocket.send('delete_task_process_node', { + task_id: TaskID, + stepId, + branchId, + nodeId, + edges, + }) + + const response = this.extractResponse<{ status: string }>(rawResponse) + return response?.status === 'success' || false + } catch (error) { + console.error('删除任务过程节点数据失败:', error) + return false + } + } + /** * 更新任务大纲数据 * @param taskId 任务ID diff --git a/frontend/src/layout/components/Main/TaskTemplate/TaskProcess/components/PlanTask.vue b/frontend/src/layout/components/Main/TaskTemplate/TaskProcess/components/PlanTask.vue index 0af2a09..dc960c7 100644 --- a/frontend/src/layout/components/Main/TaskTemplate/TaskProcess/components/PlanTask.vue +++ b/frontend/src/layout/components/Main/TaskTemplate/TaskProcess/components/PlanTask.vue @@ -39,6 +39,10 @@ const branchLoading = ref(false) const deleteDialogVisible = ref(false) const branchIdToDelete = ref(null) +// 删除单个节点弹窗相关状态 +const deleteNodeDialogVisible = ref(false) +const nodeIdToDelete = ref(null) + // 节点和边数据 const nodes = ref([]) const edges = ref([]) @@ -387,29 +391,75 @@ const getBranchParentChain = (targetNodeId: string): string[] => { } // 判断节点是否可删除(分支从底部出发连接的节点) -const getNodeDeletable = (nodeId: string): { isDeletable: boolean; branchId: string | null } => { - // 找到指向该节点的边 - const incomingEdges = edges.value.filter(edge => edge.target === nodeId) +const getNodeDeletable = ( + nodeId: string +): { isDeletable: boolean; branchId: string | null; parentNodeId: string | null } => { + // 直接从 store 的分支数据里用节点 id 查找它属于哪个分支 + const taskStepId = currentTask.value?.Id + const currentAgents = currentTask.value?.AgentSelection || [] - // 检查是否有从底部(sourceHandle='bottom')连接过来的边 - const bottomEdge = incomingEdges.find(edge => edge.sourceHandle === 'bottom') - - if (bottomEdge && bottomEdge.data?.branchId) { + if (!taskStepId) { return { - isDeletable: true, - branchId: bottomEdge.data.branchId + isDeletable: false, + branchId: null, + parentNodeId: null + } + } + + // 获取所有分支数据 + const savedBranches = selectionStore.getTaskProcessBranches(taskStepId, currentAgents) + + // 遍历所有分支,用节点 id 在 nodes 数组中查找 + for (const branch of savedBranches) { + // 检查该节点是否是该分支的第一个节点 + const firstNode = branch.nodes?.[0] + if (firstNode && firstNode.id === nodeId) { + // 只有该分支的第一个节点才显示删除分支按钮 + return { + isDeletable: true, + branchId: branch.id, + parentNodeId: branch.parentNodeId || null + } } } return { isDeletable: false, - branchId: null + branchId: null, + parentNodeId: null } } // 删除分支(显示确认对话框) -const handleDeleteBranch = (branchId: string) => { - branchIdToDelete.value = branchId +const handleDeleteBranch = (nodeId: string) => { + // 获取节点的删除信息(包括 parentNodeId) + const nodeInfo = getNodeDeletable(nodeId) + if (!nodeInfo.isDeletable || !nodeInfo.parentNodeId) { + ElMessage.error('无法删除该分支') + return + } + + const taskStepId = currentTask.value?.Id + const currentAgents = currentTask.value?.AgentSelection || [] + + if (!taskStepId) return + + // 通过 branchId 查找正确的分支(使用 branchId 精确匹配,而不是 parentNodeId) + const savedBranches = selectionStore.getTaskProcessBranches(taskStepId, currentAgents) + const targetBranch = savedBranches.find(branch => branch.id === nodeInfo.branchId) + + if (!targetBranch) { + ElMessage.error('未找到该分支') + return + } + + console.log( + '[删除分支] 找到的 branchId:', + targetBranch.id, + 'parentNodeId:', + nodeInfo.parentNodeId + ) + branchIdToDelete.value = targetBranch.id deleteDialogVisible.value = true } @@ -464,6 +514,137 @@ const confirmDeleteBranch = async () => { ElMessage.success('分支删除成功') } +// 删除单个节点(显示确认对话框) +const handleDeleteNode = (nodeId: string) => { + nodeIdToDelete.value = nodeId + deleteNodeDialogVisible.value = true +} + +// 确认删除单个节点 +const confirmDeleteNode = async () => { + if (!nodeIdToDelete.value) return + + const nodeId = nodeIdToDelete.value + const taskStepId = currentTask.value?.Id + const currentAgents = currentTask.value?.AgentSelection || [] + + // 0. 检查被删除的节点是否是分支的第一个节点(带删除分支按钮) + const nodeDeletable = getNodeDeletable(nodeId) + const deletedBranchId = nodeDeletable.isDeletable ? nodeDeletable.branchId : null + + // 1. 找到该节点的入边和出边 + const incomingEdges = edges.value.filter(e => e.target === nodeId) + const outgoingEdges = edges.value.filter(e => e.source === nodeId) + + // 1.1 保存删除前的子节点信息(用于后续更新 branchId) + const childNodeIds = outgoingEdges.map(e => e.target) + + // 2. 将子节点重新连接到父节点(使用固定的 left/right handle) + const newEdges: Edge[] = [] + outgoingEdges.forEach(outEdge => { + incomingEdges.forEach(inEdge => { + // 创建新边,连接父节点(右侧)到子节点(左侧) + const newEdge = { + id: `e-${inEdge.source}-${outEdge.target}-${Date.now()}`, + source: inEdge.source, + target: outEdge.target, + sourceHandle: 'right', // 父节点右侧 + targetHandle: 'left', // 子节点左侧 + type: 'smoothstep', + animated: true, + style: outEdge.style, + markerEnd: outEdge.markerEnd, + data: inEdge.data // 保留父节点的 branchId + } + edges.value.push(newEdge) + newEdges.push(newEdge) + }) + }) + + // 3. 删除该节点的入边和出边 + const relatedEdgeIds = [...incomingEdges.map(e => e.id), ...outgoingEdges.map(e => e.id)] + edges.value = edges.value.filter(e => !relatedEdgeIds.includes(e.id)) + + // 4. 删除该节点 + nodes.value = nodes.value.filter(n => n.id !== nodeId) + + // 4.1 如果删除的是分支的第一个节点,需要更新新第一个节点的边信息 + if (deletedBranchId && childNodeIds.length > 0) { + // 遍历所有子节点,更新它们的入边信息 + childNodeIds.forEach(childNodeId => { + // 找到连接子节点的入边 + const childIncomingEdge = edges.value.find( + e => e.target === childNodeId && e.sourceHandle === 'right' + ) + if (childIncomingEdge) { + // 更新边的 sourceHandle 为 'bottom' 并添加 branchId,使新节点成为可删除分支的入口 + childIncomingEdge.sourceHandle = 'bottom' + childIncomingEdge.data = { + ...childIncomingEdge.data, + branchId: deletedBranchId + } + console.log('[删除节点] 已更新新节点的边信息:', childNodeId, deletedBranchId) + } + }) + } + + // 5. 精准删除 store 中对应分支的节点数据,并调用后端接口删除 + if (taskStepId && currentAgents.length > 0) { + // 获取所有分支数据 + const branches = selectionStore.getTaskProcessBranches(taskStepId, currentAgents) + + // 找到被删除节点所属的分支 + const targetBranch = branches.find(branch => branch.nodes?.some((n: any) => n.id === nodeId)) + + if (targetBranch) { + // 使用精准删除方法(前端 store) + const success = selectionStore.removeNodeFromBranch( + taskStepId, + currentAgents, + targetBranch.id, + nodeId, + incomingEdges, + outgoingEdges, + newEdges + ) + + if (success) { + console.log('[删除节点] 前端精准删除成功') + + // 调用后端接口删除节点,传递更新后的 edges 数据 + const TaskID = (window as any).__CURRENT_TASK_ID__ + if (TaskID) { + const result = await selectionStore.deleteTaskProcessNodeFromDB( + TaskID, + taskStepId, + targetBranch.id, + nodeId, + edges.value + ) + console.log('[删除节点] 后端删除结果:', result) + } + } else { + console.error('[删除节点] 前端精准删除失败') + } + } else { + console.error('[删除节点] 未找到节点所属的分支') + } + } else { + console.error('[删除节点] 缺少 taskStepId 或 currentAgents') + } + + // 6. 刷新视图 + nextTick(() => { + fit({ padding: 0.15, duration: 300 }) + }) + + // 7. 清理状态 + deleteNodeDialogVisible.value = false + nodeIdToDelete.value = null + + ElMessage.success('节点删除成功') +} + // 初始化流程图 const initializeFlow = () => { if (!currentTask.value) { @@ -1043,7 +1224,7 @@ const submitBranch = async (branchContent: string) => { // 基于同一父节点下的分支数量计算位置 const currentAgents = currentTask.value?.AgentSelection || [] const sameParentBranches = selectionStore - .getTaskProcessBranches(currentTask.value.Id, currentAgents) + .getTaskProcessBranches(currentTask.value?.Id || '', currentAgents) .filter(b => b.parentNodeId === parentNodeId) // 新分支的索引 = 现有分支数量(放在最后) const newBranchIndex = sameParentBranches.length @@ -1153,7 +1334,7 @@ const submitBranch = async (branchContent: string) => { // 基于同一父节点下的分支数量计算位置 const currentAgents = currentTask.value?.AgentSelection || [] const sameParentBranches = selectionStore - .getTaskProcessBranches(currentTask.value!.Id, currentAgents) + .getTaskProcessBranches(currentTask.value?.Id || '', currentAgents) .filter(b => b.parentNodeId === parentNodeId) const branchIndex = sameParentBranches.length @@ -1257,7 +1438,7 @@ defineExpose({ v-model:edges="styledEdges" :delete-key-code="null" :default-viewport="{ zoom: 0.5, x: 0, y: 0 }" - :min-zoom="0.5" + :min-zoom="0.2" :max-zoom="4" @node-click="onNodeClick" class="vue-flow-container" @@ -1278,16 +1459,16 @@ defineExpose({
- +
-
- +
+
@@ -1308,7 +1489,16 @@ defineExpose({
+ +
+ + +
@@ -1365,16 +1555,16 @@ defineExpose({
- +
-
- +
+
@@ -1397,6 +1587,14 @@ defineExpose({ content="删除后,该分支无法恢复 !" @confirm="confirmDeleteBranch" /> + + +
@@ -1520,14 +1718,13 @@ defineExpose({ // 外部添加按钮 .external-add-btn { position: absolute; - bottom: -20px; + bottom: -5px; left: 50%; transform: translateX(-50%); - width: 32px; - height: 32px; + width: 12px; + height: 12px; border-radius: 50%; background: #fff; - border: 2px solid #409eff; display: flex; align-items: center; justify-content: center; @@ -1718,7 +1915,28 @@ defineExpose({ } &:hover { - background-color: #ff4d4f; + background-color: #0c0c0c; + } +} + +// 单节点删除按钮(右上角)- 与左侧分支删除按钮样式一致 +.node-delete-btn-single { + position: absolute; + width: 24px; + height: 24px; + border-radius: 50%; + background-color: #2d2d2d; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background-color 0.2s ease; + z-index: 100; + top: -12px; + right: -12px; + + &:hover { + background-color: #0c0c0c; } } diff --git a/frontend/src/stores/modules/selection.ts b/frontend/src/stores/modules/selection.ts index 7c769a1..f6a9630 100644 --- a/frontend/src/stores/modules/selection.ts +++ b/frontend/src/stores/modules/selection.ts @@ -134,9 +134,11 @@ export const useSelectionStore = defineStore('selection', () => { nodes: Node[] edges: Edge[] tasks: IRawStepTask[] + id?: string // 允许传入自定义 id }, ): string { - const branchId = `task-process-branch-${uuidv4()}` + const branchId = data.id || `task-process-branch-${uuidv4()}` + console.log('[store.addTaskProcessBranch] 生成的 branchId:', branchId, '传入的 id:', data.id) const agentGroupKey = getAgentGroupKey(agents) const newBranch: IBranchData = { @@ -225,6 +227,61 @@ export const useSelectionStore = defineStore('selection', () => { return false } + /** + * 精准删除分支中的单个节点 + * @param taskStepId 任务步骤 ID + * @param agents Agent 列表 + * @param branchId 分支 ID + * @param nodeId 要删除的节点 ID + * @param incomingEdges 入边(用于更新 edges) + * @param outgoingEdges 出边(用于更新 edges) + * @param newEdges 新创建的边(用于重新连接) + * @returns 是否删除成功 + */ + function removeNodeFromBranch( + taskStepId: string, + agents: string[], + branchId: string, + nodeId: string, + incomingEdges: any[] = [], + outgoingEdges: any[] = [], + newEdges: any[] = [], + ): boolean { + const agentGroupKey = getAgentGroupKey(agents) + const branches = taskProcessBranchesMap.value.get(taskStepId)?.get(agentGroupKey) + if (!branches) return false + + const branchIndex = branches.findIndex((branch) => branch.id === branchId) + if (branchIndex === -1) return false + + const branch = branches[branchIndex] + if (!branch || !branch.nodes || !branch.tasks) return false + + // 找到节点在分支中的索引 + const nodeIndex = branch.nodes.findIndex((n: any) => n.id === nodeId) + if (nodeIndex === -1) return false + + // 精准删除:只删除对应索引的节点和任务 + branch.nodes.splice(nodeIndex, 1) + branch.tasks.splice(nodeIndex, 1) + + // 更新 edges:移除相关边,添加新边 + if (incomingEdges.length > 0 || outgoingEdges.length > 0) { + const relatedEdgeIds = [ + ...incomingEdges.map((e: any) => e.id), + ...outgoingEdges.map((e: any) => e.id), + ] + // 过滤掉相关的旧边 + branch.edges = (branch.edges || []).filter((e: any) => !relatedEdgeIds.includes(e.id)) + // 添加新边 + if (newEdges.length > 0) { + branch.edges.push(...newEdges) + } + } + + return true + } + /** * 清除指定任务步骤和 agent 组合的所有分支 * @param taskStepId 任务步骤 ID @@ -330,6 +387,29 @@ export const useSelectionStore = defineStore('selection', () => { return result } + /** + * 从数据库删除指定节点 + * @param TaskID 大任务ID(数据库主键) + * @param stepId 小任务ID + * @param branchId 分支ID + * @param nodeId 要删除的节点ID + * @returns Promise 是否删除成功 + */ + async function deleteTaskProcessNodeFromDB( + TaskID: string, + stepId: string, + branchId: string, + nodeId: string, + edges: any[] = [], + ): Promise { + // 导入 api(避免循环导入问题) + const { default: api } = await import('@/api') + + const result = await api.deleteTaskProcessNode(TaskID, stepId, branchId, nodeId, edges) + + return result + } + /** * 从数据库恢复任务过程分支数据 * @param dbBranches 从数据库读取的任务过程分支数据 @@ -672,6 +752,7 @@ export const useSelectionStore = defineStore('selection', () => { getAllTaskProcessBranches, getTaskProcessBranchesByParent, removeTaskProcessBranch, + removeNodeFromBranch, clearTaskProcessBranches, // ==================== 任务过程分支生效状态管理方法 ==================== @@ -697,6 +778,7 @@ export const useSelectionStore = defineStore('selection', () => { restoreAgentCombinationsFromDB, saveTaskProcessBranchesToDB, deleteTaskProcessBranchFromDB, + deleteTaskProcessNodeFromDB, restoreTaskProcessBranchesFromDB, } })