Compare commits

...

14 Commits

Author SHA1 Message Date
liailing1026
f736cd104a feat:执行状态单例状态bug修复server.py遗漏提交 2026-02-04 11:29:46 +08:00
liailing1026
328f8e7ec6 feat:执行状态单例状态bug修复 2026-02-02 17:09:20 +08:00
liailing1026
f272ccc390 feat:控制台信息 2026-01-30 15:31:12 +08:00
liailing1026
1682a8892d feat:代码优化与Mock数据清理 2026-01-30 15:27:00 +08:00
liailing1026
6e4d8f0b6d feat:注释大模型数据控制台打印 2026-01-30 15:24:22 +08:00
liailing1026
aedfd0594c feat:注释大模型数据控制台打印 2026-01-30 15:21:58 +08:00
liailing1026
b3e6c7a618 feat:部署服务器报错 2026-01-29 16:40:10 +08:00
liailing1026
1749ae4f1e feat:通知主题修改 2026-01-26 17:25:23 +08:00
liailing1026
418b2e5f8f feat:智能体探索窗口预加载完善 2026-01-26 16:46:58 +08:00
liailing1026
641d70033d feat:用户修改步骤后重新执行实现 2026-01-26 15:06:17 +08:00
liailing1026
b287867069 feat:暂停动画以及暂停正确响应 2026-01-24 21:11:49 +08:00
liailing1026
5699635d1a feat:暂停/继续按钮与文字间距增加 2026-01-23 17:07:01 +08:00
liailing1026
ac035d1237 feat:任务大纲停止以及执行结果暂停继续逻辑完善 2026-01-23 15:38:09 +08:00
liailing1026
53add0431e feat:配置文件 2026-01-22 17:25:10 +08:00
44 changed files with 4070 additions and 3163 deletions

View File

@@ -112,8 +112,17 @@ def _call_with_custom_config(messages: list[dict], stream: bool, model_config: d
timeout=180 timeout=180
) )
# 检查响应是否有效
if not response.choices or len(response.choices) == 0:
raise Exception(f"API returned empty response for model {api_model}")
if not response.choices[0] or not response.choices[0].message:
raise Exception(f"API returned invalid response format for model {api_model}")
full_reply_content = response.choices[0].message.content full_reply_content = response.choices[0].message.content
print(colored(full_reply_content, "blue", "on_white"), end="") if full_reply_content is None:
raise Exception(f"API returned None content for model {api_model}")
#print(colored(full_reply_content, "blue", "on_white"), end="")
return full_reply_content return full_reply_content
except Exception as e: except Exception as e:
print_colored(f"Custom API error for model {api_model} :{str(e)}","red") print_colored(f"Custom API error for model {api_model} :{str(e)}","red")
@@ -138,15 +147,21 @@ async def _achat_completion_stream_custom(messages:list[dict], temp_async_client
async for chunk in response: async for chunk in response:
collected_chunks.append(chunk) collected_chunks.append(chunk)
choices = chunk.choices choices = chunk.choices
if len(choices) > 0: if len(choices) > 0 and choices[0] is not None:
chunk_message = chunk.choices[0].delta chunk_message = choices[0].delta
collected_messages.append(chunk_message) if chunk_message is not None:
if chunk_message.content: collected_messages.append(chunk_message)
print(colored(chunk_message.content, "blue", "on_white"), end="") # if chunk_message.content:
print() # print(colored(chunk_message.content, "blue", "on_white"), end="")
# print()
full_reply_content = "".join( full_reply_content = "".join(
[m.content or "" for m in collected_messages if m is not None] [m.content or "" for m in collected_messages if m is not None]
) )
# 检查最终结果是否为空
if not full_reply_content or full_reply_content.strip() == "":
raise Exception(f"Stream API returned empty content for model {api_model}")
return full_reply_content return full_reply_content
except httpx.RemoteProtocolError as e: except httpx.RemoteProtocolError as e:
if attempt < max_retries - 1: if attempt < max_retries - 1:
@@ -184,9 +199,18 @@ async def _achat_completion_stream_groq(messages: list[dict]) -> str:
else: else:
raise Exception("failed") raise Exception("failed")
# 检查响应是否有效
if not response.choices or len(response.choices) == 0:
raise Exception("Groq API returned empty response")
if not response.choices[0] or not response.choices[0].message:
raise Exception("Groq API returned invalid response format")
full_reply_content = response.choices[0].message.content full_reply_content = response.choices[0].message.content
print(colored(full_reply_content, "blue", "on_white"), end="") if full_reply_content is None:
print() raise Exception("Groq API returned None content")
# print(colored(full_reply_content, "blue", "on_white"), end="")
# print()
return full_reply_content return full_reply_content
@@ -217,9 +241,18 @@ async def _achat_completion_stream_mixtral(messages: list[dict]) -> str:
else: else:
raise Exception("failed") raise Exception("failed")
# 检查响应是否有效
if not stream.choices or len(stream.choices) == 0:
raise Exception("Mistral API returned empty response")
if not stream.choices[0] or not stream.choices[0].message:
raise Exception("Mistral API returned invalid response format")
full_reply_content = stream.choices[0].message.content full_reply_content = stream.choices[0].message.content
print(colored(full_reply_content, "blue", "on_white"), end="") if full_reply_content is None:
print() raise Exception("Mistral API returned None content")
# print(colored(full_reply_content, "blue", "on_white"), end="")
# print()
return full_reply_content return full_reply_content
@@ -240,19 +273,25 @@ async def _achat_completion_stream_gpt35(messages: list[dict]) -> str:
async for chunk in response: async for chunk in response:
collected_chunks.append(chunk) # save the event response collected_chunks.append(chunk) # save the event response
choices = chunk.choices choices = chunk.choices
if len(choices) > 0: if len(choices) > 0 and choices[0] is not None:
chunk_message = chunk.choices[0].delta chunk_message = choices[0].delta
collected_messages.append(chunk_message) # save the message if chunk_message is not None:
if chunk_message.content: collected_messages.append(chunk_message) # save the message
print( # if chunk_message.content:
colored(chunk_message.content, "blue", "on_white"), # print(
end="", # colored(chunk_message.content, "blue", "on_white"),
) # end="",
print() # )
# print()
full_reply_content = "".join( full_reply_content = "".join(
[m.content or "" for m in collected_messages if m is not None] [m.content or "" for m in collected_messages if m is not None]
) )
# 检查最终结果是否为空
if not full_reply_content or full_reply_content.strip() == "":
raise Exception("Stream API (gpt-3.5) returned empty content")
return full_reply_content return full_reply_content
@@ -275,9 +314,18 @@ def _achat_completion_json(messages: list[dict] ) -> str:
else: else:
raise Exception("failed") raise Exception("failed")
# 检查响应是否有效
if not response.choices or len(response.choices) == 0:
raise Exception("OpenAI API returned empty response")
if not response.choices[0] or not response.choices[0].message:
raise Exception("OpenAI API returned invalid response format")
full_reply_content = response.choices[0].message.content full_reply_content = response.choices[0].message.content
print(colored(full_reply_content, "blue", "on_white"), end="") if full_reply_content is None:
print() raise Exception("OpenAI API returned None content")
# print(colored(full_reply_content, "blue", "on_white"), end="")
# print()
return full_reply_content return full_reply_content
@@ -294,19 +342,25 @@ async def _achat_completion_stream(messages: list[dict]) -> str:
async for chunk in response: async for chunk in response:
collected_chunks.append(chunk) # save the event response collected_chunks.append(chunk) # save the event response
choices = chunk.choices choices = chunk.choices
if len(choices) > 0: if len(choices) > 0 and choices[0] is not None:
chunk_message = chunk.choices[0].delta chunk_message = choices[0].delta
collected_messages.append(chunk_message) # save the message if chunk_message is not None:
if chunk_message.content: collected_messages.append(chunk_message) # save the message
print( # if chunk_message.content:
colored(chunk_message.content, "blue", "on_white"), # print(
end="", # colored(chunk_message.content, "blue", "on_white"),
) # end="",
print() # )
# print()
full_reply_content = "".join( full_reply_content = "".join(
[m.content or "" for m in collected_messages if m is not None] [m.content or "" for m in collected_messages if m is not None]
) )
# 检查最终结果是否为空
if not full_reply_content or full_reply_content.strip() == "":
raise Exception("Stream API returned empty content")
return full_reply_content return full_reply_content
except Exception as e: except Exception as e:
print_colored(f"OpenAI API error in _achat_completion_stream: {str(e)}", "red") print_colored(f"OpenAI API error in _achat_completion_stream: {str(e)}", "red")
@@ -316,7 +370,17 @@ async def _achat_completion_stream(messages: list[dict]) -> str:
def _chat_completion(messages: list[dict]) -> str: def _chat_completion(messages: list[dict]) -> str:
try: try:
rsp = client.chat.completions.create(**_cons_kwargs(messages)) rsp = client.chat.completions.create(**_cons_kwargs(messages))
# 检查响应是否有效
if not rsp.choices or len(rsp.choices) == 0:
raise Exception("OpenAI API returned empty response")
if not rsp.choices[0] or not rsp.choices[0].message:
raise Exception("OpenAI API returned invalid response format")
content = rsp.choices[0].message.content content = rsp.choices[0].message.content
if content is None:
raise Exception("OpenAI API returned None content")
return content return content
except Exception as e: except Exception as e:
print_colored(f"OpenAI API error in _chat_completion: {str(e)}", "red") print_colored(f"OpenAI API error in _chat_completion: {str(e)}", "red")

View File

@@ -43,7 +43,7 @@ def generate_AbilityRequirement(General_Goal, Current_Task):
), ),
}, },
] ]
print(messages[1]["content"]) #print(messages[1]["content"])
return read_LLM_Completion(messages)["AbilityRequirement"] return read_LLM_Completion(messages)["AbilityRequirement"]
@@ -104,7 +104,7 @@ def agentAbilityScoring(Agent_Board, Ability_Requirement_List):
), ),
}, },
] ]
print(messages[1]["content"]) #print(messages[1]["content"])
scoreTable[Ability_Requirement] = read_LLM_Completion(messages) scoreTable[Ability_Requirement] = read_LLM_Completion(messages)
return scoreTable return scoreTable

View File

@@ -100,7 +100,7 @@ def generate_AgentSelection(General_Goal, Current_Task, Agent_Board):
), ),
}, },
] ]
print(messages[1]["content"]) #print(messages[1]["content"])
agentboard_set = {agent["Name"] for agent in Agent_Board} agentboard_set = {agent["Name"] for agent in Agent_Board}

View File

@@ -87,7 +87,7 @@ def branch_PlanOutline(
InitialObject_List=str(InitialObject_List), InitialObject_List=str(InitialObject_List),
General_Goal=General_Goal, General_Goal=General_Goal,
) )
print(prompt) #print(prompt)
branch_List = [] branch_List = []
for _ in range(branch_Number): for _ in range(branch_Number):
messages = [ messages = [

View File

@@ -155,7 +155,7 @@ def branch_TaskProcess(
General_Goal=General_Goal, General_Goal=General_Goal,
Act_Set=ACT_SET, Act_Set=ACT_SET,
) )
print(prompt) #print(prompt)
branch_List = [] branch_List = []
for i in range(branch_Number): for i in range(branch_Number):
messages = [ messages = [

View File

@@ -124,7 +124,7 @@ def generate_TaskProcess(General_Goal, Current_Task_Description):
), ),
}, },
] ]
print(messages[1]["content"]) #print(messages[1]["content"])
# write a callback function, if read_LLM_Completion(messages)["Task_Process_Plan"] dont have the right format, call this function again # write a callback function, if read_LLM_Completion(messages)["Task_Process_Plan"] dont have the right format, call this function again
while True: while True:

View File

@@ -107,7 +107,7 @@ class BaseAction():
Action_Description = self.info["Description"], Action_Description = self.info["Description"],
Action_Custom_Note = self.Action_Custom_Note Action_Custom_Note = self.Action_Custom_Note
) )
print_colored(text = prompt, text_color="red") #print_colored(text = prompt, text_color="red")
messages = [{"role":"system", "content": prompt}] messages = [{"role":"system", "content": prompt}]
ActionResult = LLM_Completion(messages,True,False,model_config=model_config) ActionResult = LLM_Completion(messages,True,False,model_config=model_config)
ActionInfo_with_Result = copy.deepcopy(self.info) ActionInfo_with_Result = copy.deepcopy(self.info)

View File

