feat:RESTful API架构改WebSocket架构-执行结果可以分步显示版本

This commit is contained in:
liailing1026
2026-01-22 17:22:30 +08:00
parent 1c8036adf1
commit 786c674d21
15 changed files with 2591 additions and 308 deletions

View File

@@ -1,4 +1,5 @@
import request from '@/utils/request'
import websocket from '@/utils/websocket'
import type { Agent, IApiStepTask, IRawPlanResponse, IRawStepTask } from '@/stores'
import {
mockBackendAgentSelectModifyInit,
@@ -82,7 +83,16 @@ export interface IFillAgentSelectionRequest {
}
class Api {
setAgents = (data: Pick<Agent, 'Name' | 'Profile' | 'apiUrl' | 'apiKey' | 'apiModel'>[]) => {
// 默认使用WebSocket
private useWebSocketDefault = true
setAgents = (data: Pick<Agent, 'Name' | 'Profile' | 'apiUrl' | 'apiKey' | 'apiModel'>[], useWebSocket: boolean = this.useWebSocketDefault) => {
// 如果启用WebSocket且已连接使用WebSocket
if (useWebSocket && websocket.connected) {
return websocket.send('set_agents', data)
}
// 否则使用REST API
return request({
url: '/setAgents',
data,
@@ -96,7 +106,23 @@ class Api {
apiUrl?: string
apiKey?: string
apiModel?: string
useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void
}) => {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
// 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) {
return websocket.send('generate_base_plan', {
'General Goal': data.goal,
'Initial Input Object': data.inputs,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
apiModel: data.apiModel,
}, undefined, data.onProgress)
}
// 否则使用REST API
return request<unknown, IRawPlanResponse>({
url: '/generate_basePlan',
method: 'POST',
@@ -143,13 +169,18 @@ class Api {
/**
* 优化版流式执行计划阶段1+2步骤级流式 + 动作级智能并行)
* 无依赖关系的动作并行执行,有依赖关系的动作串行执行
*
* 默认使用WebSocket如果连接失败则降级到SSE
*/
executePlanOptimized = (
plan: IRawPlanResponse,
onMessage: (event: StreamingEvent) => void,
onError?: (error: Error) => void,
onComplete?: () => void,
useWebSocket?: boolean,
) => {
const useWs = useWebSocket !== undefined ? useWebSocket : this.useWebSocketDefault
const data = {
RehearsalLog: [],
num_StepToRun: null,
@@ -174,6 +205,41 @@ class Api {
},
}
// 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) {
websocket.subscribe(
'execute_plan_optimized',
data,
// onProgress
(progressData) => {
try {
// progressData 应该已经是解析后的对象了
// 如果是字符串,说明后端发送的是 JSON 字符串,需要解析
let event: StreamingEvent
if (typeof progressData === 'string') {
event = JSON.parse(progressData)
} else {
event = progressData as StreamingEvent
}
onMessage(event)
} catch (e) {
// Failed to parse WebSocket data
}
},
// onComplete
() => {
onComplete?.()
},
// onError
(error) => {
onError?.(error)
}
)
return
}
// 否则使用原有的SSE方式
fetch('/api/executePlanOptimized', {
method: 'POST',
headers: {
@@ -215,7 +281,7 @@ class Api {
const event = JSON.parse(data)
onMessage(event)
} catch (e) {
console.error('Failed to parse SSE data:', e)
// Failed to parse SSE data
}
}
}
@@ -236,7 +302,24 @@ class Api {
Baseline_Completion: number
initialInputs: string[]
goal: string
useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void
}) => {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
// 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) {
return websocket.send('branch_plan_outline', {
branch_Number: data.branch_Number,
Modification_Requirement: data.Modification_Requirement,
Existing_Steps: data.Existing_Steps,
Baseline_Completion: data.Baseline_Completion,
'Initial Input Object': data.initialInputs,
'General Goal': data.goal,
}, undefined, data.onProgress)
}
// 否则使用REST API
return request<unknown, IRawPlanResponse>({
url: '/branch_PlanOutline',
method: 'POST',
@@ -261,7 +344,24 @@ class Api {
Baseline_Completion: number
stepTaskExisting: any
goal: string
useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void
}) => {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
// 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) {
return websocket.send('branch_task_process', {
branch_Number: data.branch_Number,
Modification_Requirement: data.Modification_Requirement,
Existing_Steps: data.Existing_Steps,
Baseline_Completion: data.Baseline_Completion,
stepTaskExisting: data.stepTaskExisting,
'General Goal': data.goal,
}, undefined, data.onProgress)
}
// 否则使用REST API
return request<unknown, BranchAction[][]>({
url: '/branch_TaskProcess',
method: 'POST',
@@ -276,38 +376,55 @@ class Api {
})
}
fillStepTask = async (data: { goal: string; stepTask: any }): Promise<IRawStepTask> => {
const response = await request<
{
'General Goal': string
stepTask: any
},
{
AgentSelection?: string[]
Collaboration_Brief_FrontEnd?: {
template: string
data: Record<string, { text: string; color: number[] }>
}
InputObject_List?: string[]
OutputObject?: string
StepName?: string
TaskContent?: string
TaskProcess?: Array<{
ID: string
ActionType: string
AgentName: string
Description: string
ImportantInput: string[]
}>
}
>({
url: '/fill_stepTask',
method: 'POST',
data: {
fillStepTask = async (data: {
goal: string
stepTask: any
useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void
}): Promise<IRawStepTask> => {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
let response: any
// 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) {
response = await websocket.send('fill_step_task', {
'General Goal': data.goal,
stepTask: data.stepTask,
},
})
}, undefined, data.onProgress)
} else {
// 否则使用REST API
response = await request<
{
'General Goal': string
stepTask: any
},
{
AgentSelection?: string[]
Collaboration_Brief_FrontEnd?: {
template: string
data: Record<string, { text: string; color: number[] }>
}
InputObject_List?: string[]
OutputObject?: string
StepName?: string
TaskContent?: string
TaskProcess?: Array<{
ID: string
ActionType: string
AgentName: string
Description: string
ImportantInput: string[]
}>
}
>({
url: '/fill_stepTask',
method: 'POST',
data: {
'General Goal': data.goal,
stepTask: data.stepTask,
},
})
}
const vec2Hsl = (color: number[]): string => {
const [h, s, l] = color
@@ -347,40 +464,15 @@ class Api {
goal: string
stepTask: IApiStepTask
agents: string[]
useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void
}): Promise<IApiStepTask> => {
const response = await request<
{
'General Goal': string
stepTask_lackTaskProcess: {
StepName: string
TaskContent: string
InputObject_List: string[]
OutputObject: string
AgentSelection: string[]
}
},
{
StepName?: string
TaskContent?: string
InputObject_List?: string[]
OutputObject?: string
AgentSelection?: string[]
TaskProcess?: Array<{
ID: string
ActionType: string
AgentName: string
Description: string
ImportantInput: string[]
}>
Collaboration_Brief_FrontEnd?: {
template: string
data: Record<string, { text: string; color: number[] }>
}
}
>({
url: '/fill_stepTask_TaskProcess',
method: 'POST',
data: {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
let response: any
// 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) {
response = await websocket.send('fill_step_task_process', {
'General Goal': data.goal,
stepTask_lackTaskProcess: {
StepName: data.stepTask.name,
@@ -389,8 +481,53 @@ class Api {
OutputObject: data.stepTask.output,
AgentSelection: data.agents,
},
},
})
}, undefined, data.onProgress)
} else {
// 否则使用REST API
response = await request<
{
'General Goal': string
stepTask_lackTaskProcess: {
StepName: string
TaskContent: string
InputObject_List: string[]
OutputObject: string
AgentSelection: string[]
}
},
{
StepName?: string
TaskContent?: string
InputObject_List?: string[]
OutputObject?: string
AgentSelection?: string[]
TaskProcess?: Array<{
ID: string
ActionType: string
AgentName: string
Description: string
ImportantInput: string[]
}>
Collaboration_Brief_FrontEnd?: {
template: string
data: Record<string, { text: string; color: number[] }>
}
}
>({
url: '/fill_stepTask_TaskProcess',
method: 'POST',
data: {
'General Goal': data.goal,
stepTask_lackTaskProcess: {
StepName: data.stepTask.name,
TaskContent: data.stepTask.content,
InputObject_List: data.stepTask.inputs,
OutputObject: data.stepTask.output,
AgentSelection: data.agents,
},
},
})
}
const vec2Hsl = (color: number[]): string => {
const [h, s, l] = color
@@ -409,7 +546,7 @@ class Api {
}
}
const process = (response.TaskProcess || []).map((action) => ({
const process = (response.TaskProcess || []).map((action: any) => ({
id: action.ID,
type: action.ActionType,
agent: action.AgentName,
@@ -437,17 +574,15 @@ class Api {
agentSelectModifyInit = async (data: {
goal: string
stepTask: any
useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void
}): Promise<Record<string, Record<string, { reason: string; score: number }>>> => {
const response = await request<
{
'General Goal': string
stepTask: any
},
Record<string, Record<string, { Reason: string; Score: number }>>
>({
url: '/agentSelectModify_init',
method: 'POST',
data: {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
let response: Record<string, Record<string, { Reason: string; Score: number }>>
// 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) {
response = await websocket.send('agent_select_modify_init', {
'General Goal': data.goal,
stepTask: {
StepName: data.stepTask.StepName || data.stepTask.name,
@@ -455,8 +590,29 @@ class Api {
InputObject_List: data.stepTask.InputObject_List || data.stepTask.inputs,
OutputObject: data.stepTask.OutputObject || data.stepTask.output,
},
},
})
}, undefined, data.onProgress)
} else {
// 否则使用REST API
response = await request<
{
'General Goal': string
stepTask: any
},
Record<string, Record<string, { Reason: string; Score: number }>>
>({
url: '/agentSelectModify_init',
method: 'POST',
data: {
'General Goal': data.goal,
stepTask: {
StepName: data.stepTask.StepName || data.stepTask.name,
TaskContent: data.stepTask.TaskContent || data.stepTask.content,
InputObject_List: data.stepTask.InputObject_List || data.stepTask.inputs,
OutputObject: data.stepTask.OutputObject || data.stepTask.output,
},
},
})
}
const transformedData: Record<string, Record<string, { reason: string; score: number }>> = {}
@@ -480,22 +636,35 @@ class Api {
*/
agentSelectModifyAddAspect = async (data: {
aspectList: string[]
useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void
}): Promise<{
aspectName: string
agentScores: Record<string, { score: number; reason: string }>
}> => {
const response = await request<
{
aspectList: string[]
},
Record<string, Record<string, { Reason: string; Score: number }>>
>({
url: '/agentSelectModify_addAspect',
method: 'POST',
data: {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
let response: Record<string, Record<string, { Reason: string; Score: number }>>
// 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) {
response = await websocket.send('agent_select_modify_add_aspect', {
aspectList: data.aspectList,
},
})
}, undefined, data.onProgress)
} else {
// 否则使用REST API
response = await request<
{
aspectList: string[]
},
Record<string, Record<string, { Reason: string; Score: number }>>
>({
url: '/agentSelectModify_addAspect',
method: 'POST',
data: {
aspectList: data.aspectList,
},
})
}
/**
* 获取新添加的维度

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769048650684" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6190" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M874.058005 149.941995a510.06838 510.06838 0 1 0 109.740156 162.738976 511.396369 511.396369 0 0 0-109.740156-162.738976z m66.278708 362.178731A428.336713 428.336713 0 1 1 512 83.663287a428.698892 428.698892 0 0 1 428.336713 428.336713z" fill="#36404f" p-id="6191"></path><path d="M417.954256 281.533601a41.046923 41.046923 0 0 0-41.77128 40.201839v385.116718a41.892007 41.892007 0 0 0 83.663287 0v-385.116718a41.167649 41.167649 0 0 0-41.892007-40.201839zM606.045744 281.533601a41.046923 41.046923 0 0 0-41.77128 40.201839v385.116718a41.892007 41.892007 0 0 0 83.663287 0v-385.116718a41.167649 41.167649 0 0 0-41.892007-40.201839z" fill="#36404f" p-id="6192"></path></svg>

After

Width:  |  Height:  |  Size: 1004 B

View File

@@ -1 +1,5 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761736278335" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5885" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M226.592 896C167.616 896 128 850.48 128 782.736V241.264C128 173.52 167.616 128 226.592 128c20.176 0 41.136 5.536 62.288 16.464l542.864 280.432C887.648 453.792 896 491.872 896 512s-8.352 58.208-64.272 87.088L288.864 879.536C267.712 890.464 246.768 896 226.592 896z m0-704.304c-31.008 0-34.368 34.656-34.368 49.568v541.472c0 14.896 3.344 49.568 34.368 49.568 9.6 0 20.88-3.2 32.608-9.248l542.864-280.432c21.904-11.328 29.712-23.232 29.712-30.608s-7.808-19.28-29.712-30.592L259.2 200.96c-11.728-6.048-23.008-9.264-32.608-9.264z" p-id="5886"></path></svg>
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1761736278335" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5885"
xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<path d="M226.592 896C167.616 896 128 850.48 128 782.736V241.264C128 173.52 167.616 128 226.592 128c20.176 0 41.136 5.536 62.288 16.464l542.864 280.432C887.648 453.792 896 491.872 896 512s-8.352 58.208-64.272 87.088L288.864 879.536C267.712 890.464 246.768 896 226.592 896z m0-704.304c-31.008 0-34.368 34.656-34.368 49.568v541.472c0 14.896 3.344 49.568 34.368 49.568 9.6 0 20.88-3.2 32.608-9.248l542.864-280.432c21.904-11.328 29.712-23.232 29.712-30.608s-7.808-19.28-29.712-30.592L259.2 200.96c-11.728-6.048-23.008-9.264-32.608-9.264z" p-id="5886"></path></svg>

Before

Width:  |  Height:  |  Size: 886 B

After

Width:  |  Height:  |  Size: 890 B

View File

@@ -1 +1,6 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761204835005" class="icon" viewBox="0 0 1171 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5692" xmlns:xlink="http://www.w3.org/1999/xlink" width="228.7109375" height="200"><path d="M502.237757 1024 644.426501 829.679301 502.237757 788.716444 502.237757 1024 502.237757 1024ZM0 566.713817 403.967637 689.088066 901.485385 266.66003 515.916344 721.68034 947.825442 855.099648 1170.285714 0 0 566.713817 0 566.713817Z" p-id="5693"></path></svg>
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1761204835005" class="icon" viewBox="0 0 1171 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="5692" xmlns:xlink="http://www.w3.org/1999/xlink" width="228.7109375" height="200">
<path d="M502.237757 1024 644.426501 829.679301 502.237757 788.716444 502.237757 1024 502.237757
1024ZM0 566.713817 403.967637 689.088066 901.485385 266.66003 515.916344 721.68034 947.825442 855.099648 1170.285714 0 0 566.713817 0 566.713817Z" p-id="5693"></path></svg>

Before

Width:  |  Height:  |  Size: 603 B

After

Width:  |  Height:  |  Size: 610 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1768992484327" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="10530" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M512 853.333333c-187.733333 0-341.333333-153.6-341.333333-341.333333s153.6-341.333333 341.333333-341.333333
341.333333 153.6 341.333333 341.333333-153.6 341.333333-341.333333 341.333333z m0-85.333333c140.8 0 256-115.2 256-256s-115.2-256-256-256-256
115.2-256 256 115.2 256 256 256z m-85.333333-341.333333h170.666666v170.666666h-170.666666v-170.666666z" fill="#ffffff" p-id="10531"></path></svg>

After

Width:  |  Height:  |  Size: 709 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769048534610" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4890" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M527.984 1001.6a480 480 0 1 1 480-480 480.384 480.384 0 0 1-480 480z m0-883.696A403.696 403.696 0 1 0 931.68 521.6 403.84 403.84 0 0 0 527.984 117.904z" fill="#36404f" p-id="4891"></path><path d="M473.136 729.6a47.088 47.088 0 0 1-18.112-3.888 38.768 38.768 0 0 1-23.056-34.992V384.384a39.632 39.632 0 0 1 23.056-34.992 46.016 46.016 0 0 1 43.632 3.888l211.568 153.168a38.72 38.72 0 0 1 16.464 31.104 37.632 37.632 0 0 1-16.464 31.104l-211.568 153.168a44.56 44.56 0 0 1-25.52 7.776z m41.168-266.704v149.296l102.896-74.64z" fill="#36404f" p-id="4892"></path></svg>

After

Width:  |  Height:  |  Size: 894 B

View File

@@ -3,6 +3,7 @@ import { ref, onMounted, computed, reactive, nextTick } from 'vue'
import SvgIcon from '@/components/SvgIcon/index.vue'
import { useAgentsStore, useConfigStore } from '@/stores'
import api from '@/api'
import websocket from '@/utils/websocket'
import { changeBriefs } from '@/utils/collaboration_Brief_FrontEnd.ts'
import { ElMessage } from 'element-plus'
import AssignmentButton from './TaskTemplate/TaskSyllabus/components/AssignmentButton.vue'
@@ -18,6 +19,10 @@ const triggerOnFocus = ref(true)
const isFocus = ref(false)
const hasAutoSearched = ref(false)
const isExpanded = ref(false)
// 添加一个状态来跟踪是否正在填充步骤数据
const isFillingSteps = ref(false)
// 存储当前填充任务的取消函数
const currentStepAbortController = ref<{ cancel: () => void } | null>(null)
// 解析URL参数
function getUrlParam(param: string): string | null {
@@ -83,6 +88,39 @@ function resetTextareaHeight() {
})
}
// 停止填充数据的处理函数
async function handleStop() {
try {
// 通过 WebSocket 发送停止信号
if (websocket.connected) {
await websocket.send('stop_generation', {
goal: searchValue.value
})
ElMessage.success('已发送停止信号,正在停止生成...')
} else {
ElMessage.warning('WebSocket 未连接,无法停止')
}
} catch (error) {
console.error('停止生成失败:', error)
ElMessage.error('停止生成失败')
} finally {
// 无论后端是否成功停止,都重置状态
isFillingSteps.value = false
currentStepAbortController.value = null
}
}
// 处理按钮点击事件
function handleButtonClick() {
if (isFillingSteps.value) {
// 如果正在填充数据,点击停止
handleStop()
} else {
// 否则开始搜索
handleSearch()
}
}
async function handleSearch() {
// 用于标记大纲是否成功加载
let outlineLoaded = false
@@ -103,6 +141,11 @@ async function handleSearch() {
inputs: []
})
// 检查是否已被停止
if (!isFillingSteps.value && currentStepAbortController.value) {
return
}
// 处理简报数据格式
outlineData['Collaboration Process'] = changeBriefs(outlineData['Collaboration Process'])
@@ -111,6 +154,9 @@ async function handleSearch() {
outlineLoaded = true
emit('search', searchValue.value)
// 开始填充步骤详情,设置状态
isFillingSteps.value = true
// 并行填充所有步骤的详情
const steps = outlineData['Collaboration Process'] || []
@@ -118,6 +164,12 @@ async function handleSearch() {
const fillStepWithRetry = async (step: any, retryCount = 0): Promise<void> => {
const maxRetries = 2 // 最多重试2次
// 检查是否已停止
if (!isFillingSteps.value) {
console.log('检测到停止信号,跳过步骤填充')
return
}
try {
if (!step.StepName) {
console.warn('步骤缺少 StepName跳过填充详情')
@@ -135,6 +187,12 @@ async function handleSearch() {
}
})
// 再次检查是否已停止(在 API 调用后)
if (!isFillingSteps.value) {
console.log('检测到停止信号,跳过更新步骤详情')
return
}
// 更新该步骤的详情到 store
updateStepDetail(step.StepName, detailedStep)
} catch (error) {
@@ -166,6 +224,9 @@ async function handleSearch() {
}
} finally {
triggerOnFocus.value = true
// 完成填充,重置状态
isFillingSteps.value = false
currentStepAbortController.value = null
// 如果大纲加载失败确保关闭loading
if (!outlineLoaded) {
agentsStore.setAgentRawPlan({ loading: false })
@@ -255,18 +316,24 @@ onMounted(() => {
class="task-button"
color="linear-gradient(to right, #00C7D2, #315AB4)"
size="large"
title="点击搜索任务"
:title="isFillingSteps ? '点击停止生成' : '点击搜索任务'"
circle
:loading="agentsStore.agentRawPlan.loading"
:disabled="!searchValue"
@click.stop="handleSearch"
@click.stop="handleButtonClick"
>
<SvgIcon
v-if="!agentsStore.agentRawPlan.loading"
v-if="!agentsStore.agentRawPlan.loading && !isFillingSteps"
icon-class="paper-plane"
size="18px"
color="#ffffff"
/>
<SvgIcon
v-if="!agentsStore.agentRawPlan.loading && isFillingSteps"
icon-class="stoprunning"
size="30px"
color="#ffffff"
/>
</el-button>
</div>
<AssignmentButton v-if="planReady" @click="openAgentAllocationDialog" />

View File

@@ -38,11 +38,12 @@ const data = computed<Data | null>(() => {
if (result.NodeId === props.nodeId) {
// LogNodeType 为 object直接渲染Content
if (result.LogNodeType === 'object') {
return {
const data = {
Description: props.nodeId,
Content: sanitize(result.content),
LogNodeType: result.LogNodeType
}
return data
}
if (!result.ActionHistory) {

View File

@@ -2,15 +2,17 @@
import { computed, onUnmounted, ref, reactive, nextTick, watch, onMounted } from 'vue'
import { throttle } from 'lodash'
import { AnchorLocations, BezierConnector } from '@jsplumb/browser-ui'
import { ElMessage, ElMessageBox } from 'element-plus'
import AdditionalOutputCard from './AdditionalOutputCard.vue'
import SvgIcon from '@/components/SvgIcon/index.vue'
import { getActionTypeDisplay, getAgentMapIcon } from '@/layout/components/config.ts'
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 { type IRawStepTask, useAgentsStore, type IRawPlanResponse } from '@/stores'
import api, { type StreamingEvent } from '@/api'
import ProcessCard from '../TaskProcess/ProcessCard.vue'
import ExecutePlan from './ExecutePlan.vue'
import websocket from '@/utils/websocket'
const emit = defineEmits<{
(e: 'refreshLine'): void
@@ -24,7 +26,106 @@ const collaborationProcess = computed(() => {
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
})
// 监听额外产物变化
// Step execution status enum
enum StepExecutionStatus {
WAITING = 'waiting', // Waiting for data
READY = 'ready', // Ready to execute
RUNNING = 'running', // Currently running
COMPLETED = 'completed', // Execution completed
FAILED = 'failed' // Execution failed
}
// Execution status for each step
const stepExecutionStatus = ref<Record<string, StepExecutionStatus>>({})
// Check if step is ready to execute (has TaskProcess data)
const isStepReady = (step: IRawStepTask) => {
return step.TaskProcess && step.TaskProcess.length > 0
}
// 判断动作是否有执行结果
const hasActionResult = (step: IRawStepTask, actionId: string) => {
const stepResult = agentsStore.executePlan.find(
r => r.NodeId === step.StepName && r.LogNodeType === 'step'
)
if (!stepResult || !stepResult.ActionHistory) {
return false
}
return stepResult.ActionHistory.some(action => action.ID === actionId)
}
// 判断 OutputObject 是否有执行结果
const hasObjectResult = (outputObject?: string) => {
if (!outputObject) return false
return agentsStore.executePlan.some(r => r.NodeId === outputObject && r.LogNodeType === 'object')
}
// Get execution status of a step
const getStepStatus = (step: IRawStepTask): StepExecutionStatus => {
const stepName = step.StepName || step.Id || ''
// If status is already recorded, return it
if (stepExecutionStatus.value[stepName]) {
return stepExecutionStatus.value[stepName]
}
// Check if has TaskProcess data
if (isStepReady(step)) {
return StepExecutionStatus.READY
} else {
return StepExecutionStatus.WAITING
}
}
// Calculate preparation status of all steps
const stepsReadyStatus = computed(() => {
const steps = collaborationProcess.value
const readySteps: string[] = []
const waitingSteps: string[] = []
steps.forEach(step => {
if (isStepReady(step)) {
readySteps.push(step.StepName || 'Unknown step')
} else {
waitingSteps.push(step.StepName || 'Unknown step')
}
})
return {
ready: readySteps,
waiting: waitingSteps,
allReady: waitingSteps.length === 0,
totalCount: steps.length,
readyCount: readySteps.length
}
})
// Watch step data changes, update waiting step status
watch(
() => collaborationProcess.value,
newSteps => {
newSteps.forEach(step => {
const stepName = step.StepName || step.Id || ''
const currentStatus = stepExecutionStatus.value[stepName]
// If step was waiting and now has data, set to ready
if (currentStatus === StepExecutionStatus.WAITING && isStepReady(step)) {
stepExecutionStatus.value[stepName] = StepExecutionStatus.READY
// 如果正在执行中,自动执行下一批就绪的步骤
if (autoExecuteEnabled.value && loading.value) {
executeNextReadyBatch()
}
}
})
},
{ deep: true }
)
// Enable auto-execution (auto-execute when new steps are ready)
const autoExecuteEnabled = ref(true)
// Watch additional outputs changes
watch(
() => agentsStore.additionalOutputs,
() => {
@@ -37,7 +138,7 @@ watch(
{ deep: true }
)
// 编辑逻辑
// Edit logic
const editMode = ref(false)
const editMap = reactive<Record<string, boolean>>({})
const editBuffer = reactive<Record<string, string | undefined>>({})
@@ -76,7 +177,7 @@ const jsplumb = new Jsplumb('task-results-main', {
}
})
// 操作折叠面板时要实时的刷新连线
// Refresh connections in real-time when collapsing panels
let timer: ReturnType<typeof setInterval> | null = null
function handleCollapse() {
if (timer) {
@@ -87,7 +188,7 @@ function handleCollapse() {
emit('refreshLine')
}, 1) as ReturnType<typeof setInterval>
// 默认三秒后已经完全打开
// Default fully open after 3 seconds
const timer1 = setTimeout(() => {
if (timer) {
clearInterval(timer)
@@ -105,7 +206,7 @@ function handleCollapse() {
})
}
// 创建内部连线
// Create internal connections
function createInternalLine(id?: string) {
const arr: ConnectArg[] = []
jsplumb.reset()
@@ -188,160 +289,340 @@ const executionProgress = ref({
currentAction: 0,
totalActions: 0,
currentStepName: '',
message: '正在执行...'
message: '准备执行任务...'
})
async function handleRun() {
// 清空之前的执行结果
agentsStore.setExecutePlan([])
const tempResults: any[] = []
// Pause functionality state
const isPaused = ref(false) // Whether paused
const isStreaming = ref(false) // Whether streaming data (backend started returning)
const isButtonLoading = ref(false) // Button brief loading state (prevent double-click)
try {
loading.value = true
// Store current step execution index (for sequential execution)
const currentExecutionIndex = ref(0)
// 使用优化版流式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
// Execute next batch of ready steps (batch execution to maintain dependencies)
async function executeNextReadyBatch() {
const steps = collaborationProcess.value
case 'action_complete':
// 动作完成
const parallelInfo = event.batch_info?.is_parallel
? ` [批次 ${event.batch_info!.batch_index + 1}, 并行 ${
event.batch_info!.batch_size
} 个]`
: ''
// Collect all ready but unexecuted steps (in order, until hitting unready step)
const readySteps: IRawStepTask[] = []
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}`
}
for (let i = 0; i < steps.length; i++) {
const step = steps[i]
if (!step) continue
console.log(
`✅ 动作 ${event.completed_actions}/${event.total_actions} 完成${parallelInfo}: ${event.action_result.ActionType} by ${event.action_result.AgentName}`
)
// 如果步骤已就绪,加入批量执行列表
if (isStepReady(step)) {
const stepName = step.StepName || step.Id || ''
const status = stepExecutionStatus.value[stepName]
// 实时更新到 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
// Only collect unexecuted steps
if (!status || status === StepExecutionStatus.READY) {
readySteps.push(step)
}
)
} catch (error) {
console.error('执行失败:', error)
executionProgress.value.message = '执行失败,请重试'
} finally {
// loading 会在 onComplete 中设置为 false
} else {
// Stop at first unready step (maintain step order)
break
}
}
if (readySteps.length > 0) {
try {
// Mark all steps to be executed as running
readySteps.forEach(step => {
const stepName = step.StepName || step.Id || ''
stepExecutionStatus.value[stepName] = StepExecutionStatus.RUNNING
})
// 构建包含所有已就绪步骤的计划数据(批量发送,保持依赖关系)
const batchPlan: IRawPlanResponse = {
'General Goal': agentsStore.agentRawPlan.data?.['General Goal'] || '',
'Initial Input Object': agentsStore.agentRawPlan.data?.['Initial Input Object'] || [],
'Collaboration Process': readySteps // Key: batch send steps
}
const tempResults: any[] = []
// Execute these steps in batch
await new Promise<void>((resolve, reject) => {
api.executePlanOptimized(
batchPlan,
// onMessage: handle each event
(event: StreamingEvent) => {
// When backend starts returning data, set isStreaming (only once)
if (!isStreaming.value) {
isStreaming.value = true
}
// If paused, ignore events
if (isPaused.value) {
return
}
switch (event.type) {
case 'step_start':
// 使用后端返回的 step_index 和 total_steps
executionProgress.value = {
currentStep: (event.step_index || 0) + 1,
totalSteps: event.total_steps || collaborationProcess.value.length,
currentAction: 0,
totalActions: 0,
currentStepName: event.step_name,
message: `正在执行步骤 ${event.step_index + 1}/${
event.total_steps || collaborationProcess.value.length
}: ${event.step_name}`
}
break
case 'action_complete':
const parallelInfo = event.batch_info?.is_parallel
? ` [并行 ${event.batch_info!.batch_size} 个动作]`
: ''
// 使用后端返回的 step_indextotal_steps 使用当前进度中的值
const stepIndexForAction = event.step_index || 0
const totalStepsValue =
executionProgress.value.totalSteps || collaborationProcess.value.length
executionProgress.value = {
...executionProgress.value,
currentAction: event.completed_actions,
totalActions: event.total_actions,
message: `步骤 ${stepIndexForAction + 1}/${totalStepsValue}: ${
event.step_name
} - 动作 ${event.completed_actions}/${event.total_actions} 完成${parallelInfo}`
}
// Update store in real-time
const existingStep = collaborationProcess.value.find(
s => s.StepName === event.step_name
)
if (existingStep) {
const currentResults = agentsStore.executePlan
const stepLogNode = currentResults.find(
r => r.NodeId === event.step_name && r.LogNodeType === 'step'
)
if (!stepLogNode) {
const newStepLog = {
LogNodeType: 'step',
NodeId: event.step_name,
InputName_List: existingStep.InputObject_List || [],
OutputName: existingStep.OutputObject || '',
chatLog: [],
inputObject_Record: [],
ActionHistory: [event.action_result]
}
tempResults.push(newStepLog)
agentsStore.setExecutePlan([...currentResults, newStepLog])
} else {
stepLogNode.ActionHistory.push(event.action_result)
agentsStore.setExecutePlan([...currentResults])
}
}
break
case 'step_complete':
stepExecutionStatus.value[event.step_name] = StepExecutionStatus.COMPLETED
// Update complete step log
const currentResults = agentsStore.executePlan
const existingLog = currentResults.find(
r => r.NodeId === event.step_name && r.LogNodeType === 'step'
)
if (existingLog) {
existingLog.ActionHistory = event.step_log_node.ActionHistory
// 触发响应式更新
agentsStore.setExecutePlan([...currentResults])
} else if (event.step_log_node) {
// 添加新的 step_log_node
agentsStore.setExecutePlan([...currentResults, event.step_log_node])
}
// 添加 object_log_node
const updatedResults = agentsStore.executePlan
if (event.object_log_node) {
agentsStore.setExecutePlan([...updatedResults, event.object_log_node])
}
break
case 'execution_complete':
// 所有步骤都标记为完成
readySteps.forEach(step => {
const stepName = step.StepName || step.Id || ''
if (stepExecutionStatus.value[stepName] !== StepExecutionStatus.COMPLETED) {
stepExecutionStatus.value[stepName] = StepExecutionStatus.COMPLETED
}
})
resolve()
break
case 'error':
console.error(' 执行错误:', event.message)
executionProgress.value.message = `执行错误: ${event.message}`
readySteps.forEach(step => {
const stepName = step.StepName || step.Id || ''
stepExecutionStatus.value[stepName] = StepExecutionStatus.FAILED
})
reject(new Error(event.message))
break
}
},
// onError
(error: Error) => {
console.error(' 流式执行错误:', error)
executionProgress.value.message = `执行失败: ${error.message}`
readySteps.forEach(step => {
const stepName = step.StepName || step.Id || ''
stepExecutionStatus.value[stepName] = StepExecutionStatus.FAILED
})
reject(error)
},
// onComplete
() => {
resolve()
}
)
})
// 批量执行成功后,递归执行下一批
await executeNextReadyBatch()
} catch (error) {
ElMessage.error('批量执行失败')
// 重置所有执行状态
loading.value = false
isPaused.value = false
isStreaming.value = false
}
} else {
// No more ready steps
loading.value = false
// 重置暂停和流式状态
isPaused.value = false
isStreaming.value = false
// Check if there are still waiting steps
const hasWaitingSteps = steps.some(step => step && !isStepReady(step))
if (hasWaitingSteps) {
const waitingStepNames = steps
.filter(step => step && !isStepReady(step))
.map(step => step?.StepName || '未知')
executionProgress.value.message = `等待 ${waitingStepNames.length} 个步骤数据填充中...`
ElMessage.info(`等待 ${waitingStepNames.length} 个步骤数据填充中...`)
} else {
executionProgress.value.message = '所有步骤已完成'
ElMessage.success('所有步骤已完成')
}
}
}
// 查看任务过程
// Pause/Resume handler
async function handlePauseResume() {
if (isPaused.value) {
// Resume execution
try {
if (websocket.connected) {
await websocket.send('resume_execution', {
goal: agentsStore.agentRawPlan.data?.['General Goal'] || ''
})
// 只有在收到成功响应后才更新状态
isPaused.value = false
ElMessage.success('已恢复执行')
} else {
ElMessage.warning('WebSocket未连接无法恢复执行')
}
} catch (error) {
ElMessage.error('恢复执行失败')
// 恢复失败时,保持原状态不变(仍然是暂停状态)
}
} else {
// Pause execution
try {
if (websocket.connected) {
await websocket.send('pause_execution', {
goal: agentsStore.agentRawPlan.data?.['General Goal'] || ''
})
// 只有在收到成功响应后才更新状态
isPaused.value = true
ElMessage.success('已暂停执行,可稍后继续')
} else {
ElMessage.warning('WebSocket未连接无法暂停')
}
} catch (error) {
ElMessage.error('暂停执行失败')
// 暂停失败时,保持原状态不变(仍然是非暂停状态)
}
}
}
// Handle execute button click
async function handleExecuteButtonClick() {
// If streaming, show pause/resume functionality
if (isStreaming.value) {
await handlePauseResume()
return
}
// Otherwise, execute normal task execution logic
await handleRun()
}
async function handleRun() {
// Check if there are ready steps
const readySteps = stepsReadyStatus.value.ready
const waitingSteps = stepsReadyStatus.value.waiting
if (readySteps.length === 0 && waitingSteps.length > 0) {
ElMessageBox.confirm(
`All ${waitingSteps.length} steps的数据还在填充中\n\n${waitingSteps.join(
'、'
)}\n\n建议等待数据填充完成后再执行。`,
'Step data not ready',
{
confirmButtonText: 'I Understand',
cancelButtonText: 'Close',
type: 'warning'
}
)
return
}
// Set button brief loading state (prevent double-click)
isButtonLoading.value = true
setTimeout(() => {
isButtonLoading.value = false
}, 1000)
// Reset pause and streaming state
isPaused.value = false
isStreaming.value = false
// Start execution
loading.value = true
currentExecutionIndex.value = 0
// Clear previous execution results and status
agentsStore.setExecutePlan([])
stepExecutionStatus.value = {}
// Start batch executing first batch of ready steps
await executeNextReadyBatch()
}
// View task process
async function handleTaskProcess() {
drawerVisible.value = true
}
// 重置执行结果
// Reset execution results
function handleRefresh() {
agentsStore.setExecutePlan([])
}
// 添加滚动状态标识
// Add scroll state indicator
const isScrolling = ref(false)
let scrollTimer: ReturnType<typeof setTimeout> | null = null
// 修改滚动处理函数
// Modify scroll handler
function handleScroll() {
isScrolling.value = true
emit('refreshLine')
@@ -357,7 +638,7 @@ function handleScroll() {
}, 300) as ReturnType<typeof setTimeout>
}
// 修改鼠标事件处理函数
// Modify mouse event handler
const handleMouseEnter = throttle(id => {
if (!isScrolling.value) {
createInternalLine(id)
@@ -374,21 +655,21 @@ function clear() {
jsplumb.reset()
}
//封装连线重绘方法
// Encapsulate line redraw method
const redrawInternalLines = (highlightId?: string) => {
// 等待 DOM 更新完成
// Waiting DOM 更新完成
nextTick(() => {
// 清除旧连线
jsplumb.reset()
// 等待 DOM 稳定后重新绘制
// Waiting DOM 稳定后重新绘制
setTimeout(() => {
createInternalLine(highlightId)
}, 100)
})
}
//监听 collaborationProcess 变化,自动重绘连线
// Watch collaborationProcess changes, auto redraw connections
watch(
() => collaborationProcess,
() => {
@@ -397,7 +678,7 @@ watch(
{ deep: true }
)
// 组件挂载后初始化连线
// Initialize connections after component mount
onMounted(() => {
// 初始化时绘制连线
nextTick(() => {
@@ -407,7 +688,7 @@ onMounted(() => {
})
})
//按钮交互状态管理
// Button interaction state management
const buttonHoverState = ref<'process' | 'execute' | 'refresh' | null>(null)
let buttonHoverTimer: ReturnType<typeof setTimeout> | null = null
const handleProcessMouseEnter = () => {
@@ -448,13 +729,13 @@ const handleButtonMouseLeave = () => {
}, 50) // 适当减少延迟时间
}
// 添加离开组件时的清理
// Cleanup when leaving component
onUnmounted(() => {
if (buttonHoverTimer) {
clearTimeout(buttonHoverTimer)
}
})
// 计算按钮类名
// Calculate button class names
const processBtnClass = computed(() => {
if (buttonHoverState.value === 'refresh' || buttonHoverState.value === 'execute') {
return 'circle'
@@ -476,7 +757,7 @@ const refreshBtnClass = computed(() => {
return agentsStore.executePlan.length > 0 ? 'ellipse' : 'circle'
})
// 计算按钮是否显示文字
// Calculate whether to show button text
const showProcessText = computed(() => {
return buttonHoverState.value === 'process'
})
@@ -490,17 +771,17 @@ const showRefreshText = computed(() => {
return buttonHoverState.value === 'refresh'
})
// 计算按钮标题
// Calculate button titles
const processBtnTitle = computed(() => {
return buttonHoverState.value === 'process' ? '任务程' : '点击查看任务程'
return buttonHoverState.value === 'process' ? '查看任务程' : '点击查看任务程'
})
const executeBtnTitle = computed(() => {
return showExecuteText.value ? '任务执行' : '点击运行'
return showExecuteText.value ? '任务执行' : '点击执行任务'
})
const refreshBtnTitle = computed(() => {
return showRefreshText.value ? '重置执行结果' : '点击重置执行状态'
return showRefreshText.value ? '重置结果' : '点击重置执行状态'
})
defineExpose({
@@ -544,7 +825,7 @@ defineExpose({
<svg-icon icon-class="refresh" />
<span v-if="showRefreshText" class="btn-text">重置</span>
</el-button>
<!-- 任务过程按钮 -->
<!-- Task Process按钮 -->
<el-button
:class="processBtnClass"
:color="variables.tertiary"
@@ -557,10 +838,10 @@ defineExpose({
<span v-if="showProcessText" class="btn-text">任务过程</span>
</el-button>
<!-- 任务执行按钮 -->
<!-- Execute按钮 -->
<el-popover
:disabled="Boolean(agentsStore.agentRawPlan.data)"
title="请先输入要执行的任务"
title="请先输入任务再执行"
:visible="showPopover"
@hide="showPopover = false"
style="order: 2"
@@ -569,14 +850,35 @@ defineExpose({
<el-button
:class="executeBtnClass"
:color="variables.tertiary"
:title="executeBtnTitle"
:disabled="!agentsStore.agentRawPlan.data || loading"
:title="isStreaming ? (isPaused ? '点击继续执行' : '点击暂停执行') : executeBtnTitle"
:disabled="
!agentsStore.agentRawPlan.data || (!isStreaming && loading) || isButtonLoading
"
@mouseenter="handleExecuteMouseEnter"
@click="handleRun"
@click="handleExecuteButtonClick"
>
<svg-icon v-if="loading" icon-class="loading" class="animate-spin" />
<!-- 按钮短暂加载状态防止双击 -->
<svg-icon v-if="isButtonLoading" icon-class="loading" class="animate-spin" />
<!-- 执行中加载状态已废弃保留以防万一 -->
<svg-icon
v-else-if="loading && !isStreaming"
icon-class="loading"
class="animate-spin"
/>
<!-- 流式传输中且未Pause显示Pause图标 -->
<svg-icon v-else-if="isStreaming && !isPaused" icon-class="Pause" size="20px" />
<!-- 流式传输中且已Pause显示播放/Resume图标 -->
<svg-icon v-else-if="isStreaming && isPaused" icon-class="video-play" size="20px" />
<!-- 默认状态显示 action 图标 -->
<svg-icon v-else icon-class="action" />
<span v-if="showExecuteText" class="btn-text">任务执行</span>
<span v-if="showExecuteText && !isStreaming" class="btn-text">任务执行</span>
<span v-else-if="isStreaming && isPaused" class="btn-text">继续执行</span>
<span v-else-if="isStreaming" class="btn-text">暂停执行</span>
</el-button>
</template>
</el-popover>
@@ -597,7 +899,7 @@ defineExpose({
</div>
</template>
<el-scrollbar height="calc(100vh - 120px)">
<el-empty v-if="!collaborationProcess.length" description="暂无任务程" />
<el-empty v-if="!collaborationProcess.length" description="暂无任务程" />
<div v-else class="process-list">
<!-- 使用ProcessCard组件显示每个AgentSelection -->
<ProcessCard
@@ -648,23 +950,26 @@ defineExpose({
v-for="item1 in item.TaskProcess"
:key="`task-results-${item.Id}-${item1.ID}`"
:name="`task-results-${item.Id}-${item1.ID}`"
:disabled="Boolean(!agentsStore.executePlan.length || loading)"
:disabled="!hasActionResult(item, item1.ID)"
@mouseenter="() => handleMouseEnter(`task-results-${item.Id}-0-${item1.ID}`)"
@mouseleave="handleMouseLeave"
>
<template v-if="loading" #icon>
<!-- 执行中且没有结果时显示 loading 图标 -->
<template v-if="loading && !hasActionResult(item, item1.ID)" #icon>
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
</template>
<!-- 没有执行计划时隐藏图标 -->
<template v-else-if="!agentsStore.executePlan.length" #icon>
<span></span>
</template>
<!-- 有结果时不提供 #icon Element Plus 显示默认箭头 -->
<template #title>
<!-- 运行之前背景颜色是var(--color-bg-detail-list),运行之后背景颜色是var(--color-bg-detail-list-run) -->
<div
class="flex items-center gap-[15px] rounded-[20px]"
:class="{
'bg-[var(--color-bg-detail-list)]': !agentsStore.executePlan.length,
'bg-[var(--color-bg-detail-list-run)]': agentsStore.executePlan.length
'bg-[var(--color-bg-detail-list)]': !hasActionResult(item, item1.ID),
'bg-[var(--color-bg-detail-list-run)]': hasActionResult(item, item1.ID)
}"
>
<!-- 右侧链接点 -->
@@ -685,9 +990,14 @@ defineExpose({
<div class="text-[16px]">
<span
:class="{
'text-[var(--color-text-result-detail)]': !agentsStore.executePlan.length,
'text-[var(--color-text-result-detail-run)]':
agentsStore.executePlan.length
'text-[var(--color-text-result-detail)]': !hasActionResult(
item,
item1.ID
),
'text-[var(--color-text-result-detail-run)]': hasActionResult(
item,
item1.ID
)
}"
>{{ item1.AgentName }}:&nbsp; &nbsp;</span
>
@@ -713,23 +1023,28 @@ defineExpose({
@click="emit('setCurrentTask', item)"
>
<!-- <div class="text-[18px]">{{ item.OutputObject }}</div>-->
<el-collapse @change="handleCollapse">
<el-collapse @change="handleCollapse" :key="agentsStore.executePlan.length">
<el-collapse-item
class="output-object"
:disabled="Boolean(!agentsStore.executePlan.length || loading)"
:disabled="!hasObjectResult(item.OutputObject)"
>
<template v-if="loading" #icon>
<!-- 执行中且没有结果时显示 loading 图标 -->
<template v-if="loading && !hasObjectResult(item.OutputObject)" #icon>
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
</template>
<!-- 没有执行计划时隐藏图标 -->
<template v-else-if="!agentsStore.executePlan.length" #icon>
<span></span>
</template>
<!-- 有结果时不提供 #icon Element Plus 显示默认箭头 -->
<template #title>
<div
class="text-[18px]"
:class="{
'text-[var(--color-text-result-detail)]': !agentsStore.executePlan.length,
'text-[var(--color-text-result-detail-run)]': agentsStore.executePlan.length
'text-[var(--color-text-result-detail)]': !hasObjectResult(item.OutputObject),
'text-[var(--color-text-result-detail-run)]': hasObjectResult(
item.OutputObject
)
}"
>
{{ item.OutputObject }}
@@ -980,7 +1295,7 @@ defineExpose({
}
}
// 圆形状态
// Circle state
.circle {
width: 40px !important;
height: 40px !important;
@@ -994,14 +1309,14 @@ defineExpose({
}
}
// 椭圆形状态
// Ellipse state
.ellipse {
height: 40px !important;
border-radius: 20px !important;
padding: 0 16px !important;
gap: 8px;
// 任务过程按钮 - 左边固定,向右展开
// Task process button - fixed left, expand right
&:nth-child(1) {
justify-content: flex-start !important;
@@ -1016,7 +1331,7 @@ defineExpose({
}
}
// 任务执行按钮 - 右边固定,向左展开
// Task execution button - fixed right, expand left
&:nth-child(2) {
justify-content: flex-end !important;

View File

@@ -41,7 +41,10 @@ const collaborationProcess = computed(() => {
// 检测是否正在填充详情(有步骤但没有 AgentSelection
const isFillingDetails = computed(() => {
const process = agentsStore.agentRawPlan.data?.['Collaboration Process'] || []
return process.length > 0 && process.some(step => !step.AgentSelection || step.AgentSelection.length === 0)
return (
process.length > 0 &&
process.some(step => !step.AgentSelection || step.AgentSelection.length === 0)
)
})
// 计算填充进度
@@ -412,39 +415,57 @@ defineExpose({
<div class="h-[1px] w-full bg-[var(--color-border-separate)] my-[8px]"></div>
<div
class="flex items-center gap-2 overflow-y-auto flex-wrap relative w-full max-h-[72px]"
class="flex items-center gap-2 flex-wrap relative w-full"
:class="!item.AgentSelection || item.AgentSelection.length === 0 ? 'min-h-[40px]' : 'overflow-y-auto max-h-[72px]'"
>
<!-- 连接到智能体库的连接点 -->
<div
class="absolute left-[-10px] top-1/2 transform -translate-y-1/2"
:id="`task-syllabus-flow-agents-${item.Id}`"
></div>
<el-tooltip
v-for="agentSelection in item.AgentSelection"
:key="agentSelection"
effect="light"
placement="right"
<!-- 未填充智能体时显示Loading -->
<div
v-if="!item.AgentSelection || item.AgentSelection.length === 0"
class="flex items-center gap-2 text-[var(--color-text-secondary)] text-[14px]"
>
<template #content>
<div class="w-[150px]">
<div class="text-[18px] font-bold">{{ agentSelection }}</div>
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
<div>
{{ item.TaskProcess.find(i => i.AgentName === agentSelection)?.Description }}
</div>
</div>
</template>
<div
class="w-[31px] h-[31px] rounded-full flex items-center justify-center"
:style="{ background: getAgentMapIcon(agentSelection).color }"
<el-icon class="is-loading" :size="20">
<Loading />
</el-icon>
<span>正在分配智能体...</span>
</div>
<!-- 已填充智能体时显示智能体列表 -->
<template v-else>
<el-tooltip
v-for="agentSelection in item.AgentSelection"
:key="agentSelection"
effect="light"
placement="right"
>
<svg-icon
:icon-class="getAgentMapIcon(agentSelection).icon"
color="#fff"
size="24px"
/>
</div>
</el-tooltip>
<template #content>
<div class="w-[150px]">
<div class="text-[18px] font-bold">{{ agentSelection }}</div>
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
<div>
{{
item.TaskProcess.find(i => i.AgentName === agentSelection)?.Description
}}
</div>
</div>
</template>
<div
class="w-[31px] h-[31px] rounded-full flex items-center justify-center"
:style="{ background: getAgentMapIcon(agentSelection).color }"
>
<svg-icon
:icon-class="getAgentMapIcon(agentSelection).icon"
color="#fff"
size="24px"
/>
</div>
</el-tooltip>
</template>
</div>
</el-card>
<!-- 产物卡片 -->

View File

@@ -7,6 +7,7 @@ import './styles/tailwindcss.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import 'virtual:svg-icons-register'
import { initService } from '@/utils/request.ts'
import websocket from '@/utils/websocket'
import { setupStore, useConfigStore } from '@/stores'
import { setupDirective } from '@/ directive'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
@@ -20,6 +21,24 @@ async function init() {
setupStore(app)
setupDirective(app)
initService()
// 初始化WebSocket连接
try {
// WebSocket需要直接连接到后端不能通过代理
const apiBaseUrl = configStore.config.apiBaseUrl || `${import.meta.env.BASE_URL || '/'}api`
// 移除 /api 后缀如果是相对路径则构造完整URL
let wsUrl = apiBaseUrl.replace(/\/api$/, '')
// 如果是相对路径(以/开头使用当前host
if (wsUrl.startsWith('/')) {
wsUrl = `${window.location.protocol}//${window.location.host}${wsUrl}`
}
console.log('🔌 Connecting to WebSocket at:', wsUrl)
await websocket.connect(wsUrl)
console.log('✅ WebSocket initialized successfully')
} catch (error) {
console.warn('⚠️ WebSocket connection failed, will use REST API fallback:', error)
}
document.title = configStore.config.centerTitle
app.use(router)
app.mount('#app')

View File

@@ -0,0 +1,270 @@
/**
* WebSocket 客户端封装
* 基于 socket.io-client 实现
*/
import { io, Socket } from 'socket.io-client'
interface WebSocketConfig {
url?: string
reconnectionAttempts?: number
reconnectionDelay?: number
timeout?: number
}
interface RequestMessage {
id: string
action: string
data: any
}
interface ResponseMessage {
id: string
status: 'success' | 'error' | 'streaming' | 'complete'
data?: any
error?: string
stage?: string
message?: string
[key: string]: any
}
interface StreamProgressCallback {
(data: any): void
}
type RequestHandler = {
resolve: (value: any) => void
reject: (error: Error) => void
timer?: ReturnType<typeof setTimeout>
onProgress?: StreamProgressCallback
}
class WebSocketClient {
private socket: Socket | null = null
private requestHandlers = new Map<string, RequestHandler>()
private streamHandlers = new Map<string, StreamProgressCallback>()
private config: Required<WebSocketConfig>
private isConnected = false
constructor() {
this.config = {
url: '',
reconnectionAttempts: 5,
reconnectionDelay: 1000,
timeout: 300000, // 5分钟超时
}
}
/**
* 连接到WebSocket服务器
*/
connect(url?: string): Promise<void> {
return new Promise((resolve, reject) => {
const wsUrl = url || this.config.url || window.location.origin
this.socket = io(wsUrl, {
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionAttempts: this.config.reconnectionAttempts,
reconnectionDelay: this.config.reconnectionDelay,
})
this.socket.on('connect', () => {
this.isConnected = true
resolve()
})
this.socket.on('connect_error', (error) => {
reject(error)
})
this.socket.on('disconnect', (reason) => {
this.isConnected = false
})
this.socket.on('connected', (data) => {
// Server connected message
})
// 监听响应消息
this.socket.on('response', (response: ResponseMessage) => {
const { id, status, data, error } = response
const handler = this.requestHandlers.get(id)
if (handler) {
// 清除超时定时器
if (handler.timer) {
clearTimeout(handler.timer)
}
if (status === 'success') {
handler.resolve(data)
} else {
handler.reject(new Error(error || 'Unknown error'))
}
// 删除处理器
this.requestHandlers.delete(id)
}
})
// 监听流式进度消息
this.socket.on('progress', (response: ResponseMessage) => {
const { id, status, data, error } = response
// 首先检查是否有对应的流式处理器
const streamCallback = this.streamHandlers.get(id)
if (streamCallback) {
if (status === 'streaming') {
// 解析 data 字段JSON 字符串)并传递给回调
try {
const parsedData = typeof data === 'string' ? JSON.parse(data) : data
streamCallback(parsedData)
} catch (e) {
// Failed to parse progress data
}
} else if (status === 'complete') {
this.streamHandlers.delete(id)
streamCallback({ type: 'complete' })
} else if (status === 'error') {
this.streamHandlers.delete(id)
streamCallback({ type: 'error', error })
}
return
}
// 检查是否有对应的普通请求处理器支持send()方法的进度回调)
const requestHandler = this.requestHandlers.get(id)
if (requestHandler && requestHandler.onProgress) {
// 解析 data 字段并传递给进度回调
try {
const parsedData = typeof data === 'string' ? JSON.parse(data) : data
requestHandler.onProgress(parsedData)
} catch (e) {
// Failed to parse progress data
}
}
})
// 心跳检测
this.socket.on('pong', () => {
// Pong received
})
})
}
/**
* 发送请求(双向通信,支持可选的进度回调)
*/
send(action: string, data: any, timeout?: number, onProgress?: StreamProgressCallback): Promise<any> {
if (!this.socket || !this.isConnected) {
return Promise.reject(new Error('WebSocket未连接'))
}
return new Promise((resolve, reject) => {
const requestId = `${action}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
// 设置超时
const timeoutMs = timeout || this.config.timeout
const timer = setTimeout(() => {
if (this.requestHandlers.has(requestId)) {
this.requestHandlers.delete(requestId)
reject(new Error(`Request timeout: ${action}`))
}
}, timeoutMs)
// 保存处理器(包含可选的进度回调)
this.requestHandlers.set(requestId, { resolve, reject, timer, onProgress })
// 发送消息
this.socket!.emit(action, {
id: requestId,
action,
data,
} as RequestMessage)
})
}
/**
* 订阅流式数据
*/
subscribe(
action: string,
data: any,
onProgress: StreamProgressCallback,
onComplete?: () => void,
onError?: (error: Error) => void,
): void {
if (!this.socket || !this.isConnected) {
onError?.(new Error('WebSocket未连接'))
return
}
const requestId = `${action}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
// 保存流式处理器
const wrappedCallback = (progressData: any) => {
if (progressData?.type === 'complete') {
this.streamHandlers.delete(requestId)
onComplete?.()
} else {
onProgress(progressData)
}
}
this.streamHandlers.set(requestId, wrappedCallback)
// 发送订阅请求
this.socket.emit(action, {
id: requestId,
action,
data,
} as RequestMessage)
}
/**
* 发送心跳
*/
ping(): void {
if (this.socket && this.isConnected) {
this.socket.emit('ping')
}
}
/**
* 断开连接
*/
disconnect(): void {
if (this.socket) {
// 清理所有处理器
this.requestHandlers.forEach((handler) => {
if (handler.timer) {
clearTimeout(handler.timer)
}
})
this.requestHandlers.clear()
this.streamHandlers.clear()
this.socket.disconnect()
this.socket = null
this.isConnected = false
}
}
/**
* 获取连接状态
*/
get connected(): boolean {
return this.isConnected && this.socket?.connected === true
}
/**
* 获取Socket ID
*/
get id(): string | undefined {
return this.socket?.id
}
}
// 导出单例
export default new WebSocketClient()