feat:任务执行结果性能优化

This commit is contained in:
liailing1026
2026-01-21 15:18:15 +08:00
parent c5848410c1
commit 45314b7be6
9 changed files with 952 additions and 338 deletions

View File

@@ -84,6 +84,9 @@ function resetTextareaHeight() {
}
async function handleSearch() {
// 用于标记大纲是否成功加载
let outlineLoaded = false
try {
triggerOnFocus.value = false
if (!searchValue.value) {
@@ -93,17 +96,102 @@ async function handleSearch() {
emit('search-start')
agentsStore.resetAgent()
agentsStore.setAgentRawPlan({ loading: true })
const data = await api.generateBasePlan({
// 获取大纲
const outlineData = await api.generateBasePlan({
goal: searchValue.value,
inputs: []
})
data['Collaboration Process'] = changeBriefs(data['Collaboration Process'])
agentsStore.setAgentRawPlan({ data })
// 处理简报数据格式
outlineData['Collaboration Process'] = changeBriefs(outlineData['Collaboration Process'])
// 立即显示大纲
agentsStore.setAgentRawPlan({ data: outlineData, loading: false })
outlineLoaded = true
emit('search', searchValue.value)
// 并行填充所有步骤的详情
const steps = outlineData['Collaboration Process'] || []
// 带重试的填充函数
const fillStepWithRetry = async (step: any, retryCount = 0): Promise<void> => {
const maxRetries = 2 // 最多重试2次
try {
if (!step.StepName) {
console.warn('步骤缺少 StepName跳过填充详情')
return
}
// 使用现有的 fillStepTask API 填充每个步骤的详情
const detailedStep = await api.fillStepTask({
goal: searchValue.value,
stepTask: {
StepName: step.StepName,
TaskContent: step.TaskContent,
InputObject_List: step.InputObject_List,
OutputObject: step.OutputObject
}
})
// 更新该步骤的详情到 store
updateStepDetail(step.StepName, detailedStep)
} catch (error) {
console.error(
`填充步骤 ${step.StepName} 详情失败 (尝试 ${retryCount + 1}/${maxRetries + 1}):`,
error
)
// 如果未达到最大重试次数,延迟后重试
if (retryCount < maxRetries) {
console.log(`正在重试步骤 ${step.StepName}...`)
// 延迟1秒后重试避免立即重试导致同样的问题
await new Promise(resolve => setTimeout(resolve, 1000))
return fillStepWithRetry(step, retryCount + 1)
} else {
console.error(`步骤 ${step.StepName}${maxRetries + 1} 次尝试后仍然失败`)
}
}
}
// // 为每个步骤并行填充详情(选人+过程)
// const fillPromises = steps.map(step => fillStepWithRetry(step))
// // 等待所有步骤填充完成(包括重试)
// await Promise.all(fillPromises)
// 串行填充所有步骤的详情(避免字段混乱)
for (const step of steps) {
await fillStepWithRetry(step)
}
} finally {
triggerOnFocus.value = true
agentsStore.setAgentRawPlan({ loading: false })
// 如果大纲加载失败确保关闭loading
if (!outlineLoaded) {
agentsStore.setAgentRawPlan({ loading: false })
}
}
}
// 辅助函数:更新单个步骤的详情
function updateStepDetail(stepId: string, detailedStep: any) {
const planData = agentsStore.agentRawPlan.data
if (!planData) return
const collaborationProcess = planData['Collaboration Process']
if (!collaborationProcess) return
const index = collaborationProcess.findIndex((s: any) => s.StepName === stepId)
if (index !== -1 && collaborationProcess[index]) {
// 保持响应式更新 - 使用 Vue 的响应式系统
Object.assign(collaborationProcess[index], {
AgentSelection: detailedStep.AgentSelection || [],
TaskProcess: detailedStep.TaskProcess || [],
Collaboration_Brief_frontEnd: detailedStep.Collaboration_Brief_frontEnd || {
template: '',
data: {}
}
})
}
}
@@ -215,8 +303,6 @@ onMounted(() => {
:deep(.el-autocomplete .el-textarea .el-textarea__inner) {
overflow-y: auto !important;
min-height: 56px !important;
// overflow-y: hidden;
// background-color: black;
}
}

View File

@@ -8,7 +8,7 @@ import { getActionTypeDisplay, getAgentMapIcon } from '@/layout/components/confi
import { type ConnectArg, Jsplumb } from '@/layout/components/Main/TaskTemplate/utils.ts'
import variables from '@/styles/variables.module.scss'
import { type IRawStepTask, useAgentsStore } from '@/stores'
import api from '@/api'
import api, { type StreamingEvent } from '@/api'
import ProcessCard from '../TaskProcess/ProcessCard.vue'
import ExecutePlan from './ExecutePlan.vue'
@@ -182,14 +182,148 @@ function createInternalLine(id?: string) {
}
const loading = ref(false)
const executionProgress = ref({
currentStep: 0,
totalSteps: 0,
currentAction: 0,
totalActions: 0,
currentStepName: '',
message: '正在执行...'
})
async function handleRun() {
// 清空之前的执行结果
agentsStore.setExecutePlan([])
const tempResults: any[] = []
try {
loading.value = true
const d = await api.executePlan(agentsStore.agentRawPlan.data!)
agentsStore.setExecutePlan(d)
// 使用优化版流式API阶段1+2步骤级流式 + 动作级智能并行)
api.executePlanOptimized(
agentsStore.agentRawPlan.data!,
// onMessage: 处理每个事件
(event: StreamingEvent) => {
switch (event.type) {
case 'step_start':
// 步骤开始
executionProgress.value = {
currentStep: event.step_index + 1,
totalSteps: event.total_steps,
currentAction: 0,
totalActions: 0,
currentStepName: event.step_name,
message: `正在执行步骤 ${event.step_index + 1}/${event.total_steps}: ${
event.step_name
}`
}
console.log(
`📋 步骤 ${event.step_index + 1}/${event.total_steps} 开始: ${event.step_name}`
)
break
case 'action_complete':
// 动作完成
const parallelInfo = event.batch_info?.is_parallel
? ` [批次 ${event.batch_info!.batch_index + 1}, 并行 ${
event.batch_info!.batch_size
} 个]`
: ''
executionProgress.value = {
...executionProgress.value,
currentAction: event.completed_actions,
totalActions: event.total_actions,
message: `步骤 ${event.step_index + 1}/${executionProgress.value.totalSteps}: ${
event.step_name
} - 动作 ${event.completed_actions}/${event.total_actions} 完成${parallelInfo}`
}
console.log(
`✅ 动作 ${event.completed_actions}/${event.total_actions} 完成${parallelInfo}: ${event.action_result.ActionType} by ${event.action_result.AgentName}`
)
// 实时更新到 store找到对应的步骤并添加 ActionHistory
const step = collaborationProcess.value.find(s => s.StepName === event.step_name)
if (step) {
const stepLogNode = tempResults.find(
r => r.NodeId === event.step_name && r.LogNodeType === 'step'
)
if (!stepLogNode) {
// 创建步骤日志节点
const newStepLog = {
LogNodeType: 'step',
NodeId: event.step_name,
InputName_List: step.InputObject_List || [],
OutputName: step.OutputObject || '',
chatLog: [],
inputObject_Record: [],
ActionHistory: [event.action_result]
}
tempResults.push(newStepLog)
} else {
// 追加动作结果
stepLogNode.ActionHistory.push(event.action_result)
}
// 更新 store
agentsStore.setExecutePlan([...tempResults])
}
break
case 'step_complete':
// 步骤完成
console.log(`🎯 步骤完成: ${event.step_name}`)
// 更新步骤日志节点
const existingStepLog = tempResults.find(
r => r.NodeId === event.step_name && r.LogNodeType === 'step'
)
if (existingStepLog) {
existingStepLog.ActionHistory = event.step_log_node.ActionHistory
} else {
tempResults.push(event.step_log_node)
}
// 添加对象日志节点
tempResults.push(event.object_log_node)
// 更新 store
agentsStore.setExecutePlan([...tempResults])
break
case 'execution_complete':
// 执行完成
executionProgress.value.message = `执行完成!共 ${event.total_steps} 个步骤`
console.log(`🎉 执行完成,共 ${event.total_steps} 个步骤`)
// 确保所有结果都保存到 store
agentsStore.setExecutePlan([...tempResults])
break
case 'error':
// 错误
console.error('❌ 执行错误:', event.message)
executionProgress.value.message = `执行错误: ${event.message}`
break
}
},
// onError: 处理错误
(error: Error) => {
console.error('❌ 流式执行错误:', error)
executionProgress.value.message = `执行失败: ${error.message}`
},
// onComplete: 执行完成
() => {
console.log('✅ 流式执行完成')
loading.value = false
}
)
} catch (error) {
console.error('执行失败:', error)
executionProgress.value.message = '执行失败,请重试'
} finally {
loading.value = false
// loading 会在 onComplete 中设置为 false
}
}
@@ -381,6 +515,15 @@ defineExpose({
id="task-results"
:class="{ 'is-running': agentsStore.executePlan.length > 0 }"
>
<!-- 执行进度提示 -->
<div v-if="loading" class="execution-progress-hint">
<el-icon class="is-loading"><Loading /></el-icon>
<span>{{ executionProgress.message }}</span>
<span v-if="executionProgress.totalSteps > 0" class="progress">
{{ executionProgress.currentStep }}/{{ executionProgress.totalSteps }}
</span>
</div>
<!-- 标题与执行按钮 -->
<div class="text-[18px] font-bold mb-[7px] flex justify-between items-center px-[20px]">
<span class="text-[var(--color-text-title-header)]">执行结果</span>
@@ -606,6 +749,48 @@ defineExpose({
</template>
<style scoped lang="scss">
// 执行进度提示样式
.execution-progress-hint {
position: fixed;
top: 80px;
right: 20px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color);
border-radius: 8px;
padding: 12px 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 8px;
z-index: 1000;
animation: slideInRight 0.3s ease-out;
max-width: 400px;
.message {
flex: 1;
font-size: 14px;
color: var(--el-text-color-primary);
}
.progress {
color: var(--el-color-primary);
font-weight: bold;
margin-left: 4px;
font-size: 14px;
}
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
#task-results.is-running {
--color-bg-detail-list: var(--color-bg-detail-list-run); // 直接指向 100 % 版本
}

View File

@@ -25,20 +25,20 @@ defineEmits<{
<!-- 背景那一根线 -->
<div class="h-full bg-[var(--color-bg-flow)] w-[5px]">
<!-- 顶部加号区域 -->
<div
<!-- <div
v-if="!isAdding"
v-dev-only
class="plus-area mt-[35px] ml-[-15px] w-[34px] h-[34px] flex items-center justify-center cursor-pointer rounded-full"
@click="$emit('start-add-output')"
>
<!-- 加号图标 -->
<svg-icon
> -->
<!-- 加号图标 -->
<!-- <svg-icon
icon-class="plus"
color="var(--color-text)"
size="20px"
class="plus-icon opacity-0 transition-opacity duration-200"
/>
</div>
</div> -->
<!-- 线底部的小圆球 -->
<div
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-flow)] w-[15px] h-[15px] rounded-full"

View File

@@ -1,5 +1,12 @@
<template>
<div class="plan-modification">
<!-- 全局加载提示 -->
<div v-if="branchFillingStatus.isFilling" class="branch-loading-hint">
<el-icon class="is-loading"><Loading /></el-icon>
<span>{{ branchFillingStatus.message }}</span>
<span class="progress">{{ branchFillingStatus.current }}/{{ branchFillingStatus.total }}</span>
</div>
<div
v-loading="agentsStore.agentRawPlan.loading || branchLoading"
class="flow-wrapper"
@@ -49,6 +56,7 @@ import { VueFlow, useVueFlow } from '@vue-flow/core'
import type { Node, Edge } from '@vue-flow/core'
import { useAgentsStore, useSelectionStore, type IRawStepTask } from '@/stores'
import { ElMessage } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import api from '@/api'
import '@vue-flow/core/dist/style.css'
import '@vue-flow/core/dist/theme-default.css'
@@ -65,6 +73,14 @@ const selectionStore = useSelectionStore()
// Mock 数据开关
const USE_MOCK_DATA = false
// 分支详情填充状态
const branchFillingStatus = ref({
isFilling: false,
current: 0,
total: 0,
message: ''
})
// 获取协作流程数据
const collaborationProcess = computed(() => {
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
@@ -782,8 +798,6 @@ const handleAddBranch = async (taskId: string, branchContent: string) => {
})
// 直接获取协作流程数据
// newTasks = response?.[0] || []
// 直接获取协作流程数据:兼容返回数组或对象的情况,优先处理二维数组返回
if (Array.isArray(response)) {
// 可能是二维数组
newTasks = (response as any[])[0] || []
@@ -793,121 +807,169 @@ const handleAddBranch = async (taskId: string, branchContent: string) => {
} else {
newTasks = []
}
// ========== 填充每个任务的 TaskProcess ==========
for (let i = 0; i < newTasks.length; i++) {
const task = newTasks[i]
if (!task) continue
try {
const filledTask = await api.fillStepTask({
goal: generalGoal,
stepTask: task
})
// 更新任务的 AgentSelection 和 TaskProcess
newTasks[i]!.AgentSelection = filledTask.AgentSelection
newTasks[i]!.TaskProcess = filledTask.TaskProcess
newTasks[i]!.Collaboration_Brief_frontEnd = filledTask.Collaboration_Brief_frontEnd
} catch (error) {
console.error(error)
// ========== 立即创建节点显示大纲 ==========
if (newTasks.length > 0) {
const branchBaseX = START_X + ROOT_WIDTH + NODE_GAP
const branchBaseY = START_Y + 300
const timestamp = Date.now()
const taskNodeIds: string[] = []
// 为每个任务创建节点(只显示大纲)
newTasks.forEach((task, index) => {
const taskNodeId = `branch-task-${taskId}-${timestamp}-${index}`
taskNodeIds.push(taskNodeId)
const taskX = branchBaseX + index * (NODE_WIDTH + NODE_GAP)
const taskY = branchBaseY
const newTaskNode: Node = {
id: taskNodeId,
type: 'task',
position: { x: taskX, y: taskY },
data: {
task,
isEditing: false,
editingContent: '',
isBranchTask: true,
branchId: String(timestamp),
updateKey: Date.now()
}
}
nodes.value.push(newTaskNode)
newBranchNodes.push(newTaskNode)
// 创建连接边
if (index === 0) {
const newEdge: Edge = {
id: `edge-${taskId}-${taskNodeId}`,
source: taskId,
target: taskNodeId,
sourceHandle: 'bottom',
targetHandle: 'left',
type: 'smoothstep',
animated: true,
style: { stroke: '#409eff', strokeWidth: 2, strokeDasharray: '5,5' },
markerEnd: {
type: 'arrow' as any,
color: '#43a8aa',
width: 20,
height: 20,
strokeWidth: 2
}
}
edges.value.push(newEdge)
newBranchEdges.push(newEdge)
} else {
const prevTaskNodeId = taskNodeIds[index - 1]
const newEdge: Edge = {
id: `edge-${prevTaskNodeId}-${taskNodeId}`,
source: prevTaskNodeId,
target: taskNodeId,
sourceHandle: 'right',
targetHandle: 'left',
type: 'smoothstep',
animated: true,
style: { stroke: '#409eff', strokeWidth: 2 },
markerEnd: {
type: 'arrow' as any,
color: '#43a8aa',
width: 20,
height: 20,
strokeWidth: 2
}
}
edges.value.push(newEdge)
newBranchEdges.push(newEdge)
}
})
// 保存分支到 store此时只有大纲
selectionStore.addFlowBranch({
parentNodeId: taskId,
branchContent: branchContent,
branchType: 'root',
nodes: newBranchNodes,
edges: newBranchEdges,
tasks: JSON.parse(JSON.stringify(newTasks))
})
ElMessage.success('任务大纲分支创建成功')
// 适应视图,让用户先看到大纲
nextTick(() => {
setTimeout(() => {
fit({ padding: 100, duration: 300 })
}, 100)
})
// ========== 关闭遮罩层Loading ==========
branchLoading.value = false
// ========== 异步填充详情(后台进行)==========
branchFillingStatus.value = {
isFilling: true,
current: 0,
total: newTasks.length,
message: '正在生成分支任务协作流程...'
}
// 带重试的填充函数
const fillStepWithRetry = async (task: IRawStepTask, index: number, retryCount = 0): Promise<void> => {
const maxRetries = 2
try {
const filledTask = await api.fillStepTask({
goal: generalGoal,
stepTask: task
})
// 更新任务的 AgentSelection 和 TaskProcess
newTasks[index]!.AgentSelection = filledTask.AgentSelection
newTasks[index]!.TaskProcess = filledTask.TaskProcess
newTasks[index]!.Collaboration_Brief_frontEnd = filledTask.Collaboration_Brief_frontEnd
// 更新节点数据
const taskNodeId = taskNodeIds[index]
updateNode(taskNodeId, {
data: {
...findNode(taskNodeId)?.data,
task: { ...newTasks[index] },
updateKey: Date.now()
}
})
// 更新进度
branchFillingStatus.value.current = index + 1
} catch (error) {
console.error(`填充分支任务 ${task.StepName} 详情失败 (尝试 ${retryCount + 1}/${maxRetries + 1}):`, error)
if (retryCount < maxRetries) {
// 延迟后重试
await new Promise(resolve => setTimeout(resolve, 1000))
return fillStepWithRetry(task, index, retryCount + 1)
} else {
console.error(`分支任务 ${task.StepName}${maxRetries + 1} 次尝试后仍然失败`)
}
}
}
// 串行填充所有任务的详情(避免并发问题导致字段混乱)
for (let i = 0; i < newTasks.length; i++) {
await fillStepWithRetry(newTasks[i], i)
}
// 更新 store 中的任务数据
const allBranches = selectionStore.getAllFlowBranches()
const currentBranch = allBranches.find(b => b.branchContent === branchContent && b.parentNodeId === taskId)
if (currentBranch) {
currentBranch.tasks = JSON.parse(JSON.stringify(newTasks))
}
branchFillingStatus.value.isFilling = false
}
ElMessage.success('任务大纲分支创建成功')
}
// 创建多个任务节点,每个任务显示为单独的流程卡片
if (newTasks.length > 0) {
// 分支任务与主流程任务对齐:从第一个主流程任务的位置开始
const branchBaseX = START_X + ROOT_WIDTH + NODE_GAP
const branchBaseY = START_Y + 300 // 向下偏移,与主流程垂直排列
const timestamp = Date.now()
const taskNodeIds: string[] = []
// 为每个任务创建节点
newTasks.forEach((task, index) => {
const taskNodeId = `branch-task-${taskId}-${timestamp}-${index}`
taskNodeIds.push(taskNodeId)
// 计算位置:横向排列,与主流程任务对齐
const taskX = branchBaseX + index * (NODE_WIDTH + NODE_GAP)
const taskY = branchBaseY
// 创建任务节点(只显示 StepName
const newTaskNode: Node = {
id: taskNodeId,
type: 'task',
position: { x: taskX, y: taskY },
data: {
task,
isEditing: false,
editingContent: '',
isBranchTask: true, // 标记为分支任务,可用于特殊样式
branchId: String(timestamp), // 添加分支ID用于识别和分组
updateKey: Date.now() // 添加更新时间戳,用于强制组件重新渲染
}
}
nodes.value.push(newTaskNode)
newBranchNodes.push(newTaskNode)
// 创建连接边
if (index === 0) {
// 连接到父节点
const newEdge: Edge = {
id: `edge-${taskId}-${taskNodeId}`,
source: taskId,
target: taskNodeId,
sourceHandle: 'bottom',
targetHandle: 'left',
type: 'smoothstep',
animated: true,
style: { stroke: '#409eff', strokeWidth: 2, strokeDasharray: '5,5' },
markerEnd: {
type: 'arrow' as any,
color: '#43a8aa',
width: 20,
height: 20,
strokeWidth: 2
}
}
edges.value.push(newEdge)
newBranchEdges.push(newEdge)
} else {
// 后续任务连接到前一个任务(左右连接)
const prevTaskNodeId = taskNodeIds[index - 1]
const newEdge: Edge = {
id: `edge-${prevTaskNodeId}-${taskNodeId}`,
source: prevTaskNodeId,
target: taskNodeId,
sourceHandle: 'right',
targetHandle: 'left',
type: 'smoothstep',
animated: true,
style: { stroke: '#409eff', strokeWidth: 2 },
markerEnd: {
type: 'arrow' as any,
color: '#43a8aa',
width: 20,
height: 20,
strokeWidth: 2
}
}
edges.value.push(newEdge)
newBranchEdges.push(newEdge)
}
})
}
// 保存分支到 store
if (newBranchNodes.length > 0) {
selectionStore.addFlowBranch({
parentNodeId: taskId,
branchContent: branchContent,
branchType: 'root',
nodes: newBranchNodes,
edges: newBranchEdges,
tasks: JSON.parse(JSON.stringify(newTasks))
})
}
} else {
// ========== 任务节点级别分支 ==========
@@ -1085,119 +1147,173 @@ const handleAddBranch = async (taskId: string, branchContent: string) => {
} else {
newTasks = []
}
for (let i = 0; i < newTasks.length; i++) {
const task = newTasks[i]
if (!task) continue
try {
const filledTask = await api.fillStepTask({
goal: generalGoal,
stepTask: task
})
// 更新任务的 AgentSelection 和 TaskProcess
newTasks[i]!.AgentSelection = filledTask.AgentSelection
newTasks[i]!.TaskProcess = filledTask.TaskProcess
newTasks[i]!.Collaboration_Brief_frontEnd = filledTask.Collaboration_Brief_frontEnd
} catch (error) {
console.error(error)
// ========== 立即创建节点显示大纲 ==========
if (newTasks.length > 0) {
// 计算分支起始位置:在父任务节点的右下方
const branchBaseX = parentNode.position.x + NODE_WIDTH + NODE_GAP
const branchBaseY = parentNode.position.y + 300 // 向下偏移,增加距离避免重叠
const timestamp = Date.now()
const taskNodeIds: string[] = []
// 为每个任务创建节点(只显示大纲)
newTasks.forEach((task, index) => {
const taskNodeId = `branch-task-${taskId}-${timestamp}-${index}`
taskNodeIds.push(taskNodeId)
// 计算位置:横向排列
const taskX = branchBaseX + index * (NODE_WIDTH + NODE_GAP)
const taskY = branchBaseY
// 创建任务节点
const newTaskNode: Node = {
id: taskNodeId,
type: 'task',
position: { x: taskX, y: taskY },
data: {
task,
isEditing: false,
editingContent: '',
isBranchTask: true,
updateKey: Date.now()
}
}
nodes.value.push(newTaskNode)
newBranchNodes.push(newTaskNode)
// 创建连接边
if (index === 0) {
// 第一个任务连接到父节点(从父任务底部到第一个分支任务左边)
const newEdge: Edge = {
id: `edge-${taskId}-${taskNodeId}`,
source: taskId,
target: taskNodeId,
sourceHandle: 'bottom',
targetHandle: 'left',
type: 'smoothstep',
animated: true,
style: { stroke: '#67c23a', strokeWidth: 2, strokeDasharray: '5,5' },
markerEnd: {
type: 'arrow' as any,
color: '#43a8aa',
width: 20,
height: 20,
strokeWidth: 2
}
}
edges.value.push(newEdge)
newBranchEdges.push(newEdge)
} else {
// 后续任务连接到前一个任务(左右连接)
const prevTaskNodeId = taskNodeIds[index - 1]
const newEdge: Edge = {
id: `edge-${prevTaskNodeId}-${taskNodeId}`,
source: prevTaskNodeId,
target: taskNodeId,
sourceHandle: 'right',
targetHandle: 'left',
type: 'smoothstep',
animated: true,
style: { stroke: '#67c23a', strokeWidth: 2 },
markerEnd: {
type: 'arrow' as any,
color: '#43a8aa',
width: 20,
height: 20,
strokeWidth: 2
}
}
edges.value.push(newEdge)
newBranchEdges.push(newEdge)
}
})
// 保存分支数据到 store此时只有大纲
selectionStore.addFlowBranch({
parentNodeId: taskId,
branchContent: branchContent,
branchType: 'task',
nodes: newBranchNodes,
edges: newBranchEdges,
tasks: JSON.parse(JSON.stringify(newTasks))
})
ElMessage.success('任务大纲分支创建成功')
// 适应视图,让用户先看到大纲
nextTick(() => {
setTimeout(() => {
fit({ padding: 100, duration: 300 })
}, 100)
})
// ========== 关闭遮罩层Loading ==========
branchLoading.value = false
// ========== 异步填充详情(后台进行)==========
branchFillingStatus.value = {
isFilling: true,
current: 0,
total: newTasks.length,
message: '正在生成分支任务协作流程...'
}
// 带重试的填充函数
const fillStepWithRetry = async (task: IRawStepTask, index: number, retryCount = 0): Promise<void> => {
const maxRetries = 2
try {
const filledTask = await api.fillStepTask({
goal: generalGoal,
stepTask: task
})
// 更新任务的 AgentSelection 和 TaskProcess
newTasks[index]!.AgentSelection = filledTask.AgentSelection
newTasks[index]!.TaskProcess = filledTask.TaskProcess
newTasks[index]!.Collaboration_Brief_frontEnd = filledTask.Collaboration_Brief_frontEnd
// 更新节点数据
const taskNodeId = taskNodeIds[index]
updateNode(taskNodeId, {
data: {
...findNode(taskNodeId)?.data,
task: { ...newTasks[index] },
updateKey: Date.now()
}
})
// 更新进度
branchFillingStatus.value.current = index + 1
} catch (error) {
console.error(`填充分支任务 ${task.StepName} 详情失败 (尝试 ${retryCount + 1}/${maxRetries + 1}):`, error)
if (retryCount < maxRetries) {
// 延迟后重试
await new Promise(resolve => setTimeout(resolve, 1000))
return fillStepWithRetry(task, index, retryCount + 1)
} else {
console.error(`分支任务 ${task.StepName}${maxRetries + 1} 次尝试后仍然失败`)
}
}
}
// 串行填充所有任务的详情(避免并发问题导致字段混乱)
for (let i = 0; i < newTasks.length; i++) {
await fillStepWithRetry(newTasks[i], i)
}
// 更新 store 中的任务数据
const allBranches = selectionStore.getAllFlowBranches()
const currentBranch = allBranches.find(b => b.branchContent === branchContent && b.parentNodeId === taskId)
if (currentBranch) {
currentBranch.tasks = JSON.parse(JSON.stringify(newTasks))
}
branchFillingStatus.value.isFilling = false
}
ElMessage.success('任务大纲分支创建成功')
}
// 创建多个任务节点,每个任务显示为单独的流程卡片
if (newTasks.length > 0) {
// 计算分支起始位置:在父任务节点的右下方
const branchBaseX = parentNode.position.x + NODE_WIDTH + NODE_GAP
const branchBaseY = parentNode.position.y + 300 // 向下偏移,增加距离避免重叠
const timestamp = Date.now()
const taskNodeIds: string[] = []
// 为每个任务创建节点
newTasks.forEach((task, index) => {
const taskNodeId = `branch-task-${taskId}-${timestamp}-${index}`
taskNodeIds.push(taskNodeId)
// 计算位置:横向排列
const taskX = branchBaseX + index * (NODE_WIDTH + NODE_GAP)
const taskY = branchBaseY
// 创建任务节点(只显示 StepName
const newTaskNode: Node = {
id: taskNodeId,
type: 'task',
position: { x: taskX, y: taskY },
data: {
task,
isEditing: false,
editingContent: '',
isBranchTask: true, // 标记为分支任务,可用于特殊样式
updateKey: Date.now() // 添加更新时间戳,用于强制组件重新渲染
}
}
nodes.value.push(newTaskNode)
newBranchNodes.push(newTaskNode)
// 创建连接边
if (index === 0) {
// 第一个任务连接到父节点(从父任务底部到第一个分支任务左边)
const newEdge: Edge = {
id: `edge-${taskId}-${taskNodeId}`,
source: taskId,
target: taskNodeId,
sourceHandle: 'bottom',
targetHandle: 'left',
type: 'smoothstep',
animated: true,
style: { stroke: '#67c23a', strokeWidth: 2, strokeDasharray: '5,5' },
markerEnd: {
type: 'arrow' as any,
color: '#43a8aa',
width: 20,
height: 20,
strokeWidth: 2
}
}
edges.value.push(newEdge)
newBranchEdges.push(newEdge)
} else {
// 后续任务连接到前一个任务(左右连接)
const prevTaskNodeId = taskNodeIds[index - 1]
const newEdge: Edge = {
id: `edge-${prevTaskNodeId}-${taskNodeId}`,
source: prevTaskNodeId,
target: taskNodeId,
sourceHandle: 'right',
targetHandle: 'left',
type: 'smoothstep',
animated: true,
style: { stroke: '#67c23a', strokeWidth: 2 },
markerEnd: {
type: 'arrow' as any,
color: '#43a8aa',
width: 20,
height: 20,
strokeWidth: 2
}
}
edges.value.push(newEdge)
newBranchEdges.push(newEdge)
}
})
}
// 保存分支数据到 store
if (newBranchNodes.length > 0) {
selectionStore.addFlowBranch({
parentNodeId: taskId,
branchContent: branchContent,
branchType: 'task',
nodes: newBranchNodes,
edges: newBranchEdges,
tasks: JSON.parse(JSON.stringify(newTasks))
})
}
}
@@ -1297,4 +1413,38 @@ defineExpose({
:deep(.vue-flow__edge.selected .vue-flow__edge-path) {
stroke: #409eff;
}
// 分支加载提示样式
.branch-loading-hint {
position: fixed;
top: 80px;
right: 20px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color);
border-radius: 8px;
padding: 12px 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 8px;
z-index: 1000;
animation: slideInRight 0.3s ease-out;
.progress {
color: var(--el-color-primary);
font-weight: bold;
margin-left: 4px;
}
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
</style>

View File

@@ -5,6 +5,7 @@ import { type ConnectArg, Jsplumb } from '@/layout/components/Main/TaskTemplate/
import { type IRawStepTask, useAgentsStore } from '@/stores'
import { computed, ref, nextTick, watch, onMounted } from 'vue'
import { AnchorLocations } from '@jsplumb/browser-ui'
import { Loading } from '@element-plus/icons-vue'
import MultiLineTooltip from '@/components/MultiLineTooltip/index.vue'
import Bg from './Bg.vue'
import BranchButton from './components/BranchButton.vue'
@@ -37,6 +38,22 @@ const collaborationProcess = computed(() => {
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
})
// 检测是否正在填充详情(有步骤但没有 AgentSelection
const isFillingDetails = computed(() => {
const process = agentsStore.agentRawPlan.data?.['Collaboration Process'] || []
return process.length > 0 && process.some(step => !step.AgentSelection || step.AgentSelection.length === 0)
})
// 计算填充进度
const completedSteps = computed(() => {
const process = agentsStore.agentRawPlan.data?.['Collaboration Process'] || []
return process.filter(step => step.AgentSelection && step.AgentSelection.length > 0).length
})
const totalSteps = computed(() => {
return agentsStore.agentRawPlan.data?.['Collaboration Process']?.length || 0
})
// 编辑状态管理
const editingTaskId = ref<string | null>(null)
const editingContent = ref('')
@@ -258,6 +275,13 @@ defineExpose({
任务大纲
</div>
<!-- 加载详情提示 -->
<div v-if="isFillingDetails" class="detail-loading-hint">
<el-icon class="is-loading"><Loading /></el-icon>
<span>正在生成任务协作流程...</span>
<span class="progress">{{ completedSteps }}/{{ totalSteps }}</span>
</div>
<div
v-loading="agentsStore.agentRawPlan.loading"
class="flex-1 w-full overflow-y-auto relative"
@@ -556,6 +580,40 @@ defineExpose({
}
}
// 加载详情提示样式
.detail-loading-hint {
position: fixed;
top: 80px;
right: 20px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color);
border-radius: 8px;
padding: 12px 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 8px;
z-index: 1000;
animation: slideInRight 0.3s ease-out;
.progress {
color: var(--el-color-primary);
font-weight: bold;
margin-left: 4px;
}
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
// 输入框样式
:deep(.el-input__wrapper) {
background: transparent;