feat:RESTful API架构改WebSocket架构-执行结果可以分步显示版本
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取新添加的维度
|
||||
|
||||
1
frontend/src/assets/icons/Pause.svg
Normal 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 |
@@ -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 |
@@ -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 |
7
frontend/src/assets/icons/stoprunning.svg
Normal 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 |
1
frontend/src/assets/icons/video-play.svg
Normal 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 |
@@ -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" />
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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_index,total_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 }}: </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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
<!-- 产物卡片 -->
|
||||
|
||||
@@ -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')
|
||||
|
||||
270
frontend/src/utils/websocket.ts
Normal 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()
|
||||