diff --git a/backend/server.py b/backend/server.py index 970193e..bd6471a 100644 --- a/backend/server.py +++ b/backend/server.py @@ -2381,6 +2381,118 @@ def handle_save_task_process_branches(data): }) +@socketio.on('delete_task_process_branch') +def handle_delete_task_process_branch(data): + """ + WebSocket版本:删除任务过程分支数据 + + 请求格式: + { + "id": "request-id", + "action": "delete_task_process_branch", + "data": { + "task_id": "task-id", // 大任务ID(数据库主键) + "stepId": "step-id", // 小任务ID + "branchId": "branch-id" // 要删除的分支ID + } + } + + 数据库存储格式: + { + "branches": { + "flow_branches": [...], + "task_process_branches": { + "stepId-1": { + '["AgentA","AgentB"]': [{...分支数据...}] + } + } + } + } + """ + 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') + + if not task_id or not step_id or not branch_id: + emit('response', { + 'id': request_id, + 'status': 'error', + 'error': '缺少必要参数:task_id, stepId, branchId' + }) + 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: + # 获取该 stepId 下的所有 agent 分支 + step_branches = task_process_branches[step_id] + + # 遍历所有 agentGroupKey,删除对应分支 + for agent_key, branches_list in step_branches.items(): + # 过滤掉要删除的分支 + filtered_branches = [b for b in branches_list if b.get('id') != branch_id] + + if len(filtered_branches) != len(branches_list): + # 有分支被删除,更新数据 + if filtered_branches: + step_branches[agent_key] = filtered_branches + else: + # 如果该 agentKey 下没有分支了,删除该 key + del step_branches[agent_key] + + # 如果该 stepId 下没有分支了,删除该 stepId + if not step_branches: + del task_process_branches[step_id] + + # 更新 branches 数据 + existing_branches['task_process_branches'] = task_process_branches + + # 直接更新数据库 + existing_task.branches = existing_branches + db.flush() + db.commit() + + print(f"[delete_task_process_branch] 删除成功,task_id={task_id}, step_id={step_id}, branch_id={branch_id}") + + emit('response', { + 'id': request_id, + 'status': 'success', + 'data': { + "message": "分支删除成功", + "deleted_branch_id": branch_id + } + }) + return + + # 如果找不到对应的分支 + print(f"[delete_task_process_branch] 警告: 找不到要删除的分支,task_id={task_id}, step_id={step_id}, branch_id={branch_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 1a96562..a37140d 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -677,6 +677,37 @@ class Api { } } + /** + * 删除任务过程分支数据 + * @param TaskID 大任务ID + * @param stepId 小任务ID + * @param branchId 要删除的分支ID + * @returns 是否删除成功 + */ + deleteTaskProcessBranch = async ( + TaskID: string, + stepId: string, + branchId: string, + ): Promise => { + if (!websocket.connected) { + throw new Error('WebSocket未连接') + } + + try { + const rawResponse = await websocket.send('delete_task_process_branch', { + task_id: TaskID, + stepId, + branchId, + }) + + 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 8354008..0af2a09 100644 --- a/frontend/src/layout/components/Main/TaskTemplate/TaskProcess/components/PlanTask.vue +++ b/frontend/src/layout/components/Main/TaskTemplate/TaskProcess/components/PlanTask.vue @@ -1,5 +1,5 @@ + + + @@ -1208,7 +1416,6 @@ const saveTaskProcessBranchesToDB = async () => { .vue-flow-container { width: 100%; height: 100%; - background-color: var(--color-bg-three); } .empty-state { @@ -1223,17 +1430,55 @@ const saveTaskProcessBranchesToDB = async () => { display: flex; align-items: center; justify-content: center; - width: 60px; - height: 60px; - background: var(--color-bg-three); - border: 2px solid var(--color-border); - border-radius: 50%; + min-width: 130px; + max-width: 180px; + height: 90px; + background: var(--color-card-bg-task); + border-radius: 12px; cursor: pointer; transition: all 0.2s ease; + // 始终显示渐变边框 + background: linear-gradient(var(--color-card-bg-task), var(--color-card-bg-task)) padding-box, + linear-gradient(to right, var(--color-accent), var(--color-accent-secondary)) border-box; + border: 2px solid transparent; + box-sizing: border-box; + &:hover { - background: var(--color-bg-three-hover); - transform: scale(1.05); + background: linear-gradient(var(--color-card-bg-task-hover), var(--color-card-bg-task-hover)) + padding-box, + linear-gradient(to right, var(--color-accent), var(--color-accent-secondary)) border-box; + border: 2px solid transparent; + box-shadow: var(--color-card-shadow-hover); + } + + .root-node-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + max-width: 120px; + overflow: hidden; + } + + .agent-name { + font-size: 13px; + font-weight: 600; + color: var(--color-text); + white-space: nowrap; + text-align: center; + } + + .agent-role { + font-size: 11px; + font-weight: 500; + color: var(--color-text-secondary); + display: -webkit-box; + -webkit-line-clamp: 3; + line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + text-align: center; } } @@ -1291,16 +1536,6 @@ const saveTaskProcessBranchesToDB = async () => { z-index: 10; box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2); - &:hover { - background: #409eff; - transform: translateX(-50%) scale(1.1); - box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3); - - .el-icon { - color: #fff; - } - } - &:active { transform: translateX(-50%) scale(0.95); } @@ -1309,99 +1544,6 @@ const saveTaskProcessBranchesToDB = async () => { &.cancel-btn { border-color: #f56c6c; box-shadow: 0 2px 8px rgba(245, 108, 108, 0.2); - - &:hover { - background: #f56c6c; - box-shadow: 0 4px 12px rgba(245, 108, 108, 0.3); - } - } -} - -// 外部输入容器 -.external-input-container { - position: absolute; - bottom: -80px; - left: 50%; - transform: translateX(-50%); - width: 260px; - background: #fff; - border: 1px solid #dcdfe6; - border-radius: 8px; - padding: 12px; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); - z-index: 100; - animation: slideDown 0.2s ease-out; - - &::before { - content: ''; - position: absolute; - top: -6px; - left: 50%; - transform: translateX(-50%) rotate(45deg); - width: 12px; - height: 12px; - background: #fff; - border-left: 1px solid #dcdfe6; - border-top: 1px solid #dcdfe6; - } -} - -@keyframes slideDown { - from { - opacity: 0; - transform: translateX(-50%) translateY(-10px); - } - to { - opacity: 1; - transform: translateX(-50%) translateY(0); - } -} - -.branch-input { - width: 100%; - - :deep(.el-input__wrapper) { - background: transparent; - border: 1px solid #dcdfe6; - border-radius: 4px; - box-shadow: none; - - &:hover { - border-color: #c0c4cc; - } - - &.is-focus { - border-color: #409eff; - box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2); - } - - .el-input__inner { - font-size: 13px; - color: #000; - padding-right: 40px; - } - } -} - -// 提交图标样式 -.submit-icon { - cursor: pointer; - transition: all 0.2s ease; - display: inline-flex; - align-items: center; - justify-content: center; - - &:hover:not(.is-disabled) { - transform: scale(1.1); - } - - &:active:not(.is-disabled) { - transform: scale(0.95); - } - - &.is-disabled { - opacity: 0.4; - cursor: not-allowed; } } @@ -1411,26 +1553,65 @@ const saveTaskProcessBranchesToDB = async () => { align-items: center; justify-content: center; border-radius: 12px; - width: 60px; - height: 60px; - opacity: 0.9; + min-width: 130px; + max-width: 180px; + height: 90px; + background-color: var(--color-card-bg-task); + border: 2px solid var(--color-node-border); transition: all 0.2s ease; + cursor: pointer; - // 分支选中高亮样式 + &:hover { + background-color: var(--color-card-bg-task-hover); + border-color: var(--color-node-border); + box-shadow: var(--color-card-shadow-hover); + } + + // 分支选中高亮样式(渐变边框) &.is-branch-selected { - box-shadow: 0 0 0 3px #000, 0 0 20px rgba(0, 0, 0, 0.5); + background: linear-gradient(var(--color-card-bg-task), var(--color-card-bg-task)) padding-box, + linear-gradient(to right, var(--color-accent), var(--color-accent-secondary)) border-box; + border: 2px solid transparent; + box-sizing: border-box; z-index: 10; } + .agent-node-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + } + + .agent-info { + display: none; + } + + .agent-name { + font-size: 13px; + font-weight: 600; + color: var(--color-text); + white-space: nowrap; + text-align: center; + } + + .agent-role { + font-size: 12px; + font-weight: 500; + white-space: nowrap; + text-align: center; + } + .agent-avatar { display: flex; align-items: center; justify-content: center; - width: 40px; - height: 40px; + width: 36px; + height: 36px; border-radius: 50%; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); transition: transform 0.2s ease; + flex-shrink: 0; &:hover { transform: scale(1.1); @@ -1514,6 +1695,49 @@ const saveTaskProcessBranchesToDB = async () => { word-break: break-word; margin-top: 4px; } + +// 节点删除按钮 +.node-delete-btn { + 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; + + // 左侧位置 + &.left { + left: -85px; + top: 50%; + transform: translateY(-50%); + } + + &:hover { + background-color: #ff4d4f; + } +} + +// 边样式 - 与PlanModification.vue保持一致 +::deep(.vue-flow__edge.selected .vue-flow__edge-path) { + stroke: #409eff; +} + +::deep(.vue-flow__edge.edge-normal .vue-flow__edge-path) { + stroke: var(--color-node-border); + stroke-width: 2; + stroke-dasharray: 5 5; +} + +::deep(.vue-flow__edge.edge-highlighted .vue-flow__edge-path) { + stroke: var(--color-node-active) !important; + stroke-width: 2; + stroke-dasharray: 5 5; +}