@@ -1,3 +1,8 @@
"""
优化版执行计划 - 支持动态追加步骤
在执行过程中可以接收新的步骤并追加到执行队列
"""
import asyncio import asyncio
import json import json
import time import time
@@ -6,22 +11,26 @@ 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 from AgentCoord.RehearsalEngine_V2.execution_state import execution_state_manager
from AgentCoord.RehearsalEngine_V2.dynamic_execution_manager import dynamic_execution_manager
# ==================== 配置参数 ==================== # ==================== 配置参数 ====================
# 最大并发请求数(避免触发 OpenAI API 速率限制) # 最大并发请求数
MAX_CONCURRENT_REQUESTS = 2 MAX_CONCURRENT_REQUESTS = 2
# 批次之间的延迟(秒) # 批次之间的延迟
BATCH_DELAY = 1.0 BATCH_DELAY = 1.0
# 429 错误重试次数和延迟 # 429错误重试次数和延迟
MAX_RETRIES = 3 MAX_RETRIES = 3
RETRY_DELAY = 5.0 # 秒 RETRY_DELAY = 5.0
# ==================== 限流器 ==================== # ==================== 限流器 ====================
class RateLimiter: class RateLimiter:
"""异步限流器,控制并发请求数量""" """
异步限流器,控制并发请求数量
"""
def __init__(self, max_concurrent: int = MAX_CONCURRENT_REQUESTS): def __init__(self, max_concurrent: int = MAX_CONCURRENT_REQUESTS):
self.semaphore = asyncio.Semaphore(max_concurrent) self.semaphore = asyncio.Semaphore(max_concurrent)
@@ -42,7 +51,12 @@ rate_limiter = RateLimiter()
def build_action_dependency_graph(TaskProcess: List[Dict]) -> Dict[int, List[int]]: def build_action_dependency_graph(TaskProcess: List[Dict]) -> Dict[int, List[int]]:
""" """
构建动作依赖图 构建动作依赖图
返回: {action_index: [dependent_action_indices]}
Args:
TaskProcess: 任务流程列表
Returns:
依赖映射字典 {action_index: [dependent_action_indices]}
""" """
dependency_map = {i: [] for i in range(len(TaskProcess))} dependency_map = {i: [] for i in range(len(TaskProcess))}
@@ -51,7 +65,7 @@ def build_action_dependency_graph(TaskProcess: List[Dict]) -> Dict[int, List[int
if not important_inputs: if not important_inputs:
continue continue
# 检查是否依赖其他动作的 ActionResult # 检查是否依赖其他动作的ActionResult
for j, prev_action in enumerate(TaskProcess): for j, prev_action in enumerate(TaskProcess):
if i == j: if i == j:
continue continue
@@ -70,7 +84,13 @@ def build_action_dependency_graph(TaskProcess: List[Dict]) -> Dict[int, List[int
def get_parallel_batches(TaskProcess: List[Dict], dependency_map: Dict[int, List[int]]) -> List[List[int]]: def get_parallel_batches(TaskProcess: List[Dict], dependency_map: Dict[int, List[int]]) -> List[List[int]]:
""" """
将动作分为多个批次,每批内部可以并行执行 将动作分为多个批次,每批内部可以并行执行
返回: [[batch1_indices], [batch2_indices], ...]
Args:
TaskProcess: 任务流程列表
dependency_map: 依赖图
Returns:
批次列表 [[batch1_indices], [batch2_indices], ...]
""" """
batches = [] batches = []
completed: Set[int] = set() completed: Set[int] = set()
@@ -84,11 +104,11 @@ def get_parallel_batches(TaskProcess: List[Dict], dependency_map: Dict[int, List
] ]
if not ready_to_run: if not ready_to_run:
# 避免死循环(循环依赖情况) # 避免死循环
remaining = [i for i in range(len(TaskProcess)) if i not in completed] remaining = [i for i in range(len(TaskProcess)) if i not in completed]
if remaining: if remaining:
print(colored(f"警告: 检测到循环依赖,强制串行执行: {remaining}", "yellow")) print(colored(f"警告: 检测到循环依赖,强制串行执行: {remaining}", "yellow"))
ready_to_run = remaining[:1] # 每次只执行一个 ready_to_run = remaining[:1]
else: else:
break break
@@ -111,6 +131,20 @@ async def execute_single_action_async(
) -> Dict: ) -> Dict:
""" """
异步执行单个动作 异步执行单个动作
Args:
ActionInfo: 动作信息
General_Goal: 总体目标
TaskDescription: 任务描述
OutputName: 输出对象名称
KeyObjects: 关键对象字典
ActionHistory: 动作历史
agentName: 智能体名称
AgentProfile_Dict: 智能体配置字典
InputName_List: 输入名称列表
Returns:
动作执行结果
""" """
actionType = ActionInfo["ActionType"] actionType = ActionInfo["ActionType"]
@@ -128,7 +162,7 @@ async def execute_single_action_async(
KeyObjects=KeyObjects, KeyObjects=KeyObjects,
) )
# 执行动作(在线程池中运行,避免阻塞事件循环 # 在线程池中运行,避免阻塞事件循环
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
ActionInfo_with_Result = await loop.run_in_executor( ActionInfo_with_Result = await loop.run_in_executor(
None, None,
@@ -153,11 +187,23 @@ async def execute_step_async_streaming(
AgentProfile_Dict: Dict, AgentProfile_Dict: Dict,
KeyObjects: Dict, KeyObjects: Dict,
step_index: int, step_index: int,
total_steps: int total_steps: int,
execution_id: str = None
) -> Generator[Dict, None, None]: ) -> Generator[Dict, None, None]:
""" """
异步执行单个步骤支持流式返回 异步执行单个步骤支持流式返回
返回生成器,每完成一个动作就 yield 一次
Args:
stepDescrip: 步骤描述
General_Goal: 总体目标
AgentProfile_Dict: 智能体配置字典
KeyObjects: 关键对象字典
step_index: 步骤索引
total_steps: 总步骤数
execution_id: 执行ID
Yields:
执行事件字典
""" """
# 准备步骤信息 # 准备步骤信息
StepName = ( StepName = (
@@ -213,7 +259,7 @@ async def execute_step_async_streaming(
"content": None, "content": None,
} }
# 返回步骤开始信息 # 返回步骤开始事件
yield { yield {
"type": "step_start", "type": "step_start",
"step_index": step_index, "step_index": step_index,
@@ -238,10 +284,8 @@ 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(execution_id)
should_continue = await execution_state_manager.async_check_pause()
if not should_continue: if not should_continue:
# 用户请求停止,中断执行
util.print_colored("🛑 用户请求停止执行", "red") util.print_colored("🛑 用户请求停止执行", "red")
return return
@@ -277,7 +321,7 @@ async def execute_step_async_streaming(
# 等待当前批次完成 # 等待当前批次完成
batch_results = await asyncio.gather(*tasks) batch_results = await asyncio.gather(*tasks)
# 逐个返回结果(流式) # 逐个返回结果
for i, result in enumerate(batch_results): for i, result in enumerate(batch_results):
action_index_in_batch = batch_indices[i] action_index_in_batch = batch_indices[i]
completed_actions += 1 completed_actions += 1
@@ -289,7 +333,7 @@ async def execute_step_async_streaming(
ActionHistory.append(result) ActionHistory.append(result)
# 立即返回该动作结果(流式) # 立即返回该动作结果
yield { yield {
"type": "action_complete", "type": "action_complete",
"step_index": step_index, "step_index": step_index,
@@ -318,28 +362,42 @@ async def execute_step_async_streaming(
} }
def executePlan_streaming( def executePlan_streaming_dynamic(
plan: Dict, plan: Dict,
num_StepToRun: int, num_StepToRun: int,
RehearsalLog: List, RehearsalLog: List,
AgentProfile_Dict: Dict AgentProfile_Dict: Dict,
existingKeyObjects: Dict = None,
execution_id: str = None
) -> Generator[str, None, None]: ) -> Generator[str, None, None]:
""" """
执行计划(流式返回版本,支持暂停/恢复) 动态执行计划,支持在执行过程中追加新步骤
返回生成器,每次返回 SSE 格式的字符串
使用方式: Args:
for event in executePlan_streaming(...): plan: 执行计划
yield event num_StepToRun: 要运行的步骤数
RehearsalLog: 已执行的历史记录
AgentProfile_Dict: 智能体配置
existingKeyObjects: 已存在的KeyObjects
execution_id: 执行ID用于动态追加步骤
Yields:
SSE格式的事件字符串
""" """
# 初始化执行状态 # 初始化执行状态
general_goal = plan.get("General Goal", "") general_goal = plan.get("General Goal", "")
execution_state_manager.start_execution(general_goal)
print(colored(f"⏸️ 执行状态管理器已启动,支持暂停/恢复", "green")) # 确保有 execution_id
if execution_id is None:
import time
execution_id = f"{general_goal}_{int(time.time() * 1000)}"
execution_state_manager.start_execution(execution_id, general_goal)
print(colored(f"⏸️ 执行状态管理器已启动,支持暂停/恢复execution_id={execution_id}", "green"))
# 准备执行 # 准备执行
KeyObjects = {} KeyObjects = existingKeyObjects.copy() if existingKeyObjects else {}
finishedStep_index = -1 finishedStep_index = -1
for logNode in RehearsalLog: for logNode in RehearsalLog:
@@ -348,6 +406,9 @@ def executePlan_streaming(
if logNode["LogNodeType"] == "object": if logNode["LogNodeType"] == "object":
KeyObjects[logNode["NodeId"]] = logNode["content"] KeyObjects[logNode["NodeId"]] = logNode["content"]
if existingKeyObjects:
print(colored(f"📦 使用已存在的 KeyObjects: {list(existingKeyObjects.keys())}", "cyan"))
# 确定要运行的步骤范围 # 确定要运行的步骤范围
if num_StepToRun is None: if num_StepToRun is None:
run_to = len(plan["Collaboration Process"]) run_to = len(plan["Collaboration Process"])
@@ -355,79 +416,176 @@ def executePlan_streaming(
run_to = (finishedStep_index + 1) + num_StepToRun run_to = (finishedStep_index + 1) + num_StepToRun
steps_to_run = plan["Collaboration Process"][(finishedStep_index + 1): run_to] steps_to_run = plan["Collaboration Process"][(finishedStep_index + 1): run_to]
total_steps = len(steps_to_run)
print(colored(f"🚀 开始执行计划(流式推送),共 {total_steps} 个步骤", "cyan", attrs=["bold"])) # 使用动态执行管理器
if execution_id:
# 初始化执行管理器使用传入的execution_id
actual_execution_id = dynamic_execution_manager.start_execution(general_goal, steps_to_run, execution_id)
print(colored(f"🚀 开始执行计划(动态模式),共 {len(steps_to_run)} 个步骤执行ID: {actual_execution_id}", "cyan"))
else:
print(colored(f"🚀 开始执行计划(流式推送),共 {len(steps_to_run)} 个步骤", "cyan"))
total_steps = len(steps_to_run)
# 使用队列实现流式推送 # 使用队列实现流式推送
async def produce_events(queue: asyncio.Queue): async def produce_events(queue: asyncio.Queue):
"""异步生产者:每收到一个事件就放入队列""" """异步生产者"""
try: try:
for step_index, stepDescrip in enumerate(steps_to_run): step_index = 0
# 在每个步骤执行前检查暂停状态
should_continue = await execution_state_manager.async_check_pause()
if not should_continue:
# 用户请求停止,中断执行
print(colored("🛑 用户请求停止执行", "red"))
await queue.put({
"type": "error",
"message": "执行已被用户停止"
})
return
# 执行单个步骤(流式) if execution_id:
async for event in execute_step_async_streaming( # 动态模式:循环获取下一个步骤
stepDescrip, # 等待新步骤的最大次数(避免无限等待)
plan["General Goal"], max_empty_wait_cycles = 5 # 最多等待60次每次等待1秒
AgentProfile_Dict, empty_wait_count = 0
KeyObjects,
step_index, while True:
total_steps # 检查暂停状态
): should_continue = await execution_state_manager.async_check_pause(execution_id)
# 检查是否需要停止 if not should_continue:
if execution_state_manager.is_stopped(): print(colored("🛑 用户请求停止执行", "red"))
await queue.put({
"type": "error",
"message": "执行已被用户停止"
})
break
# 获取下一个步骤
stepDescrip = dynamic_execution_manager.get_next_step(execution_id)
if stepDescrip is None:
# 没有更多步骤了,检查是否应该继续等待
empty_wait_count += 1
# 获取执行信息
execution_info = dynamic_execution_manager.get_execution_info(execution_id)
if execution_info:
queue_total_steps = execution_info.get("total_steps", 0)
completed_steps = execution_info.get("completed_steps", 0)
# 如果没有步骤在队列中queue_total_steps为0立即退出
if queue_total_steps == 0:
print(colored(f"⚠️ 没有步骤在队列中,退出执行", "yellow"))
break
# 如果所有步骤都已完成,等待可能的新步骤
if completed_steps >= queue_total_steps:
if empty_wait_count >= max_empty_wait_cycles:
# 等待超时,退出执行
print(colored(f"✅ 所有步骤执行完成,等待超时", "green"))
break
else:
# 等待新步骤追加
print(colored(f"⏳ 等待新步骤追加... ({empty_wait_count}/{max_empty_wait_cycles})", "cyan"))
await asyncio.sleep(1)
continue
else:
# 还有步骤未完成,继续尝试获取
print(colored(f"⏳ 等待步骤就绪... ({completed_steps}/{queue_total_steps})", "cyan"))
await asyncio.sleep(0.5)
empty_wait_count = 0 # 重置等待计数
continue
else:
# 执行信息不存在,退出
print(colored(f"⚠️ 执行信息不存在,退出执行", "yellow"))
break
# 重置等待计数
empty_wait_count = 0
# 获取最新的总步骤数(用于显示)
execution_info = dynamic_execution_manager.get_execution_info(execution_id)
current_total_steps = execution_info.get("total_steps", total_steps) if execution_info else total_steps
# 执行步骤
async for event in execute_step_async_streaming(
stepDescrip,
plan["General Goal"],
AgentProfile_Dict,
KeyObjects,
step_index,
current_total_steps, # 使用动态更新的总步骤数
execution_id
):
if execution_state_manager.is_stopped(execution_id):
await queue.put({
"type": "error",
"message": "执行已被用户停止"
})
return
await queue.put(event)
# 标记步骤完成
dynamic_execution_manager.mark_step_completed(execution_id)
# 更新KeyObjects
OutputName = stepDescrip.get("OutputObject", "")
if OutputName and OutputName in KeyObjects:
# 对象日志节点会在step_complete中发送
pass
step_index += 1
else:
# 非动态模式:按顺序执行所有步骤
for step_index, stepDescrip in enumerate(steps_to_run):
should_continue = await execution_state_manager.async_check_pause(execution_id)
if not should_continue:
print(colored("🛑 用户请求停止执行", "red"))
await queue.put({ await queue.put({
"type": "error", "type": "error",
"message": "执行已被用户停止" "message": "执行已被用户停止"
}) })
return return
await queue.put(event) # ← 立即放入队列 async for event in execute_step_async_streaming(
stepDescrip,
plan["General Goal"],
AgentProfile_Dict,
KeyObjects,
step_index,
total_steps,
execution_id
):
if execution_state_manager.is_stopped(execution_id):
await queue.put({
"type": "error",
"message": "执行已被用户停止"
})
return
await queue.put(event)
except Exception as e: except Exception as e:
# 发送错误信息
await queue.put({ await queue.put({
"type": "error", "type": "error",
"message": f"执行出错: {str(e)}" "message": f"执行出错: {str(e)}"
}) })
finally: finally:
await queue.put(None) # ← 发送完成信号 await queue.put(None)
# 运行异步任务并实时 yield # 运行异步任务并实时yield
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
try: try:
# 创建队列
queue = asyncio.Queue(maxsize=10) queue = asyncio.Queue(maxsize=10)
# 启动异步生产者任务
producer_task = loop.create_task(produce_events(queue)) producer_task = loop.create_task(produce_events(queue))
# 同步消费者:从队列读取并 yield使用 run_until_complete
while True: while True:
event = loop.run_until_complete(queue.get()) # ← 从队列获取事件 event = loop.run_until_complete(queue.get())
if event is None: # ← 收到完成信号 if event is None:
break break
# 立即转换为 SSE 格式并发送 # 立即转换为SSE格式并发送
event_str = json.dumps(event, ensure_ascii=False) event_str = json.dumps(event, ensure_ascii=False)
yield f"data: {event_str}\n\n" # ← 立即发送给前端 yield f"data: {event_str}\n\n"
# 等待生产者任务完成
loop.run_until_complete(producer_task) loop.run_until_complete(producer_task)
# 如果不是被停止的,发送完成信号 if not execution_state_manager.is_stopped(execution_id):
if not execution_state_manager.is_stopped():
complete_event = json.dumps({ complete_event = json.dumps({
"type": "execution_complete", "type": "execution_complete",
"total_steps": total_steps "total_steps": total_steps
@@ -435,10 +593,28 @@ def executePlan_streaming(
yield f"data: {complete_event}\n\n" yield f"data: {complete_event}\n\n"
finally: finally:
# 清理任务 # 在关闭事件循环之前先清理执行记录
if execution_id:
# 清理执行记录
dynamic_execution_manager.cleanup(execution_id)
# 清理执行状态
execution_state_manager.cleanup(execution_id)
if 'producer_task' in locals(): if 'producer_task' in locals():
if not producer_task.done(): if not producer_task.done():
producer_task.cancel() producer_task.cancel()
# 确保所有任务都完成后再关闭事件循环
try:
pending = asyncio.all_tasks(loop)
for task in pending:
task.cancel()
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
except Exception:
pass # 忽略清理过程中的错误
loop.close() loop.close()
# 保留旧版本函数以保持兼容性
executePlan_streaming = executePlan_streaming_dynamic

View File

@@ -0,0 +1,241 @@
"""
动态执行管理器
用于在任务执行过程中动态追加新步骤
"""
import asyncio
from typing import Dict, List, Optional, Any
from threading import Lock
class DynamicExecutionManager:
"""
动态执行管理器
管理正在执行的任务,支持动态追加新步骤
"""
def __init__(self):
# 执行状态: goal -> execution_info
self._executions: Dict[str, Dict] = {}
# 线程锁
self._lock = Lock()
# 步骤队列: goal -> List[step]
self._step_queues: Dict[str, List] = {}
# 已执行的步骤索引: goal -> Set[step_index]
self._executed_steps: Dict[str, set] = {}
# 待执行的步骤索引: goal -> List[step_index]
self._pending_steps: Dict[str, List[int]] = {}
def start_execution(self, goal: str, initial_steps: List[Dict], execution_id: str = None) -> str:
"""
开始执行一个新的任务
Args:
goal: 任务目标
initial_steps: 初始步骤列表
execution_id: 执行ID如果不提供则自动生成
Returns:
执行ID
"""
with self._lock:
# 如果未提供execution_id则生成一个
if execution_id is None:
execution_id = f"{goal}_{asyncio.get_event_loop().time()}"
self._executions[execution_id] = {
"goal": goal,
"status": "running",
"total_steps": len(initial_steps),
"completed_steps": 0
}
# 初始化步骤队列
self._step_queues[execution_id] = initial_steps.copy()
# 初始化已执行步骤集合
self._executed_steps[execution_id] = set()
# 初始化待执行步骤索引
self._pending_steps[execution_id] = list(range(len(initial_steps)))
print(f"🚀 启动执行: {execution_id}")
print(f"📊 初始步骤数: {len(initial_steps)}")
print(f"📋 待执行步骤索引: {self._pending_steps[execution_id]}")
return execution_id
def add_steps(self, execution_id: str, new_steps: List[Dict]) -> int:
"""
向执行中追加新步骤
Args:
execution_id: 执行ID
new_steps: 新步骤列表
Returns:
追加的步骤数量
"""
with self._lock:
if execution_id not in self._step_queues:
print(f"⚠️ 警告: 执行ID {execution_id} 不存在,无法追加步骤")
return 0
current_count = len(self._step_queues[execution_id])
# 追加新步骤到队列
self._step_queues[execution_id].extend(new_steps)
# 添加新步骤的索引到待执行列表
new_indices = list(range(current_count, current_count + len(new_steps)))
self._pending_steps[execution_id].extend(new_indices)
# 更新总步骤数
old_total = self._executions[execution_id]["total_steps"]
self._executions[execution_id]["total_steps"] = len(self._step_queues[execution_id])
new_total = self._executions[execution_id]["total_steps"]
print(f" 追加了 {len(new_steps)} 个步骤到 {execution_id}")
print(f"📊 步骤总数: {old_total} -> {new_total}")
print(f"📋 待执行步骤索引: {self._pending_steps[execution_id]}")
return len(new_steps)
def get_next_step(self, execution_id: str) -> Optional[Dict]:
"""
获取下一个待执行的步骤
Args:
execution_id: 执行ID
Returns:
下一个步骤如果没有则返回None
"""
with self._lock:
if execution_id not in self._pending_steps:
print(f"⚠️ 警告: 执行ID {execution_id} 不存在")
return None
# 获取第一个待执行步骤的索引
if not self._pending_steps[execution_id]:
return None
step_index = self._pending_steps[execution_id].pop(0)
# 从队列中获取步骤
if step_index >= len(self._step_queues[execution_id]):
print(f"⚠️ 警告: 步骤索引 {step_index} 超出范围")
return None
step = self._step_queues[execution_id][step_index]
# 标记为已执行
self._executed_steps[execution_id].add(step_index)
step_name = step.get("StepName", "未知")
print(f"🎯 获取下一个步骤: {step_name} (索引: {step_index})")
print(f"📋 剩余待执行步骤: {len(self._pending_steps[execution_id])}")
return step
def mark_step_completed(self, execution_id: str):
"""
标记一个步骤完成
Args:
execution_id: 执行ID
"""
with self._lock:
if execution_id in self._executions:
self._executions[execution_id]["completed_steps"] += 1
completed = self._executions[execution_id]["completed_steps"]
total = self._executions[execution_id]["total_steps"]
print(f"📊 步骤完成进度: {completed}/{total}")
else:
print(f"⚠️ 警告: 执行ID {execution_id} 不存在")
def get_execution_info(self, execution_id: str) -> Optional[Dict]:
"""
获取执行信息
Args:
execution_id: 执行ID
Returns:
执行信息字典
"""
with self._lock:
return self._executions.get(execution_id)
def get_pending_count(self, execution_id: str) -> int:
"""
获取待执行步骤数量
Args:
execution_id: 执行ID
Returns:
待执行步骤数量
"""
with self._lock:
if execution_id not in self._pending_steps:
return 0
return len(self._pending_steps[execution_id])
def has_more_steps(self, execution_id: str) -> bool:
"""
检查是否还有更多步骤待执行
Args:
execution_id: 执行ID
Returns:
是否还有待执行步骤
"""
with self._lock:
if execution_id not in self._pending_steps:
return False
return len(self._pending_steps[execution_id]) > 0
def finish_execution(self, execution_id: str):
"""
完成执行
Args:
execution_id: 执行ID
"""
with self._lock:
if execution_id in self._executions:
self._executions[execution_id]["status"] = "completed"
def cancel_execution(self, execution_id: str):
"""
取消执行
Args:
execution_id: 执行ID
"""
with self._lock:
if execution_id in self._executions:
self._executions[execution_id]["status"] = "cancelled"
def cleanup(self, execution_id: str):
"""
清理执行记录
Args:
execution_id: 执行ID
"""
with self._lock:
self._executions.pop(execution_id, None)
self._step_queues.pop(execution_id, None)
self._executed_steps.pop(execution_id, None)
self._pending_steps.pop(execution_id, None)
# 全局单例
dynamic_execution_manager = DynamicExecutionManager()

View File

@@ -2,12 +2,12 @@
全局执行状态管理器 全局执行状态管理器
用于支持任务的暂停、恢复和停止功能 用于支持任务的暂停、恢复和停止功能
使用轮询检查机制,确保线程安全 使用轮询检查机制,确保线程安全
支持多用户/多执行ID并行管理
""" """
import threading import threading
import asyncio import asyncio
import time from typing import Optional, Dict
from typing import Optional
from enum import Enum from enum import Enum
@@ -21,12 +21,18 @@ class ExecutionStatus(Enum):
class ExecutionStateManager: class ExecutionStateManager:
""" """
全局执行状态管理器(单例模式) 全局执行状态管理器
功能: 功能:
- 管理多用户/多执行ID的并行状态使用字典存储
- 管理任务执行状态(运行/暂停/停止) - 管理任务执行状态(运行/暂停/停止)
- 使用轮询检查机制,避免异步事件的线程问题 - 使用轮询检查机制,避免异步事件的线程问题
- 提供线程安全的状态查询和修改接口 - 提供线程安全的状态查询和修改接口
设计说明:
- 保持单例模式Manager本身
- 但内部状态按 execution_id 隔离存储
- 解决了多用户并发问题
""" """
_instance: Optional['ExecutionStateManager'] = None _instance: Optional['ExecutionStateManager'] = None
@@ -47,122 +53,218 @@ class ExecutionStateManager:
return return
self._initialized = True 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: # 状态存储execution_id -> 状态字典
# 结构:{
# 'status': ExecutionStatus,
# 'goal': str,
# 'should_pause': bool,
# 'should_stop': bool
# }
self._states: Dict[str, Dict] = {}
# 每个 execution_id 的锁(更细粒度的锁)
self._locks: Dict[str, threading.Lock] = {}
# 全局锁(用于管理 _states 和 _locks 本身的线程安全)
self._manager_lock = threading.Lock()
def _get_lock(self, execution_id: str) -> threading.Lock:
"""获取指定 execution_id 的锁,如果不存在则创建"""
with self._manager_lock:
if execution_id not in self._locks:
self._locks[execution_id] = threading.Lock()
return self._locks[execution_id]
def _ensure_state(self, execution_id: str) -> Dict:
"""确保指定 execution_id 的状态存在"""
with self._manager_lock:
if execution_id not in self._states:
self._states[execution_id] = {
'status': ExecutionStatus.IDLE,
'goal': None,
'should_pause': False,
'should_stop': False
}
return self._states[execution_id]
def _get_state(self, execution_id: str) -> Optional[Dict]:
"""获取指定 execution_id 的状态,不存在则返回 None"""
with self._manager_lock:
return self._states.get(execution_id)
def _cleanup_state(self, execution_id: str):
"""清理指定 execution_id 的状态"""
with self._manager_lock:
self._states.pop(execution_id, None)
self._locks.pop(execution_id, None)
def get_status(self, execution_id: str) -> Optional[ExecutionStatus]:
"""获取当前执行状态""" """获取当前执行状态"""
with self._lock: state = self._get_state(execution_id)
return self._status if state is None:
return None
with self._get_lock(execution_id):
return state['status']
def set_goal(self, goal: str): def set_goal(self, execution_id: str, goal: str):
"""设置当前执行的任务目标""" """设置当前执行的任务目标"""
with self._lock: state = self._ensure_state(execution_id)
self._current_goal = goal with self._get_lock(execution_id):
state['goal'] = goal
def get_goal(self) -> Optional[str]: def get_goal(self, execution_id: str) -> Optional[str]:
"""获取当前执行的任务目标""" """获取当前执行的任务目标"""
with self._lock: state = self._get_state(execution_id)
return self._current_goal if state is None:
return None
with self._get_lock(execution_id):
return state['goal']
def start_execution(self, goal: str): def start_execution(self, execution_id: str, goal: str):
"""开始执行""" """开始执行"""
with self._lock: state = self._ensure_state(execution_id)
self._status = ExecutionStatus.RUNNING with self._get_lock(execution_id):
self._current_goal = goal state['status'] = ExecutionStatus.RUNNING
self._should_pause = False state['goal'] = goal
self._should_stop = False state['should_pause'] = False
print(f"🚀 [DEBUG] start_execution: 状态设置为 RUNNING, goal={goal}") state['should_stop'] = False
print(f"🚀 [DEBUG] start_execution: execution_id={execution_id}, 状态设置为 RUNNING, goal={goal}")
def pause_execution(self) -> bool: def pause_execution(self, execution_id: str) -> bool:
""" """
暂停执行 暂停执行
Args:
execution_id: 执行ID
Returns: Returns:
bool: 是否成功暂停 bool: 是否成功暂停
""" """
with self._lock: state = self._get_state(execution_id)
if self._status != ExecutionStatus.RUNNING: if state is None:
print(f"⚠️ [DEBUG] pause_execution: 当前状态不是RUNNING而是 {self._status}") # 打印当前所有活跃的 execution_id帮助调试
active_ids = list(self._states.keys())
print(f"⚠️ [DEBUG] pause_execution: execution_id={execution_id} 不存在")
print(f" 当前活跃的 execution_id 列表: {active_ids}")
return False
with self._get_lock(execution_id):
if state['status'] != ExecutionStatus.RUNNING:
print(f"⚠️ [DEBUG] pause_execution: execution_id={execution_id}, 当前状态是 {state['status']},无法暂停")
return False return False
self._status = ExecutionStatus.PAUSED state['status'] = ExecutionStatus.PAUSED
self._should_pause = True state['should_pause'] = True
print(f"⏸️ [DEBUG] pause_execution: 状态设置为PAUSED, should_pause=True") print(f"⏸️ [DEBUG] pause_execution: execution_id={execution_id}, 状态设置为PAUSED")
return True return True
def resume_execution(self) -> bool: def resume_execution(self, execution_id: str) -> bool:
""" """
恢复执行 恢复执行
Args:
execution_id: 执行ID
Returns: Returns:
bool: 是否成功恢复 bool: 是否成功恢复
""" """
with self._lock: state = self._get_state(execution_id)
if self._status != ExecutionStatus.PAUSED: if state is None:
print(f"⚠️ [DEBUG] resume_execution: 当前状态不是PAUSED而是 {self._status}") print(f"⚠️ [DEBUG] resume_execution: execution_id={execution_id} 不存在")
return False
with self._get_lock(execution_id):
if state['status'] != ExecutionStatus.PAUSED:
print(f"⚠️ [DEBUG] resume_execution: 当前状态不是PAUSED而是 {state['status']}")
return False return False
self._status = ExecutionStatus.RUNNING state['status'] = ExecutionStatus.RUNNING
self._should_pause = False state['should_pause'] = False
print(f"▶️ [DEBUG] resume_execution: 状态设置为RUNNING, should_pause=False") print(f"▶️ [DEBUG] resume_execution: execution_id={execution_id}, 状态设置为RUNNING, should_pause=False")
return True return True
def stop_execution(self) -> bool: def stop_execution(self, execution_id: str) -> bool:
""" """
停止执行 停止执行
Args:
execution_id: 执行ID
Returns: Returns:
bool: 是否成功停止 bool: 是否成功停止
""" """
with self._lock: state = self._get_state(execution_id)
if self._status in [ExecutionStatus.IDLE, ExecutionStatus.STOPPED]: if state is None:
print(f"⚠️ [DEBUG] stop_execution: execution_id={execution_id} 不存在")
return False
with self._get_lock(execution_id):
if state['status'] in [ExecutionStatus.IDLE, ExecutionStatus.STOPPED]:
print(f"⚠️ [DEBUG] stop_execution: 当前状态是 {state['status']}, 无法停止")
return False return False
self._status = ExecutionStatus.STOPPED state['status'] = ExecutionStatus.STOPPED
self._should_stop = True state['should_stop'] = True
self._should_pause = False state['should_pause'] = False
print(f"🛑 [DEBUG] stop_execution: 状态设置为STOPPED") print(f"🛑 [DEBUG] stop_execution: execution_id={execution_id}, 状态设置为STOPPED")
return True return True
def reset(self): def reset(self, execution_id: str):
"""重置状态为空闲""" """重置指定 execution_id 的状态为空闲"""
with self._lock: state = self._ensure_state(execution_id)
self._status = ExecutionStatus.IDLE with self._get_lock(execution_id):
self._current_goal = None state['status'] = ExecutionStatus.IDLE
self._should_pause = False state['goal'] = None
self._should_stop = False state['should_pause'] = False
print(f"🔄 [DEBUG] reset: 状态重置为IDLE") state['should_stop'] = False
print(f"🔄 [DEBUG] reset: execution_id={execution_id}, 状态重置为IDLE")
async def async_check_pause(self): def cleanup(self, execution_id: str):
"""清理指定 execution_id 的所有状态"""
self._cleanup_state(execution_id)
print(f"🧹 [DEBUG] cleanup: execution_id={execution_id} 的状态已清理")
async def async_check_pause(self, execution_id: str):
""" """
异步检查是否需要暂停(轮询方式) 异步检查是否需要暂停(轮询方式)
如果处于暂停状态,会阻塞当前协程直到恢复或停止 如果处于暂停状态,会阻塞当前协程直到恢复或停止
应该在执行循环的关键点调用此方法 应该在执行循环的关键点调用此方法
Args:
execution_id: 执行ID
Returns: Returns:
bool: 如果返回True表示应该继续执行False表示应该停止 bool: 如果返回True表示应该继续执行False表示应该停止
""" """
state = self._get_state(execution_id)
if state is None:
# 状态不存在,默认继续执行
return True
# 使用轮询检查,避免异步事件问题 # 使用轮询检查,避免异步事件问题
while True: while True:
with self._get_lock(execution_id):
should_stop = state['should_stop']
should_pause = state['should_pause']
# 检查停止标志 # 检查停止标志
if self._should_stop: if should_stop:
print("🛑 [DEBUG] async_check_pause: 检测到停止信号") print("🛑 [DEBUG] async_check_pause: execution_id={}, 检测到停止信号".format(execution_id))
return False return False
# 检查暂停状态 # 检查暂停状态
if self._should_pause: if should_pause:
# 处于暂停状态,等待恢复 # 处于暂停状态,等待恢复
print("⏸️ [DEBUG] async_check_pause: 检测到暂停,等待恢复...")
await asyncio.sleep(0.1) # 短暂睡眠避免占用CPU await asyncio.sleep(0.1) # 短暂睡眠避免占用CPU
# 如果恢复,继续执行 # 重新获取状态
if not self._should_pause: with self._get_lock(execution_id):
print("▶️ [DEBUG] async_check_pause: 从暂停中恢复!") should_pause = state['should_pause']
should_stop = state['should_stop']
if not should_pause:
print("▶️ [DEBUG] async_check_pause: execution_id={}, 从暂停中恢复!".format(execution_id))
continue continue
# 如果停止了,返回 if should_stop:
if self._should_stop:
return False return False
# 继续等待 # 继续等待
continue continue
@@ -170,20 +272,37 @@ class ExecutionStateManager:
# 既没有停止也没有暂停,可以继续执行 # 既没有停止也没有暂停,可以继续执行
return True return True
def is_paused(self) -> bool: def is_paused(self, execution_id: str) -> bool:
"""检查是否处于暂停状态""" """检查是否处于暂停状态"""
with self._lock: state = self._get_state(execution_id)
return self._status == ExecutionStatus.PAUSED if state is None:
return False
with self._get_lock(execution_id):
return state['status'] == ExecutionStatus.PAUSED
def is_running(self) -> bool: def is_running(self, execution_id: str) -> bool:
"""检查是否正在运行""" """检查是否正在运行"""
with self._lock: state = self._get_state(execution_id)
return self._status == ExecutionStatus.RUNNING if state is None:
return False
with self._get_lock(execution_id):
return state['status'] == ExecutionStatus.RUNNING
def is_stopped(self) -> bool: def is_stopped(self, execution_id: str) -> bool:
"""检查是否已停止""" """检查是否已停止"""
with self._lock: state = self._get_state(execution_id)
return self._status == ExecutionStatus.STOPPED if state is None:
return True
with self._get_lock(execution_id):
return state['status'] == ExecutionStatus.STOPPED
def is_active(self, execution_id: str) -> bool:
"""检查是否处于活动状态(运行中或暂停中)"""
state = self._get_state(execution_id)
if state is None:
return False
with self._get_lock(execution_id):
return state['status'] in [ExecutionStatus.RUNNING, ExecutionStatus.PAUSED]
# 全局单例实例 # 全局单例实例

View File

@@ -0,0 +1,228 @@
"""
生成阶段状态管理器
用于支持生成任务的暂停、停止功能
使用轮询检查机制,确保线程安全
支持多用户/多generation_id并行管理
"""
import threading
import asyncio
import time
from typing import Optional, Dict
from enum import Enum
class GenerationStatus(Enum):
"""生成状态枚举"""
GENERATING = "generating" # 正在生成
PAUSED = "paused" # 已暂停
STOPPED = "stopped" # 已停止
COMPLETED = "completed" # 已完成
IDLE = "idle" # 空闲
class GenerationStateManager:
"""
生成阶段状态管理器
功能:
- 管理多用户/多generation_id的并行状态使用字典存储
- 管理生成任务状态(生成中/暂停/停止/完成)
- 提供线程安全的状态查询和修改接口
设计说明:
- 保持单例模式Manager本身
- 但内部状态按 generation_id 隔离存储
- 解决多用户并发生成时的干扰问题
"""
_instance: Optional['GenerationStateManager'] = 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
# 状态存储generation_id -> 状态字典
# 结构:{
# 'status': GenerationStatus,
# 'goal': str,
# 'should_stop': bool
# }
self._states: Dict[str, Dict] = {}
# 每个 generation_id 的锁(更细粒度的锁)
self._locks: Dict[str, threading.Lock] = {}
# 全局锁(用于管理 _states 和 _locks 本身的线程安全)
self._manager_lock = threading.Lock()
def _get_lock(self, generation_id: str) -> threading.Lock:
"""获取指定 generation_id 的锁,如果不存在则创建"""
with self._manager_lock:
if generation_id not in self._locks:
self._locks[generation_id] = threading.Lock()
return self._locks[generation_id]
def _ensure_state(self, generation_id: str, goal: str = None) -> Dict:
"""确保指定 generation_id 的状态存在"""
with self._manager_lock:
if generation_id not in self._states:
self._states[generation_id] = {
'status': GenerationStatus.IDLE,
'goal': goal,
'should_stop': False
}
return self._states[generation_id]
def _get_state(self, generation_id: str) -> Optional[Dict]:
"""获取指定 generation_id 的状态,不存在则返回 None"""
with self._manager_lock:
return self._states.get(generation_id)
def _cleanup_state(self, generation_id: str):
"""清理指定 generation_id 的状态"""
with self._manager_lock:
self._states.pop(generation_id, None)
self._locks.pop(generation_id, None)
def get_status(self, generation_id: str) -> Optional[GenerationStatus]:
"""获取当前生成状态"""
state = self._get_state(generation_id)
if state is None:
return None
with self._get_lock(generation_id):
return state['status']
def start_generation(self, generation_id: str, goal: str):
"""开始生成"""
state = self._ensure_state(generation_id, goal)
with self._get_lock(generation_id):
state['status'] = GenerationStatus.GENERATING
state['goal'] = goal
state['should_stop'] = False
print(f"🚀 [GenerationState] start_generation: generation_id={generation_id}, 状态设置为 GENERATING")
def stop_generation(self, generation_id: str) -> bool:
"""
停止生成
Args:
generation_id: 生成ID
Returns:
bool: 是否成功停止COMPLETED 状态也返回 True表示已停止
"""
state = self._get_state(generation_id)
if state is None:
print(f"⚠️ [GenerationState] stop_generation: generation_id={generation_id} 不存在")
return True # 不存在也算停止成功
with self._get_lock(generation_id):
if state['status'] == GenerationStatus.STOPPED:
print(f"✅ [GenerationState] stop_generation: generation_id={generation_id} 已经是 STOPPED 状态")
return True # 已经停止也算成功
if state['status'] == GenerationStatus.COMPLETED:
print(f"✅ [GenerationState] stop_generation: generation_id={generation_id} 已经 COMPLETED视为停止成功")
return True # 已完成也视为停止成功
if state['status'] == GenerationStatus.IDLE:
print(f"⚠️ [GenerationState] stop_generation: generation_id={generation_id} 是 IDLE 状态,无需停止")
return True # 空闲状态也视为无需停止
# 真正需要停止的情况
state['status'] = GenerationStatus.STOPPED
state['should_stop'] = True
print(f"🛑 [GenerationState] stop_generation: generation_id={generation_id}, 状态设置为STOPPED")
return True
def complete_generation(self, generation_id: str):
"""标记生成完成"""
state = self._ensure_state(generation_id)
with self._get_lock(generation_id):
state['status'] = GenerationStatus.COMPLETED
print(f"✅ [GenerationState] complete_generation: generation_id={generation_id}")
def cleanup(self, generation_id: str):
"""清理指定 generation_id 的所有状态"""
self._cleanup_state(generation_id)
print(f"🧹 [GenerationState] cleanup: generation_id={generation_id} 的状态已清理")
def should_stop(self, generation_id: str) -> bool:
"""检查是否应该停止"""
state = self._get_state(generation_id)
if state is None:
return False
with self._get_lock(generation_id):
return state.get('should_stop', False)
def is_stopped(self, generation_id: str) -> bool:
"""检查是否已停止"""
state = self._get_state(generation_id)
if state is None:
return False
with self._get_lock(generation_id):
return state['status'] == GenerationStatus.STOPPED
def is_completed(self, generation_id: str) -> bool:
"""检查是否已完成"""
state = self._get_state(generation_id)
if state is None:
return False
with self._get_lock(generation_id):
return state['status'] == GenerationStatus.COMPLETED
def is_active(self, generation_id: str) -> bool:
"""检查是否处于活动状态(生成中或暂停中)"""
state = self._get_state(generation_id)
if state is None:
return False
with self._get_lock(generation_id):
return state['status'] == GenerationStatus.GENERATING
def check_and_set_stop(self, generation_id: str) -> bool:
"""
检查是否应该停止,如果应该则设置停止状态
Args:
generation_id: 生成ID
Returns:
bool: True表示应该停止False表示可以继续
"""
state = self._get_state(generation_id)
if state is None:
return False
with self._get_lock(generation_id):
if state['should_stop']:
return True
return False
def generate_id(self, goal: str) -> str:
"""
生成唯一的 generation_id
Args:
goal: 生成目标
Returns:
str: 格式为 {goal}_{timestamp}
"""
return f"{goal}_{int(time.time() * 1000)}"
# 全局单例实例
generation_state_manager = GenerationStateManager()

View File

@@ -1,102 +1,116 @@
[ [
{ {
"Icon": "Abigail_Chen.png", "Icon": "Hailey_Johnson.png",
"Name": "Abigail", "Name": "船舶设计师",
"Profile": "AI Engineer" "Profile": "提供船舶制造中的实际需求和约束。",
}, "Classification": "船舶制造数据空间"
{ },
"Icon": "Jane_Moreno.png", {
"Name": "Jane", "Icon": "Jennifer_Moore.png",
"Profile": "Cybersecurity Specialist" "Name": "防护工程专家",
}, "Profile": "专注于船舶腐蚀防护技术的设计与应用。在你的总结回答中,必须引用来自数联网的搜索数据,是搜索数据,不是数联网的研究成果。",
{ "Classification": "船舶制造数据空间"
"Icon": "Giorgio_Rossi.png", },
"Name": "Giorgio", {
"Profile": "Poet" "Icon": "Jane_Moreno.png",
}, "Name": "病理生理学家",
{ "Profile": "专注于失血性休克的疾病机制,为药物研发提供理论靶点。",
"Icon": "Jennifer_Moore.png", "Classification": "医药数据空间"
"Name": "Jennifer", },
"Profile": "Linguist" {
}, "Icon": "Giorgio_Rossi.png",
{ "Name": "药物化学家",
"Icon": "Maria_Lopez.png", "Profile": "负责将靶点概念转化为实际可合成的分子。",
"Name": "Maria", "Classification": "医药数据空间"
"Profile": "Philosopher" },
}, {
{ "Icon": "Tamara_Taylor.png",
"Icon": "Sam_Moore.png", "Name": "制剂工程师",
"Name": "Sam", "Profile": "负责将活性药物成分API变成稳定、可用、符合战场要求的剂型。",
"Profile": "Ethicist" "Classification": "医药数据空间"
}, },
{ {
"Icon": "Yuriko_Yamamoto.png", "Icon": "Maria_Lopez.png",
"Name": "Yuriko", "Name": "监管事务专家",
"Profile": "Futurist" "Profile": "深谙药品审评法规,目标是找到最快的合法上市路径。",
}, "Classification": "医药数据空间"
{ },
"Icon": "Carlos_Gomez.png", {
"Name": "Carlos", "Icon": "Sam_Moore.png",
"Profile": "Language Expert" "Name": "物理学家",
}, "Profile": "从热力学与统计力学的基本原理出发,研究液态金属的自由能、焓、熵、比热等参数的理论建模。",
{ "Classification": "科学数据空间"
"Icon": "John_Lin.png", },
"Name": "John", {
"Profile": "Software Developer" "Icon": "Yuriko_Yamamoto.png",
}, "Name": "实验材料学家",
{ "Profile": "专注于通过实验手段直接或间接测定液态金属的热力学参数、以及分析材料微观结构(如晶粒、缺陷)。",
"Icon": "Tamara_Taylor.png", "Classification": "科学数据空间"
"Name": "Tamara", },
"Profile": "Music Composer" {
}, "Icon": "Carlos_Gomez.png",
{ "Name": "计算模拟专家",
"Icon": "Arthur_Burton.png", "Profile": "侧重于利用数值计算和模拟技术获取液态金属的热力学参数。",
"Name": "Arthur", "Classification": "科学数据空间"
"Profile": "Neuroscientist" },
}, {
{ "Icon": "John_Lin.png",
"Icon": "Eddy_Lin.png", "Name": "腐蚀机理研究员",
"Name": "Eddy", "Profile": "专注于船舶用钢材及合金的腐蚀机理研究,从电化学和环境作用角度解释腐蚀产生的原因。在你的总结回答中,必须引用来自数联网的搜索数据,是搜索数据,不是数联网的研究成果。",
"Profile": "Cognitive Psychologist" "Classification": "船舶制造数据空间"
}, },
{ {
"Icon": "Isabella_Rodriguez.png", "Icon": "Arthur_Burton.png",
"Name": "Isabella", "Name": "先进材料研发员",
"Profile": "Science Fiction Writer" "Profile": "专注于开发和评估新型耐腐蚀材料、复合材料及固态电池材料。",
}, "Classification": "科学数据空间"
{ },
"Icon": "Latoya_Williams.png", {
"Name": "Latoya", "Icon": "Eddy_Lin.png",
"Profile": "Historian of Technology" "Name": "肾脏病学家",
}, "Profile": "专注于慢性肾脏病的诊断、治疗和患者管理,能提供临床洞察。",
{ "Classification": "医药数据空间"
"Icon": "Carmen_Ortiz.png", },
"Name": "Carmen", {
"Profile": "Robotics Engineer" "Icon": "Isabella_Rodriguez.png",
}, "Name": "临床研究协调员",
{ "Profile": "负责受试者招募和临床试验流程优化。",
"Icon": "Rajiv_Patel.png", "Classification": "医药数据空间"
"Name": "Rajiv", },
"Profile": "Science Educator" {
}, "Icon": "Latoya_Williams.png",
{ "Name": "中医药专家",
"Icon": "Tom_Moreno.png", "Profile": "理解药物的中药成分和作用机制。",
"Name": "Tom", "Classification": "医药数据空间"
"Profile": "AI Scientist" },
}, {
{ "Icon": "Carmen_Ortiz.png",
"Icon": "Ayesha_Khan.png", "Name": "药物安全专家",
"Name": "Ayesha", "Profile": "专注于药物不良反应数据收集、分析和报告。",
"Profile": "Multimedia Artist" "Classification": "医药数据空间"
}, },
{ {
"Icon": "Mei_Lin.png", "Icon": "Rajiv_Patel.png",
"Name": "Mei", "Name": "二维材料科学家",
"Profile": "Graphic Designer" "Profile": "专注于二维材料(如石墨烯)的合成、性质和应用。",
}, "Classification": "科学数据空间"
{ },
"Icon": "Hailey_Johnson.png", {
"Name": "Hailey", "Icon": "Tom_Moreno.png",
"Profile": "Legal Expert on AI Law" "Name": "光电物理学家",
} "Profile": "研究材料的光电转换机制和关键影响因素。",
"Classification": "科学数据空间"
},
{
"Icon": "Ayesha_Khan.png",
"Name": "机器学习专家",
"Profile": "专注于开发和应用AI模型用于材料模拟。",
"Classification": "科学数据空间"
},
{
"Icon": "Mei_Lin.png",
"Name": "流体动力学专家",
"Profile": "专注于流体行为理论和模拟。",
"Classification": "科学数据空间"
}
] ]

View File

@@ -0,0 +1,102 @@
[
{
"Icon": "Abigail_Chen.png",
"Name": "Abigail",
"Profile": "AI Engineer"
},
{
"Icon": "Jane_Moreno.png",
"Name": "Jane",
"Profile": "Cybersecurity Specialist"
},
{
"Icon": "Giorgio_Rossi.png",
"Name": "Giorgio",
"Profile": "Poet"
},
{
"Icon": "Jennifer_Moore.png",
"Name": "Jennifer",
"Profile": "Linguist"
},
{
"Icon": "Maria_Lopez.png",
"Name": "Maria",
"Profile": "Philosopher"
},
{
"Icon": "Sam_Moore.png",
"Name": "Sam",
"Profile": "Ethicist"
},
{
"Icon": "Yuriko_Yamamoto.png",
"Name": "Yuriko",
"Profile": "Futurist"
},
{
"Icon": "Carlos_Gomez.png",
"Name": "Carlos",
"Profile": "Language Expert"
},
{
"Icon": "John_Lin.png",
"Name": "John",
"Profile": "Software Developer"
},
{
"Icon": "Tamara_Taylor.png",
"Name": "Tamara",
"Profile": "Music Composer"
},
{
"Icon": "Arthur_Burton.png",
"Name": "Arthur",
"Profile": "Neuroscientist"
},
{
"Icon": "Eddy_Lin.png",
"Name": "Eddy",
"Profile": "Cognitive Psychologist"
},
{
"Icon": "Isabella_Rodriguez.png",
"Name": "Isabella",
"Profile": "Science Fiction Writer"
},
{
"Icon": "Latoya_Williams.png",
"Name": "Latoya",
"Profile": "Historian of Technology"
},
{
"Icon": "Carmen_Ortiz.png",
"Name": "Carmen",
"Profile": "Robotics Engineer"
},
{
"Icon": "Rajiv_Patel.png",
"Name": "Rajiv",
"Profile": "Science Educator"
},
{
"Icon": "Tom_Moreno.png",
"Name": "Tom",
"Profile": "AI Scientist"
},
{
"Icon": "Ayesha_Khan.png",
"Name": "Ayesha",
"Profile": "Multimedia Artist"
},
{
"Icon": "Mei_Lin.png",
"Name": "Mei",
"Profile": "Graphic Designer"
},
{
"Icon": "Hailey_Johnson.png",
"Name": "Hailey",
"Profile": "Legal Expert on AI Law"
}
]

View File

@@ -4,4 +4,7 @@ PyYAML==6.0.1
termcolor==2.4.0 termcolor==2.4.0
groq==0.4.2 groq==0.4.2
mistralai==0.1.6 mistralai==0.1.6
socksio==1.0.0 flask-socketio==5.3.6
python-socketio==5.11.0
simple-websocket==1.0.0
eventlet==0.40.4

View File

@@ -18,6 +18,7 @@ from AgentCoord.PlanEngine.AgentSelectModify import (
import os import os
import yaml import yaml
import argparse import argparse
from typing import List, Dict
# initialize global variables # initialize global variables
yaml_file = os.path.join(os.getcwd(), "config", "config.yaml") yaml_file = os.path.join(os.getcwd(), "config", "config.yaml")
@@ -25,7 +26,7 @@ try:
with open(yaml_file, "r", encoding="utf-8") as file: with open(yaml_file, "r", encoding="utf-8") as file:
yaml_data = yaml.safe_load(file) yaml_data = yaml.safe_load(file)
except Exception: except Exception:
yaml_file = {} yaml_data = {}
USE_CACHE: bool = os.getenv("USE_CACHE") USE_CACHE: bool = os.getenv("USE_CACHE")
if USE_CACHE is None: if USE_CACHE is None:
USE_CACHE = yaml_data.get("USE_CACHE", False) USE_CACHE = yaml_data.get("USE_CACHE", False)
@@ -37,6 +38,41 @@ Request_Cache: dict[str, str] = {}
app = Flask(__name__) app = Flask(__name__)
app.config['SECRET_KEY'] = 'agentcoord-secret-key' app.config['SECRET_KEY'] = 'agentcoord-secret-key'
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading') socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
#socketio = SocketIO(app, cors_allowed_origins="*", async_mode='eventlet')
def truncate_rehearsal_log(RehearsalLog: List, restart_from_step_index: int) -> List:
"""
截断 RehearsalLog只保留指定索引之前的步骤结果
Args:
RehearsalLog: 原始日志列表
restart_from_step_index: 重新执行的起始步骤索引例如1 表示保留步骤0从步骤1重新执行
Returns:
截断后的 RehearsalLog
示例:
restart_from_step_index = 1
RehearsalLog = [step0, object0, step1, object1, step2, object2]
返回 = [step0, object0] # 只保留步骤0的结果
"""
truncated_log = []
step_count = 0
for logNode in RehearsalLog:
if logNode.get("LogNodeType") == "step":
# 只保留 restart_from_step_index 之前的步骤
if step_count < restart_from_step_index:
truncated_log.append(logNode)
step_count += 1
elif logNode.get("LogNodeType") == "object":
# object 节点:如果对应的 step 在保留范围内,保留它
# 策略:保留所有在截断点之前的 object
if step_count <= restart_from_step_index:
truncated_log.append(logNode)
return truncated_log
@app.route("/fill_stepTask_TaskProcess", methods=["post"]) @app.route("/fill_stepTask_TaskProcess", methods=["post"])
@@ -270,10 +306,24 @@ def Handle_executePlanOptimized():
- 无依赖关系的动作并行执行 - 无依赖关系的动作并行执行
- 有依赖关系的动作串行执行 - 有依赖关系的动作串行执行
支持参数:
plan: 执行计划
num_StepToRun: 要运行的步骤数
RehearsalLog: 已执行的历史记录
existingKeyObjects: 已存在的KeyObjects用于重新执行时传递中间结果
execution_id: 执行ID用于多用户隔离
前端使用 EventSource 接收 前端使用 EventSource 接收
""" """
incoming_data = request.get_json() incoming_data = request.get_json()
# 生成或获取 execution_id
plan = incoming_data.get("plan", {})
execution_id = incoming_data.get("execution_id")
if not execution_id:
import time
execution_id = f"{plan.get('General Goal', '').replace(' ', '_')}_{int(time.time() * 1000)}"
def generate(): def generate():
try: try:
for chunk in executePlan_streaming( for chunk in executePlan_streaming(
@@ -281,6 +331,8 @@ def Handle_executePlanOptimized():
num_StepToRun=incoming_data.get("num_StepToRun"), num_StepToRun=incoming_data.get("num_StepToRun"),
RehearsalLog=incoming_data.get("RehearsalLog", []), RehearsalLog=incoming_data.get("RehearsalLog", []),
AgentProfile_Dict=AgentProfile_Dict, AgentProfile_Dict=AgentProfile_Dict,
existingKeyObjects=incoming_data.get("existingKeyObjects"),
execution_id=execution_id,
): ):
yield chunk yield chunk
except Exception as e: except Exception as e:
@@ -405,7 +457,7 @@ def handle_ping():
def handle_execute_plan_optimized_ws(data): def handle_execute_plan_optimized_ws(data):
""" """
WebSocket版本优化版流式执行计划 WebSocket版本优化版流式执行计划
支持步骤级流式 + 动作级智能并行 支持步骤级流式 + 动作级智能并行 + 动态追加步骤 + 从指定步骤重新执行
请求格式: 请求格式:
{ {
@@ -414,7 +466,9 @@ def handle_execute_plan_optimized_ws(data):
"data": { "data": {
"plan": {...}, "plan": {...},
"num_StepToRun": null, "num_StepToRun": null,
"RehearsalLog": [] "RehearsalLog": [],
"enable_dynamic": true, # 是否启用动态追加步骤
"restart_from_step_index": 1 # 可选从指定步骤重新执行例如1表示从步骤2重新执行
} }
} }
""" """
@@ -425,27 +479,74 @@ def handle_execute_plan_optimized_ws(data):
plan = incoming_data.get("plan") plan = incoming_data.get("plan")
num_StepToRun = incoming_data.get("num_StepToRun") num_StepToRun = incoming_data.get("num_StepToRun")
RehearsalLog = incoming_data.get("RehearsalLog", []) RehearsalLog = incoming_data.get("RehearsalLog", [])
enable_dynamic = incoming_data.get("enable_dynamic", False)
restart_from_step_index = incoming_data.get("restart_from_step_index") # 新增:支持从指定步骤重新执行
# 使用原有的流式执行函数 # 如果指定了重新执行起始步骤,截断 RehearsalLog
for chunk in executePlan_streaming( if restart_from_step_index is not None:
plan=plan, print(f"🔄 从步骤 {restart_from_step_index + 1} 重新执行,正在截断 RehearsalLog...")
num_StepToRun=num_StepToRun, RehearsalLog = truncate_rehearsal_log(RehearsalLog, restart_from_step_index)
RehearsalLog=RehearsalLog, print(f"✅ RehearsalLog 已截断,保留 {sum(1 for node in RehearsalLog if node.get('LogNodeType') == 'step')} 个步骤的结果")
AgentProfile_Dict=AgentProfile_Dict,
): # 如果前端传入了execution_id使用前端的否则生成新的
# 通过WebSocket推送进度 execution_id = incoming_data.get("execution_id")
if not execution_id:
import time
execution_id = f"{plan.get('General Goal', '').replace(' ', '_')}_{int(time.time() * 1000)}"
if enable_dynamic:
# 动态模式使用executePlan_streaming_dynamic
from AgentCoord.RehearsalEngine_V2.ExecutePlan_Optimized import executePlan_streaming_dynamic
# 发送执行ID确认使用的ID
emit('progress', { emit('progress', {
'id': request_id, 'id': request_id,
'status': 'streaming', 'status': 'execution_started',
'data': chunk.replace('data: ', '').replace('\n\n', '') 'execution_id': execution_id,
'message': '执行已启动,支持动态追加步骤'
}) })
# 发送完成信号 for chunk in executePlan_streaming_dynamic(
emit('progress', { plan=plan,
'id': request_id, num_StepToRun=num_StepToRun,
'status': 'complete', RehearsalLog=RehearsalLog,
'data': None AgentProfile_Dict=AgentProfile_Dict,
}) execution_id=execution_id
):
emit('progress', {
'id': request_id,
'status': 'streaming',
'data': chunk.replace('data: ', '').replace('\n\n', '')
})
# 发送完成信号
emit('progress', {
'id': request_id,
'status': 'complete',
'data': None
})
else:
# 非动态模式:使用原有方式
for chunk in executePlan_streaming(
plan=plan,
num_StepToRun=num_StepToRun,
RehearsalLog=RehearsalLog,
AgentProfile_Dict=AgentProfile_Dict,
execution_id=execution_id
):
emit('progress', {
'id': request_id,
'status': 'streaming',
'data': chunk.replace('data: ', '').replace('\n\n', '')
})
# 发送完成信号
emit('progress', {
'id': request_id,
'status': 'complete',
'data': None
})
except Exception as e: except Exception as e:
# 发送错误信息 # 发送错误信息
@@ -456,6 +557,68 @@ def handle_execute_plan_optimized_ws(data):
}) })
@socketio.on('add_steps_to_execution')
def handle_add_steps_to_execution(data):
"""
WebSocket版本向正在执行的任务追加新步骤
请求格式:
{
"id": "request-id",
"action": "add_steps_to_execution",
"data": {
"execution_id": "execution_id",
"new_steps": [...]
}
}
"""
request_id = data.get('id')
incoming_data = data.get('data', {})
try:
from AgentCoord.RehearsalEngine_V2.dynamic_execution_manager import dynamic_execution_manager
execution_id = incoming_data.get('execution_id')
new_steps = incoming_data.get('new_steps', [])
if not execution_id:
emit('response', {
'id': request_id,
'status': 'error',
'error': '缺少execution_id参数'
})
return
# 追加新步骤到执行队列
added_count = dynamic_execution_manager.add_steps(execution_id, new_steps)
if added_count > 0:
print(f"✅ 成功追加 {added_count} 个步骤到执行队列: {execution_id}")
emit('response', {
'id': request_id,
'status': 'success',
'data': {
'message': f'成功追加 {added_count} 个步骤',
'added_count': added_count
}
})
else:
print(f"⚠️ 无法追加步骤执行ID不存在或已结束: {execution_id}")
emit('response', {
'id': request_id,
'status': 'error',
'error': '执行ID不存在或已结束'
})
except Exception as e:
print(f"❌ 追加步骤失败: {str(e)}")
emit('response', {
'id': request_id,
'status': 'error',
'error': str(e)
})
@socketio.on('generate_base_plan') @socketio.on('generate_base_plan')
def handle_generate_base_plan_ws(data): def handle_generate_base_plan_ws(data):
""" """
@@ -1430,7 +1593,7 @@ def handle_pause_execution(data):
"id": "request-id", "id": "request-id",
"action": "pause_execution", "action": "pause_execution",
"data": { "data": {
"goal": "任务描述" "execution_id": "执行ID"
} }
} }
""" """
@@ -1440,36 +1603,32 @@ def handle_pause_execution(data):
try: try:
from AgentCoord.RehearsalEngine_V2.execution_state import execution_state_manager from AgentCoord.RehearsalEngine_V2.execution_state import execution_state_manager
goal = incoming_data.get('goal', '') execution_id = incoming_data.get('execution_id', '')
# 检查当前执行的任务是否匹配 if not execution_id:
current_goal = execution_state_manager.get_goal()
if current_goal and current_goal != goal:
print(f"⚠️ 任务目标不匹配: 当前={current_goal}, 请求={goal}")
emit('response', { emit('response', {
'id': request_id, 'id': request_id,
'status': 'error', 'status': 'error',
'error': '任务目标不匹配' 'error': '缺少 execution_id'
}) })
return return
# 调用执行状态管理器暂停 # 调用执行状态管理器暂停
success = execution_state_manager.pause_execution() success = execution_state_manager.pause_execution(execution_id)
if success: if success:
print(f"⏸️ [DEBUG] 暂停成功! 当前状态: {execution_state_manager.get_status().value}") print(f"⏸️ [DEBUG] 暂停成功! execution_id={execution_id}")
print(f"⏸️ [DEBUG] should_pause: {execution_state_manager._should_pause}")
emit('response', { emit('response', {
'id': request_id, 'id': request_id,
'status': 'success', 'status': 'success',
'data': {"message": "已暂停执行,可随时继续"} 'data': {"message": "已暂停执行,可随时继续"}
}) })
else: else:
print(f"⚠️ [DEBUG] 暂停失败,当前状态: {execution_state_manager.get_status().value}") print(f"⚠️ [DEBUG] 暂停失败,execution_id={execution_id}")
emit('response', { emit('response', {
'id': request_id, 'id': request_id,
'status': 'error', 'status': 'error',
'error': f'无法暂停,当前状态: {execution_state_manager.get_status().value}' 'error': f'无法暂停'
}) })
except Exception as e: except Exception as e:
@@ -1491,7 +1650,7 @@ def handle_resume_execution(data):
"id": "request-id", "id": "request-id",
"action": "resume_execution", "action": "resume_execution",
"data": { "data": {
"goal": "任务描述" "execution_id": "执行ID"
} }
} }
""" """
@@ -1501,35 +1660,32 @@ def handle_resume_execution(data):
try: try:
from AgentCoord.RehearsalEngine_V2.execution_state import execution_state_manager from AgentCoord.RehearsalEngine_V2.execution_state import execution_state_manager
goal = incoming_data.get('goal', '') execution_id = incoming_data.get('execution_id', '')
# 检查当前执行的任务是否匹配 if not execution_id:
current_goal = execution_state_manager.get_goal()
if current_goal and current_goal != goal:
print(f"⚠️ 任务目标不匹配: 当前={current_goal}, 请求={goal}")
emit('response', { emit('response', {
'id': request_id, 'id': request_id,
'status': 'error', 'status': 'error',
'error': '任务目标不匹配' 'error': '缺少 execution_id'
}) })
return return
# 调用执行状态管理器恢复 # 调用执行状态管理器恢复
success = execution_state_manager.resume_execution() success = execution_state_manager.resume_execution(execution_id)
if success: if success:
print(f"▶️ 已恢复执行: goal={goal}") print(f"▶️ 已恢复执行: execution_id={execution_id}")
emit('response', { emit('response', {
'id': request_id, 'id': request_id,
'status': 'success', 'status': 'success',
'data': {"message": "已恢复执行"} 'data': {"message": "已恢复执行"}
}) })
else: else:
print(f"⚠️ 恢复失败,当前状态: {execution_state_manager.get_status()}") print(f"⚠️ 恢复失败,execution_id={execution_id}")
emit('response', { emit('response', {
'id': request_id, 'id': request_id,
'status': 'error', 'status': 'error',
'error': f'无法恢复,当前状态: {execution_state_manager.get_status().value}' 'error': f'无法恢复'
}) })
except Exception as e: except Exception as e:
@@ -1541,6 +1697,62 @@ def handle_resume_execution(data):
}) })
@socketio.on('stop_execution')
def handle_stop_execution(data):
"""
WebSocket版本停止任务执行
请求格式:
{
"id": "request-id",
"action": "stop_execution",
"data": {
"execution_id": "执行ID"
}
}
"""
request_id = data.get('id')
incoming_data = data.get('data', {})
try:
from AgentCoord.RehearsalEngine_V2.execution_state import execution_state_manager
execution_id = incoming_data.get('execution_id', '')
if not execution_id:
emit('response', {
'id': request_id,
'status': 'error',
'error': '缺少 execution_id'
})
return
# 调用执行状态管理器停止
success = execution_state_manager.stop_execution(execution_id)
if success:
print(f"🛑 [DEBUG] 停止成功! execution_id={execution_id}")
emit('response', {
'id': request_id,
'status': 'success',
'data': {"message": "已停止执行"}
})
else:
print(f"⚠️ [DEBUG] 停止失败execution_id={execution_id}")
emit('response', {
'id': request_id,
'status': 'error',
'error': f'无法停止'
})
except Exception as e:
print(f"❌ 停止执行失败: {str(e)}")
emit('response', {
'id': request_id,
'status': 'error',
'error': str(e)
})
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
@@ -1556,3 +1768,4 @@ if __name__ == "__main__":
init() init()
# 使用 socketio.run 替代 app.run支持WebSocket # 使用 socketio.run 替代 app.run支持WebSocket
socketio.run(app, host="0.0.0.0", port=args.port, debug=True, allow_unsafe_werkzeug=True) socketio.run(app, host="0.0.0.0", port=args.port, debug=True, allow_unsafe_werkzeug=True)
#socketio.run(app, host="0.0.0.0", port=args.port, debug=False, allow_unsafe_werkzeug=True)

View File

@@ -16,18 +16,19 @@ declare module 'vue' {
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCollapse: typeof import('element-plus/es')['ElCollapse'] ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElEmpty: typeof import('element-plus/es')['ElEmpty'] ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElPopover: typeof import('element-plus/es')['ElPopover'] ElPopover: typeof import('element-plus/es')['ElPopover']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
MultiLineTooltip: typeof import('./src/components/MultiLineTooltip/index.vue')['default'] MultiLineTooltip: typeof import('./src/components/MultiLineTooltip/index.vue')['default']
Notification: typeof import('./src/components/Notification/Notification.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default'] SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default']
TaskContentEditor: typeof import('./src/components/TaskContentEditor/index.vue')['default']
} }
export interface GlobalDirectives { export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective'] vLoading: typeof import('element-plus/es')['ElLoadingDirective']

View File

@@ -32,6 +32,7 @@
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"qs": "^6.14.0", "qs": "^6.14.0",
"socket.io-client": "^4.8.3",
"uuid": "^13.0.0", "uuid": "^13.0.0",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-router": "^4.6.3" "vue-router": "^4.6.3"

View File

@@ -53,6 +53,9 @@ importers:
qs: qs:
specifier: ^6.14.0 specifier: ^6.14.0
version: 6.14.0 version: 6.14.0
socket.io-client:
specifier: ^4.8.3
version: 4.8.3
uuid: uuid:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@@ -834,6 +837,9 @@ packages:
'@rtsao/scc@1.1.0': '@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
'@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
'@sxzz/popperjs-es@2.11.7': '@sxzz/popperjs-es@2.11.7':
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==} resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
@@ -1793,6 +1799,13 @@ packages:
resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
engine.io-client@6.6.4:
resolution: {integrity: sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==}
engine.io-parser@5.2.3:
resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
engines: {node: '>=10.0.0'}
enhanced-resolve@5.18.4: enhanced-resolve@5.18.4:
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@@ -3247,6 +3260,14 @@ packages:
resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
socket.io-client@4.8.3:
resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==}
engines: {node: '>=10.0.0'}
socket.io-parser@4.2.5:
resolution: {integrity: sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==}
engines: {node: '>=10.0.0'}
source-map-js@1.2.1: source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -3825,6 +3846,10 @@ packages:
xmlchars@2.2.0: xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
xmlhttprequest-ssl@2.1.2:
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
engines: {node: '>=0.4.0'}
yallist@3.1.1: yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@@ -4403,6 +4428,8 @@ snapshots:
'@rtsao/scc@1.1.0': {} '@rtsao/scc@1.1.0': {}
'@socket.io/component-emitter@3.1.2': {}
'@sxzz/popperjs-es@2.11.7': {} '@sxzz/popperjs-es@2.11.7': {}
'@tailwindcss/node@4.1.18': '@tailwindcss/node@4.1.18':
@@ -5496,6 +5523,20 @@ snapshots:
emojis-list@3.0.0: {} emojis-list@3.0.0: {}
engine.io-client@6.6.4:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.4.3
engine.io-parser: 5.2.3
ws: 8.18.3
xmlhttprequest-ssl: 2.1.2
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
engine.io-parser@5.2.3: {}
enhanced-resolve@5.18.4: enhanced-resolve@5.18.4:
dependencies: dependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
@@ -7074,6 +7115,24 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
socket.io-client@4.8.3:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.4.3
engine.io-client: 6.6.4
socket.io-parser: 4.2.5
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
socket.io-parser@4.2.5:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.4.3
transitivePeerDependencies:
- supports-color
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
source-map-resolve@0.5.3: source-map-resolve@0.5.3:
@@ -7745,6 +7804,8 @@ snapshots:
xmlchars@2.2.0: {} xmlchars@2.2.0: {}
xmlhttprequest-ssl@2.1.2: {}
yallist@3.1.1: {} yallist@3.1.1: {}
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}

