feat:RESTful API架构改WebSocket架构-执行结果可以分步显示版本
@@ -5,6 +5,7 @@ from typing import List, Dict, Set, Generator, Any
|
|||||||
import AgentCoord.RehearsalEngine_V2.Action as Action
|
import AgentCoord.RehearsalEngine_V2.Action as Action
|
||||||
import AgentCoord.util as util
|
import AgentCoord.util as util
|
||||||
from termcolor import colored
|
from termcolor import colored
|
||||||
|
from AgentCoord.RehearsalEngine_V2.execution_state import execution_state_manager
|
||||||
|
|
||||||
|
|
||||||
# ==================== 配置参数 ====================
|
# ==================== 配置参数 ====================
|
||||||
@@ -236,6 +237,14 @@ async def execute_step_async_streaming(
|
|||||||
|
|
||||||
# 分批执行动作
|
# 分批执行动作
|
||||||
for batch_index, batch_indices in enumerate(batches):
|
for batch_index, batch_indices in enumerate(batches):
|
||||||
|
# 在每个批次执行前检查暂停状态
|
||||||
|
print(f"🔍 [DEBUG] 步骤 {StepName}: 批次 {batch_index+1}/{len(batches)} 执行前,检查暂停状态...")
|
||||||
|
should_continue = await execution_state_manager.async_check_pause()
|
||||||
|
if not should_continue:
|
||||||
|
# 用户请求停止,中断执行
|
||||||
|
util.print_colored("🛑 用户请求停止执行", "red")
|
||||||
|
return
|
||||||
|
|
||||||
batch_size = len(batch_indices)
|
batch_size = len(batch_indices)
|
||||||
|
|
||||||
if batch_size > 1:
|
if batch_size > 1:
|
||||||
@@ -316,13 +325,19 @@ def executePlan_streaming(
|
|||||||
AgentProfile_Dict: Dict
|
AgentProfile_Dict: Dict
|
||||||
) -> Generator[str, None, None]:
|
) -> Generator[str, None, None]:
|
||||||
"""
|
"""
|
||||||
执行计划(流式返回版本)
|
执行计划(流式返回版本,支持暂停/恢复)
|
||||||
返回生成器,每次返回 SSE 格式的字符串
|
返回生成器,每次返回 SSE 格式的字符串
|
||||||
|
|
||||||
使用方式:
|
使用方式:
|
||||||
for event in executePlan_streaming(...):
|
for event in executePlan_streaming(...):
|
||||||
yield event
|
yield event
|
||||||
"""
|
"""
|
||||||
|
# 初始化执行状态
|
||||||
|
general_goal = plan.get("General Goal", "")
|
||||||
|
execution_state_manager.start_execution(general_goal)
|
||||||
|
|
||||||
|
print(colored(f"⏸️ 执行状态管理器已启动,支持暂停/恢复", "green"))
|
||||||
|
|
||||||
# 准备执行
|
# 准备执行
|
||||||
KeyObjects = {}
|
KeyObjects = {}
|
||||||
finishedStep_index = -1
|
finishedStep_index = -1
|
||||||
@@ -349,6 +364,17 @@ def executePlan_streaming(
|
|||||||
"""异步生产者:每收到一个事件就放入队列"""
|
"""异步生产者:每收到一个事件就放入队列"""
|
||||||
try:
|
try:
|
||||||
for step_index, stepDescrip in enumerate(steps_to_run):
|
for step_index, stepDescrip in enumerate(steps_to_run):
|
||||||
|
# 在每个步骤执行前检查暂停状态
|
||||||
|
should_continue = await execution_state_manager.async_check_pause()
|
||||||
|
if not should_continue:
|
||||||
|
# 用户请求停止,中断执行
|
||||||
|
print(colored("🛑 用户请求停止执行", "red"))
|
||||||
|
await queue.put({
|
||||||
|
"type": "error",
|
||||||
|
"message": "执行已被用户停止"
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
# 执行单个步骤(流式)
|
# 执行单个步骤(流式)
|
||||||
async for event in execute_step_async_streaming(
|
async for event in execute_step_async_streaming(
|
||||||
stepDescrip,
|
stepDescrip,
|
||||||
@@ -358,7 +384,21 @@ def executePlan_streaming(
|
|||||||
step_index,
|
step_index,
|
||||||
total_steps
|
total_steps
|
||||||
):
|
):
|
||||||
|
# 检查是否需要停止
|
||||||
|
if execution_state_manager.is_stopped():
|
||||||
|
await queue.put({
|
||||||
|
"type": "error",
|
||||||
|
"message": "执行已被用户停止"
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
await queue.put(event) # ← 立即放入队列
|
await queue.put(event) # ← 立即放入队列
|
||||||
|
except Exception as e:
|
||||||
|
# 发送错误信息
|
||||||
|
await queue.put({
|
||||||
|
"type": "error",
|
||||||
|
"message": f"执行出错: {str(e)}"
|
||||||
|
})
|
||||||
finally:
|
finally:
|
||||||
await queue.put(None) # ← 发送完成信号
|
await queue.put(None) # ← 发送完成信号
|
||||||
|
|
||||||
@@ -386,12 +426,13 @@ def executePlan_streaming(
|
|||||||
# 等待生产者任务完成
|
# 等待生产者任务完成
|
||||||
loop.run_until_complete(producer_task)
|
loop.run_until_complete(producer_task)
|
||||||
|
|
||||||
# 发送完成信号
|
# 如果不是被停止的,发送完成信号
|
||||||
complete_event = json.dumps({
|
if not execution_state_manager.is_stopped():
|
||||||
"type": "execution_complete",
|
complete_event = json.dumps({
|
||||||
"total_steps": total_steps
|
"type": "execution_complete",
|
||||||
}, ensure_ascii=False)
|
"total_steps": total_steps
|
||||||
yield f"data: {complete_event}\n\n"
|
}, ensure_ascii=False)
|
||||||
|
yield f"data: {complete_event}\n\n"
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# 清理任务
|
# 清理任务
|
||||||
@@ -399,3 +440,5 @@ def executePlan_streaming(
|
|||||||
if not producer_task.done():
|
if not producer_task.done():
|
||||||
producer_task.cancel()
|
producer_task.cancel()
|
||||||
loop.close()
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
190
backend/AgentCoord/RehearsalEngine_V2/execution_state.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
"""
|
||||||
|
全局执行状态管理器
|
||||||
|
用于支持任务的暂停、恢复和停止功能
|
||||||
|
使用轮询检查机制,确保线程安全
|
||||||
|
"""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class ExecutionStatus(Enum):
|
||||||
|
"""执行状态枚举"""
|
||||||
|
RUNNING = "running" # 正在运行
|
||||||
|
PAUSED = "paused" # 已暂停
|
||||||
|
STOPPED = "stopped" # 已停止
|
||||||
|
IDLE = "idle" # 空闲
|
||||||
|
|
||||||
|
|
||||||
|
class ExecutionStateManager:
|
||||||
|
"""
|
||||||
|
全局执行状态管理器(单例模式)
|
||||||
|
|
||||||
|
功能:
|
||||||
|
- 管理任务执行状态(运行/暂停/停止)
|
||||||
|
- 使用轮询检查机制,避免异步事件的线程问题
|
||||||
|
- 提供线程安全的状态查询和修改接口
|
||||||
|
"""
|
||||||
|
|
||||||
|
_instance: Optional['ExecutionStateManager'] = None
|
||||||
|
_lock = threading.Lock()
|
||||||
|
|
||||||
|
def __new__(cls):
|
||||||
|
"""单例模式"""
|
||||||
|
if cls._instance is None:
|
||||||
|
with cls._lock:
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance._initialized = False
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""初始化状态管理器"""
|
||||||
|
if self._initialized:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._initialized = True
|
||||||
|
self._status = ExecutionStatus.IDLE
|
||||||
|
self._current_goal: Optional[str] = None # 当前执行的任务目标
|
||||||
|
# 使用简单的布尔标志,而不是 asyncio.Event
|
||||||
|
self._should_pause = False
|
||||||
|
self._should_stop = False
|
||||||
|
|
||||||
|
def get_status(self) -> ExecutionStatus:
|
||||||
|
"""获取当前执行状态"""
|
||||||
|
with self._lock:
|
||||||
|
return self._status
|
||||||
|
|
||||||
|
def set_goal(self, goal: str):
|
||||||
|
"""设置当前执行的任务目标"""
|
||||||
|
with self._lock:
|
||||||
|
self._current_goal = goal
|
||||||
|
|
||||||
|
def get_goal(self) -> Optional[str]:
|
||||||
|
"""获取当前执行的任务目标"""
|
||||||
|
with self._lock:
|
||||||
|
return self._current_goal
|
||||||
|
|
||||||
|
def start_execution(self, goal: str):
|
||||||
|
"""开始执行"""
|
||||||
|
with self._lock:
|
||||||
|
self._status = ExecutionStatus.RUNNING
|
||||||
|
self._current_goal = goal
|
||||||
|
self._should_pause = False
|
||||||
|
self._should_stop = False
|
||||||
|
print(f"🚀 [DEBUG] start_execution: 状态设置为 RUNNING, goal={goal}")
|
||||||
|
|
||||||
|
def pause_execution(self) -> bool:
|
||||||
|
"""
|
||||||
|
暂停执行
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功暂停
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
if self._status != ExecutionStatus.RUNNING:
|
||||||
|
print(f"⚠️ [DEBUG] pause_execution: 当前状态不是RUNNING,而是 {self._status}")
|
||||||
|
return False
|
||||||
|
self._status = ExecutionStatus.PAUSED
|
||||||
|
self._should_pause = True
|
||||||
|
print(f"⏸️ [DEBUG] pause_execution: 状态设置为PAUSED, should_pause=True")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def resume_execution(self) -> bool:
|
||||||
|
"""
|
||||||
|
恢复执行
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功恢复
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
if self._status != ExecutionStatus.PAUSED:
|
||||||
|
print(f"⚠️ [DEBUG] resume_execution: 当前状态不是PAUSED,而是 {self._status}")
|
||||||
|
return False
|
||||||
|
self._status = ExecutionStatus.RUNNING
|
||||||
|
self._should_pause = False
|
||||||
|
print(f"▶️ [DEBUG] resume_execution: 状态设置为RUNNING, should_pause=False")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def stop_execution(self) -> bool:
|
||||||
|
"""
|
||||||
|
停止执行
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功停止
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
if self._status in [ExecutionStatus.IDLE, ExecutionStatus.STOPPED]:
|
||||||
|
return False
|
||||||
|
self._status = ExecutionStatus.STOPPED
|
||||||
|
self._should_stop = True
|
||||||
|
self._should_pause = False
|
||||||
|
print(f"🛑 [DEBUG] stop_execution: 状态设置为STOPPED")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""重置状态为空闲"""
|
||||||
|
with self._lock:
|
||||||
|
self._status = ExecutionStatus.IDLE
|
||||||
|
self._current_goal = None
|
||||||
|
self._should_pause = False
|
||||||
|
self._should_stop = False
|
||||||
|
print(f"🔄 [DEBUG] reset: 状态重置为IDLE")
|
||||||
|
|
||||||
|
async def async_check_pause(self):
|
||||||
|
"""
|
||||||
|
异步检查是否需要暂停(轮询方式)
|
||||||
|
|
||||||
|
如果处于暂停状态,会阻塞当前协程直到恢复或停止
|
||||||
|
应该在执行循环的关键点调用此方法
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 如果返回True表示应该继续执行,False表示应该停止
|
||||||
|
"""
|
||||||
|
# 使用轮询检查,避免异步事件问题
|
||||||
|
while True:
|
||||||
|
# 检查停止标志
|
||||||
|
if self._should_stop:
|
||||||
|
print("🛑 [DEBUG] async_check_pause: 检测到停止信号")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查暂停状态
|
||||||
|
if self._should_pause:
|
||||||
|
# 处于暂停状态,等待恢复
|
||||||
|
print("⏸️ [DEBUG] async_check_pause: 检测到暂停,等待恢复...")
|
||||||
|
await asyncio.sleep(0.1) # 短暂睡眠,避免占用CPU
|
||||||
|
|
||||||
|
# 如果恢复,继续执行
|
||||||
|
if not self._should_pause:
|
||||||
|
print("▶️ [DEBUG] async_check_pause: 从暂停中恢复!")
|
||||||
|
continue
|
||||||
|
# 如果停止了,返回
|
||||||
|
if self._should_stop:
|
||||||
|
return False
|
||||||
|
# 继续等待
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 既没有停止也没有暂停,可以继续执行
|
||||||
|
return True
|
||||||
|
|
||||||
|
def is_paused(self) -> bool:
|
||||||
|
"""检查是否处于暂停状态"""
|
||||||
|
with self._lock:
|
||||||
|
return self._status == ExecutionStatus.PAUSED
|
||||||
|
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
"""检查是否正在运行"""
|
||||||
|
with self._lock:
|
||||||
|
return self._status == ExecutionStatus.RUNNING
|
||||||
|
|
||||||
|
def is_stopped(self) -> bool:
|
||||||
|
"""检查是否已停止"""
|
||||||
|
with self._lock:
|
||||||
|
return self._status == ExecutionStatus.STOPPED
|
||||||
|
|
||||||
|
|
||||||
|
# 全局单例实例
|
||||||
|
execution_state_manager = ExecutionStateManager()
|
||||||
1174
backend/server.py
@@ -1,4 +1,5 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
import websocket from '@/utils/websocket'
|
||||||
import type { Agent, IApiStepTask, IRawPlanResponse, IRawStepTask } from '@/stores'
|
import type { Agent, IApiStepTask, IRawPlanResponse, IRawStepTask } from '@/stores'
|
||||||
import {
|
import {
|
||||||
mockBackendAgentSelectModifyInit,
|
mockBackendAgentSelectModifyInit,
|
||||||
@@ -82,7 +83,16 @@ export interface IFillAgentSelectionRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Api {
|
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({
|
return request({
|
||||||
url: '/setAgents',
|
url: '/setAgents',
|
||||||
data,
|
data,
|
||||||
@@ -96,7 +106,23 @@ class Api {
|
|||||||
apiUrl?: string
|
apiUrl?: string
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
apiModel?: 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>({
|
return request<unknown, IRawPlanResponse>({
|
||||||
url: '/generate_basePlan',
|
url: '/generate_basePlan',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -143,13 +169,18 @@ class Api {
|
|||||||
/**
|
/**
|
||||||
* 优化版流式执行计划(阶段1+2:步骤级流式 + 动作级智能并行)
|
* 优化版流式执行计划(阶段1+2:步骤级流式 + 动作级智能并行)
|
||||||
* 无依赖关系的动作并行执行,有依赖关系的动作串行执行
|
* 无依赖关系的动作并行执行,有依赖关系的动作串行执行
|
||||||
|
*
|
||||||
|
* 默认使用WebSocket,如果连接失败则降级到SSE
|
||||||
*/
|
*/
|
||||||
executePlanOptimized = (
|
executePlanOptimized = (
|
||||||
plan: IRawPlanResponse,
|
plan: IRawPlanResponse,
|
||||||
onMessage: (event: StreamingEvent) => void,
|
onMessage: (event: StreamingEvent) => void,
|
||||||
onError?: (error: Error) => void,
|
onError?: (error: Error) => void,
|
||||||
onComplete?: () => void,
|
onComplete?: () => void,
|
||||||
|
useWebSocket?: boolean,
|
||||||
) => {
|
) => {
|
||||||
|
const useWs = useWebSocket !== undefined ? useWebSocket : this.useWebSocketDefault
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
RehearsalLog: [],
|
RehearsalLog: [],
|
||||||
num_StepToRun: null,
|
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', {
|
fetch('/api/executePlanOptimized', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -215,7 +281,7 @@ class Api {
|
|||||||
const event = JSON.parse(data)
|
const event = JSON.parse(data)
|
||||||
onMessage(event)
|
onMessage(event)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to parse SSE data:', e)
|
// Failed to parse SSE data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,7 +302,24 @@ class Api {
|
|||||||
Baseline_Completion: number
|
Baseline_Completion: number
|
||||||
initialInputs: string[]
|
initialInputs: string[]
|
||||||
goal: 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>({
|
return request<unknown, IRawPlanResponse>({
|
||||||
url: '/branch_PlanOutline',
|
url: '/branch_PlanOutline',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -261,7 +344,24 @@ class Api {
|
|||||||
Baseline_Completion: number
|
Baseline_Completion: number
|
||||||
stepTaskExisting: any
|
stepTaskExisting: any
|
||||||
goal: 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_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[][]>({
|
return request<unknown, BranchAction[][]>({
|
||||||
url: '/branch_TaskProcess',
|
url: '/branch_TaskProcess',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -276,38 +376,55 @@ class Api {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fillStepTask = async (data: { goal: string; stepTask: any }): Promise<IRawStepTask> => {
|
fillStepTask = async (data: {
|
||||||
const response = await request<
|
goal: string
|
||||||
{
|
stepTask: any
|
||||||
'General Goal': string
|
useWebSocket?: boolean
|
||||||
stepTask: any
|
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void
|
||||||
},
|
}): Promise<IRawStepTask> => {
|
||||||
{
|
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
|
||||||
AgentSelection?: string[]
|
let response: any
|
||||||
Collaboration_Brief_FrontEnd?: {
|
|
||||||
template: string
|
// 如果启用WebSocket且已连接,使用WebSocket
|
||||||
data: Record<string, { text: string; color: number[] }>
|
if (useWs && websocket.connected) {
|
||||||
}
|
response = await websocket.send('fill_step_task', {
|
||||||
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,
|
'General Goal': data.goal,
|
||||||
stepTask: data.stepTask,
|
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 vec2Hsl = (color: number[]): string => {
|
||||||
const [h, s, l] = color
|
const [h, s, l] = color
|
||||||
@@ -347,40 +464,15 @@ class Api {
|
|||||||
goal: string
|
goal: string
|
||||||
stepTask: IApiStepTask
|
stepTask: IApiStepTask
|
||||||
agents: string[]
|
agents: string[]
|
||||||
|
useWebSocket?: boolean
|
||||||
|
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void
|
||||||
}): Promise<IApiStepTask> => {
|
}): Promise<IApiStepTask> => {
|
||||||
const response = await request<
|
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
|
||||||
{
|
let response: any
|
||||||
'General Goal': string
|
|
||||||
stepTask_lackTaskProcess: {
|
// 如果启用WebSocket且已连接,使用WebSocket
|
||||||
StepName: string
|
if (useWs && websocket.connected) {
|
||||||
TaskContent: string
|
response = await websocket.send('fill_step_task_process', {
|
||||||
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,
|
'General Goal': data.goal,
|
||||||
stepTask_lackTaskProcess: {
|
stepTask_lackTaskProcess: {
|
||||||
StepName: data.stepTask.name,
|
StepName: data.stepTask.name,
|
||||||
@@ -389,8 +481,53 @@ class Api {
|
|||||||
OutputObject: data.stepTask.output,
|
OutputObject: data.stepTask.output,
|
||||||
AgentSelection: data.agents,
|
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 vec2Hsl = (color: number[]): string => {
|
||||||
const [h, s, l] = color
|
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,
|
id: action.ID,
|
||||||
type: action.ActionType,
|
type: action.ActionType,
|
||||||
agent: action.AgentName,
|
agent: action.AgentName,
|
||||||
@@ -437,17 +574,15 @@ class Api {
|
|||||||
agentSelectModifyInit = async (data: {
|
agentSelectModifyInit = async (data: {
|
||||||
goal: string
|
goal: string
|
||||||
stepTask: any
|
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 }>>> => {
|
}): Promise<Record<string, Record<string, { reason: string; score: number }>>> => {
|
||||||
const response = await request<
|
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
|
||||||
{
|
let response: Record<string, Record<string, { Reason: string; Score: number }>>
|
||||||
'General Goal': string
|
|
||||||
stepTask: any
|
// 如果启用WebSocket且已连接,使用WebSocket
|
||||||
},
|
if (useWs && websocket.connected) {
|
||||||
Record<string, Record<string, { Reason: string; Score: number }>>
|
response = await websocket.send('agent_select_modify_init', {
|
||||||
>({
|
|
||||||
url: '/agentSelectModify_init',
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
'General Goal': data.goal,
|
'General Goal': data.goal,
|
||||||
stepTask: {
|
stepTask: {
|
||||||
StepName: data.stepTask.StepName || data.stepTask.name,
|
StepName: data.stepTask.StepName || data.stepTask.name,
|
||||||
@@ -455,8 +590,29 @@ class Api {
|
|||||||
InputObject_List: data.stepTask.InputObject_List || data.stepTask.inputs,
|
InputObject_List: data.stepTask.InputObject_List || data.stepTask.inputs,
|
||||||
OutputObject: data.stepTask.OutputObject || data.stepTask.output,
|
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 }>> = {}
|
const transformedData: Record<string, Record<string, { reason: string; score: number }>> = {}
|
||||||
|
|
||||||
@@ -480,22 +636,35 @@ class Api {
|
|||||||
*/
|
*/
|
||||||
agentSelectModifyAddAspect = async (data: {
|
agentSelectModifyAddAspect = async (data: {
|
||||||
aspectList: string[]
|
aspectList: string[]
|
||||||
|
useWebSocket?: boolean
|
||||||
|
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
aspectName: string
|
aspectName: string
|
||||||
agentScores: Record<string, { score: number; reason: string }>
|
agentScores: Record<string, { score: number; reason: string }>
|
||||||
}> => {
|
}> => {
|
||||||
const response = await request<
|
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
|
||||||
{
|
let response: Record<string, Record<string, { Reason: string; Score: number }>>
|
||||||
aspectList: string[]
|
|
||||||
},
|
// 如果启用WebSocket且已连接,使用WebSocket
|
||||||
Record<string, Record<string, { Reason: string; Score: number }>>
|
if (useWs && websocket.connected) {
|
||||||
>({
|
response = await websocket.send('agent_select_modify_add_aspect', {
|
||||||
url: '/agentSelectModify_addAspect',
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
aspectList: data.aspectList,
|
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 SvgIcon from '@/components/SvgIcon/index.vue'
|
||||||
import { useAgentsStore, useConfigStore } from '@/stores'
|
import { useAgentsStore, useConfigStore } from '@/stores'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
|
import websocket from '@/utils/websocket'
|
||||||
import { changeBriefs } from '@/utils/collaboration_Brief_FrontEnd.ts'
|
import { changeBriefs } from '@/utils/collaboration_Brief_FrontEnd.ts'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import AssignmentButton from './TaskTemplate/TaskSyllabus/components/AssignmentButton.vue'
|
import AssignmentButton from './TaskTemplate/TaskSyllabus/components/AssignmentButton.vue'
|
||||||
@@ -18,6 +19,10 @@ const triggerOnFocus = ref(true)
|
|||||||
const isFocus = ref(false)
|
const isFocus = ref(false)
|
||||||
const hasAutoSearched = ref(false)
|
const hasAutoSearched = ref(false)
|
||||||
const isExpanded = ref(false)
|
const isExpanded = ref(false)
|
||||||
|
// 添加一个状态来跟踪是否正在填充步骤数据
|
||||||
|
const isFillingSteps = ref(false)
|
||||||
|
// 存储当前填充任务的取消函数
|
||||||
|
const currentStepAbortController = ref<{ cancel: () => void } | null>(null)
|
||||||
|
|
||||||
// 解析URL参数
|
// 解析URL参数
|
||||||
function getUrlParam(param: string): string | null {
|
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() {
|
async function handleSearch() {
|
||||||
// 用于标记大纲是否成功加载
|
// 用于标记大纲是否成功加载
|
||||||
let outlineLoaded = false
|
let outlineLoaded = false
|
||||||
@@ -103,6 +141,11 @@ async function handleSearch() {
|
|||||||
inputs: []
|
inputs: []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 检查是否已被停止
|
||||||
|
if (!isFillingSteps.value && currentStepAbortController.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 处理简报数据格式
|
// 处理简报数据格式
|
||||||
outlineData['Collaboration Process'] = changeBriefs(outlineData['Collaboration Process'])
|
outlineData['Collaboration Process'] = changeBriefs(outlineData['Collaboration Process'])
|
||||||
|
|
||||||
@@ -111,6 +154,9 @@ async function handleSearch() {
|
|||||||
outlineLoaded = true
|
outlineLoaded = true
|
||||||
emit('search', searchValue.value)
|
emit('search', searchValue.value)
|
||||||
|
|
||||||
|
// 开始填充步骤详情,设置状态
|
||||||
|
isFillingSteps.value = true
|
||||||
|
|
||||||
// 并行填充所有步骤的详情
|
// 并行填充所有步骤的详情
|
||||||
const steps = outlineData['Collaboration Process'] || []
|
const steps = outlineData['Collaboration Process'] || []
|
||||||
|
|
||||||
@@ -118,6 +164,12 @@ async function handleSearch() {
|
|||||||
const fillStepWithRetry = async (step: any, retryCount = 0): Promise<void> => {
|
const fillStepWithRetry = async (step: any, retryCount = 0): Promise<void> => {
|
||||||
const maxRetries = 2 // 最多重试2次
|
const maxRetries = 2 // 最多重试2次
|
||||||
|
|
||||||
|
// 检查是否已停止
|
||||||
|
if (!isFillingSteps.value) {
|
||||||
|
console.log('检测到停止信号,跳过步骤填充')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!step.StepName) {
|
if (!step.StepName) {
|
||||||
console.warn('步骤缺少 StepName,跳过填充详情')
|
console.warn('步骤缺少 StepName,跳过填充详情')
|
||||||
@@ -135,6 +187,12 @@ async function handleSearch() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 再次检查是否已停止(在 API 调用后)
|
||||||
|
if (!isFillingSteps.value) {
|
||||||
|
console.log('检测到停止信号,跳过更新步骤详情')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 更新该步骤的详情到 store
|
// 更新该步骤的详情到 store
|
||||||
updateStepDetail(step.StepName, detailedStep)
|
updateStepDetail(step.StepName, detailedStep)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -166,6 +224,9 @@ async function handleSearch() {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
triggerOnFocus.value = true
|
triggerOnFocus.value = true
|
||||||
|
// 完成填充,重置状态
|
||||||
|
isFillingSteps.value = false
|
||||||
|
currentStepAbortController.value = null
|
||||||
// 如果大纲加载失败,确保关闭loading
|
// 如果大纲加载失败,确保关闭loading
|
||||||
if (!outlineLoaded) {
|
if (!outlineLoaded) {
|
||||||
agentsStore.setAgentRawPlan({ loading: false })
|
agentsStore.setAgentRawPlan({ loading: false })
|
||||||
@@ -255,18 +316,24 @@ onMounted(() => {
|
|||||||
class="task-button"
|
class="task-button"
|
||||||
color="linear-gradient(to right, #00C7D2, #315AB4)"
|
color="linear-gradient(to right, #00C7D2, #315AB4)"
|
||||||
size="large"
|
size="large"
|
||||||
title="点击搜索任务"
|
:title="isFillingSteps ? '点击停止生成' : '点击搜索任务'"
|
||||||
circle
|
circle
|
||||||
:loading="agentsStore.agentRawPlan.loading"
|
:loading="agentsStore.agentRawPlan.loading"
|
||||||
:disabled="!searchValue"
|
:disabled="!searchValue"
|
||||||
@click.stop="handleSearch"
|
@click.stop="handleButtonClick"
|
||||||
>
|
>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
v-if="!agentsStore.agentRawPlan.loading"
|
v-if="!agentsStore.agentRawPlan.loading && !isFillingSteps"
|
||||||
icon-class="paper-plane"
|
icon-class="paper-plane"
|
||||||
size="18px"
|
size="18px"
|
||||||
color="#ffffff"
|
color="#ffffff"
|
||||||
/>
|
/>
|
||||||
|
<SvgIcon
|
||||||
|
v-if="!agentsStore.agentRawPlan.loading && isFillingSteps"
|
||||||
|
icon-class="stoprunning"
|
||||||
|
size="30px"
|
||||||
|
color="#ffffff"
|
||||||
|
/>
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<AssignmentButton v-if="planReady" @click="openAgentAllocationDialog" />
|
<AssignmentButton v-if="planReady" @click="openAgentAllocationDialog" />
|
||||||
|
|||||||
@@ -38,11 +38,12 @@ const data = computed<Data | null>(() => {
|
|||||||
if (result.NodeId === props.nodeId) {
|
if (result.NodeId === props.nodeId) {
|
||||||
// LogNodeType 为 object直接渲染Content
|
// LogNodeType 为 object直接渲染Content
|
||||||
if (result.LogNodeType === 'object') {
|
if (result.LogNodeType === 'object') {
|
||||||
return {
|
const data = {
|
||||||
Description: props.nodeId,
|
Description: props.nodeId,
|
||||||
Content: sanitize(result.content),
|
Content: sanitize(result.content),
|
||||||
LogNodeType: result.LogNodeType
|
LogNodeType: result.LogNodeType
|
||||||
}
|
}
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.ActionHistory) {
|
if (!result.ActionHistory) {
|
||||||
|
|||||||
@@ -2,15 +2,17 @@
|
|||||||
import { computed, onUnmounted, ref, reactive, nextTick, watch, onMounted } from 'vue'
|
import { computed, onUnmounted, ref, reactive, nextTick, watch, onMounted } from 'vue'
|
||||||
import { throttle } from 'lodash'
|
import { throttle } from 'lodash'
|
||||||
import { AnchorLocations, BezierConnector } from '@jsplumb/browser-ui'
|
import { AnchorLocations, BezierConnector } from '@jsplumb/browser-ui'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import AdditionalOutputCard from './AdditionalOutputCard.vue'
|
import AdditionalOutputCard from './AdditionalOutputCard.vue'
|
||||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||||
import { getActionTypeDisplay, getAgentMapIcon } from '@/layout/components/config.ts'
|
import { getActionTypeDisplay, getAgentMapIcon } from '@/layout/components/config.ts'
|
||||||
import { type ConnectArg, Jsplumb } from '@/layout/components/Main/TaskTemplate/utils.ts'
|
import { type ConnectArg, Jsplumb } from '@/layout/components/Main/TaskTemplate/utils.ts'
|
||||||
import variables from '@/styles/variables.module.scss'
|
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 api, { type StreamingEvent } from '@/api'
|
||||||
import ProcessCard from '../TaskProcess/ProcessCard.vue'
|
import ProcessCard from '../TaskProcess/ProcessCard.vue'
|
||||||
import ExecutePlan from './ExecutePlan.vue'
|
import ExecutePlan from './ExecutePlan.vue'
|
||||||
|
import websocket from '@/utils/websocket'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'refreshLine'): void
|
(e: 'refreshLine'): void
|
||||||
@@ -24,7 +26,106 @@ const collaborationProcess = computed(() => {
|
|||||||
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
|
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(
|
watch(
|
||||||
() => agentsStore.additionalOutputs,
|
() => agentsStore.additionalOutputs,
|
||||||
() => {
|
() => {
|
||||||
@@ -37,7 +138,7 @@ watch(
|
|||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// 编辑逻辑
|
// Edit logic
|
||||||
const editMode = ref(false)
|
const editMode = ref(false)
|
||||||
const editMap = reactive<Record<string, boolean>>({})
|
const editMap = reactive<Record<string, boolean>>({})
|
||||||
const editBuffer = reactive<Record<string, string | undefined>>({})
|
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
|
let timer: ReturnType<typeof setInterval> | null = null
|
||||||
function handleCollapse() {
|
function handleCollapse() {
|
||||||
if (timer) {
|
if (timer) {
|
||||||
@@ -87,7 +188,7 @@ function handleCollapse() {
|
|||||||
emit('refreshLine')
|
emit('refreshLine')
|
||||||
}, 1) as ReturnType<typeof setInterval>
|
}, 1) as ReturnType<typeof setInterval>
|
||||||
|
|
||||||
// 默认三秒后已经完全打开
|
// Default fully open after 3 seconds
|
||||||
const timer1 = setTimeout(() => {
|
const timer1 = setTimeout(() => {
|
||||||
if (timer) {
|
if (timer) {
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
@@ -105,7 +206,7 @@ function handleCollapse() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建内部连线
|
// Create internal connections
|
||||||
function createInternalLine(id?: string) {
|
function createInternalLine(id?: string) {
|
||||||
const arr: ConnectArg[] = []
|
const arr: ConnectArg[] = []
|
||||||
jsplumb.reset()
|
jsplumb.reset()
|
||||||
@@ -188,160 +289,340 @@ const executionProgress = ref({
|
|||||||
currentAction: 0,
|
currentAction: 0,
|
||||||
totalActions: 0,
|
totalActions: 0,
|
||||||
currentStepName: '',
|
currentStepName: '',
|
||||||
message: '正在执行...'
|
message: '准备执行任务...'
|
||||||
})
|
})
|
||||||
|
|
||||||
async function handleRun() {
|
// Pause functionality state
|
||||||
// 清空之前的执行结果
|
const isPaused = ref(false) // Whether paused
|
||||||
agentsStore.setExecutePlan([])
|
const isStreaming = ref(false) // Whether streaming data (backend started returning)
|
||||||
const tempResults: any[] = []
|
const isButtonLoading = ref(false) // Button brief loading state (prevent double-click)
|
||||||
|
|
||||||
try {
|
// Store current step execution index (for sequential execution)
|
||||||
loading.value = true
|
const currentExecutionIndex = ref(0)
|
||||||
|
|
||||||
// 使用优化版流式API(阶段1+2:步骤级流式 + 动作级智能并行)
|
// Execute next batch of ready steps (batch execution to maintain dependencies)
|
||||||
api.executePlanOptimized(
|
async function executeNextReadyBatch() {
|
||||||
agentsStore.agentRawPlan.data!,
|
const steps = collaborationProcess.value
|
||||||
// onMessage: 处理每个事件
|
|
||||||
(event: StreamingEvent) => {
|
|
||||||
switch (event.type) {
|
|
||||||
case 'step_start':
|
|
||||||
// 步骤开始
|
|
||||||
executionProgress.value = {
|
|
||||||
currentStep: event.step_index + 1,
|
|
||||||
totalSteps: event.total_steps,
|
|
||||||
currentAction: 0,
|
|
||||||
totalActions: 0,
|
|
||||||
currentStepName: event.step_name,
|
|
||||||
message: `正在执行步骤 ${event.step_index + 1}/${event.total_steps}: ${
|
|
||||||
event.step_name
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
console.log(
|
|
||||||
`📋 步骤 ${event.step_index + 1}/${event.total_steps} 开始: ${event.step_name}`
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'action_complete':
|
// Collect all ready but unexecuted steps (in order, until hitting unready step)
|
||||||
// 动作完成
|
const readySteps: IRawStepTask[] = []
|
||||||
const parallelInfo = event.batch_info?.is_parallel
|
|
||||||
? ` [批次 ${event.batch_info!.batch_index + 1}, 并行 ${
|
|
||||||
event.batch_info!.batch_size
|
|
||||||
} 个]`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
executionProgress.value = {
|
for (let i = 0; i < steps.length; i++) {
|
||||||
...executionProgress.value,
|
const step = steps[i]
|
||||||
currentAction: event.completed_actions,
|
if (!step) continue
|
||||||
totalActions: event.total_actions,
|
|
||||||
message: `步骤 ${event.step_index + 1}/${executionProgress.value.totalSteps}: ${
|
|
||||||
event.step_name
|
|
||||||
} - 动作 ${event.completed_actions}/${event.total_actions} 完成${parallelInfo}`
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
// 如果步骤已就绪,加入批量执行列表
|
||||||
`✅ 动作 ${event.completed_actions}/${event.total_actions} 完成${parallelInfo}: ${event.action_result.ActionType} by ${event.action_result.AgentName}`
|
if (isStepReady(step)) {
|
||||||
)
|
const stepName = step.StepName || step.Id || ''
|
||||||
|
const status = stepExecutionStatus.value[stepName]
|
||||||
|
|
||||||
// 实时更新到 store(找到对应的步骤并添加 ActionHistory)
|
// Only collect unexecuted steps
|
||||||
const step = collaborationProcess.value.find(s => s.StepName === event.step_name)
|
if (!status || status === StepExecutionStatus.READY) {
|
||||||
if (step) {
|
readySteps.push(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
|
|
||||||
}
|
}
|
||||||
)
|
} else {
|
||||||
} catch (error) {
|
// Stop at first unready step (maintain step order)
|
||||||
console.error('执行失败:', error)
|
break
|
||||||
executionProgress.value.message = '执行失败,请重试'
|
}
|
||||||
} finally {
|
}
|
||||||
// loading 会在 onComplete 中设置为 false
|
|
||||||
|
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() {
|
async function handleTaskProcess() {
|
||||||
drawerVisible.value = true
|
drawerVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置执行结果
|
// Reset execution results
|
||||||
function handleRefresh() {
|
function handleRefresh() {
|
||||||
agentsStore.setExecutePlan([])
|
agentsStore.setExecutePlan([])
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加滚动状态标识
|
// Add scroll state indicator
|
||||||
const isScrolling = ref(false)
|
const isScrolling = ref(false)
|
||||||
let scrollTimer: ReturnType<typeof setTimeout> | null = null
|
let scrollTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
// 修改滚动处理函数
|
// Modify scroll handler
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
isScrolling.value = true
|
isScrolling.value = true
|
||||||
emit('refreshLine')
|
emit('refreshLine')
|
||||||
@@ -357,7 +638,7 @@ function handleScroll() {
|
|||||||
}, 300) as ReturnType<typeof setTimeout>
|
}, 300) as ReturnType<typeof setTimeout>
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改鼠标事件处理函数
|
// Modify mouse event handler
|
||||||
const handleMouseEnter = throttle(id => {
|
const handleMouseEnter = throttle(id => {
|
||||||
if (!isScrolling.value) {
|
if (!isScrolling.value) {
|
||||||
createInternalLine(id)
|
createInternalLine(id)
|
||||||
@@ -374,21 +655,21 @@ function clear() {
|
|||||||
jsplumb.reset()
|
jsplumb.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
//封装连线重绘方法
|
// Encapsulate line redraw method
|
||||||
const redrawInternalLines = (highlightId?: string) => {
|
const redrawInternalLines = (highlightId?: string) => {
|
||||||
// 等待 DOM 更新完成
|
// Waiting DOM 更新完成
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// 清除旧连线
|
// 清除旧连线
|
||||||
jsplumb.reset()
|
jsplumb.reset()
|
||||||
|
|
||||||
// 等待 DOM 稳定后重新绘制
|
// Waiting DOM 稳定后重新绘制
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
createInternalLine(highlightId)
|
createInternalLine(highlightId)
|
||||||
}, 100)
|
}, 100)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//监听 collaborationProcess 变化,自动重绘连线
|
// Watch collaborationProcess changes, auto redraw connections
|
||||||
watch(
|
watch(
|
||||||
() => collaborationProcess,
|
() => collaborationProcess,
|
||||||
() => {
|
() => {
|
||||||
@@ -397,7 +678,7 @@ watch(
|
|||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// 组件挂载后初始化连线
|
// Initialize connections after component mount
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 初始化时绘制连线
|
// 初始化时绘制连线
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
@@ -407,7 +688,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
//按钮交互状态管理
|
// Button interaction state management
|
||||||
const buttonHoverState = ref<'process' | 'execute' | 'refresh' | null>(null)
|
const buttonHoverState = ref<'process' | 'execute' | 'refresh' | null>(null)
|
||||||
let buttonHoverTimer: ReturnType<typeof setTimeout> | null = null
|
let buttonHoverTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
const handleProcessMouseEnter = () => {
|
const handleProcessMouseEnter = () => {
|
||||||
@@ -448,13 +729,13 @@ const handleButtonMouseLeave = () => {
|
|||||||
}, 50) // 适当减少延迟时间
|
}, 50) // 适当减少延迟时间
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加离开组件时的清理
|
// Cleanup when leaving component
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (buttonHoverTimer) {
|
if (buttonHoverTimer) {
|
||||||
clearTimeout(buttonHoverTimer)
|
clearTimeout(buttonHoverTimer)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// 计算按钮类名
|
// Calculate button class names
|
||||||
const processBtnClass = computed(() => {
|
const processBtnClass = computed(() => {
|
||||||
if (buttonHoverState.value === 'refresh' || buttonHoverState.value === 'execute') {
|
if (buttonHoverState.value === 'refresh' || buttonHoverState.value === 'execute') {
|
||||||
return 'circle'
|
return 'circle'
|
||||||
@@ -476,7 +757,7 @@ const refreshBtnClass = computed(() => {
|
|||||||
return agentsStore.executePlan.length > 0 ? 'ellipse' : 'circle'
|
return agentsStore.executePlan.length > 0 ? 'ellipse' : 'circle'
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算按钮是否显示文字
|
// Calculate whether to show button text
|
||||||
const showProcessText = computed(() => {
|
const showProcessText = computed(() => {
|
||||||
return buttonHoverState.value === 'process'
|
return buttonHoverState.value === 'process'
|
||||||
})
|
})
|
||||||
@@ -490,17 +771,17 @@ const showRefreshText = computed(() => {
|
|||||||
return buttonHoverState.value === 'refresh'
|
return buttonHoverState.value === 'refresh'
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算按钮标题
|
// Calculate button titles
|
||||||
const processBtnTitle = computed(() => {
|
const processBtnTitle = computed(() => {
|
||||||
return buttonHoverState.value === 'process' ? '任务过程' : '点击查看任务过程'
|
return buttonHoverState.value === 'process' ? '查看任务流程' : '点击查看任务流程'
|
||||||
})
|
})
|
||||||
|
|
||||||
const executeBtnTitle = computed(() => {
|
const executeBtnTitle = computed(() => {
|
||||||
return showExecuteText.value ? '任务执行' : '点击运行'
|
return showExecuteText.value ? '任务执行' : '点击执行任务'
|
||||||
})
|
})
|
||||||
|
|
||||||
const refreshBtnTitle = computed(() => {
|
const refreshBtnTitle = computed(() => {
|
||||||
return showRefreshText.value ? '重置执行结果' : '点击重置执行状态'
|
return showRefreshText.value ? '重置结果' : '点击重置执行状态'
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@@ -544,7 +825,7 @@ defineExpose({
|
|||||||
<svg-icon icon-class="refresh" />
|
<svg-icon icon-class="refresh" />
|
||||||
<span v-if="showRefreshText" class="btn-text">重置</span>
|
<span v-if="showRefreshText" class="btn-text">重置</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
<!-- 任务过程按钮 -->
|
<!-- Task Process按钮 -->
|
||||||
<el-button
|
<el-button
|
||||||
:class="processBtnClass"
|
:class="processBtnClass"
|
||||||
:color="variables.tertiary"
|
:color="variables.tertiary"
|
||||||
@@ -557,10 +838,10 @@ defineExpose({
|
|||||||
<span v-if="showProcessText" class="btn-text">任务过程</span>
|
<span v-if="showProcessText" class="btn-text">任务过程</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<!-- 任务执行按钮 -->
|
<!-- Execute按钮 -->
|
||||||
<el-popover
|
<el-popover
|
||||||
:disabled="Boolean(agentsStore.agentRawPlan.data)"
|
:disabled="Boolean(agentsStore.agentRawPlan.data)"
|
||||||
title="请先输入要执行的任务"
|
title="请先输入任务再执行"
|
||||||
:visible="showPopover"
|
:visible="showPopover"
|
||||||
@hide="showPopover = false"
|
@hide="showPopover = false"
|
||||||
style="order: 2"
|
style="order: 2"
|
||||||
@@ -569,14 +850,35 @@ defineExpose({
|
|||||||
<el-button
|
<el-button
|
||||||
:class="executeBtnClass"
|
:class="executeBtnClass"
|
||||||
:color="variables.tertiary"
|
:color="variables.tertiary"
|
||||||
:title="executeBtnTitle"
|
:title="isStreaming ? (isPaused ? '点击继续执行' : '点击暂停执行') : executeBtnTitle"
|
||||||
:disabled="!agentsStore.agentRawPlan.data || loading"
|
:disabled="
|
||||||
|
!agentsStore.agentRawPlan.data || (!isStreaming && loading) || isButtonLoading
|
||||||
|
"
|
||||||
@mouseenter="handleExecuteMouseEnter"
|
@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" />
|
<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>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
@@ -597,7 +899,7 @@ defineExpose({
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-scrollbar height="calc(100vh - 120px)">
|
<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">
|
<div v-else class="process-list">
|
||||||
<!-- 使用ProcessCard组件显示每个AgentSelection -->
|
<!-- 使用ProcessCard组件显示每个AgentSelection -->
|
||||||
<ProcessCard
|
<ProcessCard
|
||||||
@@ -648,23 +950,26 @@ defineExpose({
|
|||||||
v-for="item1 in item.TaskProcess"
|
v-for="item1 in item.TaskProcess"
|
||||||
:key="`task-results-${item.Id}-${item1.ID}`"
|
:key="`task-results-${item.Id}-${item1.ID}`"
|
||||||
:name="`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}`)"
|
@mouseenter="() => handleMouseEnter(`task-results-${item.Id}-0-${item1.ID}`)"
|
||||||
@mouseleave="handleMouseLeave"
|
@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" />
|
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
|
||||||
</template>
|
</template>
|
||||||
|
<!-- 没有执行计划时隐藏图标 -->
|
||||||
<template v-else-if="!agentsStore.executePlan.length" #icon>
|
<template v-else-if="!agentsStore.executePlan.length" #icon>
|
||||||
<span></span>
|
<span></span>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- 有结果时不提供 #icon,让 Element Plus 显示默认箭头 -->
|
||||||
<template #title>
|
<template #title>
|
||||||
<!-- 运行之前背景颜色是var(--color-bg-detail-list),运行之后背景颜色是var(--color-bg-detail-list-run) -->
|
<!-- 运行之前背景颜色是var(--color-bg-detail-list),运行之后背景颜色是var(--color-bg-detail-list-run) -->
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-[15px] rounded-[20px]"
|
class="flex items-center gap-[15px] rounded-[20px]"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-[var(--color-bg-detail-list)]': !agentsStore.executePlan.length,
|
'bg-[var(--color-bg-detail-list)]': !hasActionResult(item, item1.ID),
|
||||||
'bg-[var(--color-bg-detail-list-run)]': agentsStore.executePlan.length
|
'bg-[var(--color-bg-detail-list-run)]': hasActionResult(item, item1.ID)
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<!-- 右侧链接点 -->
|
<!-- 右侧链接点 -->
|
||||||
@@ -685,9 +990,14 @@ defineExpose({
|
|||||||
<div class="text-[16px]">
|
<div class="text-[16px]">
|
||||||
<span
|
<span
|
||||||
:class="{
|
:class="{
|
||||||
'text-[var(--color-text-result-detail)]': !agentsStore.executePlan.length,
|
'text-[var(--color-text-result-detail)]': !hasActionResult(
|
||||||
'text-[var(--color-text-result-detail-run)]':
|
item,
|
||||||
agentsStore.executePlan.length
|
item1.ID
|
||||||
|
),
|
||||||
|
'text-[var(--color-text-result-detail-run)]': hasActionResult(
|
||||||
|
item,
|
||||||
|
item1.ID
|
||||||
|
)
|
||||||
}"
|
}"
|
||||||
>{{ item1.AgentName }}: </span
|
>{{ item1.AgentName }}: </span
|
||||||
>
|
>
|
||||||
@@ -713,23 +1023,28 @@ defineExpose({
|
|||||||
@click="emit('setCurrentTask', item)"
|
@click="emit('setCurrentTask', item)"
|
||||||
>
|
>
|
||||||
<!-- <div class="text-[18px]">{{ item.OutputObject }}</div>-->
|
<!-- <div class="text-[18px]">{{ item.OutputObject }}</div>-->
|
||||||
<el-collapse @change="handleCollapse">
|
<el-collapse @change="handleCollapse" :key="agentsStore.executePlan.length">
|
||||||
<el-collapse-item
|
<el-collapse-item
|
||||||
class="output-object"
|
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" />
|
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
|
||||||
</template>
|
</template>
|
||||||
|
<!-- 没有执行计划时隐藏图标 -->
|
||||||
<template v-else-if="!agentsStore.executePlan.length" #icon>
|
<template v-else-if="!agentsStore.executePlan.length" #icon>
|
||||||
<span></span>
|
<span></span>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- 有结果时不提供 #icon,让 Element Plus 显示默认箭头 -->
|
||||||
<template #title>
|
<template #title>
|
||||||
<div
|
<div
|
||||||
class="text-[18px]"
|
class="text-[18px]"
|
||||||
:class="{
|
:class="{
|
||||||
'text-[var(--color-text-result-detail)]': !agentsStore.executePlan.length,
|
'text-[var(--color-text-result-detail)]': !hasObjectResult(item.OutputObject),
|
||||||
'text-[var(--color-text-result-detail-run)]': agentsStore.executePlan.length
|
'text-[var(--color-text-result-detail-run)]': hasObjectResult(
|
||||||
|
item.OutputObject
|
||||||
|
)
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
{{ item.OutputObject }}
|
{{ item.OutputObject }}
|
||||||
@@ -980,7 +1295,7 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 圆形状态
|
// Circle state
|
||||||
.circle {
|
.circle {
|
||||||
width: 40px !important;
|
width: 40px !important;
|
||||||
height: 40px !important;
|
height: 40px !important;
|
||||||
@@ -994,14 +1309,14 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 椭圆形状态
|
// Ellipse state
|
||||||
.ellipse {
|
.ellipse {
|
||||||
height: 40px !important;
|
height: 40px !important;
|
||||||
border-radius: 20px !important;
|
border-radius: 20px !important;
|
||||||
padding: 0 16px !important;
|
padding: 0 16px !important;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
// 任务过程按钮 - 左边固定,向右展开
|
// Task process button - fixed left, expand right
|
||||||
&:nth-child(1) {
|
&:nth-child(1) {
|
||||||
justify-content: flex-start !important;
|
justify-content: flex-start !important;
|
||||||
|
|
||||||
@@ -1016,7 +1331,7 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 任务执行按钮 - 右边固定,向左展开
|
// Task execution button - fixed right, expand left
|
||||||
&:nth-child(2) {
|
&:nth-child(2) {
|
||||||
justify-content: flex-end !important;
|
justify-content: flex-end !important;
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ const collaborationProcess = computed(() => {
|
|||||||
// 检测是否正在填充详情(有步骤但没有 AgentSelection)
|
// 检测是否正在填充详情(有步骤但没有 AgentSelection)
|
||||||
const isFillingDetails = computed(() => {
|
const isFillingDetails = computed(() => {
|
||||||
const process = agentsStore.agentRawPlan.data?.['Collaboration Process'] || []
|
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="h-[1px] w-full bg-[var(--color-border-separate)] my-[8px]"></div>
|
||||||
<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
|
<div
|
||||||
class="absolute left-[-10px] top-1/2 transform -translate-y-1/2"
|
class="absolute left-[-10px] top-1/2 transform -translate-y-1/2"
|
||||||
:id="`task-syllabus-flow-agents-${item.Id}`"
|
:id="`task-syllabus-flow-agents-${item.Id}`"
|
||||||
></div>
|
></div>
|
||||||
<el-tooltip
|
|
||||||
v-for="agentSelection in item.AgentSelection"
|
<!-- 未填充智能体时显示Loading -->
|
||||||
:key="agentSelection"
|
<div
|
||||||
effect="light"
|
v-if="!item.AgentSelection || item.AgentSelection.length === 0"
|
||||||
placement="right"
|
class="flex items-center gap-2 text-[var(--color-text-secondary)] text-[14px]"
|
||||||
>
|
>
|
||||||
<template #content>
|
<el-icon class="is-loading" :size="20">
|
||||||
<div class="w-[150px]">
|
<Loading />
|
||||||
<div class="text-[18px] font-bold">{{ agentSelection }}</div>
|
</el-icon>
|
||||||
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
|
<span>正在分配智能体...</span>
|
||||||
<div>
|
</div>
|
||||||
{{ item.TaskProcess.find(i => i.AgentName === agentSelection)?.Description }}
|
|
||||||
</div>
|
<!-- 已填充智能体时显示智能体列表 -->
|
||||||
</div>
|
<template v-else>
|
||||||
</template>
|
<el-tooltip
|
||||||
<div
|
v-for="agentSelection in item.AgentSelection"
|
||||||
class="w-[31px] h-[31px] rounded-full flex items-center justify-center"
|
:key="agentSelection"
|
||||||
:style="{ background: getAgentMapIcon(agentSelection).color }"
|
effect="light"
|
||||||
|
placement="right"
|
||||||
>
|
>
|
||||||
<svg-icon
|
<template #content>
|
||||||
:icon-class="getAgentMapIcon(agentSelection).icon"
|
<div class="w-[150px]">
|
||||||
color="#fff"
|
<div class="text-[18px] font-bold">{{ agentSelection }}</div>
|
||||||
size="24px"
|
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
|
||||||
/>
|
<div>
|
||||||
</div>
|
{{
|
||||||
</el-tooltip>
|
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>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
<!-- 产物卡片 -->
|
<!-- 产物卡片 -->
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import './styles/tailwindcss.css'
|
|||||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||||
import 'virtual:svg-icons-register'
|
import 'virtual:svg-icons-register'
|
||||||
import { initService } from '@/utils/request.ts'
|
import { initService } from '@/utils/request.ts'
|
||||||
|
import websocket from '@/utils/websocket'
|
||||||
import { setupStore, useConfigStore } from '@/stores'
|
import { setupStore, useConfigStore } from '@/stores'
|
||||||
import { setupDirective } from '@/ directive'
|
import { setupDirective } from '@/ directive'
|
||||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
@@ -20,6 +21,24 @@ async function init() {
|
|||||||
setupStore(app)
|
setupStore(app)
|
||||||
setupDirective(app)
|
setupDirective(app)
|
||||||
initService()
|
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
|
document.title = configStore.config.centerTitle
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.mount('#app')
|
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()
|
||||||