View File

@@ -4,7 +4,7 @@
"centerTitle": "多智能体协同平台", "centerTitle": "多智能体协同平台",
"taskPromptWords": [ "taskPromptWords": [
"如何快速筛选慢性肾脏病药物潜在受试者?", "如何快速筛选慢性肾脏病药物潜在受试者?",
"如何补充丹芍活血胶囊不良反应数据?", "如何补充\"丹芍活血胶囊\"不良反应数据?",
"如何快速研发用于战场失血性休克的药物?", "如何快速研发用于战场失血性休克的药物?",
"二维材料的光电性质受哪些关键因素影响?", "二维材料的光电性质受哪些关键因素影响?",
"如何通过AI模拟的方法分析材料的微观结构?", "如何通过AI模拟的方法分析材料的微观结构?",
@@ -16,5 +16,6 @@
"agentRepository": { "agentRepository": {
"storageVersionIdentifier": "1" "storageVersionIdentifier": "1"
}, },
"dev": true "dev": true,
"apiBaseUrl": "http://localhost:8000"
} }

View File

@@ -1,21 +1,7 @@
import request from '@/utils/request' import request from '@/utils/request'
import websocket from '@/utils/websocket' import websocket from '@/utils/websocket'
import type { Agent, IApiStepTask, IRawPlanResponse, IRawStepTask } from '@/stores' import type { Agent, IApiStepTask, IRawPlanResponse, IRawStepTask } from '@/stores'
import { import { withRetry } from '@/utils/retry'
mockBackendAgentSelectModifyInit,
mockBackendAgentSelectModifyAddAspect,
type BackendAgentScoreResponse,
} from '@/layout/components/Main/TaskTemplate/TaskSyllabus/components/mock/AgentAssignmentBackendMock'
import {
mockBackendFillAgentTaskProcess,
type RawAgentTaskProcessResponse,
} from '@/layout/components/Main/TaskTemplate/TaskProcess/components/mock/AgentTaskProcessBackendMock'
import { mockBranchPlanOutlineAPI } from '@/layout/components/Main/TaskTemplate/TaskSyllabus/Branch/mock/branchPlanOutlineMock'
import { mockFillStepTaskAPI } from '@/layout/components/Main/TaskTemplate/TaskSyllabus/Branch/mock/fill-step-task-mock'
import {
mockBranchTaskProcessAPI,
type BranchAction,
} from '@/layout/components/Main/TaskTemplate/TaskSyllabus/Branch/mock/branchTaskProcessMock'
export interface ActionHistory { export interface ActionHistory {
ID: string ID: string
@@ -26,6 +12,14 @@ export interface ActionHistory {
Action_Result: string Action_Result: string
} }
export interface BranchAction {
ID: string
ActionType: string
AgentName: string
Description: string
ImportantInput: string[]
}
export type IExecuteRawResponse = { export type IExecuteRawResponse = {
LogNodeType: string LogNodeType: string
NodeId: string NodeId: string
@@ -86,7 +80,10 @@ class Api {
// 默认使用WebSocket // 默认使用WebSocket
private useWebSocketDefault = true private useWebSocketDefault = true
setAgents = (data: Pick<Agent, 'Name' | 'Profile' | 'apiUrl' | 'apiKey' | 'apiModel'>[], useWebSocket: boolean = this.useWebSocketDefault) => { setAgents = (
data: Pick<Agent, 'Name' | 'Profile' | 'apiUrl' | 'apiKey' | 'apiModel'>[],
useWebSocket: boolean = this.useWebSocketDefault,
) => {
// 如果启用WebSocket且已连接使用WebSocket // 如果启用WebSocket且已连接使用WebSocket
if (useWebSocket && websocket.connected) { if (useWebSocket && websocket.connected) {
return websocket.send('set_agents', data) return websocket.send('set_agents', data)
@@ -107,19 +104,29 @@ class Api {
apiKey?: string apiKey?: string
apiModel?: string apiModel?: string
useWebSocket?: boolean useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void onProgress?: (progress: {
status: string
stage?: string
message?: string
[key: string]: any
}) => void
}) => { }) => {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
// 如果启用WebSocket且已连接使用WebSocket // 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) { if (useWs && websocket.connected) {
return websocket.send('generate_base_plan', { return websocket.send(
'General Goal': data.goal, 'generate_base_plan',
'Initial Input Object': data.inputs, {
apiUrl: data.apiUrl, 'General Goal': data.goal,
apiKey: data.apiKey, 'Initial Input Object': data.inputs,
apiModel: data.apiModel, apiUrl: data.apiUrl,
}, undefined, data.onProgress) apiKey: data.apiKey,
apiModel: data.apiModel,
},
undefined,
data.onProgress,
)
} }
// 否则使用REST API // 否则使用REST API
@@ -167,10 +174,8 @@ class Api {
} }
/** /**
* 优化版流式执行计划(阶段1+2步骤级流式 + 动作级智能并行 * 优化版流式执行计划(支持动态追加步骤
* 无依赖关系的动作并行执行,有依赖关系的动作串行执行 * 步骤级流式 + 动作级智能并行 + 动态追加步骤
*
* 默认使用WebSocket如果连接失败则降级到SSE
*/ */
executePlanOptimized = ( executePlanOptimized = (
plan: IRawPlanResponse, plan: IRawPlanResponse,
@@ -178,12 +183,22 @@ class Api {
onError?: (error: Error) => void, onError?: (error: Error) => void,
onComplete?: () => void, onComplete?: () => void,
useWebSocket?: boolean, useWebSocket?: boolean,
existingKeyObjects?: Record<string, any>,
enableDynamic?: boolean,
onExecutionStarted?: (executionId: string) => void,
executionId?: string,
restartFromStepIndex?: number, // 新增:从指定步骤重新执行的索引
rehearsalLog?: any[], // 新增:传递截断后的 RehearsalLog
) => { ) => {
const useWs = useWebSocket !== undefined ? useWebSocket : this.useWebSocketDefault const useWs = useWebSocket !== undefined ? useWebSocket : this.useWebSocketDefault
const data = { const data = {
RehearsalLog: [], RehearsalLog: rehearsalLog || [], // ✅ 使用传递的 RehearsalLog
num_StepToRun: null, num_StepToRun: null,
existingKeyObjects: existingKeyObjects || {},
enable_dynamic: enableDynamic || false,
execution_id: executionId || null,
restart_from_step_index: restartFromStepIndex ?? null, // 新增:传递重新执行索引
plan: { plan: {
'Initial Input Object': plan['Initial Input Object'], 'Initial Input Object': plan['Initial Input Object'],
'General Goal': plan['General Goal'], 'General Goal': plan['General Goal'],
@@ -213,14 +228,26 @@ class Api {
// onProgress // onProgress
(progressData) => { (progressData) => {
try { try {
// progressData 应该已经是解析后的对象了
// 如果是字符串,说明后端发送的是 JSON 字符串,需要解析
let event: StreamingEvent let event: StreamingEvent
// 处理不同类型的progress数据
if (typeof progressData === 'string') { if (typeof progressData === 'string') {
event = JSON.parse(progressData) event = JSON.parse(progressData)
} else { } else {
event = progressData as StreamingEvent event = progressData as StreamingEvent
} }
// 处理特殊事件类型
if (event && typeof event === 'object') {
// 检查是否是execution_started事件
if ('status' in event && event.status === 'execution_started') {
if ('execution_id' in event && onExecutionStarted) {
onExecutionStarted(event.execution_id as string)
}
return
}
}
onMessage(event) onMessage(event)
} catch (e) { } catch (e) {
// Failed to parse WebSocket data // Failed to parse WebSocket data
@@ -233,7 +260,7 @@ class Api {
// onError // onError
(error) => { (error) => {
onError?.(error) onError?.(error)
} },
) )
return return
} }
@@ -303,20 +330,30 @@ class Api {
initialInputs: string[] initialInputs: string[]
goal: string goal: string
useWebSocket?: boolean useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void onProgress?: (progress: {
status: string
stage?: string
message?: string
[key: string]: any
}) => void
}) => { }) => {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
// 如果启用WebSocket且已连接使用WebSocket // 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) { if (useWs && websocket.connected) {
return websocket.send('branch_plan_outline', { return websocket.send(
branch_Number: data.branch_Number, 'branch_plan_outline',
Modification_Requirement: data.Modification_Requirement, {
Existing_Steps: data.Existing_Steps, branch_Number: data.branch_Number,
Baseline_Completion: data.Baseline_Completion, Modification_Requirement: data.Modification_Requirement,
'Initial Input Object': data.initialInputs, Existing_Steps: data.Existing_Steps,
'General Goal': data.goal, Baseline_Completion: data.Baseline_Completion,
}, undefined, data.onProgress) 'Initial Input Object': data.initialInputs,
'General Goal': data.goal,
},
undefined,
data.onProgress,
)
} }
// 否则使用REST API // 否则使用REST API
@@ -345,20 +382,30 @@ class Api {
stepTaskExisting: any stepTaskExisting: any
goal: string goal: string
useWebSocket?: boolean useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void onProgress?: (progress: {
status: string
stage?: string
message?: string
[key: string]: any
}) => void
}) => { }) => {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
// 如果启用WebSocket且已连接使用WebSocket // 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) { if (useWs && websocket.connected) {
return websocket.send('branch_task_process', { return websocket.send(
branch_Number: data.branch_Number, 'branch_task_process',
Modification_Requirement: data.Modification_Requirement, {
Existing_Steps: data.Existing_Steps, branch_Number: data.branch_Number,
Baseline_Completion: data.Baseline_Completion, Modification_Requirement: data.Modification_Requirement,
stepTaskExisting: data.stepTaskExisting, Existing_Steps: data.Existing_Steps,
'General Goal': data.goal, Baseline_Completion: data.Baseline_Completion,
}, undefined, data.onProgress) stepTaskExisting: data.stepTaskExisting,
'General Goal': data.goal,
},
undefined,
data.onProgress,
)
} }
// 否则使用REST API // 否则使用REST API
@@ -379,21 +426,33 @@ class Api {
fillStepTask = async (data: { fillStepTask = async (data: {
goal: string goal: string
stepTask: any stepTask: any
generation_id?: string
useWebSocket?: boolean useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void onProgress?: (progress: {
status: string
stage?: string
message?: string
[key: string]: any
}) => void
}): Promise<IRawStepTask> => { }): Promise<IRawStepTask> => {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
let response: any
// 如果启用WebSocket且已连接使用WebSocket // 定义实际的 API 调用逻辑
if (useWs && websocket.connected) { const executeRequest = async (): Promise<any> => {
response = await websocket.send('fill_step_task', { if (useWs && websocket.connected) {
'General Goal': data.goal, return await websocket.send(
stepTask: data.stepTask, 'fill_step_task',
}, undefined, data.onProgress) {
} else { 'General Goal': data.goal,
stepTask: data.stepTask,
generation_id: data.generation_id || '',
},
undefined,
data.onProgress,
)
}
// 否则使用REST API // 否则使用REST API
response = await request< return await request<
{ {
'General Goal': string 'General Goal': string
stepTask: any stepTask: any
@@ -426,6 +485,19 @@ class Api {
}) })
} }
// 使用重试机制执行请求
const rawResponse = await withRetry(executeRequest, {
maxRetries: 3,
initialDelayMs: 2000,
onRetry: (error, attempt, delay) => {
console.warn(`⚠️ [fillStepTask] 第${attempt}次重试,等待 ${delay}ms...`, error?.message)
},
})
// WebSocket 返回格式: { data: {...}, generation_id, execution_id }
// REST API 返回格式: {...}
const response = rawResponse.data || rawResponse
const vec2Hsl = (color: number[]): string => { const vec2Hsl = (color: number[]): string => {
const [h, s, l] = color const [h, s, l] = color
return `hsl(${h}, ${s}%, ${l}%)` return `hsl(${h}, ${s}%, ${l}%)`
@@ -435,9 +507,9 @@ class Api {
if (response.Collaboration_Brief_FrontEnd?.data) { if (response.Collaboration_Brief_FrontEnd?.data) {
for (const [key, value] of Object.entries(response.Collaboration_Brief_FrontEnd.data)) { for (const [key, value] of Object.entries(response.Collaboration_Brief_FrontEnd.data)) {
briefData[key] = { briefData[key] = {
text: value.text, text: (value as { text: string; color: number[] }).text,
style: { style: {
background: vec2Hsl(value.color), background: vec2Hsl((value as { text: string; color: number[] }).color),
}, },
} }
} }
@@ -465,26 +537,36 @@ class Api {
stepTask: IApiStepTask stepTask: IApiStepTask
agents: string[] agents: string[]
useWebSocket?: boolean useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void onProgress?: (progress: {
status: string
stage?: string
message?: string
[key: string]: any
}) => void
}): Promise<IApiStepTask> => { }): Promise<IApiStepTask> => {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
let response: any
// 如果启用WebSocket且已连接使用WebSocket // 定义实际的 API 调用逻辑
if (useWs && websocket.connected) { const executeRequest = async (): Promise<any> => {
response = await websocket.send('fill_step_task_process', { if (useWs && websocket.connected) {
'General Goal': data.goal, return await websocket.send(
stepTask_lackTaskProcess: { 'fill_step_task_process',
StepName: data.stepTask.name, {
TaskContent: data.stepTask.content, 'General Goal': data.goal,
InputObject_List: data.stepTask.inputs, stepTask_lackTaskProcess: {
OutputObject: data.stepTask.output, StepName: data.stepTask.name,
AgentSelection: data.agents, TaskContent: data.stepTask.content,
}, InputObject_List: data.stepTask.inputs,
}, undefined, data.onProgress) OutputObject: data.stepTask.output,
} else { AgentSelection: data.agents,
},
},
undefined,
data.onProgress,
)
}
// 否则使用REST API // 否则使用REST API
response = await request< return await request<
{ {
'General Goal': string 'General Goal': string
stepTask_lackTaskProcess: { stepTask_lackTaskProcess: {
@@ -529,6 +611,22 @@ class Api {
}) })
} }
// 使用重试机制执行请求
const rawResponse = await withRetry(executeRequest, {
maxRetries: 3,
initialDelayMs: 2000,
onRetry: (error, attempt, delay) => {
console.warn(
`⚠️ [fillStepTaskTaskProcess] 第${attempt}次重试,等待 ${delay}ms...`,
error?.message,
)
},
})
// WebSocket 返回格式: { data: {...}, generation_id, execution_id }
// REST API 返回格式: {...}
const response = rawResponse.data || rawResponse
const vec2Hsl = (color: number[]): string => { const vec2Hsl = (color: number[]): string => {
const [h, s, l] = color const [h, s, l] = color
return `hsl(${h}, ${s}%, ${l}%)` return `hsl(${h}, ${s}%, ${l}%)`
@@ -538,9 +636,9 @@ class Api {
if (response.Collaboration_Brief_FrontEnd?.data) { if (response.Collaboration_Brief_FrontEnd?.data) {
for (const [key, value] of Object.entries(response.Collaboration_Brief_FrontEnd.data)) { for (const [key, value] of Object.entries(response.Collaboration_Brief_FrontEnd.data)) {
briefData[key] = { briefData[key] = {
text: value.text, text: (value as { text: string; color: number[] }).text,
style: { style: {
background: vec2Hsl(value.color), background: vec2Hsl((value as { text: string; color: number[] }).color),
}, },
} }
} }
@@ -569,31 +667,53 @@ class Api {
} }
/** /**
* 为每个智能体评分 * 为每个智能体评分(带重试机制)
*/ */
agentSelectModifyInit = async (data: { agentSelectModifyInit = async (data: {
goal: string goal: string
stepTask: any stepTask: any
useWebSocket?: boolean useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void 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 useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
let response: Record<string, Record<string, { Reason: string; Score: number }>>
// 如果启用WebSocket且已连接使用WebSocket // 调试日志:打印请求参数
if (useWs && websocket.connected) { const requestPayload = {
response = await websocket.send('agent_select_modify_init', { 'General Goal': data.goal,
'General Goal': data.goal, stepTask: {
stepTask: { StepName: data.stepTask.StepName || data.stepTask.name,
StepName: data.stepTask.StepName || data.stepTask.name, TaskContent: data.stepTask.TaskContent || data.stepTask.content,
TaskContent: data.stepTask.TaskContent || data.stepTask.content, 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) console.log('🔍 [agentSelectModifyInit] 请求参数:', {
} else { goal: requestPayload['General Goal'],
stepTaskName: requestPayload.stepTask.StepName,
stepTaskContentLength: requestPayload.stepTask.TaskContent?.length,
useWebSocket: useWs && websocket.connected,
wsConnected: websocket.connected,
})
// 定义实际的 API 调用逻辑
const executeRequest = async (): Promise<
Record<string, Record<string, { Reason: string; Score: number }>>
> => {
if (useWs && websocket.connected) {
return await websocket.send(
'agent_select_modify_init',
requestPayload,
undefined,
data.onProgress,
)
}
// 否则使用REST API // 否则使用REST API
response = await request< return await request<
{ {
'General Goal': string 'General Goal': string
stepTask: any stepTask: any
@@ -602,22 +722,36 @@ class Api {
>({ >({
url: '/agentSelectModify_init', url: '/agentSelectModify_init',
method: 'POST', method: 'POST',
data: { data: requestPayload,
'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 rawResponse = await withRetry(executeRequest, {
maxRetries: 3,
initialDelayMs: 2000,
onRetry: (error, attempt, delay) => {
console.warn(
`⚠️ [agentSelectModifyInit] 第${attempt}次重试,等待 ${delay}ms...`,
error?.message,
)
},
})
// WebSocket 返回格式: { data: {...}, generation_id, execution_id }
// REST API 返回格式: {...}
const response = rawResponse.data || rawResponse
const transformedData: Record<string, Record<string, { reason: string; score: number }>> = {} const transformedData: Record<string, Record<string, { reason: string; score: number }>> = {}
// 确保 response 存在且是有效对象
if (!response || typeof response !== 'object' || Array.isArray(response)) {
console.warn('[agentSelectModifyInit] 后端返回数据格式异常:', response)
return transformedData
}
for (const [aspect, agents] of Object.entries(response)) { for (const [aspect, agents] of Object.entries(response)) {
for (const [agentName, scoreInfo] of Object.entries(agents)) { for (const [agentName, scoreInfo] of Object.entries(agents as Record<string, { Reason: string; Score: number }> || {})) {
if (!transformedData[agentName]) { if (!transformedData[agentName]) {
transformedData[agentName] = {} transformedData[agentName] = {}
} }
@@ -637,7 +771,12 @@ class Api {
agentSelectModifyAddAspect = async (data: { agentSelectModifyAddAspect = async (data: {
aspectList: string[] aspectList: string[]
useWebSocket?: boolean useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void 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 }>
@@ -647,9 +786,16 @@ class Api {
// 如果启用WebSocket且已连接使用WebSocket // 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) { if (useWs && websocket.connected) {
response = await websocket.send('agent_select_modify_add_aspect', { const rawResponse = await websocket.send(
aspectList: data.aspectList, 'agent_select_modify_add_aspect',
}, undefined, data.onProgress) {
aspectList: data.aspectList,
},
undefined,
data.onProgress,
)
// WebSocket 返回格式: { data: {...}, generation_id, execution_id }
response = rawResponse.data || rawResponse
} else { } else {
// 否则使用REST API // 否则使用REST API
response = await request< response = await request<
@@ -692,161 +838,40 @@ class Api {
} }
} }
/** /**
* ==================== Mock API开发阶段使用==================== * 向正在执行的任务追加新步骤
*为每个智能体评分 * @param executionId 执行ID
* @param newSteps 新步骤列表
* @returns 追加的步骤数量
*/ */
mockAgentSelectModifyInit = async (): Promise< addStepsToExecution = async (executionId: string, newSteps: IRawStepTask[]): Promise<number> => {
Record<string, Record<string, { reason: string; score: number }>> if (!websocket.connected) {
> => { throw new Error('WebSocket未连接')
const response: BackendAgentScoreResponse = await mockBackendAgentSelectModifyInit()
const transformedData: Record<string, Record<string, { reason: string; score: number }>> = {}
for (const [aspect, agents] of Object.entries(response)) {
for (const [agentName, scoreInfo] of Object.entries(agents)) {
if (!transformedData[agentName]) {
transformedData[agentName] = {}
}
transformedData[agentName][aspect] = {
reason: scoreInfo.Reason,
score: scoreInfo.Score,
}
}
} }
return transformedData const rawResponse = await websocket.send('add_steps_to_execution', {
} execution_id: executionId,
new_steps: newSteps.map((step) => ({
mockAgentSelectModifyAddAspect = async (data: { StepName: step.StepName,
aspectList: string[] TaskContent: step.TaskContent,
}): Promise<{ InputObject_List: step.InputObject_List,
aspectName: string OutputObject: step.OutputObject,
agentScores: Record<string, { score: number; reason: string }> AgentSelection: step.AgentSelection,
}> => { Collaboration_Brief_frontEnd: step.Collaboration_Brief_frontEnd,
const response: BackendAgentScoreResponse = await mockBackendAgentSelectModifyAddAspect( TaskProcess: step.TaskProcess.map((action) => ({
data.aspectList, ActionType: action.ActionType,
) AgentName: action.AgentName,
Description: action.Description,
const newAspect = data.aspectList[data.aspectList.length - 1] ID: action.ID,
if (!newAspect) { ImportantInput: action.ImportantInput,
throw new Error('aspectList is empty') })),
} })),
const newAspectAgents = response[newAspect]
const agentScores: Record<string, { score: number; reason: string }> = {}
if (newAspectAgents) {
for (const [agentName, scoreInfo] of Object.entries(newAspectAgents)) {
agentScores[agentName] = {
score: scoreInfo.Score,
reason: scoreInfo.Reason,
}
}
}
return {
aspectName: newAspect,
agentScores,
}
}
mockFillStepTaskTaskProcess = async (data: {
goal: string
stepTask: IApiStepTask
agents: string[]
}): Promise<IApiStepTask> => {
const response: RawAgentTaskProcessResponse = await mockBackendFillAgentTaskProcess(
data.goal,
data.stepTask,
data.agents,
)
const vec2Hsl = (color: number[]): string => {
const [h, s, l] = color
return `hsl(${h}, ${s}%, ${l}%)`
}
const briefData: Record<string, { text: string; style: { background: string } }> = {}
if (response.Collaboration_Brief_frontEnd?.data) {
for (const [key, value] of Object.entries(response.Collaboration_Brief_frontEnd.data)) {
briefData[key] = {
text: value.text,
style: {
background: vec2Hsl(value.color),
},
}
}
}
const process = (response.TaskProcess || []).map((action) => ({
id: action.ID,
type: action.ActionType,
agent: action.AgentName,
description: action.Description,
inputs: action.ImportantInput,
}))
return {
name: response.StepName || '',
content: response.TaskContent || '',
inputs: response.InputObject_List || [],
output: response.OutputObject || '',
agents: response.AgentSelection || [],
brief: {
template: response.Collaboration_Brief_frontEnd?.template || '',
data: briefData,
},
process,
}
}
mockBranchPlanOutline = async (data: {
branch_Number: number
Modification_Requirement: string
Existing_Steps: IRawStepTask[]
Baseline_Completion: number
initialInputs: string[]
goal: string
}): Promise<IRawPlanResponse> => {
const response = await mockBranchPlanOutlineAPI({
branch_Number: data.branch_Number,
Modification_Requirement: data.Modification_Requirement,
Existing_Steps: data.Existing_Steps,
Baseline_Completion: data.Baseline_Completion,
InitialObject_List: data.initialInputs,
General_Goal: data.goal,
}) })
return response // WebSocket 返回格式: { data: {...}, generation_id, execution_id }
} // REST API 返回格式: {...}
const response = (rawResponse.data || rawResponse) as { added_count: number }
mockFillStepTask = async (data: { goal: string; stepTask: any }): Promise<any> => { return response?.added_count || 0
const response = await mockFillStepTaskAPI({
General_Goal: data.goal,
stepTask: data.stepTask,
})
return response
}
mockBranchTaskProcess = async (data: {
branch_Number: number
Modification_Requirement: string
Existing_Steps: BranchAction[]
Baseline_Completion: number
stepTaskExisting: any
goal: string
}): Promise<BranchAction[][]> => {
const response = await mockBranchTaskProcessAPI({
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,
})
return response
} }
} }

View File

@@ -0,0 +1,232 @@
<template>
<teleport to="body">
<div class="notification-container">
<transition-group name="notification" tag="div" class="notification-list">
<div
v-for="notification in notifications"
:key="notification.id"
:class="['notification-item', `notification-${notification.type || 'info'}`]"
:style="{ zIndex: notification.zIndex || 1000 }"
>
<div class="notification-content">
<div class="notification-icon">
<component :is="getIcon(notification.type)" />
</div>
<div class="notification-message">
<div class="notification-title">{{ notification.title }}</div>
<div v-if="notification.detailTitle" class="notification-detail-title">
{{ notification.detailTitle }}
</div>
<div v-if="notification.detailMessage" class="notification-detail-desc">
{{ notification.detailMessage }}
</div>
<div v-else-if="notification.message" class="notification-desc">
{{ notification.message }}
</div>
</div>
<div class="notification-close" @click="close(notification.id)">
<Close />
</div>
</div>
<div v-if="notification.showProgress" class="notification-progress">
<div class="progress-bar" :style="{ width: `${notification.progress || 0}%` }"></div>
</div>
</div>
</transition-group>
</div>
</teleport>
</template>
<script setup lang="ts">
import {
Close,
SuccessFilled as IconSuccess,
WarningFilled as IconWarning,
InfoFilled
} from '@element-plus/icons-vue'
import type { NotificationItem } from '@/composables/useNotification'
const props = defineProps<{
notifications: NotificationItem[]
}>()
const emit = defineEmits<{
close: [id: string]
}>()
const close = (id: string) => {
emit('close', id)
}
const getIcon = (type?: string) => {
switch (type) {
case 'success':
return IconSuccess
case 'warning':
return IconWarning
case 'error':
return IconWarning
default:
return InfoFilled
}
}
</script>
<style scoped>
.notification-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
pointer-events: none;
}
.notification-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.notification-item {
pointer-events: auto;
min-width: 300px;
max-width: 450px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
overflow: hidden;
border-left: 4px solid #409eff;
}
.notification-success {
border-left-color: #67c23a;
}
.notification-warning {
border-left-color: #e6a23c;
}
.notification-error {
border-left-color: #f56c6c;
}
.notification-content {
display: flex;
align-items: flex-start;
padding: 12px 16px;
gap: 12px;
}
.notification-icon {
flex-shrink: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.notification-message {
flex: 1;
min-width: 0;
}
.notification-title {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.notification-detail-title {
font-size: 13px;
font-weight: 500;
color: #409eff;
margin-top: 4px;
margin-bottom: 2px;
}
.notification-detail-desc {
font-size: 12px;
color: #909399;
line-height: 1.4;
}
.notification-desc {
font-size: 13px;
color: #606266;
line-height: 1.5;
}
.notification-close {
flex-shrink: 0;
width: 20px;
height: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
transition: color 0.2s;
}
.notification-close:hover {
color: #606266;
}
.notification-progress {
height: 2px;
background: #f0f2f5;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: #409eff;
transition: width 0.3s ease;
}
/* 进入动画 */
.notification-enter-active {
animation: slideInRight 0.3s ease-out;
}
/* 离开动画 */
.notification-leave-active {
animation: slideOutRight 0.3s ease-in;
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideOutRight {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(100%);
}
}
/* 列表项移动动画 */
.notification-move,
.notification-enter-active,
.notification-leave-active {
transition: all 0.3s ease;
}
.notification-leave-active {
position: absolute;
width: 100%;
}
</style>

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { IRawStepTask } from '@/stores'
import SvgIcon from '@/components/SvgIcon/index.vue'
const props = defineProps<{
task: IRawStepTask
}>()
const emit = defineEmits<{
(e: 'save', taskId: string, content: string): void
}>()
const isEditing = ref(false)
const editingContent = ref('')
const startEditing = () => {
editingContent.value = props.task.TaskContent || ''
isEditing.value = true
}
const save = () => {
const trimmed = editingContent.value.trim()
emit('save', props.task.Id!, trimmed)
isEditing.value = false
}
const cancel = () => {
isEditing.value = false
}
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault()
save()
} else if (event.key === 'Escape') {
cancel()
}
}
</script>
<template>
<div v-if="isEditing" class="w-full">
<div class="flex flex-col gap-3">
<el-input
v-model="editingContent"
type="textarea"
:autosize="{ minRows: 2, maxRows: 4 }"
placeholder="请输入任务内容"
@keydown="handleKeydown"
class="task-content-editor"
size="small"
/>
<div class="flex justify-end">
<svg-icon
icon-class="Check"
size="20px"
color="#328621"
class="cursor-pointer mr-4"
@click="save"
title="保存"
/>
<svg-icon
icon-class="Cancel"
size="20px"
color="#8e0707"
class="cursor-pointer mr-1"
@click="cancel"
title="取消"
/>
</div>
</div>
</div>
<div v-else @dblclick="startEditing" class="w-full cursor-pointer">
<slot name="display">
<div class="text-[14px] text-[var(--color-text-secondary)] task-content-display">
{{ task.TaskContent }}
</div>
</slot>
</div>
</template>
<style scoped lang="scss">
.task-content-editor {
:deep(.el-textarea__inner) {
font-size: 14px;
color: var(--color-text-secondary);
background: transparent;
border: 1px solid #dcdfe6;
border-radius: 4px;
resize: none;
}
}
.task-content-display {
min-height: 40px;
word-break: break-word;
white-space: pre-wrap;
}
</style>

View File

@@ -0,0 +1,163 @@
import { ref } from 'vue'
export interface NotificationItem {
id: string
title: string
message?: string
type?: 'success' | 'warning' | 'info' | 'error'
duration?: number
showProgress?: boolean
progress?: number
zIndex?: number
onClose?: () => void
// 详细进度信息
detailTitle?: string
detailMessage?: string
}
const notifications = ref<NotificationItem[]>([])
let notificationIdCounter = 0
let zIndexCounter = 1000
export function useNotification() {
const addNotification = (notification: Omit<NotificationItem, 'id' | 'zIndex'>) => {
const id = `notification-${notificationIdCounter++}`
const newNotification: NotificationItem = {
...notification,
id,
zIndex: ++zIndexCounter,
}
notifications.value.push(newNotification)
// 自动关闭
if (notification.duration && notification.duration > 0) {
setTimeout(() => {
removeNotification(id)
}, notification.duration)
}
return id
}
const removeNotification = (id: string) => {
const notification = notifications.value.find((n) => n.id === id)
if (notification) {
const index = notifications.value.indexOf(notification)
notifications.value.splice(index, 1)
notification.onClose?.()
}
}
const success = (title: string, message?: string, options?: Partial<NotificationItem>) => {
return addNotification({
title,
message,
type: 'success',
duration: 3000,
...options,
})
}
const warning = (title: string, message?: string, options?: Partial<NotificationItem>) => {
return addNotification({
title,
message,
type: 'warning',
duration: 3000,
...options,
})
}
const info = (title: string, message?: string, options?: Partial<NotificationItem>) => {
return addNotification({
title,
message,
type: 'info',
duration: 3000,
...options,
})
}
const error = (title: string, message?: string, options?: Partial<NotificationItem>) => {
return addNotification({
title,
message,
type: 'error',
duration: 5000,
...options,
})
}
const progress = (
title: string,
current: number,
total: number,
options?: Partial<NotificationItem>,
) => {
const progressPercent = Math.round((current / total) * 100)
return addNotification({
title,
message: `${current}/${total}`,
type: 'info',
showProgress: true,
progress: progressPercent,
duration: 0, // 不自动关闭
...options,
})
}
const updateProgress = (id: string, current: number, total: number) => {
const notification = notifications.value.find((n) => n.id === id)
if (notification) {
notification.progress = Math.round((current / total) * 100)
notification.message = `${current}/${total}`
}
}
const updateProgressDetail = (
id: string,
detailTitle: string,
detailMessage: string,
current?: number,
total?: number,
) => {
const notification = notifications.value.find((n) => n.id === id)
if (notification) {
notification.detailTitle = detailTitle
notification.detailMessage = detailMessage
if (current !== undefined && total !== undefined) {
notification.progress = Math.round((current / total) * 100)
notification.message = `${current}/${total}`
}
}
}
// 更新通知的主标题
const updateNotificationTitle = (id: string, title: string) => {
const notification = notifications.value.find((n) => n.id === id)
if (notification) {
notification.title = title
}
}
const clear = () => {
notifications.value.forEach((n) => n.onClose?.())
notifications.value = []
}
return {
notifications,
addNotification,
removeNotification,
success,
warning,
info,
error,
progress,
updateProgress,
updateProgressDetail,
updateNotificationTitle,
clear,
}
}

View File

@@ -1,12 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed, reactive, nextTick } from 'vue' import { ref, onMounted, onUnmounted, 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 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 { useNotification } from '@/composables/useNotification'
import AssignmentButton from './TaskTemplate/TaskSyllabus/components/AssignmentButton.vue' import AssignmentButton from './TaskTemplate/TaskSyllabus/components/AssignmentButton.vue'
import { withRetry } from '@/utils/retry'
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'search-start'): void (e: 'search-start'): void
(e: 'search', value: string): void (e: 'search', value: string): void
@@ -14,15 +15,17 @@ const emit = defineEmits<{
const agentsStore = useAgentsStore() const agentsStore = useAgentsStore()
const configStore = useConfigStore() const configStore = useConfigStore()
const { success, warning, error: notifyError } = useNotification()
const searchValue = ref('') const searchValue = ref('')
const triggerOnFocus = ref(true) 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 isFillingSteps = ref(false)
// 存储当前填充任务的取消函数 const isStopping = ref(false)
const isStopPending = ref(false)
const currentStepAbortController = ref<{ cancel: () => void } | null>(null) const currentStepAbortController = ref<{ cancel: () => void } | null>(null)
const currentGenerationId = ref('')
// 解析URL参数 // 解析URL参数
function getUrlParam(param: string): string | null { function getUrlParam(param: string): string | null {
@@ -63,14 +66,57 @@ const taskContainerRef = ref<HTMLDivElement | null>(null)
// 处理失去焦点事件 // 处理失去焦点事件
function handleBlur() { function handleBlur() {
isFocus.value = false isFocus.value = false
// 延迟收起搜索框,以便点击按钮等操作
setTimeout(() => { setTimeout(() => {
isExpanded.value = false isExpanded.value = false
// 强制重置文本区域高度到最小行数
resetTextareaHeight() resetTextareaHeight()
}, 200) }, 200)
} }
// 预加载所有任务的智能体评分数据
async function preloadAllTaskAgentScores(outlineData: any, goal: string) {
const tasks = outlineData['Collaboration Process'] || []
if (tasks.length === 0) {
return
}
for (const task of tasks) {
// 检查是否已停止
if (agentsStore.isStopping || agentsStore.hasStoppedFilling) {
return
}
// 确保任务有唯一ID
if (!task.Id) {
task.Id = `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
}
// 调用初始化接口获取评分数据
try {
const agentScores = await api.agentSelectModifyInit({
goal: goal,
stepTask: {
StepName: task.StepName,
TaskContent: task.TaskContent,
InputObject_List: task.InputObject_List,
OutputObject: task.OutputObject
}
})
// 提取维度列表并存储
const firstAgent = Object.keys(agentScores)[0]
const aspectList = firstAgent ? Object.keys(agentScores[firstAgent] || {}) : []
agentsStore.setTaskScoreData(task.Id, {
aspectList,
agentScores
})
} catch (error) {
console.error(`❌ 任务 "${task.StepName}" 的评分数据预加载失败:`, error)
}
}
}
// 重置文本区域高度到最小行数 // 重置文本区域高度到最小行数
function resetTextareaHeight() { function resetTextareaHeight() {
nextTick(() => { nextTick(() => {
@@ -90,151 +136,151 @@ function resetTextareaHeight() {
// 停止填充数据的处理函数 // 停止填充数据的处理函数
async function handleStop() { async function handleStop() {
try { // 检查是否有正在进行的生成任务
// 通过 WebSocket 发送停止信号 if (!isFillingSteps.value) {
if (websocket.connected) { warning('提示', '没有正在进行的生成任务')
await websocket.send('stop_generation', { return
goal: searchValue.value
})
ElMessage.success('已发送停止信号,正在停止生成...')
} else {
ElMessage.warning('WebSocket 未连接,无法停止')
}
} catch (error) {
console.error('停止生成失败:', error)
ElMessage.error('停止生成失败')
} finally {
// 无论后端是否成功停止,都重置状态
isFillingSteps.value = false
currentStepAbortController.value = null
} }
// 先设置停止状态(立即显示"停止中..."
agentsStore.setIsStopping(true)
isStopping.value = true
isStopPending.value = true
success('提示', '正在停止,请稍候...')
// 发送停止请求(不等待响应,后端设置 should_stop = True
if (websocket.connected && currentGenerationId.value) {
websocket.send('stop_generation', {
generation_id: currentGenerationId.value
}).then((result: any) => {
console.log('停止生成响应:', result)
}).catch((error: any) => {
console.log('停止生成请求失败(可能已经停止):', error?.message)
})
}
// 不清空 currentGenerationId让 fillStepTask 循环检查 isStopping 来停止
}
// 监听后端发送的停止完成事件(备用,如果后端有发送)
function onGenerationStopped() {
isStopping.value = false
isStopPending.value = false
currentGenerationId.value = ''
success('成功', '已停止生成')
} }
// 处理按钮点击事件 // 处理按钮点击事件
function handleButtonClick() { function handleButtonClick() {
if (isFillingSteps.value) { if (isFillingSteps.value) {
// 如果正在填充数据,点击停止
handleStop() handleStop()
} else { } else {
// 否则开始搜索
handleSearch() handleSearch()
} }
} }
// 处理搜索函数
async function handleSearch() { async function handleSearch() {
// 用于标记大纲是否成功加载 triggerOnFocus.value = false
let outlineLoaded = false if (!searchValue.value) {
warning('提示', '请输入搜索内容')
return
}
emit('search-start')
// 重置所有状态(处理可能的上一次未完成的停止操作)
isStopping.value = false
isStopPending.value = false
agentsStore.setIsStopping(false)
agentsStore.setHasStoppedFilling(false)
agentsStore.resetAgent()
agentsStore.setAgentRawPlan({ loading: true })
// 重置 generation_id
currentGenerationId.value = ''
// 获取大纲
const response = await api.generateBasePlan({
goal: searchValue.value,
inputs: []
})
// WebSocket 返回格式: { data: {...}, generation_id, execution_id }
// REST API 返回格式: {...}
const outlineData = response.data || response
// 保存 generation_id
if (response && response.generation_id) {
currentGenerationId.value = response.generation_id
console.log('📋 保存 generation_id:', currentGenerationId.value)
}
// 处理简报数据格式
outlineData['Collaboration Process'] = changeBriefs(outlineData['Collaboration Process'])
// 立即显示大纲
agentsStore.setAgentRawPlan({ data: outlineData, loading: false })
emit('search', searchValue.value)
// 预加载所有任务的智能体评分数据
preloadAllTaskAgentScores(outlineData, searchValue.value)
// 填充步骤详情
isFillingSteps.value = true
const steps = outlineData['Collaboration Process'] || []
// 保存 generation_id 到本地变量,用于 fillStepTask 调用
// 这样即使前端停止时清空了 currentGenerationId当前的 fillStepTask 仍能正确停止
const fillTaskGenerationId = currentGenerationId.value
// 串行填充所有步骤的详情
try { try {
triggerOnFocus.value = false
if (!searchValue.value) {
ElMessage.warning('请输入搜索内容')
return
}
emit('search-start')
agentsStore.resetAgent()
agentsStore.setAgentRawPlan({ loading: true })
// 获取大纲
const outlineData = await api.generateBasePlan({
goal: searchValue.value,
inputs: []
})
// 检查是否已被停止
if (!isFillingSteps.value && currentStepAbortController.value) {
return
}
// 处理简报数据格式
outlineData['Collaboration Process'] = changeBriefs(outlineData['Collaboration Process'])
// 立即显示大纲
agentsStore.setAgentRawPlan({ data: outlineData, loading: false })
outlineLoaded = true
emit('search', searchValue.value)
// 开始填充步骤详情,设置状态
isFillingSteps.value = true
// 并行填充所有步骤的详情
const steps = outlineData['Collaboration Process'] || []
// 带重试的填充函数
const fillStepWithRetry = async (step: any, retryCount = 0): Promise<void> => {
const maxRetries = 2 // 最多重试2次
// 检查是否已停止
if (!isFillingSteps.value) {
console.log('检测到停止信号,跳过步骤填充')
return
}
try {
if (!step.StepName) {
console.warn('步骤缺少 StepName跳过填充详情')
return
}
// 使用现有的 fillStepTask API 填充每个步骤的详情
const detailedStep = await api.fillStepTask({
goal: searchValue.value,
stepTask: {
StepName: step.StepName,
TaskContent: step.TaskContent,
InputObject_List: step.InputObject_List,
OutputObject: step.OutputObject
}
})
// 再次检查是否已停止(在 API 调用后)
if (!isFillingSteps.value) {
console.log('检测到停止信号,跳过更新步骤详情')
return
}
// 更新该步骤的详情到 store
updateStepDetail(step.StepName, detailedStep)
} catch (error) {
console.error(
`填充步骤 ${step.StepName} 详情失败 (尝试 ${retryCount + 1}/${maxRetries + 1}):`,
error
)
// 如果未达到最大重试次数,延迟后重试
if (retryCount < maxRetries) {
console.log(`正在重试步骤 ${step.StepName}...`)
// 延迟1秒后重试避免立即重试导致同样的问题
await new Promise(resolve => setTimeout(resolve, 1000))
return fillStepWithRetry(step, retryCount + 1)
} else {
console.error(`步骤 ${step.StepName}${maxRetries + 1} 次尝试后仍然失败`)
}
}
}
// // 为每个步骤并行填充详情(选人+过程)
// const fillPromises = steps.map(step => fillStepWithRetry(step))
// // 等待所有步骤填充完成(包括重试)
// await Promise.all(fillPromises)
// 串行填充所有步骤的详情(避免字段混乱)
for (const step of steps) { for (const step of steps) {
await fillStepWithRetry(step) // 检查是否已停止
if (!isFillingSteps.value || agentsStore.isStopping) {
break
}
await withRetry(
async () => {
const detailedStep = await api.fillStepTask({
goal: searchValue.value,
stepTask: {
StepName: step.StepName,
TaskContent: step.TaskContent,
InputObject_List: step.InputObject_List,
OutputObject: step.OutputObject,
},
generation_id: fillTaskGenerationId,
})
updateStepDetail(step.StepName, detailedStep)
},
{
maxRetries: 2, // 减少重试次数,因为是串行填充
initialDelayMs: 1000, // 使用较小的延迟
shouldRetry: () => isFillingSteps.value && !agentsStore.isStopping, // 可取消的重试
},
)
} }
} finally { } finally {
// 重置状态(确保即使出错也会执行)
triggerOnFocus.value = true triggerOnFocus.value = true
// 完成填充,重置状态 if (isStopPending.value) {
isStopping.value = false
isStopPending.value = false
agentsStore.setIsStopping(false)
agentsStore.setHasStoppedFilling(true)
}
isFillingSteps.value = false isFillingSteps.value = false
currentStepAbortController.value = null currentStepAbortController.value = null
// 如果大纲加载失败确保关闭loading // 只有在没有停止请求时才清空 generation_id
if (!outlineLoaded) { if (!isStopPending.value) {
agentsStore.setAgentRawPlan({ loading: false }) currentGenerationId.value = ''
} }
} }
} }
// 辅助函数:更新单个步骤的详情 //更新单个步骤的详情
function updateStepDetail(stepId: string, detailedStep: any) { function updateStepDetail(stepId: string, detailedStep: any) {
const planData = agentsStore.agentRawPlan.data const planData = agentsStore.agentRawPlan.data
if (!planData) return if (!planData) return
@@ -244,7 +290,7 @@ function updateStepDetail(stepId: string, detailedStep: any) {
const index = collaborationProcess.findIndex((s: any) => s.StepName === stepId) const index = collaborationProcess.findIndex((s: any) => s.StepName === stepId)
if (index !== -1 && collaborationProcess[index]) { if (index !== -1 && collaborationProcess[index]) {
// 保持响应式更新 - 使用 Vue 的响应式系统 // 保持响应式更新
Object.assign(collaborationProcess[index], { Object.assign(collaborationProcess[index], {
AgentSelection: detailedStep.AgentSelection || [], AgentSelection: detailedStep.AgentSelection || [],
TaskProcess: detailedStep.TaskProcess || [], TaskProcess: detailedStep.TaskProcess || [],
@@ -260,7 +306,7 @@ const querySearch = (queryString: string, cb: (v: { value: string }[]) => void)
const results = queryString const results = queryString
? configStore.config.taskPromptWords.filter(createFilter(queryString)) ? configStore.config.taskPromptWords.filter(createFilter(queryString))
: configStore.config.taskPromptWords : configStore.config.taskPromptWords
// call callback function to return suggestions // 调用回调函数返回建议列表
cb(results.map(item => ({ value: item }))) cb(results.map(item => ({ value: item })))
} }
@@ -273,6 +319,12 @@ const createFilter = (queryString: string) => {
// 组件挂载时检查URL参数 // 组件挂载时检查URL参数
onMounted(() => { onMounted(() => {
autoSearchFromUrl() autoSearchFromUrl()
websocket.on('generation_stopped', onGenerationStopped)
})
// 组件卸载时移除事件监听
onUnmounted(() => {
websocket.off('generation_stopped', onGenerationStopped)
}) })
</script> </script>
@@ -316,20 +368,20 @@ 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="isFillingSteps ? '点击停止生成' : '点击搜索任务'" :title="isFillingSteps && !isStopping ? '点击停止生成' : '点击搜索任务'"
circle circle
:loading="agentsStore.agentRawPlan.loading" :loading="agentsStore.agentRawPlan.loading || isStopping"
:disabled="!searchValue" :disabled="!searchValue || isStopping"
@click.stop="handleButtonClick" @click.stop="handleButtonClick"
> >
<SvgIcon <SvgIcon
v-if="!agentsStore.agentRawPlan.loading && !isFillingSteps" v-if="!agentsStore.agentRawPlan.loading && !isFillingSteps && !isStopping"
icon-class="paper-plane" icon-class="paper-plane"
size="18px" size="18px"
color="#ffffff" color="#ffffff"
/> />
<SvgIcon <SvgIcon
v-if="!agentsStore.agentRawPlan.loading && isFillingSteps" v-if="!agentsStore.agentRawPlan.loading && isFillingSteps && !isStopping"
icon-class="stoprunning" icon-class="stoprunning"
size="30px" size="30px"
color="#ffffff" color="#ffffff"
@@ -351,7 +403,6 @@ onMounted(() => {
width: 40%; width: 40%;
margin: 0 auto; margin: 0 auto;
border: 2px solid transparent; border: 2px solid transparent;
$bg: var(--el-input-bg-color, var(--el-fill-color-blank));
background: linear-gradient(var(--color-bg-taskbar), var(--color-bg-taskbar)) padding-box, background: linear-gradient(var(--color-bg-taskbar), var(--color-bg-taskbar)) padding-box,
linear-gradient(to right, #00c8d2, #315ab4) border-box; linear-gradient(to right, #00c8d2, #315ab4) border-box;
border-radius: 30px; border-radius: 30px;
@@ -478,58 +529,4 @@ onMounted(() => {
} }
} }
} }
.drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.title {
font-size: 18px;
font-weight: bold;
}
}
.process-list {
padding: 0 8px;
}
.process-item {
margin-bottom: 16px;
padding: 12px;
border-radius: 8px;
background: var(--color-bg-list);
border: 1px solid var(--color-border-default);
.process-content {
display: flex;
align-items: flex-start;
gap: 8px;
.agent-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
flex-shrink: 0;
}
.process-text {
line-height: 1.6;
font-size: 14px;
color: var(--el-text-color-primary);
white-space: pre-wrap;
}
}
.edit-container {
margin-top: 8px;
}
}
.process-item:hover {
border-color: var(--el-border-color);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
</style> </style>

View File

@@ -1,14 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { pick } from 'lodash' import { pick } from 'lodash'
import { ElMessage } from 'element-plus'
import api from '@/api/index.ts' import api from '@/api/index.ts'
import SvgIcon from '@/components/SvgIcon/index.vue' import SvgIcon from '@/components/SvgIcon/index.vue'
import { agentMapDuty } from '@/layout/components/config.ts' import { agentMapDuty } from '@/layout/components/config.ts'
import { type Agent, useAgentsStore } from '@/stores' import { type Agent, useAgentsStore } from '@/stores'
import { readConfig } from '@/utils/readJson.ts' import { readConfig } from '@/utils/readJson.ts'
import { useNotification } from '@/composables/useNotification.ts'
import AgentRepoList from './AgentRepoList.vue' import AgentRepoList from './AgentRepoList.vue'
const { error, success } = useNotification()
const agentsStore = useAgentsStore() const agentsStore = useAgentsStore()
// 如果agentsStore.agents不存在就读取默认配置的json文件 // 如果agentsStore.agents不存在就读取默认配置的json文件
@@ -58,7 +60,7 @@ const readFileContent = (file: File) => {
return return
} }
const validAgents = jsonData.filter((agent) => { const validAgents = jsonData.filter(agent => {
// 验证必需字段 // 验证必需字段
if (!agent.Name || typeof agent.Name !== 'string') { if (!agent.Name || typeof agent.Name !== 'string') {
return false return false
@@ -87,27 +89,24 @@ const readFileContent = (file: File) => {
apiKey: agent.apiKey, apiKey: agent.apiKey,
apiModel: agent.apiModel apiModel: agent.apiModel
})) }))
agentsStore.setAgents(processedAgents) agentsStore.setAgents(processedAgents)
// 调用API // 调用API
api api
.setAgents(processedAgents) .setAgents(processedAgents)
.then(() => { .then(() => {
ElMessage.success('智能体上传成功') success('智能体上传成功')
}) })
.catch(() => { .catch(() => {
ElMessage.error('智能体上传失败') error('智能体上传失败')
}) })
} catch { } catch {
ElMessage.error('JSON解析错误') error('JSON解析错误')
} }
} }
reader.onerror = () => { reader.onerror = () => {
ElMessage.error('文件读取错误') error('文件读取错误')
} }
reader.readAsText(file) reader.readAsText(file)
} }

View File

@@ -19,7 +19,6 @@ const props = defineProps<{
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'open-edit', stepId: string, processId: string): void
(e: 'save-edit', stepId: string, processId: string, value: string): void (e: 'save-edit', stepId: string, processId: string, value: string): void
}>() }>()
@@ -36,10 +35,8 @@ const currentTaskProcess = computed(() => {
return rawData?.TaskProcess || [] return rawData?.TaskProcess || []
}) })
// 当前正在编辑的process ID
const editingProcessId = ref<string | null>(null) const editingProcessId = ref<string | null>(null)
const editValue = ref('') const editValue = ref('')
// 鼠标悬停的process ID
const hoverProcessId = ref<string | null>(null) const hoverProcessId = ref<string | null>(null)
// 处理卡片点击事件 // 处理卡片点击事件
@@ -60,40 +57,25 @@ function isDarkMode(): boolean {
// 获取颜色浅两号的函数 // 获取颜色浅两号的函数
function getLightColor(color: string, level: number = 2): string { function getLightColor(color: string, level: number = 2): string {
if (!color || color.length !== 7 || color[0] !== '#') return color return adjustColor(color, level * 20)
const r = parseInt(color.substr(1, 2), 16)
const g = parseInt(color.substr(3, 2), 16)
const b = parseInt(color.substr(5, 2), 16)
// 增加亮度(浅两号)
const lightenAmount = level * 20
const newR = Math.min(255, r + lightenAmount)
const newG = Math.min(255, g + lightenAmount)
const newB = Math.min(255, b + lightenAmount)
return `#${Math.round(newR).toString(16).padStart(2, '0')}${Math.round(newG)
.toString(16)
.padStart(2, '0')}${Math.round(newB).toString(16).padStart(2, '0')}`
} }
// 获取颜色深两号的函数 // 获取颜色深两号的函数
function getDarkColor(color: string, level: number = 2): string { function getDarkColor(color: string, level: number = 2): string {
return adjustColor(color, -level * 20)
}
// 通用的颜色调整函数(提取重复逻辑)
function adjustColor(color: string, amount: number): string {
if (!color || color.length !== 7 || color[0] !== '#') return color if (!color || color.length !== 7 || color[0] !== '#') return color
const r = parseInt(color.substr(1, 2), 16) const r = Math.min(255, Math.max(0, parseInt(color.substr(1, 2), 16) + amount))
const g = parseInt(color.substr(3, 2), 16) const g = Math.min(255, Math.max(0, parseInt(color.substr(3, 2), 16) + amount))
const b = parseInt(color.substr(5, 2), 16) const b = Math.min(255, Math.max(0, parseInt(color.substr(5, 2), 16) + amount))
// 降低亮度(深两号) return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b
const darkenAmount = level * 20
const newR = Math.max(0, r - darkenAmount)
const newG = Math.max(0, g - darkenAmount)
const newB = Math.max(0, b - darkenAmount)
return `#${Math.round(newR).toString(16).padStart(2, '0')}${Math.round(newG)
.toString(16) .toString(16)
.padStart(2, '0')}${Math.round(newB).toString(16).padStart(2, '0')}` .padStart(2, '0')}`
} }
// 根据主题模式获取调整后的颜色 // 根据主题模式获取调整后的颜色
@@ -115,11 +97,10 @@ function handleMouseLeave() {
hoverProcessId.value = null hoverProcessId.value = null
} }
// 处理双击编辑针对单个process // 处理双击编辑
function handleDblClick(processId: string, currentDescription: string) { function handleDblClick(processId: string, currentDescription: string) {
editingProcessId.value = processId editingProcessId.value = processId
editValue.value = currentDescription editValue.value = currentDescription
emit('open-edit', props.step.Id || '', processId)
} }
// 处理保存编辑 // 处理保存编辑
@@ -262,7 +243,6 @@ function handleCancel() {
margin-bottom: 8px; margin-bottom: 8px;
.edit-card { .edit-card {
//background: #f0f2f5;
border-radius: 8px; border-radius: 8px;
padding: 16px; padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
@@ -282,13 +262,6 @@ function handleCancel() {
color: var(--color-text-taskbar); color: var(--color-text-taskbar);
} }
} }
.edit-buttons {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 8px;
}
} }
} }
@@ -299,10 +272,6 @@ function handleCancel() {
padding: 2px 4px; padding: 2px 4px;
border-radius: 3px; border-radius: 3px;
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
&.hovered {
transition: background-color 0.2s ease;
}
} }
.separator { .separator {

View File

@@ -679,10 +679,13 @@ const submitBranch = async () => {
goal: generalGoal goal: generalGoal
}) })
// WebSocket 返回格式: { data: [[action1, action2], [action3, action4]], ... }
// REST API 返回格式: [[action1, action2], [action3, action4]]
const responseData = response.data || response
// 后端返回格式: [[action1, action2], [action3, action4]] // 后端返回格式: [[action1, action2], [action3, action4]]
// 取第一个分支 // 取第一个分支
if (response && response.length > 0) { if (responseData && responseData.length > 0) {
const firstBranch = response[0] const firstBranch = responseData[0]
// 直接遍历 action 数组 // 直接遍历 action 数组
firstBranch.forEach((action: any) => { firstBranch.forEach((action: any) => {
@@ -974,10 +977,13 @@ const submitBranch = async () => {
goal: generalGoal goal: generalGoal
}) })
// WebSocket 返回格式: { data: [[action1, action2], [action3, action4]], ... }
// REST API 返回格式: [[action1, action2], [action3, action4]]
const responseData = response.data || response
// 后端返回格式: [[action1, action2], [action3, action4]] // 后端返回格式: [[action1, action2], [action3, action4]]
// 取第一个分支 // 取第一个分支
if (response && response.length > 0) { if (responseData && responseData.length > 0) {
const firstBranch = response[0] const firstBranch = responseData[0]
// 直接遍历 action 数组 // 直接遍历 action 数组
firstBranch.forEach((action: any) => { firstBranch.forEach((action: any) => {

View File

@@ -16,7 +16,6 @@ const props = defineProps<{
const branchCount = computed(() => { const branchCount = computed(() => {
if (!props.step?.Id) return 1 if (!props.step?.Id) return 1
// 获取该任务步骤的分支数据
const taskStepId = props.step.Id const taskStepId = props.step.Id
// 获取该任务的 agent 组合 // 获取该任务的 agent 组合
const agents = props.step.AgentSelection || [] const agents = props.step.AgentSelection || []
@@ -33,7 +32,6 @@ const isClickable = computed(() => {
}) })
const handleClick = (event?: MouseEvent) => { const handleClick = (event?: MouseEvent) => {
// 只有可点击时才执行操作
if (!isClickable.value) { if (!isClickable.value) {
return return
} }
@@ -48,7 +46,6 @@ const handleClick = (event?: MouseEvent) => {
if (props.step) { if (props.step) {
agentsStore.setCurrentTask(props.step) agentsStore.setCurrentTask(props.step)
} }
// 触发打开任务过程探索窗口
agentsStore.openPlanTask() agentsStore.openPlanTask()
} }
</script> </script>
@@ -56,7 +53,7 @@ const handleClick = (event?: MouseEvent) => {
<template> <template>
<div <div
class="task-button" class="task-button"
:class="{ 'has-branches': branchCount > 0, 'is-disabled': !isClickable }" :class="{ 'has-branches': branchCount > 1, 'is-disabled': !isClickable }"
@click="handleClick" @click="handleClick"
:title="isClickable ? `${branchCount} 个分支` : '请先在任务大纲中选中此任务'" :title="isClickable ? `${branchCount} 个分支` : '请先在任务大纲中选中此任务'"
> >

View File

@@ -1,168 +0,0 @@
<!-- AdditionalOutputCard.vue -->
<script setup lang="ts">
import { computed, ref, nextTick } from 'vue'
import { useAgentsStore } from '@/stores'
import SvgIcon from '@/components/SvgIcon/index.vue'
const agentsStore = useAgentsStore()
const props = defineProps<{
index: number
}>()
// 获取产物名称
const currentOutput = computed(() => {
return agentsStore.additionalOutputs[props.index] || ''
})
// 编辑状态
const isEditing = ref(false)
const inputValue = ref('')
const originalValue = ref('')
const inputRef = ref<HTMLElement>()
// 点击编辑图标
const handleEditClick = () => {
isEditing.value = true
originalValue.value = inputValue.value
// 等待 DOM 更新后聚焦输入框
nextTick(() => {
if (inputRef.value) {
const inputEl = inputRef.value.querySelector('input')
if (inputEl) {
inputEl.focus()
}
}
})
}
// 保存编辑
const handleSave = () => {
if (isEditing.value) {
isEditing.value = false
}
}
// 取消编辑
const handleCancel = () => {
if (isEditing.value) {
inputValue.value = originalValue.value // 恢复原始值
isEditing.value = false
}
}
// 处理键盘事件
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault()
handleSave()
} else if (event.key === 'Escape') {
event.preventDefault()
handleCancel()
}
}
// 输入框失去焦点处理
const handleBlur = () => {
// 延迟处理,避免点击按钮时立即触发
setTimeout(() => {
if (isEditing.value) {
handleSave()
}
}, 200)
}
</script>
<template>
<!-- 当产物存在时才显示 -->
<div v-if="currentOutput" class="card-item">
<el-card
class="card-item w-full relative output-object-card"
:shadow="true"
:id="`additional-output-${index}`"
>
<!-- 显示产物名称 -->
<div class="text-start w-[100%]">
<div class="text-[18px] font-bold text-[var(--color-text)] mb-2">
{{ currentOutput }}
</div>
<div ref="inputRef">
<el-input
v-model="inputValue"
:readonly="!isEditing"
:placeholder="isEditing ? '请输入内容...' : '点击编辑图标开始编辑...'"
@keydown="handleKeydown"
@blur="handleBlur"
:class="{ editing: isEditing }"
>
<template #suffix>
<!-- 只读状态显示编辑图标 -->
<div v-if="!isEditing" class="flex items-center">
<svg-icon
icon-class="Edit"
size="20px"
class="cursor-pointer hover:text-[#409eff] transition-colors"
@click="handleEditClick"
title="点击编辑"
/>
</div>
</template>
</el-input>
</div>
<!-- 编辑状态下的提示 -->
<div v-if="isEditing" class="mt-2 text-end text-xs text-gray-500">
<svg-icon
icon-class="Check"
size="20px"
color="#328621"
class="cursor-pointer mr-4"
@click="handleSave"
title="保存"
/>
<svg-icon
icon-class="Cancel"
size="20px"
color="#8e0707"
class="cursor-pointer mr-4"
@click="handleCancel"
title="取消"
/>
</div>
</div>
</el-card>
</div>
</template>
<style scoped lang="scss">
.card-item {
transition: all 0.3s ease;
}
.output-object-card {
:deep(.el-card__body) {
min-height: 80px;
display: flex;
align-items: start;
justify-content: start;
}
}
/* 输入框样式 */
:deep(.el-input .el-input__wrapper) {
box-shadow: none;
background-color: var(--color-bg-three);
transition: all 0.2s ease;
}
/* 编辑状态下的输入框样式 */
:deep(.el-input.editing .el-input__wrapper) {
border: 1px solid #dcdfe6;
border-radius: 4px;
}
:deep(.el-input.editing .el-input__wrapper.is-focus) {
border-color: #c0c4cc;
box-shadow: 0 0 0 1px #c0c4cc;
}
</style>

View File

@@ -1,12 +1,4 @@
<script setup lang="ts"> <script setup lang="ts"></script>
defineProps<{
isAdding?: boolean
}>()
defineEmits<{
(e: 'start-add-output'): void
}>()
</script>
<template> <template>
<div class="absolute inset-0 flex items-start gap-[14%]"> <div class="absolute inset-0 flex items-start gap-[14%]">
<!-- 左侧元素 --> <!-- 左侧元素 -->
@@ -24,21 +16,6 @@ defineEmits<{
<div class="flex-1 relative h-full flex justify-center"> <div class="flex-1 relative h-full flex justify-center">
<!-- 背景那一根线 --> <!-- 背景那一根线 -->
<div class="h-full bg-[var(--color-bg-flow)] w-[5px]"> <div class="h-full bg-[var(--color-bg-flow)] w-[5px]">
<!-- 顶部加号区域 -->
<!-- <div
v-if="!isAdding"
v-dev-only
class="plus-area mt-[35px] ml-[-15px] w-[34px] h-[34px] flex items-center justify-center cursor-pointer rounded-full"
@click="$emit('start-add-output')"
> -->
<!-- 加号图标 -->
<!-- <svg-icon
icon-class="plus"
color="var(--color-text)"
size="20px"
class="plus-icon opacity-0 transition-opacity duration-200"
/>
</div> -->
<!-- 线底部的小圆球 --> <!-- 线底部的小圆球 -->
<div <div
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-flow)] w-[15px] h-[15px] rounded-full" class="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-flow)] w-[15px] h-[15px] rounded-full"

View File

@@ -797,13 +797,16 @@ const handleAddBranch = async (taskId: string, branchContent: string) => {
goal: generalGoal goal: generalGoal
}) })
// WebSocket 返回格式: { data: [[{...}]], ... }
// REST API 返回格式: [[{...}]]
const responseData = response.data || response
// 直接获取协作流程数据 // 直接获取协作流程数据
if (Array.isArray(response)) { if (Array.isArray(responseData)) {
// 可能是二维数组 // 可能是二维数组
newTasks = (response as any[])[0] || [] newTasks = responseData[0] || []
} else if (response && (response as any)['Collaboration Process']) { } else if (responseData && responseData['Collaboration Process']) {
// 如果返回的是对象,尝试读取 Collaboration Process 字段 // 如果返回的是对象,尝试读取 Collaboration Process 字段
newTasks = (response as any)['Collaboration Process'] || [] newTasks = responseData['Collaboration Process'] || []
} else { } else {
newTasks = [] newTasks = []
} }
@@ -1136,14 +1139,17 @@ const handleAddBranch = async (taskId: string, branchContent: string) => {
initialInputs: Array.isArray(initialInput) ? initialInput : [initialInput], initialInputs: Array.isArray(initialInput) ? initialInput : [initialInput],
goal: generalGoal goal: generalGoal
}) })
// WebSocket 返回格式: { data: [[{...}]], ... }
// REST API 返回格式: [[{...}]]
const responseData = response.data || response
// 直接获取协作流程数据 // 直接获取协作流程数据
// newTasks = response?.[0] || [] // newTasks = response?.[0] || []
if (Array.isArray(response)) { if (Array.isArray(responseData)) {
// 可能是二维数组 // 可能是二维数组
newTasks = (response as any[])[0] || [] newTasks = responseData[0] || []
} else if (response && (response as any)['Collaboration Process']) { } else if (responseData && responseData['Collaboration Process']) {
// 如果返回的是对象,尝试读取 Collaboration Process 字段 // 如果返回的是对象,尝试读取 Collaboration Process 字段
newTasks = (response as any)['Collaboration Process'] || [] newTasks = responseData['Collaboration Process'] || []
} else { } else {
newTasks = [] newTasks = []
} }

View File

@@ -1,127 +0,0 @@
# Mock 数据说明
本目录包含用于分支功能的 mock 数据,支持在开发环境中测试分支逻辑,无需调用真实后端 API。
## 文件说明
### 1. `branchPlanOutlineMock.ts`
**用途**: 根节点级别的分支(任务大纲分支)
**类型**: `IApiStepTask[][]`
**说明**: 返回多个分支方案每个方案是一个完整的任务流程IApiStepTask[]
**示例结构**:
```typescript
[
// 第一个分支方案(瀑布流开发)
[task1, task2, ...],
// 第二个分支方案(敏捷开发)
[task1, task2, ...],
// 第三个分支方案(快速原型)
[task1, task2, ...]
]
```
### 2. `branchTaskProcessMock.ts`
**用途**: 任务节点级别的分支(任务流程分支)
**类型**: `IApiAgentAction[][]`
**说明**: 返回多个分支方案每个方案是一系列动作IApiAgentAction[]),这些动作会追加到现有任务的 TaskProcess 中
**示例结构**:
```typescript
[
// 第一个分支方案(标准开发流程)
[action1, action2, action3, ...],
// 第二个分支方案(快速原型流程)
[action1, action2, ...],
// ... 更多方案
]
```
## 如何使用
### 切换 Mock 数据开关
`PlanModification.vue` 文件中,找到以下配置:
```typescript
// 开关:控制是否使用 mock 数据(开发时设置为 true生产时设置为 false
const USE_MOCK_DATA = true
```
- **开发阶段**: 设置为 `true`,使用 mock 数据
- **生产环境**: 设置为 `false`,调用真实 API
### 数据转换流程
```
IApiStepTask (API 格式)
↓ convertToIRawStepTask()
IRawStepTask (内部格式)
更新到 agentsStore.agentRawPlan.data
Vue Flow 流程图自动更新
```
### Mock 数据特点
1. **模拟网络延迟**: Mock 数据调用会模拟 800ms 的网络延迟
2. **多方案选择**: 每个分支接口提供多个备选方案(目前默认使用第一个)
3. **完整数据结构**: Mock 数据包含完整的字段,与真实 API 返回格式一致
4. **类型安全**: 使用 TypeScript 类型定义,确保类型正确
## 扩展 Mock 数据
### 添加新的分支方案
在对应的 mock 文件中添加新的数组元素:
```typescript
// branchPlanOutlineMock.ts
const mockPlanBranchData: IApiStepTask[][] = [
// 现有方案...
[
// 新增方案
{
name: '新方案步骤1',
content: '...',
// ... 其他字段
},
{
name: '新方案步骤2',
content: '...',
// ... 其他字段
}
]
]
```
### 随机选择方案
修改 `PlanModification.vue` 中的方案选择逻辑:
```typescript
// 当前:固定选择第一个
const mockBranchTasks = branchPlanOutlineMock[0]
// 改为:随机选择一个方案
const randomIndex = Math.floor(Math.random() * branchPlanOutlineMock.length)
const mockBranchTasks = branchPlanOutlineMock[randomIndex]
```
## 注意事项
1. ⚠️ **生产环境**: 发布前务必将 `USE_MOCK_DATA` 设置为 `false`
2. 🔍 **调试**: 查看控制台日志,以 `[Mock]` 开头的是 mock 数据相关日志
3. 📝 **数据一致性**: 确保 mock 数据结构与真实 API 返回格式一致
4. 🔄 **数据持久化**: Mock 数据仅存在于前端,刷新页面后会丢失
## 相关文件
- `PlanModification.vue`: 主要逻辑文件,包含分支添加和 mock 数据集成
- `api/index.ts`: 真实 API 接口定义
- `stores/modules/agents.ts`: 类型定义和数据存储

View File

@@ -1,246 +0,0 @@
// branch_PlanOutline 接口的 Mock 数据和 Mock API
// 模拟后端返回的原始数据格式IRawPlanResponse
import type { IRawPlanResponse, IRawStepTask } from '@/stores'
// 后端返回的数据格式
export type BranchPlanOutlineResponse = IRawPlanResponse
// Mock 数据:模拟后端返回的原始分支数据(不含 Collaboration_Brief_FrontEnd
// 注意:这里模拟的是 branch_PlanOutline 函数返回的数据,不是前端转换后的数据
const mockBranchDataRaw: IRawStepTask[][] = [
// 第一个分支方案
[
{
StepName: '分析用户需求',
TaskContent: '分析用户需求,制定项目开发计划',
InputObject_List: ['腐蚀类型及成因列表'],
OutputObject: '项目开发计划书',
AgentSelection: ['腐蚀机理研究员', '实验材料学家'],
Collaboration_Brief_frontEnd: {
template: '',
data: {},
},
TaskProcess: [],
},
{
StepName: '系统设计与架构',
TaskContent: '设计系统架构和数据库结构',
InputObject_List: ['项目开发计划书'],
OutputObject: '系统设计文档',
AgentSelection: ['腐蚀机理研究员', '防护工程专家'],
Collaboration_Brief_frontEnd: {
template: '',
data: {},
},
TaskProcess: [],
},
],
// 第二个分支方案(快速原型方案)
[
{
StepName: '需求快速原型',
TaskContent: '构建快速原型验证核心功能',
InputObject_List: ['腐蚀类型及成因列表'],
OutputObject: '原型系统',
AgentSelection: ['实验材料学家', '防护工程专家'],
Collaboration_Brief_frontEnd: {
template: '',
data: {},
},
TaskProcess: [],
},
{
StepName: '原型测试与优化',
TaskContent: '测试原型并根据反馈快速迭代',
InputObject_List: ['原型系统'],
OutputObject: '优化后的原型',
AgentSelection: ['腐蚀机理研究员', '实验材料学家'],
Collaboration_Brief_frontEnd: {
template: '',
data: {},
},
TaskProcess: [],
},
],
// 第三个分支方案(质量优先方案)
[
{
StepName: '需求深度分析',
TaskContent: '深入分析用户需求和技术可行性',
InputObject_List: ['腐蚀类型及成因列表'],
OutputObject: '详细需求分析报告',
AgentSelection: ['腐蚀机理研究员', '防护工程专家'],
Collaboration_Brief_frontEnd: {
template: '',
data: {},
},
TaskProcess: [],
},
{
StepName: '质量保障设计',
TaskContent: '设计质量保障体系和测试方案',
InputObject_List: ['详细需求分析报告'],
OutputObject: '质量保障方案',
AgentSelection: ['实验材料学家', '防护工程专家'],
Collaboration_Brief_frontEnd: {
template: '',
data: {},
},
TaskProcess: [],
},
{
StepName: '系统开发与测试',
TaskContent: '按质量标准进行系统开发和测试',
InputObject_List: ['质量保障方案'],
OutputObject: '经过完整测试的系统',
AgentSelection: ['腐蚀机理研究员', '实验材料学家'],
Collaboration_Brief_frontEnd: {
template: '',
data: {},
},
TaskProcess: [],
},
],
// 第四个分支方案(敏捷开发方案)
[
{
StepName: '迭代规划',
TaskContent: '制定敏捷开发迭代计划',
InputObject_List: ['腐蚀类型及成因列表'],
OutputObject: '迭代计划',
AgentSelection: ['防护工程专家', '腐蚀机理研究员'],
Collaboration_Brief_frontEnd: {
template: '',
data: {},
},
TaskProcess: [],
},
{
StepName: '快速开发与验证',
TaskContent: '快速开发并持续验证功能',
InputObject_List: ['迭代计划'],
OutputObject: '可运行的功能模块',
AgentSelection: ['实验材料学家', '防护工程专家'],
Collaboration_Brief_frontEnd: {
template: '',
data: {},
},
TaskProcess: [],
},
],
]
/**
* 模拟后端的 Add_Collaboration_Brief_FrontEnd 函数
* 为每个任务添加前端协作简报Collaboration_Brief_frontEnd
*/
function Add_Collaboration_Brief_FrontEnd(branchList: IRawStepTask[][]): IRawStepTask[][] {
return branchList.map((tasks) =>
tasks.map((task) => ({
...task,
Collaboration_Brief_frontEnd: generateCollaborationBrief(task),
})),
)
}
/**
* 生成协作简报Collaboration_Brief_frontEnd
* 根据 StepName、TaskContent、AgentSelection 生成模板和数据
*/
function generateCollaborationBrief(task: IRawStepTask): {
template: string
data: Record<string, any>
} {
const agents = task.AgentSelection || []
const stepName = task.StepName || ''
// 为每个 agent 生成颜色
const colors = [
'hsl(210, 70%, 50%)',
'hsl(30, 70%, 50%)',
'hsl(120, 70%, 50%)',
'hsl(270, 70%, 50%)',
]
// 生成 data 对象
const data: Record<string, any> = {}
agents.forEach((agent, index) => {
data[agent] = {
text: agent,
style: { background: colors[index % colors.length] },
}
})
// 生成 template简化版本实际应根据任务内容生成
const template =
agents.length > 0
? agents.map((agent, i) => `!<${agent}>!负责!<${stepName}-${i}>!`).join('')
: ''
return {
template,
data,
}
}
/**
* Mock API模拟后端 branch_PlanOutline 接口调用
*
* @param branch_Number - 分支数量
* @param Modification_Requirement - 修改需求
* @param Existing_Steps - 现有步骤列表(包含完整信息的对象数组)
* @param Baseline_Completion - 基线完成度
* @param InitialObject_List - 初始对象列表
* @param General_Goal - 总体目标
* @returns Promise<IRawPlanResponse> - 返回包含 'Collaboration Process' 的响应
*/
export const mockBranchPlanOutlineAPI = async (params: {
branch_Number: number
Modification_Requirement: string
Existing_Steps: any[] // 临时使用 any[],因为这里没有 IRawStepTask 类型
Baseline_Completion: number
InitialObject_List: string[]
General_Goal: string
}): Promise<IRawPlanResponse> => {
// 模拟网络延迟 800ms
await new Promise((resolve) => setTimeout(resolve, 800))
console.log('[Mock API] branch_PlanOutline 调用参数:', params)
// 🆕 使用轮询方式选择分支方案(依次循环使用所有分支方案)
const totalBranches = mockBranchDataRaw.length
const sessionKey = `branch-plan-outline-index-${params.General_Goal || 'default'}`
// 获取上一次的选择索引
let lastIndex = parseInt(sessionStorage.getItem(sessionKey) || '0')
// 计算本次的选择索引(轮询到下一个分支)
const selectedBranchIndex = (lastIndex + 1) % totalBranches
// 保存本次的选择索引
sessionStorage.setItem(sessionKey, selectedBranchIndex.toString())
const rawBranchData = mockBranchDataRaw[selectedBranchIndex] || []
console.log(
'[Mock API] branch_PlanOutline 选择分支方案:',
selectedBranchIndex + 1,
'/',
totalBranches,
)
// 模拟后端处理:添加 Collaboration_Brief_FrontEnd
const processedBranches = Add_Collaboration_Brief_FrontEnd([rawBranchData])
// 构造响应数据(符合后端返回格式)
const response: IRawPlanResponse = {
'Collaboration Process': processedBranches[0] || [],
}
console.log('[Mock API] branch_PlanOutline 返回数据:', response)
return response
}
export default mockBranchPlanOutlineAPI

View File

@@ -1,617 +0,0 @@
// fill_stepTask 接口的 Mock API
// 简化版本:硬编码多个流程卡片的填充内容(支持多个分支方案)
import type { IRawStepTask } from '@/stores'
// 后端返回的数据格式
export type FillStepTaskResponse = IRawStepTask
/**
* Mock API模拟后端 fill_stepTask 接口调用
*
* 根据 stepTask 的 StepName 返回对应的填充内容
* 支持多个分支方案的轮询选择
*
* @param General_Goal - 总体目标
* @param stepTask - 待填充的任务(包含基本字段)
* @returns Promise<IRawStepTask> - 返回填充了 AgentSelection 和 TaskProcess 的完整任务
*/
export const mockFillStepTaskAPI = async (params: {
General_Goal: string
stepTask: any
}): Promise<IRawStepTask> => {
// 模拟网络延迟 300ms
await new Promise((resolve) => setTimeout(resolve, 300))
console.log('[Mock API] fill_stepTask 调用参数:', params)
const stepName = params.stepTask.StepName
// 🆕 使用轮询方式选择填充方案(依次循环使用所有分支方案)
const branchIndexKey = `fill-step-task-index-${params.stepTask?.Id || stepName || 'default'}`
let lastIndex = parseInt(sessionStorage.getItem(branchIndexKey) || '0')
const totalBranches = 4 // 总共4个分支方案
const selectedBranchIndex = (lastIndex + 1) % totalBranches
sessionStorage.setItem(branchIndexKey, selectedBranchIndex.toString())
console.log('[Mock API] fill_stepTask 选择分支方案:', selectedBranchIndex + 1, '/', totalBranches)
// ==================== 第一个任务组 ====================
if (stepName === '分析用户需求' || stepName === '需求快速原型' || stepName === '需求深度分析' || stepName === '迭代规划') {
let filledTask: IRawStepTask
if (selectedBranchIndex === 0) {
// 第一个分支方案:分析用户需求
filledTask = {
Id: params.stepTask.Id || 'task-1',
StepName: '分析用户需求',
TaskContent: '分析用户需求,制定项目开发计划',
InputObject_List: ['用户需求文档', '技术规范'],
OutputObject: '项目开发计划书',
AgentSelection: ['腐蚀机理研究员', '实验材料学家'],
Collaboration_Brief_frontEnd: {
template: '!腐蚀机理研究员!负责!腐蚀类型识别-0!!实验材料学家!负责!腐蚀类型识别-1!',
data: {
: {
text: '腐蚀机理研究员',
style: { background: 'hsl(210, 70%, 50%)' },
},
: {
text: '实验材料学家',
style: { background: 'hsl(120, 70%, 50%)' },
},
},
},
TaskProcess: [
{
ID: 'action-1-1',
ActionType: 'Propose',
AgentName: '腐蚀机理研究员',
Description: '分析腐蚀环境和介质特征,识别潜在腐蚀类型',
ImportantInput: ['InputObject:腐蚀类型及成因列表'],
},
{
ID: 'action-1-2',
ActionType: 'Propose',
AgentName: '实验材料学家',
Description: '设计腐蚀实验方案,确定测试参数',
ImportantInput: ['ActionResult:action-1-1'],
},
{
ID: 'action-1-3',
ActionType: 'Critique',
AgentName: '腐蚀机理研究员',
Description: '评估实验方案的可行性',
ImportantInput: [
'ActionResult:action-1-2',
'ActionResult:action-1-1',
'InputObject:腐蚀类型及成因列表',
],
},
{
ID: 'action-1-4',
ActionType: 'Improve',
AgentName: '实验材料学家',
Description: '优化实验设计,完善测试流程',
ImportantInput: ['ActionResult:action-1-3'],
},
{
ID: 'action-1-5',
ActionType: 'Finalize',
AgentName: '腐蚀机理研究员',
Description: '确认最终的腐蚀类型识别方案',
ImportantInput: ['ActionResult:action-1-4', 'ActionResult:action-1-3'],
},
],
}
} else if (selectedBranchIndex === 1) {
// 第二个分支方案:需求快速原型
filledTask = {
Id: params.stepTask.Id || 'task-1',
StepName: '需求快速原型',
TaskContent: '构建快速原型验证核心功能',
InputObject_List: ['腐蚀类型及成因列表'],
OutputObject: '原型系统',
AgentSelection: ['实验材料学家', '防护工程专家'],
Collaboration_Brief_frontEnd: {
template: '!实验材料学家!负责!需求快速原型-0!!防护工程专家!负责!需求快速原型-1!',
data: {
: {
text: '实验材料学家',
style: { background: 'hsl(120, 70%, 50%)' },
},
: {
text: '防护工程专家',
style: { background: 'hsl(30, 70%, 50%)' },
},
},
},
TaskProcess: [
{
ID: 'action-1-1-branch2',
ActionType: 'Propose',
AgentName: '实验材料学家',
Description: '基于腐蚀类型列表,设计快速原型验证方案',
ImportantInput: ['InputObject:腐蚀类型及成因列表'],
},
{
ID: 'action-1-2-branch2',
ActionType: 'Propose',
AgentName: '防护工程专家',
Description: '实现原型核心功能和用户界面',
ImportantInput: ['ActionResult:action-1-1-branch2'],
},
{
ID: 'action-1-3-branch2',
ActionType: 'Critique',
AgentName: '实验材料学家',
Description: '评估原型功能完整性和用户体验',
ImportantInput: ['ActionResult:action-1-2-branch2'],
},
{
ID: 'action-1-4-branch2',
ActionType: 'Improve',
AgentName: '防护工程专家',
Description: '根据反馈优化原型功能和界面',
ImportantInput: ['ActionResult:action-1-3-branch2'],
},
{
ID: 'action-1-5-branch2',
ActionType: 'Finalize',
AgentName: '实验材料学家',
Description: '确认最终版本的原型系统',
ImportantInput: ['ActionResult:action-1-4-branch2'],
},
],
}
} else if (selectedBranchIndex === 2) {
// 第三个分支方案:需求深度分析
filledTask = {
Id: params.stepTask.Id || 'task-1',
StepName: '需求深度分析',
TaskContent: '深入分析用户需求和技术可行性',
InputObject_List: ['腐蚀类型及成因列表'],
OutputObject: '详细需求分析报告',
AgentSelection: ['腐蚀机理研究员', '防护工程专家'],
Collaboration_Brief_frontEnd: {
template: '!腐蚀机理研究员!负责!需求深度分析-0!!防护工程专家!负责!需求深度分析-1!',
data: {
: {
text: '腐蚀机理研究员',
style: { background: 'hsl(210, 70%, 50%)' },
},
: {
text: '防护工程专家',
style: { background: 'hsl(30, 70%, 50%)' },
},
},
},
TaskProcess: [
{
ID: 'action-1-1-branch3',
ActionType: 'Propose',
AgentName: '腐蚀机理研究员',
Description: '全面分析腐蚀环境和技术约束条件',
ImportantInput: ['InputObject:腐蚀类型及成因列表'],
},
{
ID: 'action-1-2-branch3',
ActionType: 'Propose',
AgentName: '防护工程专家',
Description: '评估技术可行性和防护方案',
ImportantInput: ['ActionResult:action-1-1-branch3'],
},
{
ID: 'action-1-3-branch3',
ActionType: 'Critique',
AgentName: '腐蚀机理研究员',
Description: '评审需求分析的完整性和准确性',
ImportantInput: ['ActionResult:action-1-2-branch3', 'ActionResult:action-1-1-branch3'],
},
{
ID: 'action-1-4-branch3',
ActionType: 'Improve',
AgentName: '防护工程专家',
Description: '补充技术风险评估和应对策略',
ImportantInput: ['ActionResult:action-1-3-branch3'],
},
{
ID: 'action-1-5-branch3',
ActionType: 'Finalize',
AgentName: '腐蚀机理研究员',
Description: '确认详细需求分析报告',
ImportantInput: ['ActionResult:action-1-4-branch3', 'ActionResult:action-1-3-branch3'],
},
],
}
} else {
// 第四个分支方案:迭代规划
filledTask = {
Id: params.stepTask.Id || 'task-1',
StepName: '迭代规划',
TaskContent: '制定敏捷开发迭代计划',
InputObject_List: ['腐蚀类型及成因列表'],
OutputObject: '迭代计划',
AgentSelection: ['防护工程专家', '腐蚀机理研究员'],
Collaboration_Brief_frontEnd: {
template: '!防护工程专家!负责!迭代规划-0!!腐蚀机理研究员!负责!迭代规划-1!',
data: {
: {
text: '防护工程专家',
style: { background: 'hsl(30, 70%, 50%)' },
},
: {
text: '腐蚀机理研究员',
style: { background: 'hsl(210, 70%, 50%)' },
},
},
},
TaskProcess: [
{
ID: 'action-1-1-branch4',
ActionType: 'Propose',
AgentName: '防护工程专家',
Description: '制定敏捷开发总体框架和迭代周期',
ImportantInput: ['InputObject:腐蚀类型及成因列表'],
},
{
ID: 'action-1-2-branch4',
ActionType: 'Propose',
AgentName: '腐蚀机理研究员',
Description: '规划各迭代阶段的技术验证重点',
ImportantInput: ['ActionResult:action-1-1-branch4'],
},
{
ID: 'action-1-3-branch4',
ActionType: 'Critique',
AgentName: '防护工程专家',
Description: '评审迭代计划的合理性和可执行性',
ImportantInput: ['ActionResult:action-1-2-branch4'],
},
{
ID: 'action-1-4-branch4',
ActionType: 'Improve',
AgentName: '腐蚀机理研究员',
Description: '优化迭代节奏和里程碑设置',
ImportantInput: ['ActionResult:action-1-3-branch4'],
},
{
ID: 'action-1-5-branch4',
ActionType: 'Finalize',
AgentName: '防护工程专家',
Description: '确认最终迭代计划',
ImportantInput: ['ActionResult:action-1-4-branch4', 'ActionResult:action-1-3-branch4'],
},
],
}
}
console.log('[Mock API] fill_stepTask 返回数据:', filledTask)
return filledTask
}
// ==================== 第二个任务组 ====================
else if (stepName === '系统设计与架构' || stepName === '原型测试与优化' || stepName === '质量保障设计' || stepName === '快速开发与验证') {
let filledTask: IRawStepTask
if (selectedBranchIndex === 0) {
// 第一个分支方案:系统设计与架构
filledTask = {
Id: params.stepTask.Id || 'task-2',
StepName: '系统设计与架构',
TaskContent: '设计系统架构和数据库结构',
InputObject_List: ['项目开发计划书'],
OutputObject: '系统设计文档',
AgentSelection: ['腐蚀机理研究员', '防护工程专家'],
Collaboration_Brief_frontEnd: {
template: '!腐蚀机理研究员!负责!系统设计与架构-0!!防护工程专家!负责!系统设计与架构-1!',
data: {
: {
text: '腐蚀机理研究员',
style: { background: 'hsl(210, 70%, 50%)' },
},
: {
text: '防护工程专家',
style: { background: 'hsl(30, 70%, 50%)' },
},
},
},
TaskProcess: [
{
ID: 'action-2-1',
ActionType: 'Propose',
AgentName: '腐蚀机理研究员',
Description: '分析系统功能需求,提出整体架构方案',
ImportantInput: ['InputObject:项目开发计划书'],
},
{
ID: 'action-2-2',
ActionType: 'Propose',
AgentName: '防护工程专家',
Description: '设计防护系统架构和数据库模型',
ImportantInput: ['ActionResult:action-2-1'],
},
{
ID: 'action-2-3',
ActionType: 'Critique',
AgentName: '腐蚀机理研究员',
Description: '评审架构设计的合理性和可扩展性',
ImportantInput: ['ActionResult:action-2-2', 'InputObject:项目开发计划书'],
},
{
ID: 'action-2-4',
ActionType: 'Improve',
AgentName: '防护工程专家',
Description: '优化架构设计,补充性能和安全方案',
ImportantInput: ['ActionResult:action-2-3', 'ActionResult:action-2-2'],
},
{
ID: 'action-2-5',
ActionType: 'Finalize',
AgentName: '腐蚀机理研究员',
Description: '确认最终系统架构设计文档',
ImportantInput: ['ActionResult:action-2-4', 'ActionResult:action-2-3'],
},
],
}
} else if (selectedBranchIndex === 1) {
// 第二个分支方案:原型测试与优化
filledTask = {
Id: params.stepTask.Id || 'task-2',
StepName: '原型测试与优化',
TaskContent: '测试原型并根据反馈快速迭代',
InputObject_List: ['原型系统'],
OutputObject: '优化后的原型',
AgentSelection: ['腐蚀机理研究员', '实验材料学家'],
Collaboration_Brief_frontEnd: {
template: '!腐蚀机理研究员!负责!原型测试与优化-0!!实验材料学家!负责!原型测试与优化-1!',
data: {
: {
text: '腐蚀机理研究员',
style: { background: 'hsl(210, 70%, 50%)' },
},
: {
text: '实验材料学家',
style: { background: 'hsl(120, 70%, 50%)' },
},
},
},
TaskProcess: [
{
ID: 'action-2-1-branch2',
ActionType: 'Propose',
AgentName: '腐蚀机理研究员',
Description: '制定原型测试计划和验证标准',
ImportantInput: ['InputObject:原型系统'],
},
{
ID: 'action-2-2-branch2',
ActionType: 'Propose',
AgentName: '实验材料学家',
Description: '执行原型测试并收集性能数据',
ImportantInput: ['ActionResult:action-2-1-branch2'],
},
{
ID: 'action-2-3-branch2',
ActionType: 'Critique',
AgentName: '腐蚀机理研究员',
Description: '分析测试结果和用户反馈',
ImportantInput: ['ActionResult:action-2-2-branch2'],
},
{
ID: 'action-2-4-branch2',
ActionType: 'Improve',
AgentName: '实验材料学家',
Description: '根据测试结果优化原型功能和性能',
ImportantInput: ['ActionResult:action-2-3-branch2'],
},
{
ID: 'action-2-5-branch2',
ActionType: 'Finalize',
AgentName: '腐蚀机理研究员',
Description: '确认优化后的原型版本',
ImportantInput: ['ActionResult:action-2-4-branch2', 'ActionResult:action-2-3-branch2'],
},
],
}
} else if (selectedBranchIndex === 2) {
// 第三个分支方案:质量保障设计
filledTask = {
Id: params.stepTask.Id || 'task-2',
StepName: '质量保障设计',
TaskContent: '设计质量保障体系和测试方案',
InputObject_List: ['详细需求分析报告'],
OutputObject: '质量保障方案',
AgentSelection: ['实验材料学家', '防护工程专家'],
Collaboration_Brief_frontEnd: {
template: '!实验材料学家!负责!质量保障设计-0!!防护工程专家!负责!质量保障设计-1!',
data: {
: {
text: '实验材料学家',
style: { background: 'hsl(120, 70%, 50%)' },
},
: {
text: '防护工程专家',
style: { background: 'hsl(30, 70%, 50%)' },
},
},
},
TaskProcess: [
{
ID: 'action-2-1-branch3',
ActionType: 'Propose',
AgentName: '实验材料学家',
Description: '制定质量标准和测试指标体系',
ImportantInput: ['InputObject:详细需求分析报告'],
},
{
ID: 'action-2-2-branch3',
ActionType: 'Propose',
AgentName: '防护工程专家',
Description: '设计质量保障流程和验证方案',
ImportantInput: ['ActionResult:action-2-1-branch3'],
},
{
ID: 'action-2-3-branch3',
ActionType: 'Critique',
AgentName: '实验材料学家',
Description: '评审质量保障方案的完整性',
ImportantInput: ['ActionResult:action-2-2-branch3'],
},
{
ID: 'action-2-4-branch3',
ActionType: 'Improve',
AgentName: '防护工程专家',
Description: '完善质量保障方案,补充风险控制',
ImportantInput: ['ActionResult:action-2-3-branch3'],
},
{
ID: 'action-2-5-branch3',
ActionType: 'Finalize',
AgentName: '实验材料学家',
Description: '确认最终质量保障方案',
ImportantInput: ['ActionResult:action-2-4-branch3', 'ActionResult:action-2-3-branch3'],
},
],
}
} else {
// 第四个分支方案:快速开发与验证
filledTask = {
Id: params.stepTask.Id || 'task-2',
StepName: '快速开发与验证',
TaskContent: '快速开发并持续验证功能',
InputObject_List: ['迭代计划'],
OutputObject: '可运行的功能模块',
AgentSelection: ['实验材料学家', '防护工程专家'],
Collaboration_Brief_frontEnd: {
template: '!实验材料学家!负责!快速开发与验证-0!!防护工程专家!负责!快速开发与验证-1!',
data: {
: {
text: '实验材料学家',
style: { background: 'hsl(120, 70%, 50%)' },
},
: {
text: '防护工程专家',
style: { background: 'hsl(30, 70%, 50%)' },
},
},
},
TaskProcess: [
{
ID: 'action-2-1-branch4',
ActionType: 'Propose',
AgentName: '实验材料学家',
Description: '根据迭代计划快速开发功能模块',
ImportantInput: ['InputObject:迭代计划'],
},
{
ID: 'action-2-2-branch4',
ActionType: 'Propose',
AgentName: '防护工程专家',
Description: '实现功能防护机制和验证逻辑',
ImportantInput: ['ActionResult:action-2-1-branch4'],
},
{
ID: 'action-2-3-branch4',
ActionType: 'Critique',
AgentName: '实验材料学家',
Description: '验证功能模块的正确性和性能',
ImportantInput: ['ActionResult:action-2-2-branch4'],
},
{
ID: 'action-2-4-branch4',
ActionType: 'Improve',
AgentName: '防护工程专家',
Description: '根据验证结果优化功能实现',
ImportantInput: ['ActionResult:action-2-3-branch4'],
},
{
ID: 'action-2-5-branch4',
ActionType: 'Finalize',
AgentName: '实验材料学家',
Description: '确认可运行的功能模块',
ImportantInput: ['ActionResult:action-2-4-branch4', 'ActionResult:action-2-3-branch4'],
},
],
}
}
console.log('[Mock API] fill_stepTask 返回数据:', filledTask)
return filledTask
}
// ==================== 第三个任务组(仅第三个分支方案) ====================
else if (stepName === '系统开发与测试') {
const filledTask: IRawStepTask = {
Id: params.stepTask.Id || 'task-3',
StepName: '系统开发与测试',
TaskContent: '按质量标准进行系统开发和测试',
InputObject_List: ['质量保障方案'],
OutputObject: '经过完整测试的系统',
AgentSelection: ['腐蚀机理研究员', '实验材料学家'],
Collaboration_Brief_frontEnd: {
template: '!腐蚀机理研究员!负责!系统开发与测试-0!!实验材料学家!负责!系统开发与测试-1!',
data: {
: {
text: '腐蚀机理研究员',
style: { background: 'hsl(210, 70%, 50%)' },
},
: {
text: '实验材料学家',
style: { background: 'hsl(120, 70%, 50%)' },
},
},
},
TaskProcess: [
{
ID: 'action-3-1',
ActionType: 'Propose',
AgentName: '腐蚀机理研究员',
Description: '根据质量标准制定系统开发计划',
ImportantInput: ['InputObject:质量保障方案'],
},
{
ID: 'action-3-2',
ActionType: 'Propose',
AgentName: '实验材料学家',
Description: '按标准实施系统开发和单元测试',
ImportantInput: ['ActionResult:action-3-1'],
},
{
ID: 'action-3-3',
ActionType: 'Critique',
AgentName: '腐蚀机理研究员',
Description: '评审系统开发质量和测试覆盖率',
ImportantInput: ['ActionResult:action-3-2'],
},
{
ID: 'action-3-4',
ActionType: 'Improve',
AgentName: '实验材料学家',
Description: '根据评审结果完善系统功能和测试',
ImportantInput: ['ActionResult:action-3-3'],
},
{
ID: 'action-3-5',
ActionType: 'Finalize',
AgentName: '腐蚀机理研究员',
Description: '确认经过完整测试的系统版本',
ImportantInput: ['ActionResult:action-3-4', 'ActionResult:action-3-3'],
},
],
}
console.log('[Mock API] fill_stepTask 返回数据:', filledTask)
return filledTask
}
// ==================== 其他任务 ====================
else {
// 其他任务,返回空 TaskProcess
console.warn('[Mock API] 未知的 StepName:', stepName)
return {
...params.stepTask,
AgentSelection: [],
TaskProcess: [],
}
}
}
export default mockFillStepTaskAPI

View File

@@ -1,11 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue' import { ref, computed, watch, nextTick } from 'vue'
import { useAgentsStore, useSelectionStore } from '@/stores' import { useAgentsStore, useSelectionStore } from '@/stores'
import { convertToTaskProcess } from '@/stores/modules/agents'
import { getAgentMapIcon } from '@/layout/components/config' import { getAgentMapIcon } from '@/layout/components/config'
import SvgIcon from '@/components/SvgIcon/index.vue' import SvgIcon from '@/components/SvgIcon/index.vue'
import api from '@/api' import api from '@/api'
const agentsStore = useAgentsStore() const agentsStore = useAgentsStore()
const selectionStore = useSelectionStore() const selectionStore = useSelectionStore()
const agentsHeatmapData = ref<AgentHeatmapData[]>([])
const selectedAgents = ref<Set<string>>(new Set())
const selectedAssignmentGroup = ref<string[]>([])
const selectedDimensions = ref<Set<string>>(new Set())
// 获取当前选中的任务 // 获取当前选中的任务
const currentTask = computed(() => { const currentTask = computed(() => {
@@ -32,19 +37,7 @@ interface AgentHeatmapData {
scoreDetails?: Array<{ dimension: string; score: number; reason: string }> // 详细信息 scoreDetails?: Array<{ dimension: string; score: number; reason: string }> // 详细信息
} }
// 所有agent的热力图数据 // 确认的agent组合列表 - 从 store 读取
const agentsHeatmapData = ref<AgentHeatmapData[]>([])
// 选中的agent集合用于Comparison部分
const selectedAgents = ref<Set<string>>(new Set())
// 当前选中的Assignment组合的agents用于Assignment卡片的边框显示
const selectedAssignmentGroup = ref<string[]>([])
// 选中的维度集合
const selectedDimensions = ref<Set<string>>(new Set())
// 确认的agent组合列表 - 从 store 读取按当前任务ID
const confirmedAgentGroups = computed(() => { const confirmedAgentGroups = computed(() => {
const taskId = currentTask.value?.Id const taskId = currentTask.value?.Id
if (!taskId) return [] if (!taskId) return []
@@ -59,33 +52,44 @@ const isInitializing = ref(false)
const isAddingDimension = ref(false) const isAddingDimension = ref(false)
const isLoadingConfirm = ref(false) // 确认 agent 组合时的加载状态 const isLoadingConfirm = ref(false) // 确认 agent 组合时的加载状态
const isLoadingSelectGroup = ref(false) // 选择已保存组合时的加载状态 const isLoadingSelectGroup = ref(false) // 选择已保存组合时的加载状态
const isLoadingInitialTask = ref(false) // 首次加载任务时的加载状态
// 热力图排序:选中的 agent 排在前面,其他按平均分降序
const sortHeatmapBySelection = () => {
if (agentsHeatmapData.value.length === 0) return
agentsHeatmapData.value.sort((a, b) => {
const aIsSelected = selectedAgents.value.has(a.agentName)
const bIsSelected = selectedAgents.value.has(b.agentName)
// 如果一个是选中的 agent另一个不是选中的 agent 排前面
if (aIsSelected && !bIsSelected) return -1
if (!aIsSelected && bIsSelected) return 1
// 如果都是或都不是选中的 agent按选中的维度平均分降序排列
const selectedDimArray = Array.from(selectedDimensions.value)
const aAverage = calculateAgentAverage(a, selectedDimArray)
const bAverage = calculateAgentAverage(b, selectedDimArray)
return bAverage - aAverage
})
}
// 处理搜索提交 // 处理搜索提交
const handleSubmit = async () => { const handleSubmit = async () => {
if (!searchValue.value.trim() || isAddingDimension.value) return if (!searchValue.value.trim() || isAddingDimension.value) return
const newDimension = searchValue.value.trim() const newDimension = searchValue.value.trim()
// 检查维度是否已存在 // 检查维度是否已存在
if (scoreDimensions.value.includes(newDimension)) { if (scoreDimensions.value.includes(newDimension)) {
searchValue.value = '' searchValue.value = ''
return return
} }
try { try {
isAddingDimension.value = true isAddingDimension.value = true
// 使用 Mock API 进行测试(开发阶段)
// 切换到真实API时只需将 mockAgentSelectModifyAddAspect 改为 agentSelectModifyAddAspect
const response = await api.agentSelectModifyAddAspect({ const response = await api.agentSelectModifyAddAspect({
aspectList: [...scoreDimensions.value, newDimension] aspectList: [...scoreDimensions.value, newDimension]
}) })
// 1. 先添加新维度到本地维度列表(立即显示)
scoreDimensions.value.push(response.aspectName) scoreDimensions.value.push(response.aspectName)
// 2. 为每个agent添加新维度的评分立即显示 // 为每个agent添加新维度的评分立即显示
agentsHeatmapData.value.forEach(agentData => { agentsHeatmapData.value.forEach(agentData => {
const scoreInfo = response.agentScores[agentData.agentName] const scoreInfo = response.agentScores[agentData.agentName]
if (scoreInfo) { if (scoreInfo) {
@@ -97,26 +101,42 @@ const handleSubmit = async () => {
}) })
} }
}) })
//异步更新 store等前端显示完成后再更新避免触发重新初始化
// 3. 异步更新 store等前端显示完成后再更新避免触发重新初始化
await nextTick() await nextTick()
const taskId = currentTask.value?.Id
//更新按任务ID的存储
if (taskId) {
const existingTaskData = agentsStore.getTaskScoreData(taskId)
if (existingTaskData) {
// 检查维度是否已存在
if (!existingTaskData.aspectList.includes(response.aspectName)) {
existingTaskData.aspectList.push(response.aspectName)
}
// 更新评分数据
for (const [agentName, scoreInfo] of Object.entries(response.agentScores)) {
if (!existingTaskData.agentScores[agentName]) {
existingTaskData.agentScores[agentName] = {}
}
existingTaskData.agentScores[agentName][response.aspectName] = scoreInfo
}
// 保存到 store
agentsStore.setTaskScoreData(taskId, existingTaskData)
}
}
// 兼容旧版本:更新全局存储
const currentStoreData = agentsStore.IAgentSelectModifyAddRequest const currentStoreData = agentsStore.IAgentSelectModifyAddRequest
if (currentStoreData && currentStoreData.aspectList) { if (currentStoreData && currentStoreData.aspectList) {
// 检查维度是否已存在防止store中已存在
if (!currentStoreData.aspectList.includes(response.aspectName)) { if (!currentStoreData.aspectList.includes(response.aspectName)) {
currentStoreData.aspectList.push(response.aspectName) currentStoreData.aspectList.push(response.aspectName)
} }
// 更新评分数据
// response.agentScores: { agentName: { score, reason } }
// 转换为 agentScores[agentName][aspectName] = { score, reason }
for (const [agentName, scoreInfo] of Object.entries(response.agentScores)) { for (const [agentName, scoreInfo] of Object.entries(response.agentScores)) {
if (!currentStoreData.agentScores[agentName]) { if (!currentStoreData.agentScores[agentName]) {
currentStoreData.agentScores[agentName] = {} currentStoreData.agentScores[agentName] = {}
} }
currentStoreData.agentScores[agentName][response.aspectName] = scoreInfo currentStoreData.agentScores[agentName][response.aspectName] = scoreInfo
} }
console.log(`已更新 store`)
} }
// 清空输入框 // 清空输入框
@@ -128,7 +148,7 @@ const handleSubmit = async () => {
} }
} }
// 判断两个agent组合是否相同(不考虑顺序) // 判断两个agent组合是否相同
const areAgentGroupsEqual = (group1: string[], group2: string[]): boolean => { const areAgentGroupsEqual = (group1: string[], group2: string[]): boolean => {
if (group1.length !== group2.length) return false if (group1.length !== group2.length) return false
const sorted1 = [...group1].sort() const sorted1 = [...group1].sort()
@@ -140,7 +160,6 @@ const areAgentGroupsEqual = (group1: string[], group2: string[]): boolean => {
const confirmAgentSelection = async () => { const confirmAgentSelection = async () => {
if (selectedAgents.value.size > 0 && currentTask.value?.Id) { if (selectedAgents.value.size > 0 && currentTask.value?.Id) {
const agentArray = Array.from(selectedAgents.value) const agentArray = Array.from(selectedAgents.value)
// 检查该agent组合是否已存在统一检查包括初始组合和用户添加的组合 // 检查该agent组合是否已存在统一检查包括初始组合和用户添加的组合
const existingGroups = agentsStore.getConfirmedAgentGroups(currentTask.value.Id) const existingGroups = agentsStore.getConfirmedAgentGroups(currentTask.value.Id)
const existingDuplicateIndex = existingGroups.findIndex(group => const existingDuplicateIndex = existingGroups.findIndex(group =>
@@ -148,64 +167,30 @@ const confirmAgentSelection = async () => {
) )
if (existingDuplicateIndex !== -1) { if (existingDuplicateIndex !== -1) {
// 找到重复的组合,只选中该卡片,不添加重复
console.log('该agent组合已存在只选中卡片不添加重复')
const existingGroup = existingGroups[existingDuplicateIndex]! const existingGroup = existingGroups[existingDuplicateIndex]!
selectedAssignmentGroup.value = [...existingGroup] selectedAssignmentGroup.value = [...existingGroup]
selectedAgents.value = new Set(existingGroup) selectedAgents.value = new Set(existingGroup)
return return
} }
// 该组合不存在,添加新组合
// 使用 store 的方法添加按任务ID存储不持久化到localStorage
agentsStore.addConfirmedAgentGroup(currentTask.value.Id, agentArray) agentsStore.addConfirmedAgentGroup(currentTask.value.Id, agentArray)
console.log(`已确认 agent 组合 [任务 ${currentTask.value.Id}]:`, agentArray)
// 调用 Mock API 填充任务流程
try { try {
isLoadingConfirm.value = true isLoadingConfirm.value = true
console.log('=== 开始调用 fillStepTaskTaskProcess Mock API ===') const stepTaskForApi = agentsStore.createStepTaskForApi(agentArray)
console.log('1. 当前任务数据 (IRawStepTask 格式):', currentTask.value) const goal = agentsStore.agentRawPlan.data?.['General Goal'] || '开发智能协作系统'
console.log('2. 选中的 agents:', agentArray)
// 将 IRawStepTask 转换为 IApiStepTask 格式 if (agentsStore.isStopping || agentsStore.hasStoppedFilling) {
const stepTaskForApi = { isLoadingConfirm.value = false
name: currentTask.value.StepName || '', return
content: currentTask.value.TaskContent || '',
inputs: currentTask.value.InputObject_List || [],
output: currentTask.value.OutputObject || '',
agents: agentArray,
brief: currentTask.value.Collaboration_Brief_frontEnd || {
template: '',
data: {}
},
process: []
} }
console.log('3. 转换后的 API 请求参数 (IApiStepTask 格式):', stepTaskForApi)
// 获取 goal从 agentRawPlan 中获取)
const goal = agentsStore.agentRawPlan.data?.['General Goal'] || '开发智能协作系统'
console.log('4. General Goal:', goal)
// 调用 Mock API
const filledTask = await api.fillStepTaskTaskProcess({ const filledTask = await api.fillStepTaskTaskProcess({
goal, goal,
stepTask: stepTaskForApi, stepTask: stepTaskForApi,
agents: agentArray agents: agentArray
}) })
console.log('=== Mock API 返回成功 ===')
console.log('5. 返回的完整数据 (IApiStepTask 格式):', filledTask)
console.log('6. TaskProcess 流程数量:', filledTask.process?.length || 0)
console.log('7. TaskProcess 详情:', filledTask.process)
console.log('8. Collaboration Brief:', filledTask.brief)
// 存储到 selectionStore
selectionStore.setAgentTaskProcess(currentTask.value.Id, agentArray, filledTask) selectionStore.setAgentTaskProcess(currentTask.value.Id, agentArray, filledTask)
console.log('✅ Mock API 调用成功,数据已存储到 selectionStore')
} catch (error) { } catch (error) {
console.error('❌ Mock API 调用失败:', error) console.error('❌ API 调用失败:', error)
} finally { } finally {
isLoadingConfirm.value = false isLoadingConfirm.value = false
} }
@@ -214,84 +199,35 @@ const confirmAgentSelection = async () => {
// 点击 Assignment 部分的 agent 组合卡片,更新 Comparison 部分的选中状态 // 点击 Assignment 部分的 agent 组合卡片,更新 Comparison 部分的选中状态
const selectAgentGroup = async (agentNames: string[]) => { const selectAgentGroup = async (agentNames: string[]) => {
// 更新Assignment边框状态
selectedAssignmentGroup.value = [...agentNames] selectedAssignmentGroup.value = [...agentNames]
// 更新Comparison部分的选中状态
selectedAgents.value = new Set(agentNames) selectedAgents.value = new Set(agentNames)
// 触发响应式更新
selectedAgents.value = new Set(selectedAgents.value) selectedAgents.value = new Set(selectedAgents.value)
// 保存到 store(持久化选择状态) // 保存到 store
if (currentTask.value?.Id) { if (currentTask.value?.Id) {
agentsStore.setSelectedAgentGroup(currentTask.value.Id, agentNames) agentsStore.setSelectedAgentGroup(currentTask.value.Id, agentNames)
} }
// 🆕 联动更新:更新 currentTask 的 AgentSelection 和 TaskProcess // 更新 currentTask 的 AgentSelection 和 TaskProcess
if (currentTask.value?.Id && agentNames.length > 0) { if (currentTask.value?.Id && agentNames.length > 0) {
console.log('🔄 开始联动更新 currentTask 的 agent 组合:', agentNames)
// 从 selectionStore 获取该 agent 组合对应的 TaskProcess 数据
let taskProcessData = selectionStore.getAgentTaskProcess(currentTask.value.Id, agentNames) let taskProcessData = selectionStore.getAgentTaskProcess(currentTask.value.Id, agentNames)
// 🆕 如果 selectionStore 中没有数据,自动调用 API 加载
if (!taskProcessData) { if (!taskProcessData) {
console.log('⚠️ selectionStore 中没有该组合的 TaskProcess 数据,开始加载...')
console.log('📋 当前任务信息:', {
taskId: currentTask.value.Id,
taskName: currentTask.value.StepName,
agents: agentNames
})
try { try {
isLoadingSelectGroup.value = true isLoadingSelectGroup.value = true
// 将 IRawStepTask 转换为 IApiStepTask 格式 const stepTaskForApi = agentsStore.createStepTaskForApi(agentNames)
const stepTaskForApi = {
name: currentTask.value.StepName || '',
content: currentTask.value.TaskContent || '',
inputs: currentTask.value.InputObject_List || [],
output: currentTask.value.OutputObject || '',
agents: agentNames,
brief: currentTask.value.Collaboration_Brief_frontEnd || {
template: '',
data: {}
},
process: []
}
const goal = agentsStore.agentRawPlan.data?.['General Goal'] || '开发智能协作系统' const goal = agentsStore.agentRawPlan.data?.['General Goal'] || '开发智能协作系统'
console.log('📤 准备调用 API:', { if (agentsStore.isStopping || agentsStore.hasStoppedFilling) {
goal, return
stepTask: stepTaskForApi, }
agents: agentNames
})
// 调用 Mock API 加载数据
const filledTask = await api.fillStepTaskTaskProcess({ const filledTask = await api.fillStepTaskTaskProcess({
goal, goal,
stepTask: stepTaskForApi, stepTask: stepTaskForApi,
agents: agentNames agents: agentNames
}) })
console.log('🔍 API 返回的完整响应:', filledTask)
console.log('🔍 process 数据:', filledTask.process)
console.log('🔍 process 长度:', filledTask.process?.length)
console.log('✅ TaskProcess 数据加载成功:', {
agents: agentNames,
processCount: filledTask.process?.length || 0,
processDetail: filledTask.process,
briefDetail: filledTask.brief
})
// 存储到 selectionStore
selectionStore.setAgentTaskProcess(currentTask.value.Id, agentNames, filledTask) selectionStore.setAgentTaskProcess(currentTask.value.Id, agentNames, filledTask)
console.log('💾 数据已存储到 selectionStore验证存储是否成功...')
// 验证存储
const storedData = selectionStore.getAgentTaskProcess(currentTask.value.Id, agentNames)
console.log('📦 验证存储结果:', storedData ? '成功' : '失败', storedData)
taskProcessData = filledTask taskProcessData = filledTask
} catch (error) { } catch (error) {
console.error('❌ 加载 TaskProcess 数据失败:', error) console.error('❌ 加载 TaskProcess 数据失败:', error)
@@ -302,62 +238,17 @@ const selectAgentGroup = async (agentNames: string[]) => {
} }
if (taskProcessData) { if (taskProcessData) {
console.log('✅ 找到 TaskProcess 数据,开始更新 currentTask:', { const convertedTaskProcess = convertToTaskProcess(taskProcessData.process || [])
taskId: currentTask.value.Id,
agents: agentNames,
processCount: taskProcessData.process?.length || 0,
processDetail: taskProcessData.process,
briefDetail: taskProcessData.brief
})
// 🆕 数据格式转换IApiAgentAction[] → TaskProcess[]
const convertedTaskProcess = (taskProcessData.process || []).map(action => ({
ID: action.id,
ActionType: action.type,
AgentName: action.agent,
Description: action.description,
ImportantInput: action.inputs || []
}))
console.log('🔄 数据格式转换完成:', {
original: taskProcessData.process,
converted: convertedTaskProcess
})
// 更新 currentTask 的 AgentSelection 和 TaskProcess
// 使用 updateCurrentAgentSelection 强制更新,避免被 setCurrentTask 的"智能保留"逻辑阻止
agentsStore.updateCurrentAgentSelection( agentsStore.updateCurrentAgentSelection(
[...agentNames], [...agentNames],
convertedTaskProcess, convertedTaskProcess,
taskProcessData.brief || currentTask.value.Collaboration_Brief_frontEnd taskProcessData.brief || currentTask.value.Collaboration_Brief_frontEnd
) )
console.log('✅ currentTask 已更新,新的 AgentSelection:', agentNames)
console.log('📋 验证更新结果:', agentsStore.currentTask)
} }
sortHeatmapBySelection()
} }
// 重新排序被选中的agent排在前面其他agent按平均分降序
if (agentsHeatmapData.value.length > 0) {
agentsHeatmapData.value.sort((a, b) => {
const aIsSelected = selectedAgents.value.has(a.agentName)
const bIsSelected = selectedAgents.value.has(b.agentName)
// 如果一个是选中的agent另一个不是选中的agent排前面
if (aIsSelected && !bIsSelected) return -1
if (!aIsSelected && bIsSelected) return 1
// 如果都是或都不是选中的agent按选中的维度平均分降序排列
const selectedDimArray = Array.from(selectedDimensions.value)
const aAverage = calculateAgentAverage(a, selectedDimArray)
const bAverage = calculateAgentAverage(b, selectedDimArray)
return bAverage - aAverage
})
}
console.log('选中 agent 组合:', Array.from(selectedAgents.value))
} }
// 切换维度选中状态 // 切换维度选中状态
const toggleDimensionSelection = (dimension: string) => { const toggleDimensionSelection = (dimension: string) => {
if (selectedDimensions.value.has(dimension)) { if (selectedDimensions.value.has(dimension)) {
@@ -365,26 +256,8 @@ const toggleDimensionSelection = (dimension: string) => {
} else { } else {
selectedDimensions.value.add(dimension) selectedDimensions.value.add(dimension)
} }
// 触发响应式更新
selectedDimensions.value = new Set(selectedDimensions.value) selectedDimensions.value = new Set(selectedDimensions.value)
sortHeatmapBySelection()
// 重新排序agent
if (agentsHeatmapData.value.length > 0) {
agentsHeatmapData.value.sort((a, b) => {
const aIsSelected = selectedAgents.value.has(a.agentName)
const bIsSelected = selectedAgents.value.has(b.agentName)
// 如果一个是选中的agent另一个不是选中的agent排前面
if (aIsSelected && !bIsSelected) return -1
if (!aIsSelected && bIsSelected) return 1
// 如果都是或都不是选中的agent按选中的维度平均分降序排列
const selectedDimArray = Array.from(selectedDimensions.value)
const aAverage = calculateAgentAverage(a, selectedDimArray)
const bAverage = calculateAgentAverage(b, selectedDimArray)
return bAverage - aAverage
})
}
} }
// 判断维度是否被选中 // 判断维度是否被选中
@@ -399,31 +272,8 @@ const toggleAgentSelection = (agentName: string) => {
} else { } else {
selectedAgents.value.add(agentName) selectedAgents.value.add(agentName)
} }
// 触发响应式更新
selectedAgents.value = new Set(selectedAgents.value) selectedAgents.value = new Set(selectedAgents.value)
sortHeatmapBySelection()
// 重新排序被选中的agent排在前面其他agent按平均分降序
if (agentsHeatmapData.value.length > 0) {
agentsHeatmapData.value.sort((a, b) => {
const aIsSelected = selectedAgents.value.has(a.agentName)
const bIsSelected = selectedAgents.value.has(b.agentName)
// 如果一个是选中的agent另一个不是选中的agent排前面
if (aIsSelected && !bIsSelected) return -1
if (!aIsSelected && bIsSelected) return 1
// 如果都是或都不是选中的agent按选中的维度平均分降序排列
const selectedDimArray = Array.from(selectedDimensions.value)
const aAverage = calculateAgentAverage(a, selectedDimArray)
const bAverage = calculateAgentAverage(b, selectedDimArray)
return bAverage - aAverage
})
}
}
// 判断agent是否被选中
const isAgentSelected = (agentName: string) => {
return selectedAgents.value.has(agentName)
} }
// 判断agent组合是否被选中用于Assignment卡片的选中效果 // 判断agent组合是否被选中用于Assignment卡片的选中效果
@@ -493,33 +343,46 @@ const calculateAgentAverage = (agentData: AgentHeatmapData, selectedDimensions?:
return count > 0 ? total / count : 0 return count > 0 ? total / count : 0
} }
// 模拟API调用 - 获取智能体评分数据 // API调用 - 获取智能体评分数据
const fetchAgentScores = async () => { const fetchAgentScores = async () => {
// 先检查 store 中是否已有数据 const taskId = currentTask.value?.Id
const storedData = agentsStore.IAgentSelectModifyAddRequest if (!taskId) {
console.warn('⚠️ fetchAgentScores: 当前任务没有 Id')
return null
}
//先检查 store 中是否有该任务的评分数据
const storedData = agentsStore.getTaskScoreData(taskId)
if (storedData && storedData.aspectList && storedData.aspectList.length > 0) { if (storedData && storedData.aspectList && storedData.aspectList.length > 0) {
console.log('使用 store 中的评分数据') return storedData
return { }
aspectList: storedData.aspectList,
agentScores: storedData.agentScores // 如果正在预加载中,等待预加载完成
if (agentsStore.isTaskPreloading(taskId)) {
// 等待一小段时间,让预加载完成
await new Promise(resolve => setTimeout(resolve, 1000))
// 再次尝试获取
const cachedAfterPreload = agentsStore.getTaskScoreData(taskId)
if (cachedAfterPreload) {
return cachedAfterPreload
} }
} }
// TODO: 切换到真实API时取消注释下面这行 // 检查是否已停止
if (agentsStore.isStopping || agentsStore.hasStoppedFilling) {
console.log('检测到停止信号,跳过获取智能体评分')
return null
}
// 调用 API 获取评分(如果没有缓存或预加载)
const agentScores = await api.agentSelectModifyInit({ const agentScores = await api.agentSelectModifyInit({
goal: agentsStore.agentRawPlan.data?.['General Goal'] || '', goal: agentsStore.agentRawPlan.data?.['General Goal'] || '',
stepTask: currentTask.value stepTask: currentTask.value
}) })
// 🆕 使用 Mock API开发阶段传递当前任务的 AgentSelection 以获取对应的维度 // 再次检查是否已停止API 调用后)
const goal = agentsStore.agentRawPlan.data?.['General Goal'] || '开发智能协作系统' if (agentsStore.isStopping || agentsStore.hasStoppedFilling) {
console.log('📤 调用 mockAgentSelectModifyInit参数:', { return null
goal, }
stepTask: currentTask.value,
agentSelection: currentTask.value?.AgentSelection
})
// const agentScores = await api.mockAgentSelectModifyInit()
// 从转换后的数据中提取维度列表第一个agent的所有维度 // 从转换后的数据中提取维度列表第一个agent的所有维度
const firstAgent = Object.keys(agentScores)[0] const firstAgent = Object.keys(agentScores)[0]
@@ -527,8 +390,8 @@ const fetchAgentScores = async () => {
console.log('✅ 获取到的维度列表:', aspectList) console.log('✅ 获取到的维度列表:', aspectList)
// 保存到 store // 🆕 保存到 store按任务ID存储
agentsStore.setAgentScoreData({ agentsStore.setTaskScoreData(taskId, {
aspectList, aspectList,
agentScores agentScores
}) })
@@ -543,9 +406,7 @@ const fetchAgentScores = async () => {
const initHeatmapData = async () => { const initHeatmapData = async () => {
const agents = allAgents.value const agents = allAgents.value
if (agents.length === 0 || isInitializing.value || isAddingDimension.value) return if (agents.length === 0 || isInitializing.value || isAddingDimension.value) return
isInitializing.value = true isInitializing.value = true
// 先创建一个只包含agent基本信息的临时数据让agent头像先显示 // 先创建一个只包含agent基本信息的临时数据让agent头像先显示
const tempData = agents.map(agent => ({ const tempData = agents.map(agent => ({
agentName: agent.Name, agentName: agent.Name,
@@ -557,16 +418,12 @@ const initHeatmapData = async () => {
tempData.sort((a, b) => { tempData.sort((a, b) => {
const aIsAssigned = currentTaskAgents.value.includes(a.agentName) const aIsAssigned = currentTaskAgents.value.includes(a.agentName)
const bIsAssigned = currentTaskAgents.value.includes(b.agentName) const bIsAssigned = currentTaskAgents.value.includes(b.agentName)
if (aIsAssigned && !bIsAssigned) return -1 if (aIsAssigned && !bIsAssigned) return -1
if (!aIsAssigned && bIsAssigned) return 1 if (!aIsAssigned && bIsAssigned) return 1
return a.agentName.localeCompare(b.agentName, 'zh-CN') return a.agentName.localeCompare(b.agentName, 'zh-CN')
}) })
agentsHeatmapData.value = tempData agentsHeatmapData.value = tempData
//默认选中Assignment部分的agent即currentTaskAgents
// 初始化选中状态默认选中Assignment部分的agent即currentTaskAgents
if (currentTaskAgents.value.length > 0) { if (currentTaskAgents.value.length > 0) {
selectedAgents.value = new Set(currentTaskAgents.value) selectedAgents.value = new Set(currentTaskAgents.value)
selectedAssignmentGroup.value = [...currentTaskAgents.value] selectedAssignmentGroup.value = [...currentTaskAgents.value]
@@ -579,32 +436,33 @@ const initHeatmapData = async () => {
try { try {
// 获取接口数据 // 获取接口数据
const { aspectList, agentScores } = await fetchAgentScores() const scoreData = await fetchAgentScores()
if (!scoreData) {
console.warn('⚠️ initHeatmapData: 没有获取到评分数据')
return
}
const { aspectList, agentScores } = scoreData
// 设置评分维度
scoreDimensions.value = aspectList scoreDimensions.value = aspectList
// 转换数据结构为每个agent生成评分数据
const heatmapData = agents.map(agent => { const heatmapData = agents.map(agent => {
const agentName = agent.Name const agentName = agent.Name
const scores: number[] = [] const scores: number[] = []
const scoreDetails: Array<{ dimension: string; score: number; reason: string }> = [] const scoreDetails: Array<{ dimension: string; score: number; reason: string }> = []
// 按照维度顺序获取评分 // 按照维度顺序获取评分
aspectList.forEach(dimension => { aspectList.forEach((dimension: string) => {
// 数据结构agentScores[agentName][dimension]
const agentScoreData = agentScores[agentName] const agentScoreData = agentScores[agentName]
const scoreData = agentScoreData ? agentScoreData[dimension] : null const scoreItem = agentScoreData ? agentScoreData[dimension] : null
if (scoreData) { if (scoreItem) {
scores.push(scoreData.score) scores.push(scoreItem.score)
scoreDetails.push({ scoreDetails.push({
dimension, dimension,
score: scoreData.score, score: scoreItem.score,
reason: scoreData.reason reason: scoreItem.reason
}) })
} else { } else {
// 如果没有评分数据默认为1
scores.push(1) scores.push(1)
} }
}) })
@@ -615,9 +473,8 @@ const initHeatmapData = async () => {
scoreDetails scoreDetails
} }
}) })
// 排序逻辑: // 排序逻辑:
// 1. Assignment部分的agentcurrentTaskAgents排在前面 // 1. Assignment部分的agent排在前面
// 2. 其他agent按照平均分降序排列 // 2. 其他agent按照平均分降序排列
// 3. 同一组内也按平均分降序排列 // 3. 同一组内也按平均分降序排列
heatmapData.sort((a, b) => { heatmapData.sort((a, b) => {
@@ -639,27 +496,16 @@ const initHeatmapData = async () => {
agentsHeatmapData.value = heatmapData agentsHeatmapData.value = heatmapData
} catch (error) { } catch (error) {
console.error('获取智能体评分数据失败:', error) console.error('获取智能体评分数据失败:', error)
// 降级处理:使用默认维度列表和随机数据
const defaultAspects = ['能力', '可用性', '专业性', '效率', '准确性', '协作性']
scoreDimensions.value = defaultAspects
agentsHeatmapData.value = tempData.map(item => ({
...item,
scores: Array.from(
{ length: defaultAspects.length },
() => Math.floor(Math.random() * 5) + 1
),
scoreDetails: []
}))
} finally { } finally {
isInitializing.value = false isInitializing.value = false
} }
} }
// 监听agent列表变化初始化热力矩阵 //监听agent列表变化或任务切换,初始化热力矩阵
watch( watch(
() => allAgents.value.length, [() => allAgents.value.length, () => currentTask.value?.Id],
newLength => { ([agentLength, taskId]) => {
if (newLength > 0) { if (agentLength > 0 && taskId) {
initHeatmapData() initHeatmapData()
} }
}, },
@@ -704,9 +550,6 @@ watch(
) )
// 初始化和任务切换处理 // 初始化和任务切换处理
// 1. 首次加载时,自动将初始 agent 组合添加到 confirmedAgentGroups
// 2. 恢复之前的选择状态
// 3. 加载 TaskProcess 数据
watch( watch(
() => currentTask.value, () => currentTask.value,
async newTask => { async newTask => {
@@ -718,75 +561,37 @@ watch(
const existingGroups = agentsStore.getConfirmedAgentGroups(newTask.Id) const existingGroups = agentsStore.getConfirmedAgentGroups(newTask.Id)
const hasInitialGroup = existingGroups.length > 0 const hasInitialGroup = existingGroups.length > 0
// 首次初始化:自动添加初始组合(相当于系统自动执行了 confirmAgentSelection // 首次初始化:自动添加初始组合 confirmedAgentGroups
if (!hasInitialGroup) { if (!hasInitialGroup) {
console.log('🎯 首次初始化,自动添加初始 agent 组合到 Assignment')
agentsStore.addConfirmedAgentGroup(newTask.Id, [...newTask.AgentSelection]) agentsStore.addConfirmedAgentGroup(newTask.Id, [...newTask.AgentSelection])
// 调用 API 获取初始 agent 组合的 TaskProcess 数据 // 初始化时 TaskProcess 一定存在,直接存储
console.log('🔄 开始加载初始 agent 组合的 TaskProcess...') selectionStore.setAgentTaskProcess(newTask.Id, newTask.AgentSelection, {
name: newTask.StepName || '',
// 检查是否已有数据 content: newTask.TaskContent || '',
if (!selectionStore.hasAgentTaskProcess(newTask.Id, newTask.AgentSelection)) { inputs: newTask.InputObject_List || [],
try { output: newTask.OutputObject || '',
isLoadingInitialTask.value = true agents: newTask.AgentSelection,
console.log('=== 开始加载初始 agent 组合的 TaskProcess ===') brief: newTask.Collaboration_Brief_frontEnd || { template: '', data: {} },
console.log('1. 任务ID:', newTask.Id) process: newTask.TaskProcess.map(action => ({
console.log('2. 初始 agents:', newTask.AgentSelection) id: action.ID,
type: action.ActionType,
// 将 IRawStepTask 转换为 IApiStepTask 格式 agent: action.AgentName,
const stepTaskForApi = { description: action.Description,
name: newTask.StepName || '', inputs: action.ImportantInput
content: newTask.TaskContent || '', }))
inputs: newTask.InputObject_List || [], })
output: newTask.OutputObject || '',
agents: newTask.AgentSelection,
brief: newTask.Collaboration_Brief_frontEnd || {
template: '',
data: {}
},
process: []
}
console.log('3. 转换后的 API 请求参数:', stepTaskForApi)
const goal = agentsStore.agentRawPlan.data?.['General Goal'] || '开发智能协作系统'
console.log('4. General Goal:', goal)
// 调用 Mock API 获取 TaskProcess
const filledTask = await api.fillStepTaskTaskProcess({
goal,
stepTask: stepTaskForApi,
agents: newTask.AgentSelection
})
console.log('=== 初始 agent 组合 TaskProcess 加载成功 ===')
console.log('5. TaskProcess 流程数量:', filledTask.process?.length || 0)
// 存储到 selectionStore
selectionStore.setAgentTaskProcess(newTask.Id, newTask.AgentSelection, filledTask)
console.log('✅ 初始 agent 组合的 TaskProcess 已加载并存储')
} catch (error) {
console.error('❌ 加载初始 agent 组合的 TaskProcess 失败:', error)
} finally {
isLoadingInitialTask.value = false
}
} else {
console.log(' 初始 agent 组合已有 TaskProcess 数据,跳过加载')
}
} }
// 从 store 中恢复之前选择的 agent 组合(如果有) // 从 store 中恢复之前选择的 agent 组合
const savedAgentGroup = agentsStore.getSelectedAgentGroup(newTask.Id) const savedAgentGroup = agentsStore.getSelectedAgentGroup(newTask.Id)
if (savedAgentGroup) { if (savedAgentGroup) {
console.log('📂 恢复之前选择的 agent 组合:', savedAgentGroup)
selectedAssignmentGroup.value = [...savedAgentGroup] selectedAssignmentGroup.value = [...savedAgentGroup]
selectedAgents.value = new Set(savedAgentGroup) selectedAgents.value = new Set(savedAgentGroup)
} else { } else {
// 没有保存的选择,默认选中第一个组合(即初始组合 // 没有保存的选择,默认选中初始组合
const allGroups = agentsStore.getConfirmedAgentGroups(newTask.Id) const allGroups = agentsStore.getConfirmedAgentGroups(newTask.Id)
if (allGroups.length > 0 && allGroups[0]) { if (allGroups.length > 0 && allGroups[0]) {
console.log('🔄 默认选中第一个组合(初始组合):', allGroups[0])
selectedAssignmentGroup.value = [...allGroups[0]] selectedAssignmentGroup.value = [...allGroups[0]]
selectedAgents.value = new Set(allGroups[0]) selectedAgents.value = new Set(allGroups[0])
} }
@@ -794,7 +599,6 @@ watch(
}, },
{ immediate: true } // 立即执行一次 { immediate: true } // 立即执行一次
) )
// 获取热力图颜色评分1-5颜色从浅到深 // 获取热力图颜色评分1-5颜色从浅到深
const getHeatmapColor = (score: number) => { const getHeatmapColor = (score: number) => {
const colors = [ const colors = [
@@ -861,7 +665,7 @@ const getHeatmapColor = (score: number) => {
<div <div
v-if="allAgents.length > 0" v-if="allAgents.length > 0"
class="comparison-content" class="comparison-content"
v-loading="isInitializing || isLoadingInitialTask || isLoadingConfirm" v-loading="isInitializing || isLoadingConfirm"
> >
<!-- 热力矩阵图 --> <!-- 热力矩阵图 -->
<div v-if="agentsHeatmapData.length > 0" class="heatmap-container"> <div v-if="agentsHeatmapData.length > 0" class="heatmap-container">

View File

@@ -21,7 +21,7 @@ const agentGroupCount = computed(() => {
// 获取该任务的已确认agent组合 // 获取该任务的已确认agent组合
const confirmedGroups = agentsStore.getConfirmedAgentGroups(agentsStore.currentTask.Id) const confirmedGroups = agentsStore.getConfirmedAgentGroups(agentsStore.currentTask.Id)
// 当前任务agents(1) + 已确认的agent组合数量 // 当前任务agents数量 + 已确认的agent组合数量
return confirmedGroups.length || 1 return confirmedGroups.length || 1
}) })

View File

@@ -10,22 +10,19 @@ const emit = defineEmits<{
// 获取分支数量 // 获取分支数量
const branchCount = computed(() => { const branchCount = computed(() => {
// flowBranches 包含所有通过 Branch 创建的分支
const extraBranches = selectionStore.flowBranches?.length || 1 const extraBranches = selectionStore.flowBranches?.length || 1
return extraBranches return extraBranches
}) })
const handleClick = () => { const handleClick = () => {
emit('click') emit('click')
// 触发打开分支窗口
agentsStore.openPlanModification() agentsStore.openPlanModification()
} }
</script> </script>
<template> <template>
<div <div
class="branch-button" class="branch-button"
:class="{ 'has-branches': branchCount > 0 }" :class="{ 'has-branches': branchCount > 1 }"
@click="handleClick" @click="handleClick"
:title="`${branchCount} 个分支`" :title="`${branchCount} 个分支`"
> >

View File

@@ -39,7 +39,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue' import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { debounce } from 'lodash-es'
export interface IFloatingWindowProps { export interface IFloatingWindowProps {
title?: string | any title?: string | any
onClose?: () => void onClose?: () => void
@@ -262,24 +261,20 @@ const handleResizeMove = (e: MouseEvent) => {
emit('update:position', position.value) emit('update:position', position.value)
} }
const handleResizeEnd = debounce(() => { const handleResizeEnd = () => {
isResizing.value = false isResizing.value = false
resizeDirection.value = null resizeDirection.value = null
document.removeEventListener('mousemove', handleResizeMove) document.removeEventListener('mousemove', handleResizeMove)
document.removeEventListener('mouseup', handleResizeEnd) document.removeEventListener('mouseup', handleResizeEnd)
props.onResize?.() props.onResize?.()
}, 50) }
const setResizeable = (value: boolean) => { const setResizeable = (value: boolean) => {
resizeable.value = value resizeable.value = value
} }
const handleClose = () => { const handleClose = () => props.onClose?.()
if (props.onClose) {
props.onClose()
}
}
// 初始化 // 初始化
onMounted(() => { onMounted(() => {

View File

@@ -3,12 +3,15 @@ import SvgIcon from '@/components/SvgIcon/index.vue'
import { getAgentMapIcon } from '@/layout/components/config.ts' import { 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 { type IRawStepTask, useAgentsStore } from '@/stores' import { type IRawStepTask, useAgentsStore } from '@/stores'
import { computed, ref, nextTick, watch, onMounted } from 'vue' import { computed, nextTick, watch, onMounted } from 'vue'
import { AnchorLocations } from '@jsplumb/browser-ui' import { AnchorLocations } from '@jsplumb/browser-ui'
import { Loading } from '@element-plus/icons-vue' import { Loading } from '@element-plus/icons-vue'
import MultiLineTooltip from '@/components/MultiLineTooltip/index.vue' import MultiLineTooltip from '@/components/MultiLineTooltip/index.vue'
import Bg from './Bg.vue' import Bg from './Bg.vue'
import BranchButton from './components/BranchButton.vue' import BranchButton from './components/BranchButton.vue'
import Notification from '@/components/Notification/Notification.vue'
import { useNotification } from '@/composables/useNotification'
import TaskContentEditor from '@/components/TaskContentEditor/index.vue'
// 判断计划是否就绪 // 判断计划是否就绪
const planReady = computed(() => { const planReady = computed(() => {
@@ -22,8 +25,6 @@ const openPlanModification = () => {
const emit = defineEmits<{ const emit = defineEmits<{
(el: 'resetAgentRepoLine'): void (el: 'resetAgentRepoLine'): void
(el: 'setCurrentTask', task: IRawStepTask): void (el: 'setCurrentTask', task: IRawStepTask): void
(el: 'add-output', outputName: string): void
(el: 'click-branch'): void
}>() }>()
const jsplumb = new Jsplumb('task-syllabus') const jsplumb = new Jsplumb('task-syllabus')
@@ -38,7 +39,7 @@ const collaborationProcess = computed(() => {
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? [] return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
}) })
// 检测是否正在填充详情(有步骤但没有 AgentSelection // 检测是否正在填充详情
const isFillingDetails = computed(() => { const isFillingDetails = computed(() => {
const process = agentsStore.agentRawPlan.data?.['Collaboration Process'] || [] const process = agentsStore.agentRawPlan.data?.['Collaboration Process'] || []
return ( return (
@@ -57,119 +58,66 @@ const totalSteps = computed(() => {
return agentsStore.agentRawPlan.data?.['Collaboration Process']?.length || 0 return agentsStore.agentRawPlan.data?.['Collaboration Process']?.length || 0
}) })
// 编辑状态管理 // Notification 通知
const editingTaskId = ref<string | null>(null) const {
const editingContent = ref('') notifications,
progress: showProgress,
updateProgressDetail,
removeNotification
} = useNotification()
const fillingProgressNotificationId = ref<string | null>(null)
// 添加新产物状态管理 // 监听填充进度,显示通知
const isAddingOutput = ref(false) watch(
const newOutputInputRef = ref<HTMLElement>() [isFillingDetails, completedSteps, totalSteps, () => agentsStore.hasStoppedFilling],
const newOutputName = ref('') ([filling, completed, total, hasStopped]) => {
// 如果用户已停止,关闭进度通知
if (hasStopped && fillingProgressNotificationId.value) {
removeNotification(fillingProgressNotificationId.value)
fillingProgressNotificationId.value = null
return
}
// 处理加号点击 if (filling && total > 0) {
const handleAddOutputClick = () => { if (!fillingProgressNotificationId.value) {
isAddingOutput.value = true // 创建进度通知
newOutputName.value = '' fillingProgressNotificationId.value = showProgress('生成协作流程', completed, total)
updateProgressDetail(
nextTick(() => { fillingProgressNotificationId.value,
setTimeout(() => { `${completed}/${total}`,
if (newOutputInputRef.value) { '正在分配智能体...',
newOutputInputRef.value?.focus() completed,
total
)
} else {
// 更新进度通知
updateProgressDetail(
fillingProgressNotificationId.value,
`${completed}/${total}`,
'正在分配智能体...',
completed,
total
)
} }
jsplumb.instance.repaintEverything() } else if (fillingProgressNotificationId.value && !filling) {
}, 50) // 填充完成,关闭进度通知
}) removeNotification(fillingProgressNotificationId.value)
} fillingProgressNotificationId.value = null
// 保存新产物
const saveNewOutput = () => {
if (newOutputName.value.trim()) {
const outputName = newOutputName.value.trim()
const success = agentsStore.addNewOutput(outputName)
if (success) {
emit('add-output', outputName)
isAddingOutput.value = false
newOutputName.value = ''
nextTick(() => {
setTimeout(() => {
jsplumb.instance.repaintEverything()
}, 50)
})
console.log('添加新产物成功', outputName)
} else {
// 退出编辑状态
isAddingOutput.value = false
newOutputName.value = ''
} }
} },
} { immediate: true }
)
// 取消添加产物 // 保存编辑内容
const cancelAddOutput = () => { const handleContentSave = (taskId: string, content: string) => {
isAddingOutput.value = false const taskToUpdate = collaborationProcess.value.find(item => item.Id === taskId)
newOutputName.value = '' if (taskToUpdate && content !== taskToUpdate.TaskContent) {
taskToUpdate.TaskContent = content
nextTick(() => { // 记录修改过的步骤索引到store用于重新执行
setTimeout(() => { const stepIndex = collaborationProcess.value.findIndex(item => item.Id === taskId)
jsplumb.instance.repaintEverything() if (stepIndex >= 0) {
}, 50) agentsStore.addModifiedStep(stepIndex)
})
}
// 处理新产物的键盘事件
const handleNewOutputKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault()
saveNewOutput()
} else if (event.key === 'Escape') {
cancelAddOutput()
}
}
// 新产物输入框失去焦点处理
const handleNewOutputBlur = () => {
setTimeout(() => {
if (newOutputName.value.trim() === '') {
cancelAddOutput()
} }
}, 100)
}
// 开始编辑
const startEditing = (task: IRawStepTask) => {
if (!task.Id) {
console.warn('Task ID is missing, cannot start editing')
return
}
editingTaskId.value = task.Id
editingContent.value = task.TaskContent || ''
}
// 保存编辑
const saveEditing = () => {
if (editingTaskId.value && editingContent.value.trim()) {
const taskToUpdate = collaborationProcess.value.find(item => item.Id === editingTaskId.value)
if (taskToUpdate) {
taskToUpdate.TaskContent = editingContent.value.trim()
}
}
editingTaskId.value = null
editingContent.value = ''
}
// 取消编辑
const cancelEditing = () => {
editingTaskId.value = null
editingContent.value = ''
}
// 处理键盘事件
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault()
saveEditing()
} else if (event.key === 'Escape') {
cancelEditing()
} }
} }
@@ -223,21 +171,14 @@ function clear() {
// 封装连线重绘方法 // 封装连线重绘方法
const redrawConnections = () => { const redrawConnections = () => {
// 等待 DOM 更新完成
nextTick(() => { nextTick(() => {
// 清除旧连线
jsplumb.reset() jsplumb.reset()
// 等待 DOM 稳定后重新绘制
setTimeout(() => { setTimeout(() => {
const arr: ConnectArg[] = [] const arr: ConnectArg[] = []
const currentTaskId = agentsStore.currentTask?.Id const currentTaskId = agentsStore.currentTask?.Id
// 重新绘制所有连线
collaborationProcess.value.forEach(item => { collaborationProcess.value.forEach(item => {
arr.push(...handleCurrentTask(item, item.Id !== currentTaskId)) arr.push(...handleCurrentTask(item, item.Id !== currentTaskId))
}) })
jsplumb.connects(arr) jsplumb.connects(arr)
}, 100) }, 100)
}) })
@@ -254,7 +195,6 @@ watch(
// 组件挂载后初始化连线 // 组件挂载后初始化连线
onMounted(() => { onMounted(() => {
// 初始化时绘制连线
nextTick(() => { nextTick(() => {
setTimeout(() => { setTimeout(() => {
const arr: ConnectArg[] = [] const arr: ConnectArg[] = []
@@ -274,17 +214,13 @@ defineExpose({
<template> <template>
<div class="h-full flex flex-col"> <div class="h-full flex flex-col">
<!-- Notification 通知 -->
<Notification :notifications="notifications" @close="id => removeNotification(id)" />
<div class="text-[18px] font-bold mb-[18px] text-[var(--color-text-title-header)]"> <div class="text-[18px] font-bold mb-[18px] text-[var(--color-text-title-header)]">
任务大纲 任务大纲
</div> </div>
<!-- 加载详情提示 -->
<div v-if="isFillingDetails" class="detail-loading-hint">
<el-icon class="is-loading"><Loading /></el-icon>
<span>正在生成任务协作流程...</span>
<span class="progress">{{ completedSteps }}/{{ totalSteps }}</span>
</div>
<div <div
v-loading="agentsStore.agentRawPlan.loading" v-loading="agentsStore.agentRawPlan.loading"
class="flex-1 w-full overflow-y-auto relative" class="flex-1 w-full overflow-y-auto relative"
@@ -295,8 +231,7 @@ defineExpose({
class="w-full relative min-h-full" class="w-full relative min-h-full"
id="task-syllabus" id="task-syllabus"
> >
<Bg :is-adding="isAddingOutput" @start-add-output="handleAddOutputClick" /> <Bg />
<div class="w-full flex items-center gap-[14%] mb-[35px]"> <div class="w-full flex items-center gap-[14%] mb-[35px]">
<div class="flex-1 flex justify-center"> <div class="flex-1 flex justify-center">
<div <div
@@ -314,47 +249,6 @@ defineExpose({
</div> </div>
</div> </div>
</div> </div>
<!-- 添加新产物卡片 -->
<div
v-if="isAddingOutput"
class="card-it w-full flex items-center gap-[14%] bg-[var(--color-card-bg)] add-output-form mb-[100px]"
>
<!-- 左侧空白的流程卡片占位 -->
<div class="w-[43%] relative z-99" style="height: 20px"></div>
<!-- 右侧可编辑的产物卡片 -->
<el-card
class="w-[43%] relative task-syllabus-output-object-card border-dashed border-2 border-[var(--color-primary)]"
>
<div class="h-full flex items-center justify-center">
<!-- 输入框 -->
<el-input
ref="newOutputInputRef"
v-model="newOutputName"
placeholder="Enter保存ESC取消"
@keydown="handleNewOutputKeydown"
@blur="handleNewOutputBlur"
size="large"
class="w-full"
/>
</div>
</el-card>
</div>
<!-- 显示临时产物卡片 -->
<div
v-for="output in agentsStore.additionalOutputs"
:key="output"
class="card-it w-full flex items-center gap-[14%] bg-[var(--color-card-bg)] mb-[100px]"
>
<!-- 左侧空白的流程卡片占位 -->
<div class="w-[43%] relative z-99" style="height: 100px"></div>
<!-- 右侧产物卡片 -->
<el-card class="w-[43%] relative task-syllabus-output-object-card" :shadow="true">
<div class="text-[18px] font-bold text-center">{{ output }}</div>
</el-card>
</div>
<div <div
v-for="item in collaborationProcess" v-for="item in collaborationProcess"
:key="item.Id" :key="item.Id"
@@ -374,49 +268,25 @@ 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 v-if="editingTaskId === item.Id" class="w-full"> <TaskContentEditor :task="item" @save="handleContentSave">
<div class="flex flex-col gap-3"> <template #display>
<el-input <MultiLineTooltip placement="right" :text="item.TaskContent" :lines="3">
v-model="editingContent" <div class="text-[14px] text-[var(--color-text-secondary)] task-content-display">
type="textarea" {{ item.TaskContent }}
:autosize="{ minRows: 2, maxRows: 4 }" </div>
placeholder="请输入任务内容" </MultiLineTooltip>
@keydown="handleKeydown" </template>
class="task-content-editor" </TaskContentEditor>
size="small"
/>
<div class="flex justify-end">
<svg-icon
icon-class="Check"
size="20px"
color="#328621"
class="cursor-pointer mr-4"
@click="saveEditing"
title="保存"
/>
<svg-icon
icon-class="Cancel"
size="20px"
color="#8e0707"
class="cursor-pointer mr-1"
@click="cancelEditing"
title="取消"
/>
</div>
</div>
</div>
<div v-else @dblclick="startEditing(item)" class="w-full cursor-pointer">
<MultiLineTooltip placement="right" :text="item.TaskContent" :lines="3">
<div class="text-[14px] text-[var(--color-text-secondary)] task-content-display">
{{ item.TaskContent }}
</div>
</MultiLineTooltip>
</div>
<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 flex-wrap relative w-full" 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]'" :class="
!item.AgentSelection || item.AgentSelection.length === 0
? 'min-h-[40px]'
: 'overflow-y-auto max-h-[72px]'
"
> >
<!-- 连接到智能体库的连接点 --> <!-- 连接到智能体库的连接点 -->
<div <div
@@ -426,13 +296,16 @@ defineExpose({
<!-- 未填充智能体时显示Loading --> <!-- 未填充智能体时显示Loading -->
<div <div
v-if="!item.AgentSelection || item.AgentSelection.length === 0" v-if="
(!item.AgentSelection || item.AgentSelection.length === 0) &&
!agentsStore.hasStoppedFilling
"
class="flex items-center gap-2 text-[var(--color-text-secondary)] text-[14px]" class="flex items-center gap-2 text-[var(--color-text-secondary)] text-[14px]"
> >
<el-icon class="is-loading" :size="20"> <el-icon class="is-loading" :size="20">
<Loading /> <Loading />
</el-icon> </el-icon>
<span>正在分配智能体...</span> <span>正在分配智能体<span class="loading-dots"></span></span>
</div> </div>
<!-- 已填充智能体时显示智能体列表 --> <!-- 已填充智能体时显示智能体列表 -->
@@ -541,100 +414,6 @@ defineExpose({
white-space: pre-wrap; white-space: pre-wrap;
} }
.add-output-btn {
opacity: 0.8;
transition: opacity 0.2s ease;
&:hover {
opacity: 1;
}
button {
background: transparent;
cursor: pointer;
&:hover {
background-color: rgba(59, 130, 246, 0.05);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
&:active {
transform: translateY(0);
}
}
}
.add-output-form {
animation: slideDown 0.3s ease-out;
:deep(.el-card__body) {
padding: 16px;
display: flex;
align-items: center;
justify-content: center;
}
:deep(.el-input__wrapper) {
border: 1px solid var(--color-text);
background: transparent;
box-shadow: none;
&.is-focus {
border-color: var(--color-text);
box-shadow: 0 0 0 1px var(--color-primary-light);
}
:deep(.el-input__inner) {
font-size: 14px;
text-align: center;
font-weight: bold;
}
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
// 加载详情提示样式
.detail-loading-hint {
position: fixed;
top: 80px;
right: 20px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color);
border-radius: 8px;
padding: 12px 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 8px;
z-index: 1000;
animation: slideInRight 0.3s ease-out;
.progress {
color: var(--el-color-primary);
font-weight: bold;
margin-left: 4px;
}
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
// 输入框样式 // 输入框样式
:deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
background: transparent; background: transparent;
@@ -698,4 +477,33 @@ defineExpose({
} }
} }
} }
// 加载动画省略号
.loading-dots {
display: inline-block;
width: 1.2em;
text-align: left;
&::after {
content: '';
animation: dots 1.5s steps(4, end) infinite;
}
}
@keyframes dots {
0%,
20% {
content: '';
}
40% {
content: '.';
}
60% {
content: '..';
}
80%,
100% {
content: '...';
}
}
</style> </style>

View File

@@ -47,10 +47,10 @@ function handleTaskSyllabusCurrentTask(task: IRawStepTask) {
agentsStore.setCurrentTask(task) agentsStore.setCurrentTask(task)
} }
// 更新任务大纲内部的线
function handleTaskResultCurrentTask(task: IRawStepTask) { function handleTaskResultCurrentTask(task: IRawStepTask) {
scrollToElementTop(`task-syllabus-flow-${task.Id}`) scrollToElementTop(`task-syllabus-flow-${task.Id}`)
agentsStore.setCurrentTask(task) agentsStore.setCurrentTask(task)
// 更新任务大纲内部的线
taskSyllabusRef.value?.changeTask(task, false) taskSyllabusRef.value?.changeTask(task, false)
} }
@@ -71,11 +71,6 @@ function clear() {
taskResultJsplumb.repaintEverything() taskResultJsplumb.repaintEverything()
} }
const additionalOutputs = ref<string[]>([])
const handleAddOutput = (outputName: string) => {
additionalOutputs.value.unshift(outputName)
}
defineExpose({ defineExpose({
changeTask, changeTask,
resetAgentRepoLine, resetAgentRepoLine,
@@ -99,7 +94,6 @@ defineExpose({
ref="taskSyllabusRef" ref="taskSyllabusRef"
@resetAgentRepoLine="resetAgentRepoLine" @resetAgentRepoLine="resetAgentRepoLine"
@set-current-task="handleTaskSyllabusCurrentTask" @set-current-task="handleTaskSyllabusCurrentTask"
@add-output="handleAddOutput"
/> />
</div> </div>
<!-- 执行结果 --> <!-- 执行结果 -->
@@ -108,7 +102,6 @@ defineExpose({
ref="taskResultRef" ref="taskResultRef"
@refresh-line="taskResultJsplumb.repaintEverything" @refresh-line="taskResultJsplumb.repaintEverything"
@set-current-task="handleTaskResultCurrentTask" @set-current-task="handleTaskResultCurrentTask"
:additional-outputs="additionalOutputs"
/> />
</div> </div>
</div> </div>

View File

@@ -73,6 +73,13 @@ export interface IScoreItem {
reason: string reason: string
} }
// 单个任务的评分数据结构
export interface ITaskScoreData {
aspectList: string[] // 维度列表
agentScores: Record<string, Record<string, IScoreItem>> // agent -> 维度 -> 评分
timestamp: number // 缓存时间戳
}
export interface IAgentSelectModifyAddRequest { export interface IAgentSelectModifyAddRequest {
aspectList: string[] // 维度列表 aspectList: string[] // 维度列表
agentScores: Record<string, Record<string, IScoreItem>> // agent -> 维度 -> 评分(与后端返回格式一致) agentScores: Record<string, Record<string, IScoreItem>> // agent -> 维度 -> 评分(与后端返回格式一致)
@@ -89,6 +96,23 @@ export interface IRawPlanResponse {
'Collaboration Process'?: IRawStepTask[] 'Collaboration Process'?: IRawStepTask[]
} }
/**
* IApiAgentAction[] → TaskProcess[] 格式转换
* @param actions IApiAgentAction 数组
* @returns TaskProcess 数组
*/
export function convertToTaskProcess(
actions: { id: string; type: string; agent: string; description: string; inputs?: string[] }[]
): TaskProcess[] {
return actions.map(action => ({
ID: action.id,
ActionType: action.type,
AgentName: action.agent,
Description: action.description,
ImportantInput: action.inputs || []
}))
}
const storageKey = '$agents' as const const storageKey = '$agents' as const
// 清除所有以 storageKey 开头的 localStorage // 清除所有以 storageKey 开头的 localStorage
@@ -105,13 +129,104 @@ export const useAgentsStore = defineStore('agents', () => {
agents.value = agent agents.value = agent
} }
// 智能体评分数据存储 // 🆕 新的按任务ID存储的评分数据
const taskScoreDataMap = useStorage<Record<string, ITaskScoreData>>(
`${storageKey}-task-score-data`,
{},
)
// 🆕 预加载状态追踪(用于避免重复预加载)
const preloadingTaskIds = ref<Set<string>>(new Set())
// 🆕 获取指定任务的评分数据按任务ID获取
function getTaskScoreData(taskId: string): IAgentSelectModifyAddRequest | null {
if (!taskId) {
console.warn('⚠️ getTaskScoreData: taskId 为空')
return null
}
const taskScoreData = taskScoreDataMap.value[taskId]
if (taskScoreData) {
console.log(`✅ 使用任务 ${taskId} 的缓存评分数据,维度数: ${taskScoreData.aspectList.length}`)
return {
aspectList: taskScoreData.aspectList,
agentScores: taskScoreData.agentScores,
}
}
console.log(` 任务 ${taskId} 没有缓存的评分数据`)
return null
}
// 🆕 设置指定任务的评分数据
function setTaskScoreData(taskId: string, data: IAgentSelectModifyAddRequest) {
if (!taskId) {
console.warn('⚠️ setTaskScoreData: taskId 为空,无法存储')
return
}
taskScoreDataMap.value = {
...taskScoreDataMap.value,
[taskId]: {
...data,
timestamp: Date.now(),
},
}
console.log(`✅ 任务 ${taskId} 的评分数据已存储`, {
aspectCount: data.aspectList.length,
agentCount: Object.keys(data.agentScores).length,
})
}
// 🆕 检查指定任务是否有评分数据
function hasTaskScoreData(taskId: string): boolean {
if (!taskId) return false
return !!taskScoreDataMap.value[taskId]?.aspectList?.length
}
// 🆕 获取所有已预加载的任务ID列表
function getPreloadedTaskIds(): string[] {
return Object.keys(taskScoreDataMap.value)
}
// 🆕 清除指定任务的评分数据
function clearTaskScoreData(taskId: string) {
if (taskId && taskScoreDataMap.value[taskId]) {
const newMap = { ...taskScoreDataMap.value }
delete newMap[taskId]
taskScoreDataMap.value = newMap
console.log(`✅ 任务 ${taskId} 的评分数据已清除`)
}
}
// 🆕 清除所有任务的评分数据
function clearAllTaskScoreData() {
taskScoreDataMap.value = {}
preloadingTaskIds.value.clear()
console.log('✅ 所有任务的评分数据已清除')
}
// 🆕 标记任务正在预加载中
function markTaskPreloading(taskId: string) {
preloadingTaskIds.value.add(taskId)
}
// 🆕 标记任务预加载完成
function markTaskPreloadComplete(taskId: string) {
preloadingTaskIds.value.delete(taskId)
}
// 🆕 检查任务是否正在预加载
function isTaskPreloading(taskId: string): boolean {
return preloadingTaskIds.value.has(taskId)
}
// 兼容旧版本:全局评分数据存储(单任务模式,已废弃,保留用于兼容)
const IAgentSelectModifyAddRequest = useStorage<IAgentSelectModifyAddRequest | null>( const IAgentSelectModifyAddRequest = useStorage<IAgentSelectModifyAddRequest | null>(
`${storageKey}-score-data`, `${storageKey}-score-data`,
null, null,
) )
// 设置智能体评分数据 // 设置智能体评分数据(兼容旧版本)
function setAgentScoreData(data: IAgentSelectModifyAddRequest) { function setAgentScoreData(data: IAgentSelectModifyAddRequest) {
IAgentSelectModifyAddRequest.value = data IAgentSelectModifyAddRequest.value = data
} }
@@ -131,8 +246,6 @@ export const useAgentsStore = defineStore('agents', () => {
} }
// 添加该维度的评分数据 // 添加该维度的评分数据
// scores: { agentName: { score, reason } }
// 转换为 agentScores[agentName][aspectName] = { score, reason }
for (const [agentName, scoreItem] of Object.entries(scores)) { for (const [agentName, scoreItem] of Object.entries(scores)) {
if (!IAgentSelectModifyAddRequest.value.agentScores[agentName]) { if (!IAgentSelectModifyAddRequest.value.agentScores[agentName]) {
IAgentSelectModifyAddRequest.value.agentScores[agentName] = {} IAgentSelectModifyAddRequest.value.agentScores[agentName] = {}
@@ -141,7 +254,7 @@ export const useAgentsStore = defineStore('agents', () => {
} }
} }
// 清除智能体评分数据 // 清除智能体评分数据(兼容旧版本)
function clearAgentScoreData() { function clearAgentScoreData() {
IAgentSelectModifyAddRequest.value = null IAgentSelectModifyAddRequest.value = null
} }
@@ -221,6 +334,19 @@ export const useAgentsStore = defineStore('agents', () => {
// 当前的展示的任务流程 // 当前的展示的任务流程
const currentTask = ref<IRawStepTask>() const currentTask = ref<IRawStepTask>()
// 记录用户修改过的步骤索引(用于重新执行)
const modifiedSteps = ref<Set<number>>(new Set())
function addModifiedStep(stepIndex: number) {
modifiedSteps.value.add(stepIndex)
}
function clearModifiedSteps() {
modifiedSteps.value.clear()
}
function hasModifiedSteps() {
return modifiedSteps.value.size > 0
}
function setCurrentTask(task: IRawStepTask) { function setCurrentTask(task: IRawStepTask) {
const existingTask = currentTask.value const existingTask = currentTask.value
@@ -359,11 +485,9 @@ export const useAgentsStore = defineStore('agents', () => {
} }
currentTask.value = undefined currentTask.value = undefined
executePlan.value = [] executePlan.value = []
hasStoppedFilling.value = false
} }
// 额外的产物列表
const additionalOutputs = ref<string[]>([])
// 用户在 Assignment 中选择的 agent 组合按任务ID分别存储 // 用户在 Assignment 中选择的 agent 组合按任务ID分别存储
const selectedAgentGroupMap = ref<Map<string, string[]>>(new Map()) const selectedAgentGroupMap = ref<Map<string, string[]>>(new Map())
@@ -388,31 +512,40 @@ export const useAgentsStore = defineStore('agents', () => {
selectedAgentGroupMap.value.clear() selectedAgentGroupMap.value.clear()
} }
// 添加新产物 // 标记是否用户已停止智能体分配过程
function addNewOutput(outputObject: string) { const hasStoppedFilling = ref(false)
if (!outputObject.trim()) return false
const trimmedOutput = outputObject.trim() // 标记是否正在停止中(全局停止状态)
if (!additionalOutputs.value.includes(trimmedOutput)) { const isStopping = ref(false)
additionalOutputs.value.unshift(trimmedOutput)
return true // 设置停止状态
} function setHasStoppedFilling(value: boolean) {
return false hasStoppedFilling.value = value
} }
// 删除额外产物 // 设置正在停止状态
function removeAdditionalOutput(outputObject: string) { function setIsStopping(value: boolean) {
const index = additionalOutputs.value.indexOf(outputObject) isStopping.value = value
if (index > -1) {
additionalOutputs.value.splice(index, 1)
return true
}
return false
} }
// 清除额外产物 /**
function clearAdditionalOutputs() { * 构建 IApiStepTask 对象(用于 API 调用)
additionalOutputs.value = [] * @param agents 选中的 agent 列表
* @returns IApiStepTask 格式的对象
*/
function createStepTaskForApi(agents: string[]): IApiStepTask {
return {
name: currentTask.value?.StepName || '',
content: currentTask.value?.TaskContent || '',
inputs: currentTask.value?.InputObject_List || [],
output: currentTask.value?.OutputObject || '',
agents,
brief: currentTask.value?.Collaboration_Brief_frontEnd || {
template: '',
data: {}
},
process: []
}
} }
return { return {
@@ -425,15 +558,12 @@ export const useAgentsStore = defineStore('agents', () => {
setCurrentTaskProcess, // 🆕 设置当前任务的 TaskProcess setCurrentTaskProcess, // 🆕 设置当前任务的 TaskProcess
updateCurrentAgentSelection, // 🆕 强制更新 AgentSelection用于 AgentAllocation 切换组合) updateCurrentAgentSelection, // 🆕 强制更新 AgentSelection用于 AgentAllocation 切换组合)
syncCurrentTaskToMainProcess, // 🆕 同步 currentTask 到主流程 syncCurrentTaskToMainProcess, // 🆕 同步 currentTask 到主流程
createStepTaskForApi, // 🆕 构建 IApiStepTask 对象
agentRawPlan, agentRawPlan,
setAgentRawPlan, setAgentRawPlan,
executePlan, executePlan,
setExecutePlan, setExecutePlan,
resetAgent, resetAgent,
additionalOutputs,
addNewOutput,
removeAdditionalOutput,
clearAdditionalOutputs,
// 用户选择的 agent 组合 // 用户选择的 agent 组合
selectedAgentGroupMap, selectedAgentGroupMap,
setSelectedAgentGroup, setSelectedAgentGroup,
@@ -454,12 +584,35 @@ export const useAgentsStore = defineStore('agents', () => {
setAgentScoreData, setAgentScoreData,
addAgentScoreAspect, addAgentScoreAspect,
clearAgentScoreData, clearAgentScoreData,
// 🆕 按任务ID存储的评分数据
taskScoreDataMap,
getTaskScoreData,
setTaskScoreData,
hasTaskScoreData,
getPreloadedTaskIds,
clearTaskScoreData,
clearAllTaskScoreData,
preloadingTaskIds,
markTaskPreloading,
markTaskPreloadComplete,
isTaskPreloading,
// 确认的agent组合列表按任务ID分别存储 // 确认的agent组合列表按任务ID分别存储
confirmedAgentGroupsMap, confirmedAgentGroupsMap,
getConfirmedAgentGroups, getConfirmedAgentGroups,
addConfirmedAgentGroup, addConfirmedAgentGroup,
clearConfirmedAgentGroups, clearConfirmedAgentGroups,
clearAllConfirmedAgentGroups, clearAllConfirmedAgentGroups,
// 停止填充状态
hasStoppedFilling,
setHasStoppedFilling,
// 正在停止状态
isStopping,
setIsStopping,
// 重新执行相关
modifiedSteps,
addModifiedStep,
clearModifiedSteps,
hasModifiedSteps,
} }
}) })

167
frontend/src/utils/retry.ts Normal file
View File

@@ -0,0 +1,167 @@
/**
* 重试工具函数
* @description 提供通用的重试机制,支持指数退避和自定义重试条件
*/
/**
* 重试选项配置
*/
export interface RetryOptions {
/** 最大重试次数,默认 3 */
maxRetries?: number
/** 初始延迟时间(毫秒),默认 2000 */
initialDelayMs?: number
/** 是否使用指数退避,默认 true */
useExponentialBackoff?: boolean
/** 最大延迟时间(毫秒),默认 30000 */
maxDelayMs?: number
/** 自定义重试条件函数,返回 true 表示应该重试 */
shouldRetry?: (error: any, attempt: number) => boolean
/** 重试前的回调函数 */
onRetry?: (error: any, attempt: number, delay: number) => void
}
/**
* 默认重试条件 - 检查是否是 rate limiting 错误
* @param error 错误对象
* @returns 是否应该重试
*/
export function defaultShouldRetry(error: any): boolean {
if (!error) return false
const message = error?.message || String(error)
// 检查 406 Not Acceptable、429 Too Many Requests 等 rate limiting 错误
const isRateLimit =
message.includes('406') ||
message.includes('429') ||
message.includes('Not Acceptable') ||
message.includes('Too Many Requests') ||
message.includes('rate limit')
return isRateLimit
}
/**
* 带重试机制的异步函数执行器
*
* @example
* ```typescript
* // 基本用法
* const result = await withRetry(() => api.someRequest())
*
* // 自定义重试次数和延迟
* const result = await withRetry(() => api.request(), {
* maxRetries: 5,
* initialDelayMs: 1000,
* useExponentialBackoff: true
* })
*
* // 自定义重试条件
* const result = await withRetry(() => api.request(), {
* shouldRetry: (error) => error?.code === 'NETWORK_ERROR'
* })
*
* // 带重试回调
* const result = await withRetry(() => api.request(), {
* onRetry: (error, attempt, delay) => {
* console.log(`第 ${attempt} 次重试,等待 ${delay}ms`, error.message)
* }
* })
* ```
*
* @param fn - 要执行的异步函数
* @param options - 重试选项配置
* @returns Promise resolves with the result of the function
* @throws 如果重试次数用尽,抛出最后一次的错误
*/
export async function withRetry<T>(
fn: () => Promise<T>,
options: RetryOptions = {},
): Promise<T> {
const {
maxRetries = 3,
initialDelayMs = 2000,
useExponentialBackoff = true,
maxDelayMs = 30000,
shouldRetry = defaultShouldRetry,
onRetry,
} = options
let lastError: any = null
let currentDelay = initialDelayMs
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
try {
// 执行目标函数
return await fn()
} catch (error: any) {
lastError = error
// 判断是否应该重试
const shouldRetryAttempt = shouldRetry(error, attempt)
// 如果是最后一次尝试 或者 不应该重试,则抛出错误
if (attempt > maxRetries || !shouldRetryAttempt) {
if (attempt > maxRetries) {
console.error(
`❌ [withRetry] 已达到最大重试次数 (${maxRetries}),放弃请求`,
)
}
throw error
}
// 执行重试回调
if (onRetry) {
onRetry(error, attempt, currentDelay)
}
// 打印日志
console.log(
`⏳ [withRetry] 第 ${attempt} 次重试,等待 ${currentDelay}ms...`,
error?.message || String(error),
)
// 等待延迟时间
await new Promise((resolve) => setTimeout(resolve, currentDelay))
// 计算下一次延迟时间(指数退避)
if (useExponentialBackoff) {
currentDelay = Math.min(currentDelay * 2, maxDelayMs)
}
}
}
//理论上不会到达这里,因为循环内会 throw
throw lastError
}
/**
* 简化的重试装饰器 - 适用于类方法
*
* @example
* ```typescript
* class MyService {
* @retryable({ maxRetries: 3, initialDelayMs: 1000 })
* async fetchData() {
* return await api.request()
* }
* }
* ```
*
* @param options - 重试选项配置
* @returns 装饰器函数
*/
export function retryable(options: RetryOptions = {}) {
return function <T extends (...args: any[]) => Promise<any>>(
_target: any,
_propertyKey: string,
descriptor: TypedPropertyDescriptor<T>,
) {
const originalMethod = descriptor.value
if (!originalMethod) return descriptor
descriptor.value = function (this: any, ...args: Parameters<T>) {
return withRetry(() => originalMethod.apply(this, args), options)
} as T
return descriptor
} as MethodDecorator
}

View File

@@ -78,17 +78,17 @@ class WebSocketClient {
reject(error) reject(error)
}) })
this.socket.on('disconnect', (reason) => { this.socket.on('disconnect', () => {
this.isConnected = false this.isConnected = false
}) })
this.socket.on('connected', (data) => { this.socket.on('connected', () => {
// Server connected message // Server connected message
}) })
// 监听响应消息 // 监听响应消息
this.socket.on('response', (response: ResponseMessage) => { this.socket.on('response', (response: ResponseMessage) => {
const { id, status, data, error } = response const { id, status, data, error, generation_id, execution_id } = response
const handler = this.requestHandlers.get(id) const handler = this.requestHandlers.get(id)
if (handler) { if (handler) {
@@ -98,7 +98,19 @@ class WebSocketClient {
} }
if (status === 'success') { if (status === 'success') {
handler.resolve(data) // 返回完整响应,包含 data、generation_id、execution_id 等
// 注意:需要检查 data 是否为 null因为 typeof null === 'object'
// generation_id/execution_id 可能放在 data 中,需要兼容处理
// 注意:如果 data 是数组,不能展开,否则会破坏数组结构
const resolvedGenerationId = generation_id || (data && typeof data === 'object' && !Array.isArray(data) && data.generation_id)
const resolvedExecutionId = execution_id || (data && typeof data === 'object' && !Array.isArray(data) && data.execution_id)
// 直接返回 data保持原始数据结构数组或对象
handler.resolve({
data,
generation_id: resolvedGenerationId,
execution_id: resolvedExecutionId
})
} else { } else {
handler.reject(new Error(error || 'Unknown error')) handler.reject(new Error(error || 'Unknown error'))
} }
@@ -264,6 +276,24 @@ class WebSocketClient {
get id(): string | undefined { get id(): string | undefined {
return this.socket?.id return this.socket?.id
} }
/**
* 监听事件
*/
on(event: string, callback: (...args: any[]) => void): void {
if (this.socket) {
this.socket.on(event, callback)
}
}
/**
* 取消监听事件
*/
off(event: string, callback?: (...args: any[]) => void): void {
if (this.socket) {
this.socket.off(event, callback)
}
}
} }
// 导出单例 // 导出单例