Compare commits

...

35 Commits

Author SHA1 Message Date
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
liailing1026
786c674d21 feat:RESTful API架构改WebSocket架构-执行结果可以分步显示版本 2026-01-22 17:22:30 +08:00
liailing1026
1c8036adf1 feat:任务执行结果性能优化 2026-01-21 15:36:20 +08:00
liailing1026
45314b7be6 feat:任务执行结果性能优化 2026-01-21 15:18:15 +08:00
liailing1026
c5848410c1 feat:代码优化及格式化注释 2026-01-19 09:12:06 +08:00
liailing1026
571b5101ff feat:任务过程探索分支提示词优化 2026-01-16 11:11:57 +08:00
liailing1026
029df6b5a5 feat:冗余代码清理 2026-01-14 17:54:00 +08:00
liailing1026
edb39d4c1f feat:任务大纲探索窗口分支创建根节点基线完成度计算修改 2026-01-14 10:03:22 +08:00
liailing1026
0e87777ae8 feat:修改任务过程大纲探索后端返回不符合用户要求的bug 2026-01-13 17:58:52 +08:00
liailing1026
244deceb91 Merge branch 'web' of https://gitea.internetapi.cn/iod/AgentCoord into web
合并远程web分支更新
2026-01-13 14:08:52 +08:00
liailing1026
69587c0481 feat:智能体探索窗口与任务过程探索窗口联动修改 2026-01-13 13:57:56 +08:00
zhaoweijie
e0cc11647f feat(plan): 添加中文响应要求并本地化操作提示
- 在多个规划引擎文件中添加中文响应的语言要求说明
- 将操作相关的提示文本从英文翻译为中文
- 确保代理协作中的所有解释和推理使用中文输出
- 保持代理名称等标识符的原始格式不变
2026-01-13 09:49:53 +08:00
liailing1026
59fd94e783 feat:智能体探索窗口与任务大纲探索窗口联动 2026-01-12 17:27:37 +08:00
liailing1026
3ff70463ca feat:任务执行执行状态加载动画添加 2026-01-12 11:17:18 +08:00
liailing1026
82e92f12aa feat:任务大纲卡片超出3行提示框设置最大宽度 2026-01-10 22:07:23 +08:00
liailing1026
920588b063 feat:三个窗口接口联调版本 2026-01-09 13:54:32 +08:00
liailing1026
5847365eee feat:三个浮动窗口功能新增 2025-12-31 19:04:58 +08:00
liailing1026
d42554ce03 feat:智能体库agent左对齐样式问题 2025-12-22 17:15:06 +08:00
zhaoweijie
bcc0c53ba1 feat(config): 支持配置 API 基础路径和开发环境标识
- 在配置存储中新增 dev 和 apiBaseUrl 字段
- 设置默认配置对象,包含基础 URL 构造逻辑
- 更新请求工具以使用动态配置的 baseURL
- 调整应用初始化顺序确保配置先行加载
- 移除指令中不必要的调试日志输出
2025-12-21 20:26:21 +08:00
zhaoweijie
7da5e82d40 feat(ui): 更新任务流程卡片样式和图标
- 引入 Element Plus 图标组件替代原有文字图标
- 移除卡片阴影属性优化视觉效果
- 调整结果区域按钮组布局对齐方式
- 更新文本域样式增加颜色变量支持
- 注释掉编辑卡片背景色定义
- 添加图标依赖包到项目配置
2025-12-21 19:57:33 +08:00
zhaoweijie
cc22655a1e feat(dev): 添加开发模式专用指令和配置支持
- 在 config.json 中新增 dev 配置项用于区分开发与生产环境
- 实现 v-dev-only 指令,仅在开发模式下渲染元素
- 注册全局自定义指令 dev-only,支持通过 binding.value 控制启用状态
- 在 TaskSyllabus/Bg.vue 中使用 v-dev-only 指令隐藏生产环境下的加号区域
- 移除 card 样式中的固定 margin-bottom,改由容器控制间距
- 统一使用 CSS 变量 --color-border-separate 替代硬编码的分割线颜色
- 为 Task.vue 的搜索框添加 clearable 属性并移除弹出项的阴影效果
- Layout 组件名称规范化为首字母大写
- 在主题样式中定义深色与浅色模式下的 --color-border-separate 颜色值
- 覆盖 Element Plus 的 --el-fill-color-blank 以适配暗黑模式背景透明度需求
2025-12-21 18:10:37 +08:00
liailing1026
f0db3c88e4 feat:AdditionalOutputCard.vue未开发完毕版本 2025-12-21 15:56:40 +08:00
liailing1026
b987fe70ad feat:额外产物添加 2025-12-21 15:28:59 +08:00
liailing1026
b42ab5aedd feat:单个agent配置各自的apiurl、apimodel、apikey 2025-12-18 09:51:07 +08:00
zhaoweijie
5ef86c6fa9 fix(LLMAPI): 增加API调用超时时间
- 将LLM API调用的超时时间从15秒延长到60秒
- 防止因网络延迟或模型响应慢导致的超时错误
- 提高长文本生成任务的稳定性
2025-12-17 09:27:00 +08:00
zhaoweijie
907310365a feat(task): 添加任务过程编辑功能
- 新增 ProcessCard 组件用于展示和编辑任务流程
- 实现双击编辑任务描述功能
- 添加编辑状态下的卡片式输入界面
- 支持保存和取消编辑操作
- 实现鼠标悬停高亮效果
- 添加颜色处理函数用于界面美化
- 集成到 TaskResult 组件中展示任务过程
- 支持动态创建和管理任务流程连接线
- 添加额外产物编辑功能
- 实现按钮交互状态管理
- 添加滚动和折叠面板事件处理
- 集成 AgentAllocation 组件用于智能体分配
- 实现椭圆框交互效果展示选中状态
- 添加智能体等级颜色配置
- 支持智能体选中状态切换和排序
2025-12-15 20:47:51 +08:00
zhaoweijie
5dace5f788 feat(assets): 添加 SVG 图标和环境配置文件
- 新增 icons.svg 文件用于定义应用图标
- 添加 .env 配置文件设置 API 基础地址
- 配置图标视图框和路径数据
- 设置图标宽度为 100%,高度为 6
2025-12-15 20:47:28 +08:00
zhaoweijie
77530c49f8 feat(agent): 支持自定义API配置并优化UI交互
- 为agent.json添加apiUrl、apiKey、apiModel字段支持
- 更新API接口类型定义,支持传递自定义API配置
- 优化AgentRepoList组件UI样式和交互效果
- 增强JSON文件上传校验逻辑,支持API配置验证
- 改进任务结果页面布局和视觉呈现
- 添加任务过程查看抽屉功能
- 实现执行按钮动态样式和悬停效果
- 优化节点连接线渲染逻辑和性能
2025-12-15 20:46:54 +08:00
94 changed files with 22780 additions and 4656 deletions

View File

@@ -1,5 +1,7 @@
import asyncio import asyncio
from openai import OpenAI, AsyncOpenAI
import httpx
from openai import OpenAI, AsyncOpenAI, max_retries
import yaml import yaml
from termcolor import colored from termcolor import colored
import os import os
@@ -21,6 +23,9 @@ OPENAI_API_BASE = os.getenv("OPENAI_API_BASE") or yaml_data.get(
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") or yaml_data.get( OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") or yaml_data.get(
"OPENAI_API_KEY", "" "OPENAI_API_KEY", ""
) )
OPENAI_API_MODEL = os.getenv("OPENAI_API_MODEL") or yaml_data.get(
"OPENAI_API_MODEL", ""
)
# Initialize OpenAI clients # Initialize OpenAI clients
client = OpenAI(api_key=OPENAI_API_KEY, base_url=OPENAI_API_BASE) client = OpenAI(api_key=OPENAI_API_KEY, base_url=OPENAI_API_BASE)
@@ -41,8 +46,11 @@ MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY") or yaml_data.get(
# for LLM completion # for LLM completion
def LLM_Completion( def LLM_Completion(
messages: list[dict], stream: bool = True, useGroq: bool = True messages: list[dict], stream: bool = True, useGroq: bool = True,model_config: dict = None
) -> str: ) -> str:
if model_config:
print_colored(f"Using model config: {model_config}", "blue")
return _call_with_custom_config(messages,stream,model_config)
if not useGroq or not FAST_DESIGN_MODE: if not useGroq or not FAST_DESIGN_MODE:
force_gpt4 = True force_gpt4 = True
useGroq = False useGroq = False
@@ -75,6 +83,97 @@ def LLM_Completion(
return _chat_completion(messages=messages) return _chat_completion(messages=messages)
def _call_with_custom_config(messages: list[dict], stream: bool, model_config: dict) ->str:
"使用自定义配置调用API"
api_url = model_config.get("apiUrl", OPENAI_API_BASE)
api_key = model_config.get("apiKey", OPENAI_API_KEY)
api_model = model_config.get("apiModel", OPENAI_API_MODEL)
temp_client = OpenAI(api_key=api_key, base_url=api_url)
temp_async_client = AsyncOpenAI(api_key=api_key, base_url=api_url)
try:
if stream:
try:
loop = asyncio.get_event_loop()
except RuntimeError as ex:
if "There is no current event loop in thread" in str(ex):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(
_achat_completion_stream_custom(messages=messages, temp_async_client=temp_async_client, api_model=api_model)
)
else:
response = temp_client.chat.completions.create(
messages=messages,
model=api_model,
temperature=0.3,
max_tokens=4096,
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
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
except Exception as e:
print_colored(f"Custom API error for model {api_model} :{str(e)}","red")
raise
async def _achat_completion_stream_custom(messages:list[dict], temp_async_client, api_model: str ) -> str:
max_retries=3
for attempt in range(max_retries):
try:
response = await temp_async_client.chat.completions.create(
messages=messages,
model=api_model,
temperature=0.3,
max_tokens=4096,
stream=True,
timeout=180
)
collected_chunks = []
collected_messages = []
async for chunk in response:
collected_chunks.append(chunk)
choices = chunk.choices
if len(choices) > 0 and choices[0] is not None:
chunk_message = choices[0].delta
if chunk_message is not None:
collected_messages.append(chunk_message)
if chunk_message.content:
print(colored(chunk_message.content, "blue", "on_white"), end="")
print()
full_reply_content = "".join(
[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
except httpx.RemoteProtocolError as e:
if attempt < max_retries - 1:
wait_time = (attempt + 1) *2
print_colored(f"⚠️ Stream connection interrupted (attempt {attempt+1}/{max_retries}). Retrying in {wait_time}s...", text_color="yellow")
await asyncio.sleep(wait_time)
continue
except Exception as e:
print_colored(f"Custom API stream error for model {api_model} :{str(e)}","red")
raise
async def _achat_completion_stream_groq(messages: list[dict]) -> str: async def _achat_completion_stream_groq(messages: list[dict]) -> str:
from groq import AsyncGroq from groq import AsyncGroq
groq_client = AsyncGroq(api_key=GROQ_API_KEY) groq_client = AsyncGroq(api_key=GROQ_API_KEY)
@@ -100,7 +199,16 @@ 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
if full_reply_content is None:
raise Exception("Groq API returned None content")
print(colored(full_reply_content, "blue", "on_white"), end="") print(colored(full_reply_content, "blue", "on_white"), end="")
print() print()
return full_reply_content return full_reply_content
@@ -133,7 +241,16 @@ 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
if full_reply_content is None:
raise Exception("Mistral API returned None content")
print(colored(full_reply_content, "blue", "on_white"), end="") print(colored(full_reply_content, "blue", "on_white"), end="")
print() print()
return full_reply_content return full_reply_content
@@ -144,7 +261,7 @@ async def _achat_completion_stream_gpt35(messages: list[dict]) -> str:
messages=messages, messages=messages,
max_tokens=4096, max_tokens=4096,
temperature=0.3, temperature=0.3,
timeout=30, timeout=600,
model="gpt-3.5-turbo-16k", model="gpt-3.5-turbo-16k",
stream=True, stream=True,
) )
@@ -156,8 +273,9 @@ 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
if chunk_message is not None:
collected_messages.append(chunk_message) # save the message collected_messages.append(chunk_message) # save the message
if chunk_message.content: if chunk_message.content:
print( print(
@@ -169,19 +287,23 @@ async def _achat_completion_stream_gpt35(messages: list[dict]) -> str:
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
async def _achat_completion_json(messages: list[dict]) -> str: def _achat_completion_json(messages: list[dict] ) -> str:
max_attempts = 5 max_attempts = 5
for attempt in range(max_attempts): for attempt in range(max_attempts):
try: try:
response = await async_client.chat.completions.create( response = async_client.chat.completions.create(
messages=messages, messages=messages,
max_tokens=4096, max_tokens=4096,
temperature=0.3, temperature=0.3,
timeout=30, timeout=600,
model=MODEL, model=MODEL,
response_format={"type": "json_object"}, response_format={"type": "json_object"},
) )
@@ -192,7 +314,16 @@ async 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
if full_reply_content is None:
raise Exception("OpenAI API returned None content")
print(colored(full_reply_content, "blue", "on_white"), end="") print(colored(full_reply_content, "blue", "on_white"), end="")
print() print()
return full_reply_content return full_reply_content
@@ -211,8 +342,9 @@ 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
if chunk_message is not None:
collected_messages.append(chunk_message) # save the message collected_messages.append(chunk_message) # save the message
if chunk_message.content: if chunk_message.content:
print( print(
@@ -224,6 +356,11 @@ async def _achat_completion_stream(messages: list[dict]) -> str:
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")
@@ -233,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")
@@ -245,7 +392,7 @@ def _cons_kwargs(messages: list[dict]) -> dict:
"messages": messages, "messages": messages,
"max_tokens": 2000, "max_tokens": 2000,
"temperature": 0.3, "temperature": 0.3,
"timeout": 15, "timeout": 600,
} }
kwargs_mode = {"model": MODEL} kwargs_mode = {"model": MODEL}
kwargs.update(kwargs_mode) kwargs.update(kwargs_mode)

View File

@@ -7,6 +7,8 @@ PROMPT_ABILITY_REQUIREMENT_GENERATION = """
## Instruction ## Instruction
Based on "General Goal" and "Current Task", output a formatted "Ability Requirement" which lists at least 3 different ability requirement that is required by the "Current Task". The ability should be summarized concisely within a few words. Based on "General Goal" and "Current Task", output a formatted "Ability Requirement" which lists at least 3 different ability requirement that is required by the "Current Task". The ability should be summarized concisely within a few words.
**IMPORTANT LANGUAGE REQUIREMENT: You must respond in Chinese (中文) for all ability requirements.**
## General Goal (The general goal for the collaboration plan, "Current Task" is just one of its substep) ## General Goal (The general goal for the collaboration plan, "Current Task" is just one of its substep)
{General_Goal} {General_Goal}
@@ -49,6 +51,8 @@ PROMPT_AGENT_ABILITY_SCORING = """
## Instruction ## Instruction
Based on "Agent Board" and "Ability Requirement", output a score for each agent to estimate the possibility that the agent can fulfil the "Ability Requirement". The score should be 1-5. Provide a concise reason before you assign the score. Based on "Agent Board" and "Ability Requirement", output a score for each agent to estimate the possibility that the agent can fulfil the "Ability Requirement". The score should be 1-5. Provide a concise reason before you assign the score.
**IMPORTANT LANGUAGE REQUIREMENT: You must respond in Chinese (中文) for all reasons and explanations.**
## AgentBoard ## AgentBoard
{Agent_Board} {Agent_Board}
@@ -133,5 +137,6 @@ def AgentSelectModify_init(stepTask, General_Goal, Agent_Board):
def AgentSelectModify_addAspect(aspectList, Agent_Board): def AgentSelectModify_addAspect(aspectList, Agent_Board):
scoreTable = agentAbilityScoring(Agent_Board, aspectList) newAspect = aspectList[-1]
scoreTable = agentAbilityScoring(Agent_Board, [newAspect])
return scoreTable return scoreTable

View File

@@ -35,6 +35,8 @@ PROMPT_AGENT_SELECTION_GENERATION = """
## Instruction ## Instruction
Based on "General Goal", "Current Task" and "Agent Board", output a formatted "Agent Selection Plan". Your selection should consider the ability needed for "Current Task" and the profile of each agent in "Agent Board". Based on "General Goal", "Current Task" and "Agent Board", output a formatted "Agent Selection Plan". Your selection should consider the ability needed for "Current Task" and the profile of each agent in "Agent Board".
**IMPORTANT LANGUAGE REQUIREMENT: You must respond in Chinese (中文) for all explanations and reasoning, though agent names should remain in their original form.**
## General Goal (Specify the general goal for the collaboration plan) ## General Goal (Specify the general goal for the collaboration plan)
{General_Goal} {General_Goal}

View File

@@ -1,55 +1,53 @@
from AgentCoord.PlanEngine.planOutline_Generator import generate_PlanOutline from AgentCoord.PlanEngine.planOutline_Generator import generate_PlanOutline
from AgentCoord.PlanEngine.AgentSelection_Generator import ( # from AgentCoord.PlanEngine.AgentSelection_Generator import (
generate_AgentSelection, # generate_AgentSelection,
) # )
from AgentCoord.PlanEngine.taskProcess_Generator import generate_TaskProcess
import AgentCoord.util as util
def generate_basePlan( def generate_basePlan(
General_Goal, Agent_Board, AgentProfile_Dict, InitialObject_List General_Goal, Agent_Board, AgentProfile_Dict, InitialObject_List
): ):
basePlan = { """
"Initial Input Object": InitialObject_List, 优化模式:生成大纲 + 智能体选择,但不生成任务流程
"Collaboration Process": [], 优化用户体验:
} 1. 快速生成大纲和分配智能体
2. 用户可以看到完整的大纲和智能体图标
3. TaskProcess由前端通过 fillStepTask API 异步填充
"""
# 参数保留以保持接口兼容性
_ = AgentProfile_Dict
PlanOutline = generate_PlanOutline( PlanOutline = generate_PlanOutline(
InitialObject_List=[], General_Goal=General_Goal InitialObject_List=InitialObject_List, General_Goal=General_Goal
) )
basePlan = {
"General Goal": General_Goal,
"Initial Input Object": InitialObject_List,
"Collaboration Process": []
}
for stepItem in PlanOutline: for stepItem in PlanOutline:
Current_Task = { # # 为每个步骤分配智能体
"TaskName": stepItem["StepName"], # Current_Task = {
"InputObject_List": stepItem["InputObject_List"], # "TaskName": stepItem["StepName"],
"OutputObject": stepItem["OutputObject"], # "InputObject_List": stepItem["InputObject_List"],
"TaskContent": stepItem["TaskContent"], # "OutputObject": stepItem["OutputObject"],
# "TaskContent": stepItem["TaskContent"],
# }
# AgentSelection = generate_AgentSelection(
# General_Goal=General_Goal,
# Current_Task=Current_Task,
# Agent_Board=Agent_Board,
# )
# 添加智能体选择,但不添加任务流程
stepItem["AgentSelection"] = []
stepItem["TaskProcess"] = [] # 空数组,由前端异步填充
stepItem["Collaboration_Brief_frontEnd"] = {
"template": "",
"data": {}
} }
AgentSelection = generate_AgentSelection(
General_Goal=General_Goal,
Current_Task=Current_Task,
Agent_Board=Agent_Board,
)
Current_Task_Description = {
"TaskName": stepItem["StepName"],
"AgentInvolved": [
{"Name": name, "Profile": AgentProfile_Dict[name]}
for name in AgentSelection
],
"InputObject_List": stepItem["InputObject_List"],
"OutputObject": stepItem["OutputObject"],
"CurrentTaskDescription": util.generate_template_sentence_for_CollaborationBrief(
stepItem["InputObject_List"],
stepItem["OutputObject"],
AgentSelection,
stepItem["TaskContent"],
),
}
TaskProcess = generate_TaskProcess(
General_Goal=General_Goal,
Current_Task_Description=Current_Task_Description,
)
# add the generated AgentSelection and TaskProcess to the stepItem
stepItem["AgentSelection"] = AgentSelection
stepItem["TaskProcess"] = TaskProcess
basePlan["Collaboration Process"].append(stepItem) basePlan["Collaboration Process"].append(stepItem)
basePlan["General Goal"] = General_Goal
return basePlan return basePlan

View File

@@ -9,6 +9,8 @@ PROMPT_PLAN_OUTLINE_BRANCHING = """
Based on "Existing Steps", your task is to comeplete the "Remaining Steps" for the plan for "General Goal". Based on "Existing Steps", your task is to comeplete the "Remaining Steps" for the plan for "General Goal".
Note: "Modification Requirement" specifies how to modify the "Baseline Completion" for a better/alternative solution. Note: "Modification Requirement" specifies how to modify the "Baseline Completion" for a better/alternative solution.
**IMPORTANT LANGUAGE REQUIREMENT: You must respond in Chinese (中文) for all content, including StepName, TaskContent, and OutputObject fields.**
## General Goal (Specify the general goal for the plan) ## General Goal (Specify the general goal for the plan)
{General_Goal} {General_Goal}

View File

@@ -29,6 +29,8 @@ PROMPT_TASK_PROCESS_BRANCHING = """
Based on "Existing Steps", your task is to comeplete the "Remaining Steps" for the "Task for Current Step". Based on "Existing Steps", your task is to comeplete the "Remaining Steps" for the "Task for Current Step".
Note: "Modification Requirement" specifies how to modify the "Baseline Completion" for a better/alternative solution. Note: "Modification Requirement" specifies how to modify the "Baseline Completion" for a better/alternative solution.
**IMPORTANT LANGUAGE REQUIREMENT: You must respond in Chinese (中文) for the Description field and all explanations, while keeping ID, ActionType, and AgentName in their original format.**
## General Goal (The general goal for the collaboration plan, you just design the plan for one of its step (i.e. "Task for Current Step")) ## General Goal (The general goal for the collaboration plan, you just design the plan for one of its step (i.e. "Task for Current Step"))
{General_Goal} {General_Goal}
@@ -55,27 +57,40 @@ Note: "Modification Requirement" specifies how to modify the "Baseline Completio
"ID": "Action4", "ID": "Action4",
"ActionType": "Propose", "ActionType": "Propose",
"AgentName": "Mia", "AgentName": "Mia",
"Description": "Propose psychological theories on love and attachment that could be applied to AI's emotional development.", "Description": "提议关于人工智能情感发展的心理学理论,重点关注爱与依恋的概念。",
"ImportantInput": [ "ImportantInput": [
"InputObject:Story Outline" "InputObject:Story Outline"
] ]
}}, }},
{{ {{
"ID": "Action5", "ID": "Action5",
"ActionType": "Propose", "ActionType": "Critique",
"AgentName": "Noah", "AgentName": "Noah",
"Description": "Propose ethical considerations and philosophical questions regarding AI's capacity for love.", "Description": "对Mia提出的心理学理论进行批判性评估分析其在AI情感发展场景中的适用性和局限性。",
"ImportantInput": [] "ImportantInput": [
"ActionResult:Action4"
]
}}, }},
{{ {{
"ID": "Action6", "ID": "Action6",
"ActionType": "Finalize", "ActionType": "Improve",
"AgentName": "Liam", "AgentName": "Liam",
"Description": "Combine the poetic elements and ethical considerations into a cohesive set of core love elements for the story.", "Description": "基于Noah的批判性反馈改进和完善心理学理论框架使其更贴合AI情感发展的实际需求。",
"ImportantInput": [ "ImportantInput": [
"ActionResult:Action1", "ActionResult:Action4",
"ActionResult:Action5" "ActionResult:Action5"
] ]
}},
{{
"ID": "Action7",
"ActionType": "Finalize",
"AgentName": "Mia",
"Description": "综合所有提议、批判和改进意见整合并提交最终的AI情感发展心理学理论框架。",
"ImportantInput": [
"ActionResult:Action4",
"ActionResult:Action5",
"ActionResult:Action6"
]
}} }}
] ]
}} }}
@@ -84,7 +99,12 @@ Note: "Modification Requirement" specifies how to modify the "Baseline Completio
ImportantInput: Specify if there is any previous result that should be taken special consideration during the execution the action. Should be of format "InputObject:xx" or "ActionResult:xx". ImportantInput: Specify if there is any previous result that should be taken special consideration during the execution the action. Should be of format "InputObject:xx" or "ActionResult:xx".
InputObject_List: List existing objects that should be utilized in current step. InputObject_List: List existing objects that should be utilized in current step.
AgentName: Specify the agent who will perform the action, You CAN ONLY USE THE NAME APPEARS IN "AgentInvolved". AgentName: Specify the agent who will perform the action, You CAN ONLY USE THE NAME APPEARS IN "AgentInvolved".
ActionType: Specify the type of action, note that only the last action can be of type "Finalize", and the last action must be "Finalize". ActionType: Specify the type of action. **CRITICAL REQUIREMENTS:**
1. The "Remaining Steps" MUST include ALL FOUR action types in the following order: Propose -> Critique -> Improve -> Finalize
2. Each action type (Propose, Critique, Improve, Finalize) MUST appear at least once
3. The actions must follow the sequence: Propose actions first, then Critique actions, then Improve actions, and Finalize must be the last action
4. Even if only one agent is involved in a phase, that phase must still have its corresponding action type
5. The last action must ALWAYS be of type "Finalize"
""" """

View File

@@ -7,6 +7,8 @@ PROMPT_PLAN_OUTLINE_GENERATION = """
## Instruction ## Instruction
Based on "Output Format Example", "General Goal", and "Initial Key Object List", output a formatted "Plan_Outline". Based on "Output Format Example", "General Goal", and "Initial Key Object List", output a formatted "Plan_Outline".
**IMPORTANT LANGUAGE REQUIREMENT: You must respond in Chinese (中文) for all content, including StepName, TaskContent, and OutputObject fields.**
## Initial Key Object List (Specify the list of initial key objects available, each initial key object should be the input object of at least one Step) ## Initial Key Object List (Specify the list of initial key objects available, each initial key object should be the input object of at least one Step)
{InitialObject_List} {InitialObject_List}

View File

@@ -25,6 +25,8 @@ PROMPT_TASK_PROCESS_GENERATION = """
## Instruction ## Instruction
Based on "General Goal", "Task for Current Step", "Action Set" and "Output Format Example", design a plan for "Task for Current Step", output a formatted "Task_Process_Plan". Based on "General Goal", "Task for Current Step", "Action Set" and "Output Format Example", design a plan for "Task for Current Step", output a formatted "Task_Process_Plan".
**IMPORTANT LANGUAGE REQUIREMENT: You must respond in Chinese (中文) for the Description field and all explanations, while keeping ID, ActionType, and AgentName in their original format.**
## General Goal (The general goal for the collaboration plan, you just design the plan for one of its step (i.e. "Task for Current Step")) ## General Goal (The general goal for the collaboration plan, you just design the plan for one of its step (i.e. "Task for Current Step"))
{General_Goal} {General_Goal}

View File

@@ -9,6 +9,8 @@ You are within a multi-agent collaboration for the "Current Task".
Now it's your turn to take action. Read the "Context Information" and take your action following "Instruction for Your Current Action". Now it's your turn to take action. Read the "Context Information" and take your action following "Instruction for Your Current Action".
Note: Important Input for your action are marked with *Important Input* Note: Important Input for your action are marked with *Important Input*
**IMPORTANT LANGUAGE REQUIREMENT: You must respond in Chinese (中文) for all your answers and outputs.**
## Context Information ## Context Information
### General Goal (The "Current Task" is indeed a substep of the general goal) ### General Goal (The "Current Task" is indeed a substep of the general goal)
@@ -81,16 +83,33 @@ class BaseAction():
action_Record += PROMPT_TEMPLATE_ACTION_RECORD.format(AgentName = actionInfo["AgentName"], Action_Description = actionInfo["AgentName"], Action_Result = actionInfo["Action_Result"], Important_Mark = Important_Mark) action_Record += PROMPT_TEMPLATE_ACTION_RECORD.format(AgentName = actionInfo["AgentName"], Action_Description = actionInfo["AgentName"], Action_Result = actionInfo["Action_Result"], Important_Mark = Important_Mark)
# Handle missing agent profiles gracefully # Handle missing agent profiles gracefully
model_config = None
if agentName not in AgentProfile_Dict: if agentName not in AgentProfile_Dict:
print_colored(text=f"Warning: Agent '{agentName}' not found in AgentProfile_Dict. Using default profile.", text_color="yellow") print_colored(text=f"Warning: Agent '{agentName}' not found in AgentProfile_Dict. Using default profile.", text_color="yellow")
agentProfile = f"AI Agent named {agentName}" agentProfile = f"AI Agent named {agentName}"
else: else:
agentProfile = AgentProfile_Dict[agentName] # agentProfile = AgentProfile_Dict[agentName]
agent_config = AgentProfile_Dict[agentName]
prompt = PROMPT_TEMPLATE_TAKE_ACTION_BASE.format(agentName = agentName, agentProfile = agentProfile, General_Goal = General_Goal, Current_Task_Description = TaskDescription, Input_Objects = inputObject_Record, History_Action = action_Record, Action_Description = self.info["Description"], Action_Custom_Note = self.Action_Custom_Note) agentProfile = agent_config.get("profile",f"AI Agent named {agentName}")
if agent_config.get("useCustomAPI",False):
model_config = {
"apiModel":agent_config.get("apiModel"),
"apiUrl":agent_config.get("apiUrl"),
"apiKey":agent_config.get("apiKey"),
}
prompt = PROMPT_TEMPLATE_TAKE_ACTION_BASE.format(
agentName = agentName,
agentProfile = agentProfile,
General_Goal = General_Goal,
Current_Task_Description = TaskDescription,
Input_Objects = inputObject_Record,
History_Action = action_Record,
Action_Description = self.info["Description"],
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) ActionResult = LLM_Completion(messages,True,False,model_config=model_config)
ActionInfo_with_Result = copy.deepcopy(self.info) ActionInfo_with_Result = copy.deepcopy(self.info)
ActionInfo_with_Result["Action_Result"] = ActionResult ActionInfo_with_Result["Action_Result"] = ActionResult

View File

@@ -1,7 +1,7 @@
from AgentCoord.RehearsalEngine_V2.Action import BaseAction from AgentCoord.RehearsalEngine_V2.Action import BaseAction
ACTION_CUSTOM_NOTE = ''' ACTION_CUSTOM_NOTE = '''
Note: Since you are in a conversation, your critique must be concise, clear and easy to read, don't overwhelm others. If you want to list some points, list at most 2 points. 注意由于你在对话中你的批评必须简洁、清晰且易于阅读不要让人感到压力过大。如果你要列出一些观点最多列出2点。
''' '''

View File

@@ -2,9 +2,9 @@ from AgentCoord.util.converter import read_outputObject_content
from AgentCoord.RehearsalEngine_V2.Action import BaseAction from AgentCoord.RehearsalEngine_V2.Action import BaseAction
ACTION_CUSTOM_NOTE = ''' ACTION_CUSTOM_NOTE = '''
Note: You can say something before you give the final content of {OutputName}. When you decide to give the final content of {OutputName}, it should be enclosed like this: 注意:你可以在给出{OutputName}的最终内容之前先说一些话。当你决定给出{OutputName}的最终内容时,应该这样包含:
```{OutputName} ```{OutputName}
(the content of {OutputName}) {OutputName}的内容)
``` ```
''' '''

View File

@@ -1,12 +1,12 @@
from AgentCoord.RehearsalEngine_V2.Action import BaseAction from AgentCoord.RehearsalEngine_V2.Action import BaseAction
ACTION_CUSTOM_NOTE = ''' ACTION_CUSTOM_NOTE = '''
Note: You can say something before you provide the improved version of the content. 注意:你可以在提供改进版本的内容之前先说一些话。
The improved version you provide must be a completed version (e.g. if you provide a improved story, you should give completed story content, rather than just reporting where you have improved). 你提供的改进版本必须是完整的版本(例如,如果你提供改进的故事,你应该给出完整的故事内容,而不仅仅是报告你在哪里改进了)。
When you decide to give the improved version of the content, it should be start like this: 当你决定提供内容的改进版本时,应该这样开始:
## Improved version of xxx ## xxx的改进版本
(the improved version of the content) (改进版本的内容)
``` ```
''' '''

View File

@@ -0,0 +1,608 @@
"""
优化版执行计划 - 支持动态追加步骤
在执行过程中可以接收新的步骤并追加到执行队列
"""
import asyncio
import json
import time
from typing import List, Dict, Set, Generator, Any
import AgentCoord.RehearsalEngine_V2.Action as Action
import AgentCoord.util as util
from termcolor import colored
from AgentCoord.RehearsalEngine_V2.execution_state import execution_state_manager
from AgentCoord.RehearsalEngine_V2.dynamic_execution_manager import dynamic_execution_manager
# ==================== 配置参数 ====================
# 最大并发请求数
MAX_CONCURRENT_REQUESTS = 2
# 批次之间的延迟
BATCH_DELAY = 1.0
# 429错误重试次数和延迟
MAX_RETRIES = 3
RETRY_DELAY = 5.0
# ==================== 限流器 ====================
class RateLimiter:
"""
异步限流器,控制并发请求数量
"""
def __init__(self, max_concurrent: int = MAX_CONCURRENT_REQUESTS):
self.semaphore = asyncio.Semaphore(max_concurrent)
self.max_concurrent = max_concurrent
async def __aenter__(self):
await self.semaphore.acquire()
return self
async def __aexit__(self, *args):
self.semaphore.release()
# 全局限流器实例
rate_limiter = RateLimiter()
def build_action_dependency_graph(TaskProcess: List[Dict]) -> Dict[int, List[int]]:
"""
构建动作依赖图
Args:
TaskProcess: 任务流程列表
Returns:
依赖映射字典 {action_index: [dependent_action_indices]}
"""
dependency_map = {i: [] for i in range(len(TaskProcess))}
for i, action in enumerate(TaskProcess):
important_inputs = action.get('ImportantInput', [])
if not important_inputs:
continue
# 检查是否依赖其他动作的ActionResult
for j, prev_action in enumerate(TaskProcess):
if i == j:
continue
# 判断是否依赖前一个动作的结果
if any(
inp.startswith('ActionResult:') and
inp == f'ActionResult:{prev_action["ID"]}'
for inp in important_inputs
):
dependency_map[i].append(j)
return dependency_map
def get_parallel_batches(TaskProcess: List[Dict], dependency_map: Dict[int, List[int]]) -> List[List[int]]:
"""
将动作分为多个批次,每批内部可以并行执行
Args:
TaskProcess: 任务流程列表
dependency_map: 依赖图
Returns:
批次列表 [[batch1_indices], [batch2_indices], ...]
"""
batches = []
completed: Set[int] = set()
while len(completed) < len(TaskProcess):
# 找出所有依赖已满足的动作
ready_to_run = [
i for i in range(len(TaskProcess))
if i not in completed and
all(dep in completed for dep in dependency_map[i])
]
if not ready_to_run:
# 避免死循环
remaining = [i for i in range(len(TaskProcess)) if i not in completed]
if remaining:
print(colored(f"警告: 检测到循环依赖,强制串行执行: {remaining}", "yellow"))
ready_to_run = remaining[:1]
else:
break
batches.append(ready_to_run)
completed.update(ready_to_run)
return batches
async def execute_single_action_async(
ActionInfo: Dict,
General_Goal: str,
TaskDescription: str,
OutputName: str,
KeyObjects: Dict,
ActionHistory: List,
agentName: str,
AgentProfile_Dict: Dict,
InputName_List: List[str]
) -> Dict:
"""
异步执行单个动作
Args:
ActionInfo: 动作信息
General_Goal: 总体目标
TaskDescription: 任务描述
OutputName: 输出对象名称
KeyObjects: 关键对象字典
ActionHistory: 动作历史
agentName: 智能体名称
AgentProfile_Dict: 智能体配置字典
InputName_List: 输入名称列表
Returns:
动作执行结果
"""
actionType = ActionInfo["ActionType"]
# 创建动作实例
if actionType in Action.customAction_Dict:
currentAction = Action.customAction_Dict[actionType](
info=ActionInfo,
OutputName=OutputName,
KeyObjects=KeyObjects,
)
else:
currentAction = Action.BaseAction(
info=ActionInfo,
OutputName=OutputName,
KeyObjects=KeyObjects,
)
# 在线程池中运行,避免阻塞事件循环
loop = asyncio.get_event_loop()
ActionInfo_with_Result = await loop.run_in_executor(
None,
lambda: currentAction.run(
General_Goal=General_Goal,
TaskDescription=TaskDescription,
agentName=agentName,
AgentProfile_Dict=AgentProfile_Dict,
InputName_List=InputName_List,
OutputName=OutputName,
KeyObjects=KeyObjects,
ActionHistory=ActionHistory,
)
)
return ActionInfo_with_Result
async def execute_step_async_streaming(
stepDescrip: Dict,
General_Goal: str,
AgentProfile_Dict: Dict,
KeyObjects: Dict,
step_index: int,
total_steps: int
) -> Generator[Dict, None, None]:
"""
异步执行单个步骤,支持流式返回
Args:
stepDescrip: 步骤描述
General_Goal: 总体目标
AgentProfile_Dict: 智能体配置字典
KeyObjects: 关键对象字典
step_index: 步骤索引
total_steps: 总步骤数
Yields:
执行事件字典
"""
# 准备步骤信息
StepName = (
util.camel_case_to_normal(stepDescrip["StepName"])
if util.is_camel_case(stepDescrip["StepName"])
else stepDescrip["StepName"]
)
TaskContent = stepDescrip["TaskContent"]
InputName_List = (
[
(
util.camel_case_to_normal(obj)
if util.is_camel_case(obj)
else obj
)
for obj in stepDescrip["InputObject_List"]
]
if stepDescrip["InputObject_List"] is not None
else None
)
OutputName = (
util.camel_case_to_normal(stepDescrip["OutputObject"])
if util.is_camel_case(stepDescrip["OutputObject"])
else stepDescrip["OutputObject"]
)
Agent_List = stepDescrip["AgentSelection"]
TaskProcess = stepDescrip["TaskProcess"]
TaskDescription = (
util.converter.generate_template_sentence_for_CollaborationBrief(
input_object_list=InputName_List,
output_object=OutputName,
agent_list=Agent_List,
step_task=TaskContent,
)
)
# 初始化日志节点
inputObject_Record = [
{InputName: KeyObjects[InputName]} for InputName in InputName_List
]
stepLogNode = {
"LogNodeType": "step",
"NodeId": StepName,
"InputName_List": InputName_List,
"OutputName": OutputName,
"chatLog": [],
"inputObject_Record": inputObject_Record,
}
objectLogNode = {
"LogNodeType": "object",
"NodeId": OutputName,
"content": None,
}
# 返回步骤开始事件
yield {
"type": "step_start",
"step_index": step_index,
"total_steps": total_steps,
"step_name": StepName,
"task_description": TaskDescription,
}
# 构建动作依赖图
dependency_map = build_action_dependency_graph(TaskProcess)
batches = get_parallel_batches(TaskProcess, dependency_map)
ActionHistory = []
total_actions = len(TaskProcess)
completed_actions = 0
util.print_colored(
f"📋 步骤 {step_index + 1}/{total_steps}: {StepName} ({total_actions} 个动作, 分 {len(batches)} 批并行执行)",
text_color="cyan"
)
# 分批执行动作
for batch_index, batch_indices in enumerate(batches):
# 在每个批次执行前检查暂停状态
should_continue = await execution_state_manager.async_check_pause()
if not should_continue:
util.print_colored("🛑 用户请求停止执行", "red")
return
batch_size = len(batch_indices)
if batch_size > 1:
util.print_colored(
f"🚦 批次 {batch_index + 1}/{len(batches)}: 并行执行 {batch_size} 个动作",
text_color="blue"
)
else:
util.print_colored(
f"🔄 动作 {completed_actions + 1}/{total_actions}: 串行执行",
text_color="yellow"
)
# 并行执行当前批次的所有动作
tasks = [
execute_single_action_async(
TaskProcess[i],
General_Goal=General_Goal,
TaskDescription=TaskDescription,
OutputName=OutputName,
KeyObjects=KeyObjects,
ActionHistory=ActionHistory,
agentName=TaskProcess[i]["AgentName"],
AgentProfile_Dict=AgentProfile_Dict,
InputName_List=InputName_List
)
for i in batch_indices
]
# 等待当前批次完成
batch_results = await asyncio.gather(*tasks)
# 逐个返回结果
for i, result in enumerate(batch_results):
action_index_in_batch = batch_indices[i]
completed_actions += 1
util.print_colored(
f"✅ 动作 {completed_actions}/{total_actions} 完成: {result['ActionType']} by {result['AgentName']}",
text_color="green"
)
ActionHistory.append(result)
# 立即返回该动作结果
yield {
"type": "action_complete",
"step_index": step_index,
"step_name": StepName,
"action_index": action_index_in_batch,
"total_actions": total_actions,
"completed_actions": completed_actions,
"action_result": result,
"batch_info": {
"batch_index": batch_index,
"batch_size": batch_size,
"is_parallel": batch_size > 1
}
}
# 步骤完成
objectLogNode["content"] = KeyObjects[OutputName]
stepLogNode["ActionHistory"] = ActionHistory
yield {
"type": "step_complete",
"step_index": step_index,
"step_name": StepName,
"step_log_node": stepLogNode,
"object_log_node": objectLogNode,
}
def executePlan_streaming_dynamic(
plan: Dict,
num_StepToRun: int,
RehearsalLog: List,
AgentProfile_Dict: Dict,
existingKeyObjects: Dict = None,
execution_id: str = None
) -> Generator[str, None, None]:
"""
动态执行计划,支持在执行过程中追加新步骤
Args:
plan: 执行计划
num_StepToRun: 要运行的步骤数
RehearsalLog: 已执行的历史记录
AgentProfile_Dict: 智能体配置
existingKeyObjects: 已存在的KeyObjects
execution_id: 执行ID用于动态追加步骤
Yields:
SSE格式的事件字符串
"""
# 初始化执行状态
general_goal = plan.get("General Goal", "")
execution_state_manager.start_execution(general_goal)
print(colored(f"⏸️ 执行状态管理器已启动,支持暂停/恢复", "green"))
# 准备执行
KeyObjects = existingKeyObjects.copy() if existingKeyObjects else {}
finishedStep_index = -1
for logNode in RehearsalLog:
if logNode["LogNodeType"] == "step":
finishedStep_index += 1
if logNode["LogNodeType"] == "object":
KeyObjects[logNode["NodeId"]] = logNode["content"]
if existingKeyObjects:
print(colored(f"📦 使用已存在的 KeyObjects: {list(existingKeyObjects.keys())}", "cyan"))
# 确定要运行的步骤范围
if num_StepToRun is None:
run_to = len(plan["Collaboration Process"])
else:
run_to = (finishedStep_index + 1) + num_StepToRun
steps_to_run = plan["Collaboration Process"][(finishedStep_index + 1): run_to]
# 使用动态执行管理器
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):
"""异步生产者"""
try:
step_index = 0
if execution_id:
# 动态模式:循环获取下一个步骤
# 等待新步骤的最大次数(避免无限等待)
max_empty_wait_cycles = 5 # 最多等待60次每次等待1秒
empty_wait_count = 0
while True:
# 检查暂停状态
should_continue = await execution_state_manager.async_check_pause()
if not should_continue:
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 # 使用动态更新的总步骤数
):
if execution_state_manager.is_stopped():
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()
if not should_continue:
print(colored("🛑 用户请求停止执行", "red"))
await queue.put({
"type": "error",
"message": "执行已被用户停止"
})
return
async for event in execute_step_async_streaming(
stepDescrip,
plan["General Goal"],
AgentProfile_Dict,
KeyObjects,
step_index,
total_steps
):
if execution_state_manager.is_stopped():
await queue.put({
"type": "error",
"message": "执行已被用户停止"
})
return
await queue.put(event)
except Exception as e:
await queue.put({
"type": "error",
"message": f"执行出错: {str(e)}"
})
finally:
await queue.put(None)
# 运行异步任务并实时yield
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
queue = asyncio.Queue(maxsize=10)
producer_task = loop.create_task(produce_events(queue))
while True:
event = loop.run_until_complete(queue.get())
if event is None:
break
# 立即转换为SSE格式并发送
event_str = json.dumps(event, ensure_ascii=False)
yield f"data: {event_str}\n\n"
loop.run_until_complete(producer_task)
if not execution_state_manager.is_stopped():
complete_event = json.dumps({
"type": "execution_complete",
"total_steps": total_steps
}, ensure_ascii=False)
yield f"data: {complete_event}\n\n"
finally:
# 在关闭事件循环之前先清理执行记录
if execution_id:
# 清理执行记录
dynamic_execution_manager.cleanup(execution_id)
if 'producer_task' in locals():
if not producer_task.done():
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()
# 保留旧版本函数以保持兼容性
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

@@ -0,0 +1,190 @@
"""
全局执行状态管理器
用于支持任务的暂停、恢复和停止功能
使用轮询检查机制,确保线程安全
"""
import threading
import asyncio
import time
from typing import Optional
from enum import Enum
class ExecutionStatus(Enum):
"""执行状态枚举"""
RUNNING = "running" # 正在运行
PAUSED = "paused" # 已暂停
STOPPED = "stopped" # 已停止
IDLE = "idle" # 空闲
class ExecutionStateManager:
"""
全局执行状态管理器(单例模式)
功能:
- 管理任务执行状态(运行/暂停/停止)
- 使用轮询检查机制,避免异步事件的线程问题
- 提供线程安全的状态查询和修改接口
"""
_instance: Optional['ExecutionStateManager'] = None
_lock = threading.Lock()
def __new__(cls):
"""单例模式"""
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
"""初始化状态管理器"""
if self._initialized:
return
self._initialized = True
self._status = ExecutionStatus.IDLE
self._current_goal: Optional[str] = None # 当前执行的任务目标
# 使用简单的布尔标志,而不是 asyncio.Event
self._should_pause = False
self._should_stop = False
def get_status(self) -> ExecutionStatus:
"""获取当前执行状态"""
with self._lock:
return self._status
def set_goal(self, goal: str):
"""设置当前执行的任务目标"""
with self._lock:
self._current_goal = goal
def get_goal(self) -> Optional[str]:
"""获取当前执行的任务目标"""
with self._lock:
return self._current_goal
def start_execution(self, goal: str):
"""开始执行"""
with self._lock:
self._status = ExecutionStatus.RUNNING
self._current_goal = goal
self._should_pause = False
self._should_stop = False
print(f"🚀 [DEBUG] start_execution: 状态设置为 RUNNING, goal={goal}")
def pause_execution(self) -> bool:
"""
暂停执行
Returns:
bool: 是否成功暂停
"""
with self._lock:
if self._status != ExecutionStatus.RUNNING:
print(f"⚠️ [DEBUG] pause_execution: 当前状态不是RUNNING而是 {self._status}")
return False
self._status = ExecutionStatus.PAUSED
self._should_pause = True
print(f"⏸️ [DEBUG] pause_execution: 状态设置为PAUSED, should_pause=True")
return True
def resume_execution(self) -> bool:
"""
恢复执行
Returns:
bool: 是否成功恢复
"""
with self._lock:
if self._status != ExecutionStatus.PAUSED:
print(f"⚠️ [DEBUG] resume_execution: 当前状态不是PAUSED而是 {self._status}")
return False
self._status = ExecutionStatus.RUNNING
self._should_pause = False
print(f"▶️ [DEBUG] resume_execution: 状态设置为RUNNING, should_pause=False")
return True
def stop_execution(self) -> bool:
"""
停止执行
Returns:
bool: 是否成功停止
"""
with self._lock:
if self._status in [ExecutionStatus.IDLE, ExecutionStatus.STOPPED]:
return False
self._status = ExecutionStatus.STOPPED
self._should_stop = True
self._should_pause = False
print(f"🛑 [DEBUG] stop_execution: 状态设置为STOPPED")
return True
def reset(self):
"""重置状态为空闲"""
with self._lock:
self._status = ExecutionStatus.IDLE
self._current_goal = None
self._should_pause = False
self._should_stop = False
print(f"🔄 [DEBUG] reset: 状态重置为IDLE")
async def async_check_pause(self):
"""
异步检查是否需要暂停(轮询方式)
如果处于暂停状态,会阻塞当前协程直到恢复或停止
应该在执行循环的关键点调用此方法
Returns:
bool: 如果返回True表示应该继续执行False表示应该停止
"""
# 使用轮询检查,避免异步事件问题
while True:
# 检查停止标志
if self._should_stop:
print("🛑 [DEBUG] async_check_pause: 检测到停止信号")
return False
# 检查暂停状态
if self._should_pause:
# 处于暂停状态,等待恢复
print("⏸️ [DEBUG] async_check_pause: 检测到暂停,等待恢复...")
await asyncio.sleep(0.1) # 短暂睡眠避免占用CPU
# 如果恢复,继续执行
if not self._should_pause:
print("▶️ [DEBUG] async_check_pause: 从暂停中恢复!")
continue
# 如果停止了,返回
if self._should_stop:
return False
# 继续等待
continue
# 既没有停止也没有暂停,可以继续执行
return True
def is_paused(self) -> bool:
"""检查是否处于暂停状态"""
with self._lock:
return self._status == ExecutionStatus.PAUSED
def is_running(self) -> bool:
"""检查是否正在运行"""
with self._lock:
return self._status == ExecutionStatus.RUNNING
def is_stopped(self) -> bool:
"""检查是否已停止"""
with self._lock:
return self._status == ExecutionStatus.STOPPED
# 全局单例实例
execution_state_manager = ExecutionStateManager()

View File

@@ -1,10 +1,10 @@
## config for default LLM ## config for default LLM
OPENAI_API_BASE: "" OPENAI_API_BASE: "https://ai.gitee.com/v1"
OPENAI_API_KEY: "" OPENAI_API_KEY: "HYCNGM39GGFNSB1F8MBBMI9QYJR3P1CRSYS2PV1A"
OPENAI_API_MODEL: "gpt-4-turbo-preview" OPENAI_API_MODEL: "DeepSeek-V3"
## config for fast mode ## config for fast mode
FAST_DESIGN_MODE: True FAST_DESIGN_MODE: False
GROQ_API_KEY: "" GROQ_API_KEY: ""
MISTRAL_API_KEY: "" MISTRAL_API_KEY: ""

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

File diff suppressed because it is too large Load Diff

1
frontend/.env Normal file
View File

@@ -0,0 +1 @@
API_BASE=http://127.0.0.1:8000

View File

@@ -16,9 +16,16 @@ 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']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElIcon: typeof import('element-plus/es')['ElIcon']
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']
ElTag: typeof import('element-plus/es')['ElTag']
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']

View File

@@ -2,7 +2,7 @@
<html lang=""> <html lang="">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/logo.jpg"> <link rel="icon" href="/logo.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>多智能体协同平台</title> <title>多智能体协同平台</title>
</head> </head>

View File

@@ -17,8 +17,13 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@jsplumb/browser-ui": "^6.2.10", "@jsplumb/browser-ui": "^6.2.10",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.3",
"@vue-flow/core": "^1.48.1",
"@vue-flow/minimap": "^1.5.4",
"@vueuse/core": "^14.0.0", "@vueuse/core": "^14.0.0",
"axios": "^1.12.2", "axios": "^1.12.2",
"dompurify": "^3.3.0", "dompurify": "^3.3.0",
@@ -27,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"

9113
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

BIN
frontend/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -0,0 +1,42 @@
import type { Directive, DirectiveBinding } from 'vue'
import { useConfigStoreHook } from '@/stores'
/**
* 开发模式专用指令
* 只在开发模式下显示元素,生产模式下会移除该元素
*
* \@param binding.value - 是否开启该功能,默认为 true
* @example
* \!-- 默认开启,开发模式显示 --
* \!div v-dev-only开发模式内容</div>
*
* \!-- 传入参数控制 --
* <div v-dev-only="true">开启指令</div>
* <div v-dev-only="false">取消指令</div>
*/
export const devOnly: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
checkAndRemoveElement(el, binding)
},
updated(el: HTMLElement, binding: DirectiveBinding) {
checkAndRemoveElement(el, binding)
},
}
const configStore = useConfigStoreHook()
/**
* 检查并移除元素的逻辑
*/
function checkAndRemoveElement(el: HTMLElement, binding: DirectiveBinding) {
const isDev = typeof configStore.config.dev === 'boolean' ? configStore.config.dev : import.meta.env.DEV
// 默认值为 true如果没有传值或者传值为 true 都启用
const shouldEnable = binding.value !== false
// 如果不是开发模式或者明确禁用,移除该元素
if (!isDev && shouldEnable) {
if (el.parentNode) {
el.parentNode.removeChild(el)
}
}
}

View File

@@ -0,0 +1,9 @@
import type { App } from 'vue'
import { devOnly } from './devOnly'
// 全局注册 directive
export function setupDirective(app: App<Element>) {
app.directive('dev-only', devOnly)
}

View File

@@ -1,15 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from 'vue'
import Layout from './layout/index.vue' import Layout from './layout/index.vue'
onMounted(() => {
document.documentElement.classList.add('dark')
})
</script> </script>
<template> <template>
<layout /> <Layout />
</template> </template>
<style lang="scss"> <style lang="scss">
@@ -29,11 +23,12 @@ onMounted(() => {
.el-card { .el-card {
border-radius: 8px; border-radius: 8px;
background: var(--color-bg-tertiary); background: var(--color-bg-tertiary);
border: 2px solid var(--color-bg-tertiary); border: 2px solid var(--color-card-border);
box-shadow: var(--color-card-border-hover);
&:hover { &:hover {
background: #171B22; background: var(--color-bg-content-hover);
box-shadow: none !important; box-shadow: none;
transition: background-color 0.3s ease-in-out; transition: background-color 0.3s ease-in-out;
} }
@@ -50,14 +45,16 @@ onMounted(() => {
.active-card { .active-card {
border: 2px solid transparent; border: 2px solid transparent;
$bg: var(--el-input-bg-color, var(--el-fill-color-blank)); $bg: var(--el-input-bg-color, var(--el-fill-color-blank));
background: background: linear-gradient(
linear-gradient(var(--color-bg-tertiary), var(--color-bg-tertiary)) padding-box, var(--color-agent-list-selected-bg),
var(--color-agent-list-selected-bg)
)
padding-box,
linear-gradient(to right, #00c8d2, #315ab4) border-box; linear-gradient(to right, #00c8d2, #315ab4) border-box;
color: var(--color-text); color: var(--color-text);
} }
} }
/* 1. 定义流动动画:让虚线沿路径移动 */ /* 1. 定义流动动画:让虚线沿路径移动 */
@keyframes flowAnimation { @keyframes flowAnimation {
to { to {
@@ -71,14 +68,13 @@ onMounted(() => {
} }
} }
/* 2. 为jsPlumb连线绑定动画作用于SVG的path元素 */ /* 2. 为jsPlumb连线绑定动画作用于SVG的path元素 */
/* jtk-connector是jsPlumb连线的默认SVG类path是实际的线条元素 */ /* jtk-connector是jsPlumb连线的默认SVG类path是实际的线条元素 */
.jtk-connector-output path { .jtk-connector-output path {
/* 定义虚线规则线段长度5px + 间隙3px总长度8px与动画偏移量匹配 */ /* 定义虚线规则线段长度5px + 间隙3px总长度8px与动画偏移量匹配 */
stroke-dasharray: 5 3; stroke-dasharray: 5 3;
/* 应用动画:名称+时长+线性速度+无限循环 */ /* 应用动画:名称+时长+线性速度+无限循环 */
animation: flowAnimationReverse .5s linear infinite; animation: flowAnimationReverse 0.5s linear infinite;
/* 可选:设置线条基础样式(颜色、宽度) */ /* 可选:设置线条基础样式(颜色、宽度) */
stroke-width: 2; stroke-width: 2;
} }
@@ -89,7 +85,7 @@ onMounted(() => {
/* 定义虚线规则线段长度5px + 间隙3px总长度8px与动画偏移量匹配 */ /* 定义虚线规则线段长度5px + 间隙3px总长度8px与动画偏移量匹配 */
stroke-dasharray: 5 3; stroke-dasharray: 5 3;
/* 应用动画:名称+时长+线性速度+无限循环 */ /* 应用动画:名称+时长+线性速度+无限循环 */
animation: flowAnimationReverse .5s linear infinite; animation: flowAnimationReverse 0.5s linear infinite;
/* 可选:设置线条基础样式(颜色、宽度) */ /* 可选:设置线条基础样式(颜色、宽度) */
stroke-width: 2; stroke-width: 2;
} }

View File

@@ -1,5 +1,21 @@
import request from '@/utils/request' import request from '@/utils/request'
import type { Agent, IRawPlanResponse } from '@/stores' import websocket from '@/utils/websocket'
import type { Agent, IApiStepTask, IRawPlanResponse, IRawStepTask } from '@/stores'
import {
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
@@ -19,9 +35,64 @@ export type IExecuteRawResponse = {
ActionHistory: ActionHistory[] ActionHistory: ActionHistory[]
} }
/**
* SSE 流式事件类型
*/
export type StreamingEvent =
| {
type: 'step_start'
step_index: number
total_steps: number
step_name: string
task_description?: string
}
| {
type: 'action_complete'
step_index: number
step_name: string
action_index: number
total_actions: number
completed_actions: number
action_result: ActionHistory
batch_info?: {
batch_index: number
batch_size: number
is_parallel: boolean
}
}
| {
type: 'step_complete'
step_index: number
step_name: string
step_log_node: any
object_log_node: any
}
| {
type: 'execution_complete'
total_steps: number
}
| {
type: 'error'
message: string
}
export interface IFillAgentSelectionRequest {
goal: string
stepTask: IApiStepTask
agents: string[]
}
class Api { class Api {
// 智能体信息 // 默认使用WebSocket
setAgents = (data: Pick<Agent, 'Name' | 'Profile'>[]) => { private useWebSocketDefault = true
setAgents = (data: Pick<Agent, 'Name' | 'Profile' | 'apiUrl' | 'apiKey' | 'apiModel'>[], useWebSocket: boolean = this.useWebSocketDefault) => {
// 如果启用WebSocket且已连接使用WebSocket
if (useWebSocket && websocket.connected) {
return websocket.send('set_agents', data)
}
// 否则使用REST API
return request({ return request({
url: '/setAgents', url: '/setAgents',
data, data,
@@ -29,13 +100,38 @@ class Api {
}) })
} }
generateBasePlan = (data: { goal: string; inputs: string[] }) => { generateBasePlan = (data: {
goal: string
inputs: string[]
apiUrl?: string
apiKey?: string
apiModel?: string
useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void
}) => {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
// 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) {
return websocket.send('generate_base_plan', {
'General Goal': data.goal,
'Initial Input Object': data.inputs,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
apiModel: data.apiModel,
}, undefined, data.onProgress)
}
// 否则使用REST API
return request<unknown, IRawPlanResponse>({ return request<unknown, IRawPlanResponse>({
url: '/generate_basePlan', url: '/generate_basePlan',
method: 'POST', method: 'POST',
data: { data: {
'General Goal': data.goal, 'General Goal': data.goal,
'Initial Input Object': data.inputs, 'Initial Input Object': data.inputs,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
apiModel: data.apiModel,
}, },
}) })
} }
@@ -50,14 +146,14 @@ class Api {
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'],
'Collaboration Process': plan['Collaboration Process']?.map(step => ({ 'Collaboration Process': plan['Collaboration Process']?.map((step) => ({
StepName: step.StepName, StepName: step.StepName,
TaskContent: step.TaskContent, TaskContent: step.TaskContent,
InputObject_List: step.InputObject_List, InputObject_List: step.InputObject_List,
OutputObject: step.OutputObject, OutputObject: step.OutputObject,
AgentSelection: step.AgentSelection, AgentSelection: step.AgentSelection,
Collaboration_Brief_frontEnd: step.Collaboration_Brief_FrontEnd, Collaboration_Brief_frontEnd: step.Collaboration_Brief_frontEnd,
TaskProcess: step.TaskProcess.map(action => ({ TaskProcess: step.TaskProcess.map((action) => ({
ActionType: action.ActionType, ActionType: action.ActionType,
AgentName: action.AgentName, AgentName: action.AgentName,
Description: action.Description, Description: action.Description,
@@ -69,6 +165,742 @@ class Api {
}, },
}) })
} }
/**
* 优化版流式执行计划(支持动态追加步骤)
* 步骤级流式 + 动作级智能并行 + 动态追加步骤
*/
executePlanOptimized = (
plan: IRawPlanResponse,
onMessage: (event: StreamingEvent) => void,
onError?: (error: Error) => void,
onComplete?: () => void,
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 data = {
RehearsalLog: rehearsalLog || [], // ✅ 使用传递的 RehearsalLog
num_StepToRun: null,
existingKeyObjects: existingKeyObjects || {},
enable_dynamic: enableDynamic || false,
execution_id: executionId || null,
restart_from_step_index: restartFromStepIndex ?? null, // 新增:传递重新执行索引
plan: {
'Initial Input Object': plan['Initial Input Object'],
'General Goal': plan['General Goal'],
'Collaboration Process': plan['Collaboration Process']?.map((step) => ({
StepName: step.StepName,
TaskContent: step.TaskContent,
InputObject_List: step.InputObject_List,
OutputObject: step.OutputObject,
AgentSelection: step.AgentSelection,
Collaboration_Brief_frontEnd: step.Collaboration_Brief_frontEnd,
TaskProcess: step.TaskProcess.map((action) => ({
ActionType: action.ActionType,
AgentName: action.AgentName,
Description: action.Description,
ID: action.ID,
ImportantInput: action.ImportantInput,
})),
})),
},
}
// 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) {
websocket.subscribe(
'execute_plan_optimized',
data,
// onProgress
(progressData) => {
try {
let event: StreamingEvent
// 处理不同类型的progress数据
if (typeof progressData === 'string') {
event = JSON.parse(progressData)
} else {
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)
} catch (e) {
// Failed to parse WebSocket data
}
},
// onComplete
() => {
onComplete?.()
},
// onError
(error) => {
onError?.(error)
}
)
return
}
// 否则使用原有的SSE方式
fetch('/api/executePlanOptimized', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(async (response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const reader = response.body?.getReader()
const decoder = new TextDecoder()
if (!reader) {
throw new Error('Response body is null')
}
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) {
onComplete?.()
break
}
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6)
try {
const event = JSON.parse(data)
onMessage(event)
} catch (e) {
// Failed to parse SSE data
}
}
}
}
})
.catch((error) => {
onError?.(error)
})
}
/**
* 分支任务大纲
*/
branchPlanOutline = (data: {
branch_Number: number
Modification_Requirement: string
Existing_Steps: IRawStepTask[]
Baseline_Completion: number
initialInputs: string[]
goal: string
useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void
}) => {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
// 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) {
return websocket.send('branch_plan_outline', {
branch_Number: data.branch_Number,
Modification_Requirement: data.Modification_Requirement,
Existing_Steps: data.Existing_Steps,
Baseline_Completion: data.Baseline_Completion,
'Initial Input Object': data.initialInputs,
'General Goal': data.goal,
}, undefined, data.onProgress)
}
// 否则使用REST API
return request<unknown, IRawPlanResponse>({
url: '/branch_PlanOutline',
method: 'POST',
data: {
branch_Number: data.branch_Number,
Modification_Requirement: data.Modification_Requirement,
Existing_Steps: data.Existing_Steps,
Baseline_Completion: data.Baseline_Completion,
'Initial Input Object': data.initialInputs,
'General Goal': data.goal,
},
})
}
/**
* 分支任务流程
*/
branchTaskProcess = (data: {
branch_Number: number
Modification_Requirement: string
Existing_Steps: BranchAction[]
Baseline_Completion: number
stepTaskExisting: any
goal: string
useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void
}) => {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
// 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) {
return websocket.send('branch_task_process', {
branch_Number: data.branch_Number,
Modification_Requirement: data.Modification_Requirement,
Existing_Steps: data.Existing_Steps,
Baseline_Completion: data.Baseline_Completion,
stepTaskExisting: data.stepTaskExisting,
'General Goal': data.goal,
}, undefined, data.onProgress)
}
// 否则使用REST API
return request<unknown, BranchAction[][]>({
url: '/branch_TaskProcess',
method: 'POST',
data: {
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,
},
})
}
fillStepTask = async (data: {
goal: string
stepTask: any
useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void
}): Promise<IRawStepTask> => {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
let response: any
// 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) {
response = await websocket.send('fill_step_task', {
'General Goal': data.goal,
stepTask: data.stepTask,
}, undefined, data.onProgress)
} else {
// 否则使用REST API
response = await request<
{
'General Goal': string
stepTask: any
},
{
AgentSelection?: string[]
Collaboration_Brief_FrontEnd?: {
template: string
data: Record<string, { text: string; color: number[] }>
}
InputObject_List?: string[]
OutputObject?: string
StepName?: string
TaskContent?: string
TaskProcess?: Array<{
ID: string
ActionType: string
AgentName: string
Description: string
ImportantInput: string[]
}>
}
>({
url: '/fill_stepTask',
method: 'POST',
data: {
'General Goal': data.goal,
stepTask: data.stepTask,
},
})
}
const vec2Hsl = (color: number[]): string => {
const [h, s, l] = color
return `hsl(${h}, ${s}%, ${l}%)`
}
const briefData: Record<string, { text: string; style?: Record<string, 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),
},
}
}
}
/**
* 构建前端格式的 IRawStepTask
*/
return {
StepName: response.StepName || '',
TaskContent: response.TaskContent || '',
InputObject_List: response.InputObject_List || [],
OutputObject: response.OutputObject || '',
AgentSelection: response.AgentSelection || [],
Collaboration_Brief_frontEnd: {
template: response.Collaboration_Brief_FrontEnd?.template || '',
data: briefData,
},
TaskProcess: response.TaskProcess || [],
}
}
fillStepTaskTaskProcess = async (data: {
goal: string
stepTask: IApiStepTask
agents: string[]
useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void
}): Promise<IApiStepTask> => {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
let response: any
// 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) {
response = await websocket.send('fill_step_task_process', {
'General Goal': data.goal,
stepTask_lackTaskProcess: {
StepName: data.stepTask.name,
TaskContent: data.stepTask.content,
InputObject_List: data.stepTask.inputs,
OutputObject: data.stepTask.output,
AgentSelection: data.agents,
},
}, undefined, data.onProgress)
} else {
// 否则使用REST API
response = await request<
{
'General Goal': string
stepTask_lackTaskProcess: {
StepName: string
TaskContent: string
InputObject_List: string[]
OutputObject: string
AgentSelection: string[]
}
},
{
StepName?: string
TaskContent?: string
InputObject_List?: string[]
OutputObject?: string
AgentSelection?: string[]
TaskProcess?: Array<{
ID: string
ActionType: string
AgentName: string
Description: string
ImportantInput: string[]
}>
Collaboration_Brief_FrontEnd?: {
template: string
data: Record<string, { text: string; color: number[] }>
}
}
>({
url: '/fill_stepTask_TaskProcess',
method: 'POST',
data: {
'General Goal': data.goal,
stepTask_lackTaskProcess: {
StepName: data.stepTask.name,
TaskContent: data.stepTask.content,
InputObject_List: data.stepTask.inputs,
OutputObject: data.stepTask.output,
AgentSelection: data.agents,
},
},
})
}
const vec2Hsl = (color: number[]): string => {
const [h, s, l] = color
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: any) => ({
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,
}
}
/**
* 为每个智能体评分
*/
agentSelectModifyInit = async (data: {
goal: string
stepTask: any
useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void
}): Promise<Record<string, Record<string, { reason: string; score: number }>>> => {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
let response: Record<string, Record<string, { Reason: string; Score: number }>>
// 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) {
response = await websocket.send('agent_select_modify_init', {
'General Goal': data.goal,
stepTask: {
StepName: data.stepTask.StepName || data.stepTask.name,
TaskContent: data.stepTask.TaskContent || data.stepTask.content,
InputObject_List: data.stepTask.InputObject_List || data.stepTask.inputs,
OutputObject: data.stepTask.OutputObject || data.stepTask.output,
},
}, undefined, data.onProgress)
} else {
// 否则使用REST API
response = await request<
{
'General Goal': string
stepTask: any
},
Record<string, Record<string, { Reason: string; Score: number }>>
>({
url: '/agentSelectModify_init',
method: 'POST',
data: {
'General Goal': data.goal,
stepTask: {
StepName: data.stepTask.StepName || data.stepTask.name,
TaskContent: data.stepTask.TaskContent || data.stepTask.content,
InputObject_List: data.stepTask.InputObject_List || data.stepTask.inputs,
OutputObject: data.stepTask.OutputObject || data.stepTask.output,
},
},
})
}
const transformedData: Record<string, Record<string, { reason: string; score: number }>> = {}
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
}
/**
* 添加新的评估维度
*/
agentSelectModifyAddAspect = async (data: {
aspectList: string[]
useWebSocket?: boolean
onProgress?: (progress: { status: string; stage?: string; message?: string; [key: string]: any }) => void
}): Promise<{
aspectName: string
agentScores: Record<string, { score: number; reason: string }>
}> => {
const useWs = data.useWebSocket !== undefined ? data.useWebSocket : this.useWebSocketDefault
let response: Record<string, Record<string, { Reason: string; Score: number }>>
// 如果启用WebSocket且已连接使用WebSocket
if (useWs && websocket.connected) {
response = await websocket.send('agent_select_modify_add_aspect', {
aspectList: data.aspectList,
}, undefined, data.onProgress)
} else {
// 否则使用REST API
response = await request<
{
aspectList: string[]
},
Record<string, Record<string, { Reason: string; Score: number }>>
>({
url: '/agentSelectModify_addAspect',
method: 'POST',
data: {
aspectList: data.aspectList,
},
})
}
/**
* 获取新添加的维度
*/
const newAspect = data.aspectList[data.aspectList.length - 1]
if (!newAspect) {
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,
}
}
/**
* ==================== Mock API开发阶段使用====================
*为每个智能体评分
*/
mockAgentSelectModifyInit = async (): Promise<
Record<string, Record<string, { reason: string; score: number }>>
> => {
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
}
mockAgentSelectModifyAddAspect = async (data: {
aspectList: string[]
}): Promise<{
aspectName: string
agentScores: Record<string, { score: number; reason: string }>
}> => {
const response: BackendAgentScoreResponse = await mockBackendAgentSelectModifyAddAspect(
data.aspectList,
)
const newAspect = data.aspectList[data.aspectList.length - 1]
if (!newAspect) {
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
}
mockFillStepTask = async (data: { goal: string; stepTask: any }): Promise<any> => {
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
}
/**
* 向正在执行的任务追加新步骤
* @param executionId 执行ID
* @param newSteps 新步骤列表
* @returns 追加的步骤数量
*/
addStepsToExecution = async (executionId: string, newSteps: IRawStepTask[]): Promise<number> => {
if (!websocket.connected) {
throw new Error('WebSocket未连接')
}
const response = await websocket.send('add_steps_to_execution', {
execution_id: executionId,
new_steps: newSteps.map(step => ({
StepName: step.StepName,
TaskContent: step.TaskContent,
InputObject_List: step.InputObject_List,
OutputObject: step.OutputObject,
AgentSelection: step.AgentSelection,
Collaboration_Brief_frontEnd: step.Collaboration_Brief_frontEnd,
TaskProcess: step.TaskProcess.map(action => ({
ActionType: action.ActionType,
AgentName: action.AgentName,
Description: action.Description,
ID: action.ID,
ImportantInput: action.ImportantInput,
})),
})),
}) as { added_count: number }
return response?.added_count || 0
}
} }
export default new Api() export default new Api()

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1766247549273" class="icon"
viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="10050" xmlns:xlink="http://www.w3.org/1999/xlink"
width="16" height="16">
<path d="M512 1024C229.248 1024
0 794.752 0 512S229.248 0 512 0s512
229.248 512 512-229.248 512-512 512z
m0-938.666667C276.352 85.333333 85.333333
276.352 85.333333 512s191.018667 426.666667
426.666667 426.666667 426.666667-191.018667
426.666667-426.666667S747.648 85.333333 512
85.333333z m198.698667 625.365334a42.666667
42.666667 0 0 1-60.330667
0L512 572.330667l-138.368 138.368a42.666667 42.666667 0 0 1-60.330667-60.330667L451.669333 512 313.301333 373.632a42.666667 42.666667 0 0 1 60.330667-60.330667L512 451.669333l138.368-138.368a42.624 42.624 0 1 1 60.330667 60.330667L572.330667 512l138.368 138.368a42.666667 42.666667 0 0 1 0 60.330667z"
fill="#8e0707" p-id="10051">
</path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1766247454540" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9049"
xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16">
<path d="M512 1024C230.4 1024 0 793.6 0 512S230.4 0 512 0s512 230.4 512 512-230.4 512-512 512z m0-938.666667C277.333333 85.333333 85.333333 277.333333 85.333333 512s192 426.666667 426.666667 426.666667 426.666667-192 426.666667-426.666667S746.666667 85.333333 512 85.333333z" p-id="9050">
</path>
<path d="M708.266667 375.466667c-17.066667-17.066667-44.8-17.066667-59.733334 0l-181.333333 181.333333-91.733333-91.733333c-14.933333-14.933333-40.533333-14.933333-55.466667 0l-6.4 4.266666c-14.933333 14.933333-14.933333 40.533333 0 55.466667l125.866667 125.866667c14.933333 14.933333 40.533333 14.933333 55.466666 0l4.266667-4.266667 209.066667-209.066667c17.066667-17.066667 17.066667-44.8 0-61.866666z" p-id="9051">
</path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1766136633767" class="icon" viewBox="0 0 1028 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7819" width="32.125" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M1018.319924 112.117535q4.093748 9.210934 6.652341 21.492179t2.558593 25.585928-5.117186 26.609365-16.374994 25.585928q-12.281245 12.281245-22.003898 21.492179t-16.886712 16.374994q-8.187497 8.187497-15.351557 14.32812l-191.382739-191.382739q12.281245-11.257808 29.167958-27.121083t28.144521-25.074209q14.32812-11.257808 29.679676-15.863275t30.191395-4.093748 28.656239 4.605467 24.050772 9.210934q21.492179 11.257808 47.589826 39.402329t40.425766 58.847634zM221.062416 611.554845q6.140623-6.140623 28.656239-29.167958t56.289041-56.80076l74.710909-74.710909 82.898406-82.898406 220.038979-220.038979 191.382739 192.406177-220.038979 220.038979-81.874969 82.898406q-40.937484 39.914047-73.687472 73.175753t-54.242167 54.753885-25.585928 24.562491q-10.234371 9.210934-23.539054 19.445305t-27.632802 16.374994q-14.32812 7.16406-41.960921 17.398431t-57.824197 19.957024-57.312478 16.886712-40.425766 9.210934q-27.632802 3.070311-36.843736-8.187497t-5.117186-37.867173q2.046874-14.32812 9.722653-41.449203t16.374994-56.289041 16.886712-53.730448 13.304682-33.773425q6.140623-14.32812 13.816401-26.097646t22.003898-26.097646z" p-id="7820" fill="#000000"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

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

After

Width:  |  Height:  |  Size: 1004 B

View File

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

Before

Width:  |  Height:  |  Size: 886 B

After

Width:  |  Height:  |  Size: 890 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767084779395" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="21382" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M304.79872 108.70784c-118.75328 2.56-194.5856 51.74784-259.92192 144.73216-69.6832 99.1232-37.41696 233.31328-37.41696 233.31328s31.7696-100.06528 88.81664-147.74784c50.53952-42.22976 99.60448-78.11072 208.52224-82.97984v54.68672l141.69088-126.94016-141.69088-129.024v53.95968zM719.19616 915.25632c118.75328-2.56512 194.5856-51.712 259.96288-144.6912 69.64224-99.1232 37.37088-233.31328 37.37088-233.31328s-31.7696 100.06528-88.81152 147.74784c-50.51904 42.20928-99.6096 78.08512-208.52736 82.95936v-54.66624l-141.66528 126.93504 141.66528 129.024v-53.99552zM794.82368 304.37376h-88.64256c-72.77056 0-131.712 58.96192-131.712 131.712v110.96064c54.29248 22.21056 113.75616 34.49856 176.06656 34.49856 62.26944 0 121.728-12.288 176.02048-34.49856V436.08064c-0.00512-72.75008-58.96192-131.70688-131.73248-131.70688zM863.34464 167.58272c0 62.336-50.49856 112.87552-112.80896 112.87552-62.37696 0-112.87552-50.53952-112.87552-112.87552s50.49856-112.83456 112.87552-112.83456c62.30528 0 112.80896 50.49856 112.80896 112.83456z" fill="#ffffff" p-id="21383"></path><path d="M333.27616 692.08576H244.6336c-72.77056 0-131.73248 58.9824-131.73248 131.73248v110.94016c54.30784 22.23104 113.75104 34.49856 176.06656 34.49856 62.28992 0 121.74848-12.26752 176.02048-34.49856v-110.94016c0-72.75008-58.96192-131.73248-131.712-131.73248zM401.80224 555.31008c0 62.31552-50.47808 112.88064-112.83456 112.88064-62.37696 0-112.88064-50.56512-112.88064-112.88064 0-62.35136 50.50368-112.85504 112.88064-112.85504 62.35648 0 112.83456 50.5088 112.83456 112.85504z" fill="#ffffff" p-id="21384"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1766289101374" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2562" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M801.796646 348.507817c-6.599924 7.099918-9.599889 16.799806-8.299904 26.399694 1.499983 10.899874 2.399972 21.799748 2.699969 32.299627 1.399984 51.499404-11.099872 96.098888-37.09957 132.698464-45.099478 63.199269-117.398641 83.499034-170.098032 98.298863-35.799586 9.999884-61.599287 12.599854-82.399047 14.599831-22.799736 2.299973-34.299603 3.399961-50.599414 12.799851-3.199963 1.899978-6.399926 3.899955-9.49989 6.09993-29.499659 21.199755-46.399463 55.699355-46.399463 91.998935v28.599669c0 10.699876 5.499936 20.599762 14.399833 26.599692 30.299649 20.399764 50.199419 55.199361 49.599426 94.598906-0.799991 60.299302-49.999421 109.598732-110.398722 110.398722C291.002557 1024.89999 240.003148 974.400574 240.003148 912.001296c0-38.89955 19.799771-73.199153 49.899422-93.198921 8.799898-5.899932 14.099837-15.899816 14.099837-26.499694V231.60917c0-10.699876-5.499936-20.599762-14.399833-26.599692-30.299649-20.399764-50.199419-55.199361-49.599426-94.598906C240.803138 50.11127 290.002569 0.911839 350.40187 0.01185 413.001146-0.88814 464.000555 49.611276 464.000555 112.010554c0 38.89955-19.799771 73.199153-49.899422 93.198921-8.799898 5.899932-14.099837 15.899816-14.099837 26.499694v346.095994c0 4.099953 4.399949 6.599924 7.999908 4.599947h0.099998c34.299603-19.699772 62.099281-22.399741 88.99897-25.099709 18.799782-1.799979 38.299557-3.799956 65.899238-11.499867 43.299499-12.09986 92.398931-25.8997 117.898635-61.599287 16.999803-23.799725 20.599762-53.399382 19.099779-79.599079-0.699992-12.599854-8.699899-23.499728-20.399763-28.099675-42.099513-16.299811-71.899168-57.299337-71.599172-105.298781 0.399995-61.699286 51.299406-111.698707 112.998692-111.198714 61.399289 0.499994 110.998715 50.499416 110.998716 111.998704 0 29.599657-11.499867 56.499346-30.199651 76.499115z" p-id="2563" fill="#ffffff"></path></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764755052115" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6234" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M886.784 746.496q29.696 30.72 43.52 56.32t-4.608 58.368q-4.096 6.144-11.264 14.848t-14.848 16.896-15.36 14.848-12.8 9.728q-25.6 15.36-60.416 8.192t-62.464-34.816l-43.008-43.008-57.344-57.344-67.584-67.584-73.728-73.728-131.072 131.072q-60.416 60.416-98.304 99.328-38.912 38.912-77.312 48.128t-68.096-17.408l-7.168-7.168-11.264-11.264-11.264-11.264q-6.144-6.144-7.168-8.192-11.264-14.336-13.312-29.184t2.56-29.184 13.824-27.648 20.48-24.576q9.216-8.192 32.768-30.72l55.296-57.344q33.792-32.768 75.264-73.728t86.528-86.016q-49.152-49.152-93.696-93.184t-79.872-78.848-57.856-56.832-27.648-27.136q-26.624-26.624-27.136-52.736t17.92-52.736q8.192-10.24 23.552-24.064t21.504-17.92q30.72-20.48 55.296-17.92t49.152 28.16l31.744 31.744q23.552 23.552 58.368 57.344t78.336 76.288 90.624 88.576q38.912-38.912 76.288-75.776t69.632-69.12 58.368-57.856 43.52-43.008q24.576-23.552 53.248-31.232t55.296 12.8q1.024 1.024 6.656 5.12t11.264 9.216 10.752 9.728 7.168 5.632q27.648 26.624 27.136 57.856t-27.136 57.856q-18.432 18.432-45.568 46.08t-60.416 60.416-70.144 69.632l-77.824 77.824q37.888 36.864 74.24 72.192t67.584 66.048 56.32 56.32 41.472 41.984z" p-id="6235"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,14 @@
<svg
className="icon"
viewBox="0 0 8960 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="9634"
width="100%"
height="60"
>
<path
d="M8960 0c-451.52 181.184-171.2 1024-992 1024H992C171.232 1024 451.392 181.184 0 0h8960z"
p-id="9635"
></path>
</svg>

After

Width:  |  Height:  |  Size: 336 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764757574909" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9019" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M248.88888875 558.88888844l401.7777778 401.77777782c28.44444469 28.44444469 74.66666625 28.44444469 99.55555594 0 28.44444469-28.44444469 28.44444469-74.66666625 1e-8-99.555555l-352.00000031-352.00000032 352.00000031-351.99999937c28.44444469-28.44444469 28.44444469-71.11111125 0-99.55555594s-74.66666625-28.44444469-99.55555594 0L248.88888875 459.33333344c-14.22222188 14.22222188-21.33333375 31.99999969-21.33333281 49.7777775 0 17.7777778 7.11111094 35.55555562 21.33333281 49.7777775" fill="#ffffff" p-id="9020"></path></svg>

After

Width:  |  Height:  |  Size: 860 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764820450888" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="23116" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M854.442667 725.376c-117.845333 204.117333-378.837333 274.048-582.954667 156.202667A425.173333 425.173333 0 0 1 133.546667 754.346667a32 32 0 0 1 15.573333-48.298667c160.725333-57.514667 246.826667-124.16 296.789333-219.562667 52.565333-100.394667 66.176-210.346667 29.397334-361.088a32 32 0 0 1 32.810666-39.552 425.002667 425.002667 0 0 1 190.165334 56.618667c204.117333 117.845333 274.048 378.837333 156.16 582.912z" fill="#ffffff" p-id="23117"></path></svg>

After

Width:  |  Height:  |  Size: 793 B

View File

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

Before

Width:  |  Height:  |  Size: 603 B

After

Width:  |  Height:  |  Size: 610 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764757483582" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7204" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M775.11111125 465.11111156l-401.7777778-401.77777782c-28.44444469-28.44444469-74.66666625-28.44444469-99.55555594 0-28.44444469 28.44444469-28.44444469 74.66666625-1e-8 99.555555l352.00000031 352.00000032-352.00000031 351.99999937c-28.44444469 28.44444469-28.44444469 71.11111125 0 99.55555594s74.66666625 28.44444469 99.55555594 0L775.11111125 564.66666656c14.22222188-14.22222188 21.33333375-31.99999969 21.33333281-49.7777775 0-17.7777778-7.11111094-35.55555562-21.33333281-49.7777775" fill="#d8d8d8" p-id="7205"></path></svg>

After

Width:  |  Height:  |  Size: 860 B

View File

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

After

Width:  |  Height:  |  Size: 709 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764820260653" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13095" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M152.00256 792.00256l76.00128-77.99808 60.00128 60.00128-76.00128 77.99808z m317.99808 165.99552V832h83.99872v125.99808H470.00064zM512 233.99936c141.99808 0 256 114.00192 256 256s-114.00192 256-256 256-256-114.00192-256-256 114.00192-256 256-256zM854.00064 448h128v86.00064h-128V448z m-118.00064 326.00064l60.00128-57.99936 76.00128 76.00128-60.00128 60.00128z m136.00256-584.00256l-76.00128 76.00128-60.00128-60.00128 76.00128-76.00128z m-318.0032-165.99552v125.99808H470.00064V24.00256h83.99872z m-384 423.99744v86.00064h-128V448h128zM288 205.99808L227.99872 265.99936 151.99744 189.99808 211.99872 129.9968z" p-id="13096" fill="#ffa300"></path></svg>

After

Width:  |  Height:  |  Size: 985 B

View File

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

After

Width:  |  Height:  |  Size: 894 B

View File

@@ -1,5 +1,11 @@
<template> <template>
<el-tooltip :disabled="!isOverflow" effect="light" placement="top" :content="text"> <el-tooltip
:disabled="!isOverflow"
effect="light"
placement="top"
:content="text"
popper-class="multi-line-tooltip-popper"
>
<div <div
ref="containerRef" ref="containerRef"
class="multi-line-ellipsis" class="multi-line-ellipsis"
@@ -28,7 +34,7 @@ interface Props {
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
text: '', text: '',
lines: 3, lines: 3,
maxWidth: '100%', maxWidth: '100%'
}) })
const isOverflow = ref(false) const isOverflow = ref(false)
@@ -45,8 +51,8 @@ const containerStyle = computed(
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
lineHeight: '1.5', lineHeight: '1.5',
wordBreak: 'break-all', wordBreak: 'break-all'
}) as HTMLAttributes['style'], } as HTMLAttributes['style'])
) )
// 检查文字是否溢出 // 检查文字是否溢出
@@ -91,3 +97,9 @@ onMounted(() => {
white-space: normal; white-space: normal;
} }
</style> </style>
<style>
.multi-line-tooltip-popper {
max-width: 400px;
}
</style>

View File

@@ -0,0 +1,260 @@
<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 { computed } from 'vue'
import {
Close,
SuccessFilled as IconSuccess,
WarningFilled as IconWarning,
CircleCloseFilled,
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-icon .success {
color: #67c23a;
}
.notification-icon .warning {
color: #e6a23c;
}
.notification-icon .error {
color: #f56c6c;
}
.notification-icon .info {
color: #409eff;
}
.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

@@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
iconClass: string iconClass: string

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 index = notifications.value.findIndex((n) => n.id === id)
if (index !== -1) {
const notification = notifications.value[index]
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,21 +1,71 @@
<script setup lang="ts"> <script setup lang="ts">
import { useConfigStore } from '@/stores' import { useConfigStore } from '@/stores'
import { ref, onMounted, onUnmounted } from 'vue'
defineOptions({ defineOptions({
name: 'AppHeader' name: 'AppHeader'
}) })
const configStore = useConfigStore() const configStore = useConfigStore()
const isDark = ref(false)
// 检测当前主题
const checkDarkMode = () => {
isDark.value =
document.documentElement.classList.contains('dark') ||
window.matchMedia('(prefers-color-scheme: dark)').matches
}
onMounted(() => {
checkDarkMode()
// 监听系统主题变化
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
mediaQuery.addEventListener('change', checkDarkMode)
// 监听类名变化
const observer = new MutationObserver(checkDarkMode)
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
})
onUnmounted(() => {
mediaQuery.removeEventListener('change', checkDarkMode)
observer.disconnect()
})
})
</script> </script>
<template> <template>
<div class="bg-[var(--color-bg-secondary)] h-[60px] relative pl-[27px] font-[900] text-[24px]"> <div
<div class="absolute left-0 h-full flex items-center"> class="bg-[var(--color-bg)] h-[60px] relative pl-[28px] font-[900] text-[24px] absolute left-1/2 transform -translate-x-1/2 w-[600px] h-[60px] dark:bg-black flex items-center justify-center transition-[top]"
<img class="w-[36.8px] h-[36.8px] rounded-full mr-[12px]" src="/logo.jpg" alt="logo" /> >
<span class="text-[#9E0000]">{{ configStore.config.title }}</span>{{ configStore.config.subTitle }} <svg-icon
</div> icon-class="icons"
<div class="text-center h-full w-full tracking-[8.5px] flex items-center justify-center"> class="header-icon"
{{ configStore.config.centerTitle }} size="100% "
</div> :style="{
filter: isDark
? 'drop-shadow(0px 0px 5px rgba(0, 0, 0, 0.5))'
: 'drop-shadow(0px 0px 5px rgba(161, 161, 161, 0.2))',
stroke: 'var(--color-border)',
strokeWidth: '1px',
strokeLinejoin: 'round'
}"
/>
<h2
className="flex items-center gap-3 text-xl font-bold text-zinc-700 dark:text-zinc-300 mr-3 absolute left-1/2 transform -translate-x-1/2"
>
<img src="/logo.png" alt="logo" className="w-8" />
<p>
<span className="text-[#9e0000]">数联网</span
><span className="text-[var(--color-text-title)]">多智能体协同平台</span>
</p>
</h2>
</div> </div>
</template> </template>
<style scoped lang="scss">
.header-icon {
color: var(--color-header-bg);
}
</style>

View File

@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, onMounted, computed, reactive, nextTick } from 'vue'
import SvgIcon from '@/components/SvgIcon/index.vue' import SvgIcon from '@/components/SvgIcon/index.vue'
import { useAgentsStore, useConfigStore } from '@/stores' import { useAgentsStore, useConfigStore } from '@/stores'
import api from '@/api' import api from '@/api'
import websocket from '@/utils/websocket'
import { changeBriefs } from '@/utils/collaboration_Brief_FrontEnd.ts' import { changeBriefs } from '@/utils/collaboration_Brief_FrontEnd.ts'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import AssignmentButton from './TaskTemplate/TaskSyllabus/components/AssignmentButton.vue'
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,12 +14,190 @@ const emit = defineEmits<{
const agentsStore = useAgentsStore() const agentsStore = useAgentsStore()
const configStore = useConfigStore() const configStore = useConfigStore()
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 isExpanded = ref(false)
// 添加一个状态来跟踪是否正在填充步骤数据
const isFillingSteps = ref(false)
// 存储当前填充任务的取消函数
const currentStepAbortController = ref<{ cancel: () => void } | null>(null)
// 解析URL参数
function getUrlParam(param: string): string | null {
const urlParams = new URLSearchParams(window.location.search)
return urlParams.get(param)
}
const planReady = computed(() => {
return agentsStore.agentRawPlan.data !== undefined
})
const openAgentAllocationDialog = () => {
agentsStore.openAgentAllocationDialog()
}
// 自动搜索函数
async function autoSearchFromUrl() {
const query = getUrlParam('q')
if (query && !hasAutoSearched.value) {
// 解码URL参数
const decodedQuery = decodeURIComponent(query)
searchValue.value = decodedQuery
hasAutoSearched.value = true
// 延迟执行搜索,确保组件已完全渲染
setTimeout(() => {
handleSearch()
}, 100)
}
}
// 处理获取焦点事件
function handleFocus() {
isFocus.value = true
isExpanded.value = true // 搜索框展开
}
const taskContainerRef = ref<HTMLDivElement | null>(null)
// 处理失去焦点事件
function handleBlur() {
isFocus.value = false
// 延迟收起搜索框,以便点击按钮等操作
setTimeout(() => {
isExpanded.value = false
// 强制重置文本区域高度到最小行数
resetTextareaHeight()
}, 200)
}
// 🆕 预加载所有任务的智能体评分数据(顺序加载,确保任务详情已填充)
async function preloadAllTaskAgentScores(outlineData: any, goal: string) {
const tasks = outlineData['Collaboration Process'] || []
if (tasks.length === 0) {
console.log(' 没有任务需要预加载评分数据')
return
}
console.log(`🚀 开始预加载 ${tasks.length} 个任务的智能体评分数据...`)
// 🆕 顺序预加载:等待每个任务详情填充完成后再预加载其评分
for (const task of tasks) {
// 确保任务有 Id
if (!task.Id) {
task.Id = `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
}
const taskId = task.Id
// 检查是否已有缓存数据
if (agentsStore.hasTaskScoreData(taskId)) {
console.log(`⏭️ 任务 "${task.StepName}" (${taskId}) 已有缓存数据,跳过`)
continue
}
// 🆕 等待任务详情填充完成(通过检查 AgentSelection 是否存在)
// 最多等待 60 秒,超时则跳过该任务
let waitCount = 0
const maxWait = 60 // 60 * 500ms = 30秒
while (!task.AgentSelection && waitCount < maxWait) {
await new Promise(resolve => setTimeout(resolve, 500))
waitCount++
}
if (!task.AgentSelection) {
console.warn(`⚠️ 任务 "${task.StepName}" (${taskId}) 详情未填充完成,跳过评分预加载`)
continue
}
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] || {}) : []
// 存储到 store按任务ID存储
agentsStore.setTaskScoreData(taskId, {
aspectList,
agentScores
})
console.log(`✅ 任务 "${task.StepName}" (${taskId}) 的评分数据预加载完成,维度数: ${aspectList.length}`)
} catch (error) {
console.error(`❌ 任务 "${task.StepName}" (${taskId}) 的评分数据预加载失败:`, error)
}
}
console.log(`🎉 所有 ${tasks.length} 个任务的智能体评分数据预加载完成(或已跳过)`)
}
// 重置文本区域高度到最小行数
function resetTextareaHeight() {
nextTick(() => {
// 获取textarea元素
const textarea =
document.querySelector('#task-container .el-textarea__inner') ||
document.querySelector('#task-container textarea')
if (textarea instanceof HTMLElement) {
// 强制设置最小高度
textarea.style.height = 'auto'
textarea.style.minHeight = '56px'
textarea.style.overflowY = 'hidden'
}
})
}
// 停止填充数据的处理函数
async function handleStop() {
try {
// 通过 WebSocket 发送停止信号
if (websocket.connected) {
await websocket.send('stop_generation', {
goal: searchValue.value
})
ElMessage.success('已发送停止信号,正在停止生成...')
} else {
ElMessage.warning('WebSocket 未连接,无法停止')
}
} catch (error) {
console.error('停止生成失败:', error)
ElMessage.error('停止生成失败')
} finally {
// 无论后端是否成功停止,都重置状态
isFillingSteps.value = false
currentStepAbortController.value = null
// 标记用户已停止填充
agentsStore.setHasStoppedFilling(true)
}
}
// 处理按钮点击事件
function handleButtonClick() {
if (isFillingSteps.value) {
// 如果正在填充数据,点击停止
handleStop()
} else {
// 否则开始搜索
handleSearch()
}
}
async function handleSearch() { async function handleSearch() {
// 用于标记大纲是否成功加载
let outlineLoaded = false
try { try {
triggerOnFocus.value = false triggerOnFocus.value = false
if (!searchValue.value) { if (!searchValue.value) {
@@ -29,25 +207,139 @@ async function handleSearch() {
emit('search-start') emit('search-start')
agentsStore.resetAgent() agentsStore.resetAgent()
agentsStore.setAgentRawPlan({ loading: true }) agentsStore.setAgentRawPlan({ loading: true })
const data = await api.generateBasePlan({ // 重置停止状态
agentsStore.setHasStoppedFilling(false)
// 获取大纲
const outlineData = await api.generateBasePlan({
goal: searchValue.value, goal: searchValue.value,
inputs: [], inputs: []
}) })
data['Collaboration Process'] = changeBriefs(data['Collaboration Process'])
agentsStore.setAgentRawPlan({ data }) // 检查是否已被停止
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) emit('search', searchValue.value)
// 🆕 预加载所有任务的智能体评分数据(在后台静默执行)
preloadAllTaskAgentScores(outlineData, 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) {
await fillStepWithRetry(step)
}
} finally { } finally {
triggerOnFocus.value = true triggerOnFocus.value = true
// 完成填充,重置状态
isFillingSteps.value = false
currentStepAbortController.value = null
// 如果大纲加载失败确保关闭loading
if (!outlineLoaded) {
agentsStore.setAgentRawPlan({ loading: false }) agentsStore.setAgentRawPlan({ loading: false })
} }
} }
}
// 辅助函数:更新单个步骤的详情
function updateStepDetail(stepId: string, detailedStep: any) {
const planData = agentsStore.agentRawPlan.data
if (!planData) return
const collaborationProcess = planData['Collaboration Process']
if (!collaborationProcess) return
const index = collaborationProcess.findIndex((s: any) => s.StepName === stepId)
if (index !== -1 && collaborationProcess[index]) {
// 保持响应式更新 - 使用 Vue 的响应式系统
Object.assign(collaborationProcess[index], {
AgentSelection: detailedStep.AgentSelection || [],
TaskProcess: detailedStep.TaskProcess || [],
Collaboration_Brief_frontEnd: detailedStep.Collaboration_Brief_frontEnd || {
template: '',
data: {}
}
})
}
}
const querySearch = (queryString: string, cb: (v: { value: string }[]) => void) => { 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 // call callback function to return suggestions
cb(results.map((item) => ({ value: item }))) cb(results.map(item => ({ value: item })))
} }
const createFilter = (queryString: string) => { const createFilter = (queryString: string) => {
@@ -56,7 +348,10 @@ const createFilter = (queryString: string) => {
} }
} }
const taskContainerRef = ref<HTMLDivElement | null>(null) // 组件挂载时检查URL参数
onMounted(() => {
autoSearchFromUrl()
})
</script> </script>
<template> <template>
@@ -67,13 +362,20 @@ const taskContainerRef = ref<HTMLDivElement | null>(null)
:disabled="agentsStore.agents.length > 0" :disabled="agentsStore.agents.length > 0"
> >
<div class="task-root-container"> <div class="task-root-container">
<div class="task-container" ref="taskContainerRef" id="task-container"> <div
<span class="text-[var(--color-text)] font-bold task-title">任务</span> class="task-container"
ref="taskContainerRef"
id="task-container"
:class="{ expanded: isExpanded }"
>
<span class="text-[var(--color-text-task)] font-bold task-title">任务</span>
<el-autocomplete <el-autocomplete
ref="autocompleteRef"
v-model.trim="searchValue" v-model.trim="searchValue"
class="task-input" class="task-input"
size="large" size="large"
:rows="isFocus ? 3 : 1" :rows="1"
:autosize="{ minRows: 1, maxRows: 10 }"
placeholder="请输入您的任务" placeholder="请输入您的任务"
type="textarea" type="textarea"
:append-to="taskContainerRef" :append-to="taskContainerRef"
@@ -81,9 +383,10 @@ const taskContainerRef = ref<HTMLDivElement | null>(null)
@change="agentsStore.setSearchValue" @change="agentsStore.setSearchValue"
:disabled="!(agentsStore.agents.length > 0)" :disabled="!(agentsStore.agents.length > 0)"
:debounce="0" :debounce="0"
:clearable="true"
:trigger-on-focus="triggerOnFocus" :trigger-on-focus="triggerOnFocus"
@focus="isFocus = true" @focus="handleFocus"
@blur="isFocus = false" @blur="handleBlur"
@select="isFocus = false" @select="isFocus = false"
> >
</el-autocomplete> </el-autocomplete>
@@ -91,20 +394,27 @@ const taskContainerRef = ref<HTMLDivElement | null>(null)
class="task-button" class="task-button"
color="linear-gradient(to right, #00C7D2, #315AB4)" color="linear-gradient(to right, #00C7D2, #315AB4)"
size="large" size="large"
title="点击搜索任务" :title="isFillingSteps ? '点击停止生成' : '点击搜索任务'"
circle circle
:loading="agentsStore.agentRawPlan.loading" :loading="agentsStore.agentRawPlan.loading"
:disabled="!searchValue" :disabled="!searchValue"
@click.stop="handleSearch" @click.stop="handleButtonClick"
> >
<SvgIcon <SvgIcon
v-if="!agentsStore.agentRawPlan.loading" v-if="!agentsStore.agentRawPlan.loading && !isFillingSteps"
icon-class="paper-plane" icon-class="paper-plane"
size="18px" size="18px"
color="var(--color-text)" color="#ffffff"
/>
<SvgIcon
v-if="!agentsStore.agentRawPlan.loading && isFillingSteps"
icon-class="stoprunning"
size="30px"
color="#ffffff"
/> />
</el-button> </el-button>
</div> </div>
<AssignmentButton v-if="planReady" @click="openAgentAllocationDialog" />
</div> </div>
</el-tooltip> </el-tooltip>
</template> </template>
@@ -120,10 +430,9 @@ const taskContainerRef = ref<HTMLDivElement | null>(null)
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)); $bg: var(--el-input-bg-color, var(--el-fill-color-blank));
background: background: linear-gradient(var(--color-bg-taskbar), var(--color-bg-taskbar)) padding-box,
linear-gradient(var(--color-bg-tertiary), var(--color-bg-tertiary)) padding-box,
linear-gradient(to right, #00c8d2, #315ab4) border-box; linear-gradient(to right, #00c8d2, #315ab4) border-box;
border-radius: 40px; border-radius: 30px;
position: absolute; position: absolute;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
@@ -131,28 +440,49 @@ const taskContainerRef = ref<HTMLDivElement | null>(null)
min-height: 100%; min-height: 100%;
overflow: hidden; overflow: hidden;
padding: 0 55px 0 47px; padding: 0 55px 0 47px;
transition: all 0.3s ease;
/* 搜索框展开时的样式 */
&.expanded {
box-shadow: var(--color-task-shadow);
:deep(.el-autocomplete .el-textarea .el-textarea__inner) {
overflow-y: auto !important;
min-height: 56px !important;
}
}
/* 非展开状态时,确保文本区域高度固定 */
&:not(.expanded) {
:deep(.el-textarea__inner) {
height: 56px !important;
overflow-y: hidden !important;
min-height: 56px !important;
}
}
:deep(.el-popper) { :deep(.el-popper) {
position: static !important; position: static !important;
width: 100%; width: calc(100% + 102px); /*增加左右padding的总和 */
min-width: 100%; min-width: calc(100% + 102px); /* 确保最小宽度也增加 */
background: var(--color-bg-tertiary); margin-left: -47px; /* 向左偏移左padding的值 */
box-shadow: none; margin-right: -55px; /*向右偏移右padding的值 */
background: var(--color-bg-taskbar);
border: none; border: none;
transition: height 0s ease-in-out; transition: height 0s ease-in-out;
border-top: 1px solid #494B51; border-top: 1px solid var(--color-border);
border-radius: 0; border-radius: 0;
box-shadow: none;
li { li {
height: 45px; height: 45px;
box-sizing: border-box; box-sizing: border-box;
line-height: 45px; line-height: 45px;
font-size: 14px; font-size: 14px;
padding-left: 27px;
&:hover { &:hover {
background: #1C1E25; background: var(--color-bg-hover);
color: #00F3FF; color: var(--color-text-hover);
} }
} }
@@ -175,8 +505,14 @@ const taskContainerRef = ref<HTMLDivElement | null>(null)
font-size: 14px; font-size: 14px;
height: 100%; height: 100%;
line-height: 1.5; line-height: 1.5;
padding: 18px 0; padding: 18px 0 0 18px;
resize: none; resize: none;
color: var(--color-text-taskbar);
/* 聚焦时的样式 */
.expanded & {
overflow-y: auto;
}
&::placeholder { &::placeholder {
line-height: 1.2; line-height: 1.2;
@@ -192,11 +528,10 @@ const taskContainerRef = ref<HTMLDivElement | null>(null)
} }
} }
.task-title { .task-title {
position: absolute; position: absolute;
top: 28px; top: 28px;
left: 10px; left: 27px;
z-index: 999; z-index: 999;
transform: translateY(-50%); transform: translateY(-50%);
} }
@@ -221,4 +556,58 @@ const taskContainerRef = ref<HTMLDivElement | null>(null)
} }
} }
} }
.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,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { getActionTypeDisplay, getAgentMapIcon } from '@/layout/components/config.ts' import { getActionTypeDisplay, getAgentMapIcon } from '@/layout/components/config.ts'
import SvgIcon from '@/components/SvgIcon/index.vue' import SvgIcon from '@/components/SvgIcon/index.vue'
import { type Agent, useAgentsStore } from '@/stores' import { type Agent, useAgentsStore } from '@/stores'
@@ -10,9 +11,9 @@ const porps = defineProps<{
const taskProcess = computed(() => { const taskProcess = computed(() => {
const list = agentsStore.currentTask?.TaskProcess ?? [] const list = agentsStore.currentTask?.TaskProcess ?? []
return list.map((item) => ({ return list.map(item => ({
...item, ...item,
key: uuidv4(), key: uuidv4()
})) }))
}) })
@@ -26,35 +27,40 @@ const agentsStore = useAgentsStore()
class="user-item" class="user-item"
:class="agentsStore.currentTask?.AgentSelection?.includes(item.Name) ? 'active-card' : ''" :class="agentsStore.currentTask?.AgentSelection?.includes(item.Name) ? 'active-card' : ''"
> >
<div class="flex items-center justify-between relative h-[41px]"> <div class="flex items-center justify-between relative h-[43px]">
<!-- 图标区域 -->
<div <div
class="w-[44px] h-[44px] rounded-full flex items-center justify-center flex-shrink-0 relative right-[2px] icon-container" class="w-[44px] h-[44px] rounded-full flex items-center justify-center flex-shrink-0 relative right-[2px] icon-container"
:style="{ background: getAgentMapIcon(item.Name).color }" :style="{ background: getAgentMapIcon(item.Name).color }"
> >
<svg-icon <svg-icon :icon-class="getAgentMapIcon(item.Name).icon" color="#fff" size="24px" />
:icon-class="getAgentMapIcon(item.Name).icon"
color="var(--color-text)"
size="24px"
/>
</div> </div>
<div class="flex-1 text-[14px] flex flex-col items-end justify-end truncate ml-1">
<div class="flex-1 text-[14px] textClass flex flex-col items-end justify-start truncate ml-1">
<div class="flex items-center justify-start gap-2 w-full">
<span <span
class="w-full truncate text-right" class="truncate"
:style=" :style="
agentsStore.currentTask?.AgentSelection?.includes(item.Name) ? 'color:#00F3FF' : '' agentsStore.currentTask?.AgentSelection?.includes(item.Name)
? 'color:var(--color-accent)'
: ''
" "
>{{ item.Name }}</span >{{ item.Name }}</span
> >
</div>
<div <div
v-if="agentsStore.currentTask?.AgentSelection?.includes(item.Name)" v-if="agentsStore.currentTask?.AgentSelection?.includes(item.Name)"
class="flex items-center gap-[7px] h-[8px] mr-1" class="flex items-center gap-[7px] h-[8px] mr-1"
> >
<!-- 小圆点 --> <!-- 小圆点 -->
<div <div
v-for="item1 in taskProcess.filter((i) => i.AgentName === item.Name)" v-for="item1 in taskProcess.filter(i => i.AgentName === item.Name)"
:key="item1.key" :key="item1.key"
class="w-[6px] h-[6px] rounded-full" class="w-[6px] h-[6px] rounded-full"
:style="{ background: getActionTypeDisplay(item1.ActionType)?.color }" :style="{
background: getActionTypeDisplay(item1.ActionType)?.color,
border: `1px solid ${getActionTypeDisplay(item1.ActionType)?.border}`
}"
></div> ></div>
</div> </div>
</div> </div>
@@ -71,7 +77,7 @@ const agentsStore = useAgentsStore()
</div> </div>
<div class="p-[8px] pt-0"> <div class="p-[8px] pt-0">
<div <div
v-for="(item1, index1) in taskProcess.filter((i) => i.AgentName === item.Name)" v-for="(item1, index1) in taskProcess.filter(i => i.AgentName === item.Name)"
:key="item1.key" :key="item1.key"
class="text-[12px]" class="text-[12px]"
> >
@@ -89,9 +95,11 @@ const agentsStore = useAgentsStore()
</div> </div>
<!-- 分割线 --> <!-- 分割线 -->
<div <div
v-if="index1 !== taskProcess.filter((i) => i.AgentName === item.Name).length - 1" v-if="index1 !== taskProcess.filter(i => i.AgentName === item.Name).length - 1"
class="h-[1px] w-full bg-[#494B51] my-[8px]" class="h-[1px] w-full bg-[var(--color-border-default)] my-[8px]"
></div> ></div>
<AssignmentButton />
</div> </div>
</div> </div>
</div> </div>
@@ -100,13 +108,14 @@ const agentsStore = useAgentsStore()
<style scoped lang="scss"> <style scoped lang="scss">
.user-item { .user-item {
background: #1d222b; background: var(--color-agent-list-bg);
border-radius: 40px; border-radius: 40px;
padding-right: 12px; padding-right: 12px;
cursor: pointer; cursor: pointer;
transition: all 0.25s ease; transition: all 0.25s ease;
color: #969696; color: var(--color-text-detail);
border: 2px solid transparent; border: 1px solid var(--color-agent-list-border);
box-sizing: border-box;
.duty-info { .duty-info {
transition: height 0.25s ease; transition: height 0.25s ease;
height: 0; height: 0;
@@ -118,16 +127,30 @@ const agentsStore = useAgentsStore()
} }
&:hover { &:hover {
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2); box-shadow: var(--color-agent-list-hover-shadow);
color: #b8b8b8; color: var(--color-text);
background: var(--color-agent-list-hover-bg);
border: 1px solid var(--color-agent-list-hover-border);
}
}
.textClass {
color: var(--color-text-agent-list);
&:hover {
color: var(--color-text-agent-list-hover);
} }
} }
.active-card { .active-card {
background: background: linear-gradient(var(--color-bg-quaternary), var(--color-bg-quaternary)) padding-box,
linear-gradient(#171B22, #171B22) padding-box, linear-gradient(to right, var(--color-accent), var(--color-accent-secondary)) border-box;
linear-gradient(to right, #00c8d2, #315ab4) border-box; border: 1px solid var(--color-agent-list-border);
&:hover { &:hover {
box-shadow: var(--color-agent-list-hover-shadow);
color: var(--color-text);
background: var(--color-agent-list-hover-bg);
border: 1px solid var(--color-agent-list-hover-border);
border-radius: 20px; border-radius: 20px;
.duty-info { .duty-info {
height: auto; height: auto;
@@ -138,4 +161,10 @@ const agentsStore = useAgentsStore()
} }
} }
} }
// 添加头像容器样式修复
.icon-container {
right: 0 !important;
margin-left: 0px;
}
</style> </style>

View File

@@ -1,13 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ElNotification } from 'element-plus' 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 { onMounted } from 'vue'
import { readConfig } from '@/utils/readJson.ts' import { readConfig } from '@/utils/readJson.ts'
import AgentRepoList from './AgentRepoList.vue' import AgentRepoList from './AgentRepoList.vue'
@@ -19,7 +17,9 @@ onMounted(async () => {
const res = await readConfig<Agent[]>('agent.json') const res = await readConfig<Agent[]>('agent.json')
agentsStore.setAgents(res) agentsStore.setAgents(res)
} }
await api.setAgents(agentsStore.agents.map((item) => pick(item, ['Name', 'Profile']))) await api.setAgents(
agentsStore.agents.map(item => pick(item, ['Name', 'Profile', 'apiUrl', 'apiKey', 'apiModel']))
)
}) })
// 上传agent文件 // 上传agent文件
@@ -37,50 +37,77 @@ const handleFileSelect = (event: Event) => {
} }
} }
const readFileContent = async (file: File) => { // 验证API配置三个字段必须同时存在或同时不存在
const validateApiConfig = (agent: any) => {
const hasApiUrl = 'apiUrl' in agent
const hasApiKey = 'apiKey' in agent
const hasApiModel = 'apiModel' in agent
return hasApiUrl === hasApiKey && hasApiKey === hasApiModel
}
const readFileContent = (file: File) => {
const reader = new FileReader() const reader = new FileReader()
reader.onload = async (e) => { reader.onload = e => {
if (!e.target?.result) { try {
const content = e.target?.result as string
const jsonData = JSON.parse(content)
if (!Array.isArray(jsonData)) {
ElMessage.error('JSON格式错误: 必须为数组格式')
return return
} }
try {
const json = JSON.parse(e.target.result?.toString?.() ?? '{}') const validAgents = jsonData.filter((agent) => {
// 处理 JSON 数据 // 验证必需字段
if (Array.isArray(json)) { if (!agent.Name || typeof agent.Name !== 'string') {
const isValid = json.every( return false
(item) => }
typeof item.Name === 'string' && if (!agent.Icon || typeof agent.Icon !== 'string') {
typeof item.Icon === 'string' && return false
typeof item.Profile === 'string', }
) if (!agent.Profile || typeof agent.Profile !== 'string') {
if (isValid) { return false
// 处理有效的 JSON 数据 }
agentsStore.setAgents(
json.map((item) => ({ // 验证API配置
Name: item.Name, if (!validateApiConfig(agent)) {
Icon: item.Icon.replace(/\.png$/, ''), return false
Profile: item.Profile, }
Classification: item.Classification,
})), return true
)
await api.setAgents(json.map((item) => pick(item, ['Name', 'Profile'])))
} else {
ElNotification.error({
title: '错误',
message: 'JSON 格式错误',
}) })
} // 修改发送到后端的数据
} else { const processedAgents = validAgents.map(agent => ({
console.error('JSON is not an array') Name: agent.Name,
ElNotification.error({ Profile: agent.Profile,
title: '错误', Icon: agent.Icon,
message: 'JSON 格式错误', Classification: agent.Classification || '',
apiUrl: agent.apiUrl,
apiKey: agent.apiKey,
apiModel: agent.apiModel
}))
agentsStore.setAgents(processedAgents)
// 调用API
api
.setAgents(processedAgents)
.then(() => {
ElMessage.success('智能体上传成功')
}) })
} .catch(() => {
} catch (e) { ElMessage.error('智能体上传失败')
console.error(e) })
} catch {
ElMessage.error('JSON解析错误')
} }
} }
reader.onerror = () => {
ElMessage.error('文件读取错误')
}
reader.readAsText(file) reader.readAsText(file)
} }
@@ -95,7 +122,7 @@ const agentList = computed(() => {
if (!agentsStore.agents.length) { if (!agentsStore.agents.length) {
return { return {
selected, selected,
unselected, unselected
} }
} }
for (const agent of agentsStore.agents) { for (const agent of agentsStore.agents) {
@@ -110,14 +137,14 @@ const agentList = computed(() => {
obj[agent.Classification] = arr obj[agent.Classification] = arr
unselected.push({ unselected.push({
title: agent.Classification, title: agent.Classification,
data: arr, data: arr
}) })
} }
} }
return { return {
selected, selected,
unselected: unselected, unselected: unselected
} }
}) })
</script> </script>
@@ -126,7 +153,7 @@ const agentList = computed(() => {
<div class="agent-repo h-full flex flex-col" id="agent-repo"> <div class="agent-repo h-full flex flex-col" id="agent-repo">
<!-- 头部 --> <!-- 头部 -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-[18px] font-bold">智能体库</span> <span class="text-[18px] font-bold text-[var(--color-text-title-header)]">智能体库</span>
<!-- 上传文件 --> <!-- 上传文件 -->
<input type="file" accept=".json" @change="handleFileSelect" class="hidden" ref="fileInput" /> <input type="file" accept=".json" @change="handleFileSelect" class="hidden" ref="fileInput" />
<div class="plus-button" @click="triggerFileSelect"> <div class="plus-button" @click="triggerFileSelect">
@@ -144,14 +171,22 @@ const agentList = computed(() => {
</div> </div>
</div> </div>
<!-- 底部提示栏 --> <!-- 底部提示栏 -->
<div class="w-full grid grid-cols-3 gap-x-[10px] bg-[#1d222b] rounded-[20px] p-[8px] mt-[10px]"> <div
class="w-full grid grid-cols-3 gap-x-[10px] bg-[var(--color-bg-indicator)] rounded-[20px] p-[8px] mt-[10px]"
>
<div <div
v-for="item in Object.values(agentMapDuty)" v-for="item in Object.values(agentMapDuty)"
:key="item.key" :key="item.key"
class="flex items-center justify-center gap-x-1" class="flex items-center justify-center gap-x-1"
> >
<div
class="w-[8px] h-[8px] rounded-full"
:style="{
background: item.color,
border: `1px solid ${item.border}`
}"
></div>
<span class="text-[12px]">{{ item.name }}</span> <span class="text-[12px]">{{ item.name }}</span>
<div class="w-[8px] h-[8px] rounded-full" :style="{ background: item.color }"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -162,7 +197,7 @@ const agentList = computed(() => {
padding: 0 8px; padding: 0 8px;
.plus-button { .plus-button {
background: #1d2128; background: var(--color-bg-tertiary);
width: 24px; width: 24px;
height: 24px; height: 24px;
padding: 0; padding: 0;
@@ -174,7 +209,7 @@ const agentList = computed(() => {
transition: all 0.3s ease; transition: all 0.3s ease;
&:hover { &:hover {
background: #374151; background: var(--color-bg-quaternary);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
} }
} }

View File

@@ -0,0 +1,324 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { getActionTypeDisplay } from '@/layout/components/config.ts'
import { useAgentsStore } from '@/stores'
import BranchButton from './components/TaskButton.vue'
const agentsStore = useAgentsStore()
const props = defineProps<{
step: {
Id?: string
TaskProcess: Array<{
ID: string
ActionType: string
AgentName: string
Description: string
}>
}
}>()
const emit = defineEmits<{
(e: 'open-edit', stepId: string, processId: string): void
(e: 'save-edit', stepId: string, processId: string, value: string): void
}>()
//从 currentTask 中获取数据
const currentTaskProcess = computed(() => {
const currentTask = agentsStore.currentTask
if (currentTask && currentTask.Id === props.step.Id && currentTask.TaskProcess) {
return currentTask.TaskProcess
}
//从 agentRawPlan 中获取原始数据
const collaborationProcess = agentsStore.agentRawPlan.data?.['Collaboration Process'] || []
const rawData = collaborationProcess.find((task: any) => task.Id === props.step.Id)
return rawData?.TaskProcess || []
})
// 当前正在编辑的process ID
const editingProcessId = ref<string | null>(null)
const editValue = ref('')
// 鼠标悬停的process ID
const hoverProcessId = ref<string | null>(null)
// 处理卡片点击事件
function handleCardClick() {
// 如果正在编辑,不处理点击
if (editingProcessId.value) return
// 设置当前任务,与任务大纲联动
if (props.step.Id) {
agentsStore.setCurrentTask(props.step as any)
}
}
// 检测当前是否是深色模式
function isDarkMode(): boolean {
return document.documentElement.classList.contains('dark')
}
// 获取颜色浅两号的函数
function getLightColor(color: string, level: number = 2): string {
if (!color || color.length !== 7 || color[0] !== '#') return color
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 {
if (!color || color.length !== 7 || color[0] !== '#') return color
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 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)
.padStart(2, '0')}${Math.round(newB).toString(16).padStart(2, '0')}`
}
// 根据主题模式获取调整后的颜色
function getAdjustedColor(color: string, level: number = 2): string {
if (isDarkMode()) {
return getDarkColor(color, level)
} else {
return getLightColor(color, level)
}
}
// 处理鼠标进入
function handleMouseEnter(processId: string) {
hoverProcessId.value = processId
}
// 处理鼠标离开
function handleMouseLeave() {
hoverProcessId.value = null
}
// 处理双击编辑针对单个process
function handleDblClick(processId: string, currentDescription: string) {
editingProcessId.value = processId
editValue.value = currentDescription
emit('open-edit', props.step.Id || '', processId)
}
// 处理保存编辑
function handleSave(processId: string) {
if (!editingProcessId.value) return
emit('save-edit', props.step.Id || '', processId, editValue.value)
editingProcessId.value = null
editValue.value = ''
}
// 处理取消编辑
function handleCancel() {
editingProcessId.value = null
editValue.value = ''
}
</script>
<template>
<div class="process-card" @click="handleCardClick">
<div class="process-content">
<!-- 显示模式 -->
<div class="display-content">
<span
v-for="process in currentTaskProcess"
:key="process.ID"
class="process-segment"
@mouseenter="handleMouseEnter(process.ID)"
@mouseleave="handleMouseLeave"
>
<span
class="agent-name"
:style="{
backgroundColor: getActionTypeDisplay(process.ActionType)?.color || '#909399',
color: '#fff',
padding: '2px 6px',
borderRadius: '3px',
marginRight: '4px'
}"
>
{{ process.AgentName }}
</span>
<!-- 编辑模式 - 修改为卡片样式 -->
<div v-if="editingProcessId === process.ID" class="edit-container">
<div class="edit-card">
<div class="flex flex-col gap-3">
<el-input
v-model="editValue"
type="textarea"
:autosize="{ minRows: 3, maxRows: 6 }"
placeholder="请输入描述内容"
autofocus
/>
<div class="flex justify-end">
<svg-icon
icon-class="Check"
size="20px"
color="#328621"
class="cursor-pointer mr-4"
@click="handleSave(process.ID)"
title="保存"
/>
<svg-icon
icon-class="Cancel"
size="20px"
color="#8e0707"
class="cursor-pointer mr-1"
@click="handleCancel"
title="取消"
/>
</div>
</div>
</div>
</div>
<!-- 显示模式 -->
<span
v-else
class="process-description"
:class="{ hovered: hoverProcessId === process.ID }"
:style="{
border: `1px solid ${getActionTypeDisplay(process.ActionType)?.border}`,
backgroundColor:
hoverProcessId === process.ID
? getAdjustedColor(getActionTypeDisplay(process.ActionType)?.color || '#909399')
: 'transparent'
}"
@dblclick="handleDblClick(process.ID, process.Description)"
>
{{ process.Description }}
</span>
<span class="separator" v-if="process.Description && !process.Description.endsWith('。')"
></span
>
</span>
</div>
</div>
<!-- 按钮点击不会冒泡到卡片 -->
<BranchButton :step="step" />
</div>
</template>
<style scoped lang="scss">
.process-card {
position: relative;
margin-bottom: 16px;
padding: 16px;
border-radius: 8px;
background: var(--color-bg-list);
border: 1px solid var(--color-border-default);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.2s ease;
.process-content {
min-height: 20px;
.display-content {
line-height: 1.6;
font-size: 14px;
color: var(--el-text-color-primary);
.process-segment {
display: inline;
position: relative;
.agent-name {
display: inline-block;
font-weight: 500;
font-size: 13px;
margin-right: 4px;
cursor: default;
}
.edit-container {
display: block; // 改为块级元素,使其换行显示
margin-top: 8px;
margin-bottom: 8px;
.edit-card {
//background: #f0f2f5;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 600px;
:deep(.el-textarea) {
width: 100%;
.el-textarea__inner {
font-size: 14px;
line-height: 1.6;
padding: 8px 12px;
border-radius: 4px;
resize: vertical;
min-height: 60px;
color: var(--color-text-taskbar);
}
}
.edit-buttons {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 8px;
}
}
}
.process-description {
display: inline;
white-space: normal;
cursor: pointer;
padding: 2px 4px;
border-radius: 3px;
transition: background-color 0.2s ease;
&.hovered {
transition: background-color 0.2s ease;
}
}
.separator {
margin-right: 8px;
}
&:last-child .separator {
display: none;
}
}
}
}
}
.process-card:hover {
border-color: var(--el-border-color);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
</style>

View File

@@ -0,0 +1,141 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useAgentsStore, useSelectionStore } from '@/stores'
const agentsStore = useAgentsStore()
const selectionStore = useSelectionStore()
const emit = defineEmits<{
(e: 'click'): void
}>()
const props = defineProps<{
step?: any
}>()
// 获取分支数量
const branchCount = computed(() => {
if (!props.step?.Id) return 1
// 获取该任务步骤的分支数据
const taskStepId = props.step.Id
// 获取该任务的 agent 组合
const agents = props.step.AgentSelection || []
const branches = selectionStore.getTaskProcessBranches(taskStepId, agents)
return branches.length || 1
})
// 判断按钮是否可点击
const isClickable = computed(() => {
if (!props.step?.Id || !agentsStore.currentTask?.Id) {
return false
}
return props.step.Id === agentsStore.currentTask.Id
})
const handleClick = (event?: MouseEvent) => {
// 只有可点击时才执行操作
if (!isClickable.value) {
return
}
// 阻止冒泡,避免触发卡片点击
if (event) {
event.stopPropagation()
}
emit('click')
// 设置当前任务
if (props.step) {
agentsStore.setCurrentTask(props.step)
}
// 触发打开任务过程探索窗口
agentsStore.openPlanTask()
}
</script>
<template>
<div
class="task-button"
:class="{ 'has-branches': branchCount > 0, 'is-disabled': !isClickable }"
@click="handleClick"
:title="isClickable ? `${branchCount} 个分支` : '请先在任务大纲中选中此任务'"
>
<!-- 流程图标 -->
<svg-icon icon-class="branch" size="20px" class="task-icon" />
<!-- 分支数量显示 -->
<span class="branch-count">
{{ branchCount }}
</span>
</div>
</template>
<style scoped lang="scss">
.task-button {
/* 定位 - 右下角 */
position: absolute;
right: 0;
bottom: 0;
/* 尺寸 */
width: 36px;
height: 32px;
/* 样式 */
background-color: #43a8aa;
border-radius: 10px 0 0 0;
cursor: pointer;
user-select: none;
z-index: 100;
/* 布局 */
display: flex;
align-items: center;
justify-content: center;
/* 交互 */
transition: all 0.2s ease;
&:hover {
filter: brightness(0.9);
}
// 禁用状态
&.is-disabled {
background-color: #bdc3c7;
cursor: not-allowed;
opacity: 0.6;
&:hover {
filter: none;
}
}
&.has-branches::after {
content: '';
position: absolute;
top: -2px;
right: -2px;
width: 8px;
height: 8px;
background: #ff6b6b;
border-radius: 50%;
border: 2px solid white;
}
}
.task-icon {
color: white;
}
.branch-count {
position: absolute;
right: 4px;
bottom: 2px;
font-size: 12px;
color: white;
font-weight: 800;
text-align: right;
line-height: 1;
}
</style>

View File

@@ -0,0 +1,183 @@
// 模拟后端原始返回格式的 Mock 数据 - fill_stepTask_TaskProcess 接口
// 后端返回格式: IRawStepTask { StepName, TaskContent, InputObject_List, OutputObject, AgentSelection, TaskProcess, Collaboration_Brief_frontEnd }
import type { IRawStepTask } from '@/stores'
// TaskProcess 项格式
interface RawTaskProcessItem {
ID: string
ActionType: string
AgentName: string
Description: string
ImportantInput: string[]
}
// Collaboration_Brief_frontEnd 数据项格式
interface RawBriefDataItem {
text: string
color: number[] // [h, s, l]
}
// 后端返回的完整数据格式
export interface RawAgentTaskProcessResponse {
StepName: string
TaskContent: string
InputObject_List: string[]
OutputObject: string
AgentSelection: string[]
TaskProcess: RawTaskProcessItem[]
Collaboration_Brief_frontEnd?: {
template: string
data: Record<string, RawBriefDataItem>
}
}
// 模拟后端返回的原始数据结构(与后端缓存数据格式一致)
// 使用与 AgentAssignmentBackendMock 相同的 agent 列表
export const mockBackendAgentTaskProcessData: RawAgentTaskProcessResponse = {
StepName: '腐蚀类型识别',
TaskContent: '分析船舶制造中常见的材料腐蚀类型及其成因。',
InputObject_List: [],
OutputObject: '腐蚀类型及成因列表',
AgentSelection: ['腐蚀机理研究员', '实验材料学家', '防护工程专家'],
TaskProcess: [
{
ID: 'action_101',
ActionType: 'Propose',
AgentName: '腐蚀机理研究员',
Description: '分析海洋环境下的腐蚀机理,确定关键防护要素',
ImportantInput: ['海洋环境参数', '防护性能指标'],
},
{
ID: 'action_102',
ActionType: 'Critique',
AgentName: '实验材料学家',
Description: '基于腐蚀机理分析结果,设计涂层材料的基础配方',
ImportantInput: ['腐蚀机理分析结果', '涂层材料配方'],
},
{
ID: 'action_103',
ActionType: 'Improve',
AgentName: '防护工程专家',
Description: '筛选适用于防护涂层的二维材料,评估其性能潜力',
ImportantInput: ['材料配方设计', '涂层材料配方'],
},
{
ID: 'action_104',
ActionType: 'Finalize',
AgentName: '实验材料学家',
Description: '制定涂层材料性能测试实验方案,包括测试指标和方法',
ImportantInput: ['二维材料筛选结果', '防护性能指标'],
},
{
ID: 'action_105',
ActionType: 'Critique',
AgentName: '防护工程专家',
Description: '模拟海洋流体环境对涂层材料的影响,优化涂层结构',
ImportantInput: ['实验方案', '海洋环境参数'],
},
{
ID: 'action_106',
ActionType: 'Improve',
AgentName: '腐蚀机理研究员',
Description: '综合评估涂层材料的防护性能,提出改进建议',
ImportantInput: ['流体力学模拟结果', '实验材料学测试结果', '二维材料性能数据'],
},
{
ID: 'action_107',
ActionType: 'Improve',
AgentName: '实验材料学家',
Description: '整理研发数据和测试结果,撰写完整的研发报告',
ImportantInput: ['综合性能评估', '所有研发数据'],
},
],
Collaboration_Brief_frontEnd: {
template: '基于!<0>!、!<1>!和!<2>!!<3>!、!<4>!、!<5>!和!<6>!执行!<7>!任务,以获得!<8>!。',
data: {
'0': {
text: '涂层材料配方',
color: [120, 60, 70], // hsl(120, 60%, 70%)
},
'1': {
text: '海洋环境参数',
color: [120, 60, 70], // hsl(120, 60%, 70%)
},
'2': {
text: '防护性能指标',
color: [120, 60, 70], // hsl(120, 60%, 70%)
},
'3': {
text: '腐蚀机理研究员',
color: [0, 0, 90], // hsl(0, 0%, 90%)
},
'4': {
text: '先进材料研发员',
color: [0, 0, 90], // hsl(0, 0%, 90%)
},
'5': {
text: '二维材料科学家',
color: [0, 0, 90], // hsl(0, 0%, 90%)
},
'6': {
text: '实验材料学家',
color: [0, 0, 90], // hsl(0, 0%, 90%)
},
'7': {
text: '研发适用于海洋环境的耐腐蚀防护涂层材料,并进行性能测试与评估',
color: [0, 0, 87], // hsl(0, 0%, 87%)
},
'8': {
text: '防护涂层材料研发报告',
color: [30, 100, 80], // hsl(30, 100%, 80%)
},
},
},
}
// 模拟后端API调用 - fill_stepTask_TaskProcess
export const mockBackendFillAgentTaskProcess = async (
goal: string,
stepTask: any,
agents: string[],
): Promise<RawAgentTaskProcessResponse> => {
// 模拟网络延迟 500ms
await new Promise((resolve) => setTimeout(resolve, 500))
// 在真实场景中,后端会根据传入的 goal、stepTask 和 agents 生成不同的 TaskProcess
// 这里我们直接返回预设的 Mock 数据
// 可以根据传入的 agents 动态修改 AgentSelection 和 TaskProcess
// 确保 agents 数组不为空
const safeAgents = agents.length > 0 ? agents : ['腐蚀机理研究员']
const responseData: RawAgentTaskProcessResponse = {
...mockBackendAgentTaskProcessData,
AgentSelection: agents,
TaskProcess: mockBackendAgentTaskProcessData.TaskProcess.map((action, index) => ({
...action,
AgentName: safeAgents[index % safeAgents.length],
})),
Collaboration_Brief_frontEnd: mockBackendAgentTaskProcessData.Collaboration_Brief_frontEnd
? {
template: mockBackendAgentTaskProcessData.Collaboration_Brief_frontEnd.template,
data: { ...mockBackendAgentTaskProcessData.Collaboration_Brief_frontEnd.data },
}
: undefined,
}
// 更新 Collaboration_Brief_frontEnd.data 中的 agent 引用
if (responseData.Collaboration_Brief_frontEnd?.data) {
const agentCount = Math.min(safeAgents.length, 4) // 最多4个agent
for (let i = 0; i < agentCount; i++) {
const key = String(i + 3) // agent从索引3开始
if (responseData.Collaboration_Brief_frontEnd.data[key]) {
responseData.Collaboration_Brief_frontEnd.data[key] = {
...responseData.Collaboration_Brief_frontEnd.data[key],
text: safeAgents[i]!,
}
}
}
}
return responseData
}

View File

@@ -0,0 +1,168 @@
<!-- 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

@@ -15,16 +15,14 @@ const md = new MarkdownIt({
html: true, html: true,
linkify: true, linkify: true,
typographer: true, typographer: true,
breaks: true, breaks: true
}) })
function sanitize(str?: string) { function sanitize(str?: string) {
if (!str) { if (!str) {
return '' return ''
} }
const cleanStr = str const cleanStr = str.replace(/\\n/g, '\n').replace(/\n\s*\d+\./g, '\n$&')
.replace(/\\n/g, '\n')
.replace(/\n\s*\d+\./g, '\n$&')
const html = md.render(cleanStr) const html = md.render(cleanStr)
return html return html
// return DOMPurify.sanitize(html) // return DOMPurify.sanitize(html)
@@ -40,11 +38,12 @@ const data = computed<Data | null>(() => {
if (result.NodeId === props.nodeId) { if (result.NodeId === props.nodeId) {
// LogNodeType 为 object直接渲染Content // LogNodeType 为 object直接渲染Content
if (result.LogNodeType === 'object') { if (result.LogNodeType === 'object') {
return { const data = {
Description: props.nodeId, Description: props.nodeId,
Content: sanitize(result.content), Content: sanitize(result.content),
LogNodeType: result.LogNodeType, LogNodeType: result.LogNodeType
} }
return data
} }
if (!result.ActionHistory) { if (!result.ActionHistory) {
@@ -56,7 +55,7 @@ const data = computed<Data | null>(() => {
return { return {
Description: action.Description, Description: action.Description,
Content: sanitize(action.Action_Result), Content: sanitize(action.Action_Result),
LogNodeType: result.LogNodeType, LogNodeType: result.LogNodeType
} }
} }
} }
@@ -77,7 +76,9 @@ const data = computed<Data | null>(() => {
{{ data.Description }} {{ data.Description }}
<Iod v-if="data.LogNodeType !== 'object'" /> <Iod v-if="data.LogNodeType !== 'object'" />
</div> </div>
<div class="rounded-[8px] p-[15px] text-[14px] bg-[var(--color-bg-quaternary)]"> <div
class="rounded-[8px] p-[15px] text-[14px] bg-[var(--color-bg-result-detail)] text-[var(--color-text-detail)]"
>
<div <div
class="markdown-content max-h-[240px] overflow-y-auto max-w-full" class="markdown-content max-h-[240px] overflow-y-auto max-w-full"
v-html="data.Content" v-html="data.Content"

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { readConfig } from '@/utils/readJson.ts' import { readConfig } from '@/utils/readJson.ts'
import { onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
interface Iod { interface Iod {
name: string name: string
@@ -28,10 +28,22 @@ function handleNext() {
displayIndex.value++ displayIndex.value++
} }
} }
function handlePrev() {
if (displayIndex.value === 0) {
displayIndex.value = data.value.length - 1
} else {
displayIndex.value--
}
}
</script> </script>
<template> <template>
<el-popover trigger="hover" width="440"> <el-popover
trigger="hover"
width="440"
popper-style="background-color: var(--color-bg-result); border: none;"
>
<template #reference> <template #reference>
<div <div
class="rounded-full w-[20px] h-[20px] bg-[var(--color-bg-quaternary)] flex justify-center items-center cursor-pointer" class="rounded-full w-[20px] h-[20px] bg-[var(--color-bg-quaternary)] flex justify-center items-center cursor-pointer"
@@ -40,12 +52,14 @@ function handleNext() {
</div> </div>
</template> </template>
<template #default v-if="data.length"> <template #default v-if="data.length">
<div> <div class="bg-[var(--color-bg-result)]">
<div class="flex justify-between items-center p-2 pb-0 rounded-[8px] text-[16px] font-bold"> <div class="flex justify-between items-center p-2 pb-0 rounded-[8px] text-[16px] font-bold">
<span>数联网搜索结果</span> <span>数联网搜索结果</span>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div>{{ `${displayIndex + 1}/${data.length}` }}</div> <!-- <div>{{ `${displayIndex + 1}/${data.length}` }}</div>
<el-button type="primary" size="small" @click="handleNext">下一个</el-button> <el-button type="primary" size="small" @click="handleNext">下一个</el-button> -->
<!-- 关闭 -->
<!-- <SvgIcon icon-class="close" size="15px" /> -->
</div> </div>
</div> </div>
<!-- 分割线 --> <!-- 分割线 -->
@@ -53,19 +67,50 @@ function handleNext() {
<div class="p-2 pt-0"> <div class="p-2 pt-0">
<div class="flex items-center w-full gap-3"> <div class="flex items-center w-full gap-3">
<div class="font-bold w-[75px] text-right flex-shrink-0">名称:</div> <div class="font-bold w-[75px] text-right flex-shrink-0">名称:</div>
<div class="text-[var(--color-text-secondary)] flex-1 break-words">{{ displayIod.name }}</div> <div class="text-[var(--color-text-detail)] flex-1 break-words">
{{ displayIod.name }}
</div>
</div> </div>
<div class="flex items-center w-full gap-3"> <div class="flex items-center w-full gap-3">
<div class="font-bold w-[75px] text-right flex-shrink-0">数据空间:</div> <div class="font-bold w-[75px] text-right flex-shrink-0">数据空间:</div>
<div class="text-[var(--color-text-secondary)] lex-1 break-words">{{ displayIod.data_space }}</div> <div class="text-[var(--color-text-detail)] lex-1 break-words">
{{ displayIod.data_space }}
</div>
</div> </div>
<div class="flex items-center w-full gap-3"> <div class="flex items-center w-full gap-3">
<div class="font-bold w-[75px] text-right flex-shrink-0">DOID:</div> <div class="font-bold w-[75px] text-right flex-shrink-0">DOID:</div>
<div class="text-[var(--color-text-secondary)] lex-1 break-words">{{ displayIod.doId }}</div> <div class="text-[var(--color-text-detail)] lex-1 break-words">
{{ displayIod.doId }}
</div>
</div> </div>
<div class="flex items-center w-full gap-3"> <div class="flex items-center w-full gap-3">
<div class="font-bold w-[75px] text-right flex-shrink-0">来源仓库:</div> <div class="font-bold w-[75px] text-right flex-shrink-0">来源仓库:</div>
<div class="text-[var(--color-text-secondary)] flex-1 break-words break-al">{{ displayIod.fromRepo }}</div> <div class="text-[var(--color-text-detail)] flex-1 break-words break-al">
{{ displayIod.fromRepo }}
</div>
</div>
<div>
<div
class="card-item w-[80px] h-[25px] flex justify-between items-center rounded-[25px] bg-[#b1b1b1] ml-auto px-2"
>
<div class="text-[14px] text-[#ffffff] font-medium">
{{ `${displayIndex + 1}/${data.length}` }}
</div>
<div class="flex items-center gap-1">
<svg-icon
icon-class="left"
size="15px"
@click="handlePrev"
class="cursor-pointer hover:opacity-70"
></svg-icon>
<svg-icon
icon-class="right"
size="15px"
@click="handleNext"
class="cursor-pointer hover:opacity-70"
></svg-icon>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,31 +1,56 @@
<script setup lang="ts">
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%]">
<!-- 左侧元素 --> <!-- 左侧元素 -->
<div class="flex-1 relative h-full flex justify-center"> <div class="flex-1 relative h-full flex justify-center">
<!-- 背景那一根线 --> <!-- 背景那一根线 -->
<div <div class="h-full bg-[var(--color-bg-flow)] w-[5px]">
class="h-full bg-[var(--color-bg-tertiary)] w-[5px]"
>
<!-- 线底部的小圆球 --> <!-- 线底部的小圆球 -->
<div <div
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-tertiary)] 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"
></div> ></div>
</div> </div>
</div> </div>
<!-- 右侧元素 --> <!-- 右侧元素 -->
<div class="flex-1 relative h-full flex justify-center"> <div class="flex-1 relative h-full flex justify-center">
<!-- 背景那一根线 --> <!-- 背景那一根线 -->
<div <div class="h-full bg-[var(--color-bg-flow)] w-[5px]">
class="h-full bg-[var(--color-bg-tertiary)] 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-tertiary)] 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"
></div> ></div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <style scoped>
</script> .plus-area:hover .plus-icon {
opacity: 1;
border: 1px dashed var(--color-text);
}
</style>

View File

@@ -0,0 +1,365 @@
<template>
<div class="root-node-wrapper">
<!-- 左侧连接点输入 -->
<Handle type="target" :position="Position.Left" id="left" />
<!-- 右侧连接点输出 -->
<Handle type="source" :position="Position.Right" id="right" />
<!-- 底部连接点用于分支 -->
<Handle type="source" :position="Position.Bottom" id="bottom" />
<el-card class="root-node-card" :shadow="true">
<!-- 目标内容 -->
<div class="goal-content">
<div class="goal-label">初始目标</div>
<div class="goal-text">{{ goal }}</div>
</div>
</el-card>
<!-- 底部添加按钮 -->
<div v-if="!isAddingBranch" class="external-add-btn" @click="startAddBranch">
<el-icon :size="20" color="#409eff">
<CirclePlus />
</el-icon>
</div>
<!-- 取消按钮输入框显示时 -->
<div v-else class="external-add-btn cancel-btn" @click="cancelAddBranch">
<el-icon :size="20" color="#f56c6c">
<Remove />
</el-icon>
</div>
<!-- 外部输入框 -->
<div v-if="isAddingBranch" class="external-input-container">
<el-input
v-model="branchInput"
placeholder="输入分支需求..."
size="small"
@keydown="handleBranchKeydown"
ref="branchInputRef"
class="branch-input"
>
<template #suffix>
<svg-icon
icon-class="paper-plane"
size="16px"
color="#409eff"
class="submit-icon"
:class="{ 'is-disabled': !branchInput.trim() }"
@click="submitBranch"
/>
</template>
</el-input>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'
import { CirclePlus, Remove } from '@element-plus/icons-vue'
import { Handle, Position } from '@vue-flow/core'
import SvgIcon from '@/components/SvgIcon/index.vue'
interface RootNodeData {
goal: string
initialInput?: string[] | string
isRoot?: boolean
}
const props = defineProps<{
id: string
data: RootNodeData
isAddingBranch?: boolean
[key: string]: any
}>()
const emit = defineEmits<{
(e: 'add-branch', nodeId: string, branchContent: string): void
(e: 'start-add-branch', nodeId: string): void
(e: 'cancel-add-branch'): void
}>()
const goal = computed(() => props.data.goal || '')
const initialInput = computed(() => props.data.initialInput)
const displayInput = computed(() => {
if (!initialInput.value) return false
if (Array.isArray(initialInput.value)) {
return initialInput.value.length > 0
}
return initialInput.value.trim().length > 0
})
// 分支添加相关状态(使用父组件传入的 prop
const branchInput = ref('')
const branchInputRef = ref<InstanceType<typeof HTMLInputElement>>()
// 计算属性,使用父组件传入的状态
const isAddingBranch = computed(() => props.isAddingBranch || false)
// 分支添加相关方法
const startAddBranch = () => {
emit('start-add-branch', props.id)
branchInput.value = ''
nextTick(() => {
branchInputRef.value?.focus()
})
}
const cancelAddBranch = () => {
emit('cancel-add-branch')
branchInput.value = ''
}
const submitBranch = () => {
if (branchInput.value.trim()) {
emit('add-branch', props.id, branchInput.value.trim())
branchInput.value = ''
}
}
const handleBranchKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault()
submitBranch()
} else if (event.key === 'Escape') {
cancelAddBranch()
}
}
</script>
<style scoped lang="scss">
.root-node-card {
width: 200px;
min-height: 100px;
background: var(--color-bg-three);
border: 2px solid #409eff;
box-sizing: border-box;
transition: all 0.2s ease;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
&:hover {
box-shadow: 0 6px 16px rgba(64, 158, 255, 0.3);
transform: translateY(-2px);
}
:deep(.el-card__body) {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
position: relative;
}
}
.root-badge {
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
}
.goal-icon {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 4px;
}
.goal-content {
text-align: center;
}
.goal-label {
font-size: 12px;
font-weight: bold;
color: #409eff;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.goal-text {
font-size: 14px;
font-weight: 600;
color: var(--color-text-title-header);
line-height: 1.6;
word-break: break-word;
white-space: pre-wrap;
}
.initial-input {
margin-top: 4px;
}
.input-divider {
height: 1px;
background: linear-gradient(to right, transparent, #409eff, transparent);
margin: 8px 0;
}
.input-label {
font-size: 12px;
font-weight: bold;
color: var(--color-text-secondary);
margin-bottom: 6px;
}
.input-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: center;
}
.input-text {
font-size: 13px;
color: var(--color-text-secondary);
text-align: center;
word-break: break-word;
line-height: 1.5;
}
// 节点包装器
.root-node-wrapper {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
// 外部添加按钮
.external-add-btn {
position: absolute;
bottom: -20px;
left: 50%;
transform: translateX(-50%);
width: 32px;
height: 32px;
border-radius: 50%;
background: #fff;
border: 2px solid #409eff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
z-index: 10;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
&:hover {
background: #409eff;
transform: translateX(-50%) scale(1.1);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
.el-icon {
color: #fff;
}
}
&:active {
transform: translateX(-50%) scale(0.95);
}
// 取消按钮样式
&.cancel-btn {
border-color: #f56c6c;
box-shadow: 0 2px 8px rgba(245, 108, 108, 0.2);
&:hover {
background: #f56c6c;
box-shadow: 0 4px 12px rgba(245, 108, 108, 0.3);
}
}
}
// 外部输入容器
.external-input-container {
position: absolute;
bottom: -80px;
left: 50%;
transform: translateX(-50%);
width: 260px;
background: #fff;
border: 1px solid #dcdfe6;
border-radius: 8px;
padding: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
z-index: 100;
animation: slideDown 0.2s ease-out;
&::before {
content: '';
position: absolute;
top: -6px;
left: 50%;
transform: translateX(-50%);
width: 12px;
height: 12px;
background: #fff;
border-left: 1px solid #dcdfe6;
border-top: 1px solid #dcdfe6;
transform: translateX(-50%) rotate(45deg);
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateX(-50%) translateY(-10px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
.branch-input {
width: 100%;
:deep(.el-input__wrapper) {
background: transparent;
border: 1px solid #dcdfe6;
border-radius: 4px;
box-shadow: none;
&:hover {
border-color: #c0c4cc;
}
&.is-focus {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
.el-input__inner {
font-size: 13px;
color: #000;
padding-right: 40px;
}
}
}
// 提交图标样式
.submit-icon {
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
&:hover:not(.is-disabled) {
transform: scale(1.1);
}
&:active:not(.is-disabled) {
transform: scale(0.95);
}
&.is-disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
</style>

View File

@@ -0,0 +1,518 @@
<template>
<div class="task-node-wrapper">
<!-- 左侧连接点输入 -->
<Handle type="target" :position="Position.Left" id="left" />
<!-- 右侧连接点输出 -->
<Handle type="source" :position="Position.Right" id="right" />
<!-- 底部连接点用于分支 -->
<Handle type="source" :position="Position.Bottom" id="bottom" />
<el-card
class="task-node-card"
:class="{
'is-editing': isEditing,
'is-active': isActive,
'is-branch-selected': props.isBranchSelected
}"
:shadow="true"
>
<!-- 任务名称 -->
<div class="task-name">{{ task.StepName }}</div>
<!-- 智能体列表 -->
<div class="agents-container">
<el-tooltip
v-for="agentSelection in task.AgentSelection"
:key="agentSelection"
effect="light"
placement="top"
:show-after="500"
popper-class="task-syllabus-tooltip-popper"
>
<template #content>
<div class="agent-tooltip">
<div class="text-[16px] font-bold">{{ agentSelection }}</div>
<div class="h-[1px] w-full bg-[#494B51] my-[4px]"></div>
<div class="text-[12px]">
{{ task.TaskProcess.find(i => i.AgentName === agentSelection)?.Description }}
</div>
</div>
</template>
<div class="agent-icon" :style="{ background: getAgentIcon(agentSelection).color }">
<svg-icon :icon-class="getAgentIcon(agentSelection).icon" color="#fff" size="20px" />
</div>
</el-tooltip>
</div>
</el-card>
<!-- 底部添加按钮卡片外部 -->
<div v-if="!isAddingBranch" class="external-add-btn" @click="startAddBranch">
<el-icon :size="20" color="#409eff">
<CirclePlus />
</el-icon>
</div>
<!-- 取消按钮输入框显示时 -->
<div v-else class="external-add-btn cancel-btn" @click="cancelAddBranch">
<el-icon :size="20" color="#f56c6c">
<Remove />
</el-icon>
</div>
<!-- 外部输入框 -->
<div v-if="isAddingBranch" class="external-input-container">
<el-input
v-model="branchInput"
placeholder="输入分支需求..."
size="small"
@keydown="handleBranchKeydown"
ref="branchInputRef"
class="branch-input"
>
<template #suffix>
<svg-icon
icon-class="paper-plane"
size="16px"
color="#409eff"
class="submit-icon"
:class="{ 'is-disabled': !branchInput.trim() }"
@click="submitBranch"
/>
</template>
</el-input>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'
import { CirclePlus, Remove } from '@element-plus/icons-vue'
import { Handle, Position } from '@vue-flow/core'
import { type IRawStepTask } from '@/stores'
import { getAgentMapIcon } from '@/layout/components/config'
import SvgIcon from '@/components/SvgIcon/index.vue'
interface TaskNodeData {
task: IRawStepTask
isEditing: boolean
editingContent: string
}
// 使用更宽松的类型定义来避免类型错误
const props = defineProps<{
id: string
data: TaskNodeData
isAddingBranch?: boolean
isBranchSelected?: boolean // 是否属于选中的分支路径
[key: string]: any
}>()
const emit = defineEmits<{
(e: 'edit-task', nodeId: string): void
(e: 'save-task', nodeId: string, content: string): void
(e: 'cancel-edit', nodeId: string): void
(e: 'add-branch', nodeId: string, branchContent: string): void
(e: 'start-add-branch', nodeId: string): void
(e: 'cancel-add-branch'): void
}>()
const editingContent = ref(props.data.task.TaskContent || '')
// 分支添加相关状态(使用父组件传入的 prop
const branchInput = ref('')
const branchInputRef = ref<InstanceType<typeof HTMLInputElement>>()
// 计算属性,使用父组件传入的状态
const isAddingBranch = computed(() => props.isAddingBranch || false)
const isEditing = computed({
get: () => props.data.isEditing,
set: value => {
if (!value) {
emit('cancel-edit', props.id)
}
}
})
const isActive = computed(() => {
return props.data.task.StepName === props.data.task.StepName
})
const task = computed(() => props.data.task)
const getAgentIcon = (agentName: string) => {
return getAgentMapIcon(agentName)
}
const startEdit = () => {
emit('edit-task', props.id)
}
const saveEdit = () => {
emit('save-task', props.id, editingContent.value)
}
const cancelEdit = () => {
emit('cancel-edit', props.id)
}
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault()
saveEdit()
} else if (event.key === 'Escape') {
cancelEdit()
}
}
// 分支添加相关方法
const startAddBranch = () => {
emit('start-add-branch', props.id)
branchInput.value = ''
nextTick(() => {
branchInputRef.value?.focus()
})
}
const cancelAddBranch = () => {
emit('cancel-add-branch')
branchInput.value = ''
}
const submitBranch = () => {
if (branchInput.value.trim()) {
emit('add-branch', props.id, branchInput.value.trim())
branchInput.value = ''
}
}
const handleBranchKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault()
submitBranch()
} else if (event.key === 'Escape') {
cancelAddBranch()
}
}
</script>
<style scoped lang="scss">
.task-node-card {
width: 150px;
min-height: 100px;
background-color: var(--color-card-bg-task);
border: 1px solid var(--color-card-border-task);
box-sizing: border-box;
transition: all 0.2s ease;
cursor: pointer;
&:hover {
background-color: var(--color-card-bg-task-hover);
border-color: var(--color-card-border-hover);
box-shadow: var(--color-card-shadow-hover);
}
&.is-active {
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
// 分支选中高亮样式(绿色)
&.is-branch-selected {
border-color: #67c23a;
box-shadow: 0 0 0 3px rgba(103, 194, 58, 0.3);
background-color: rgba(103, 194, 58, 0.05);
&:hover {
border-color: #67c23a;
box-shadow: 0 0 0 3px rgba(103, 194, 58, 0.4);
background-color: rgba(103, 194, 58, 0.08);
}
}
&.is-editing {
cursor: default;
}
:deep(.el-card__body) {
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
position: relative;
}
}
.task-name {
font-size: 16px;
font-weight: bold;
text-align: center;
color: var(--color-text-title-header);
word-break: break-word;
}
.divider {
height: 1px;
width: 100%;
background: var(--color-border-separate);
margin: 4px 0;
}
.task-content {
font-size: 14px;
color: var(--color-text-secondary);
min-height: 40px;
word-break: break-word;
white-space: pre-wrap;
line-height: 1.5;
}
.task-content-editing {
display: flex;
flex-direction: column;
gap: 8px;
}
.edit-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.agents-container {
display: flex;
gap: 6px;
flex-wrap: wrap;
justify-content: center;
}
.agent-icon {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.1);
}
}
.input-outputs {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
font-size: 12px;
}
.input-label {
font-weight: bold;
color: var(--color-text-secondary);
}
.agent-tooltip {
padding: 8px;
max-width: 200px;
}
// 编辑器样式
.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-node-wrapper {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
// 外部添加按钮
.external-add-btn {
position: absolute;
bottom: -20px;
left: 50%;
transform: translateX(-50%);
width: 32px;
height: 32px;
border-radius: 50%;
background: #fff;
border: 2px solid #409eff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
z-index: 10;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
&:hover {
background: #409eff;
transform: translateX(-50%) scale(1.1);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
.el-icon {
color: #fff;
}
}
&:active {
transform: translateX(-50%) scale(0.95);
}
// 取消按钮样式
&.cancel-btn {
border-color: #f56c6c;
box-shadow: 0 2px 8px rgba(245, 108, 108, 0.2);
&:hover {
background: #f56c6c;
box-shadow: 0 4px 12px rgba(245, 108, 108, 0.3);
}
}
}
// 外部输入容器
.external-input-container {
position: absolute;
bottom: -80px;
left: 50%;
transform: translateX(-50%);
width: 260px;
background: #fff;
border: 1px solid #dcdfe6;
border-radius: 8px;
padding: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
z-index: 100;
animation: slideDown 0.2s ease-out;
&::before {
content: '';
position: absolute;
top: -6px;
left: 50%;
transform: translateX(-50%);
width: 12px;
height: 12px;
background: #fff;
border-left: 1px solid #dcdfe6;
border-top: 1px solid #dcdfe6;
transform: translateX(-50%) rotate(45deg);
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateX(-50%) translateY(-10px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
.add-branch-section {
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed var(--color-border-separate);
display: flex;
justify-content: center;
align-items: center;
}
.add-branch-btn {
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
&:hover {
transform: scale(1.1);
}
&:active {
transform: scale(0.95);
}
}
.branch-input-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
}
.branch-input {
width: 100%;
:deep(.el-input__wrapper) {
background: transparent;
border: 1px solid #dcdfe6;
border-radius: 4px;
box-shadow: none;
&:hover {
border-color: #c0c4cc;
}
&.is-focus {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
.el-input__inner {
font-size: 13px;
color: #000;
padding-right: 40px;
}
}
}
// 提交图标样式
.submit-icon {
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
&:hover:not(.is-disabled) {
transform: scale(1.1);
}
&:active:not(.is-disabled) {
transform: scale(0.95);
}
&.is-disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
.is-adding-branch {
.task-node-card {
border-color: #409eff;
}
}
</style>
<style>
.task-syllabus-tooltip-popper {
z-index: 4000 !important;
}
</style>

View File

@@ -0,0 +1,127 @@
# 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

@@ -0,0 +1,246 @@
// 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

@@ -0,0 +1,155 @@
// branch_TaskProcess 接口的 Mock 数据和 Mock API
export interface BranchAction {
ID: string
ActionType: string
AgentName: string
Description: string
ImportantInput: string[]
}
export type BranchTaskProcessResponse = BranchAction[][]
// Mock 数据模拟后端返回的原始任务流程数据2D 数组)
// 格式:[[action1, action2], [action3, action4]]
const mockBranchTaskProcessDataRaw: BranchAction[][] = [
// 第一个任务分支方案
[
{
ID: 'agent3',
ActionType: 'Critique',
AgentName: '实验材料学家',
Description: '详细分析用户需求文档',
ImportantInput: ['agent2'],
},
{
ID: 'agent4',
ActionType: 'Critique',
AgentName: '腐蚀机理研究员',
Description: '设计系统整体架构',
ImportantInput: ['agent3'],
},
{
ID: 'agent5',
ActionType: 'Improve',
AgentName: '防护工程专家',
Description: '实现系统核心功能',
ImportantInput: ['agent4'],
},
{
ID: 'agent6',
ActionType: 'Finalize',
AgentName: '实验材料学家',
Description: '进行系统集成测试',
ImportantInput: ['agent5'],
},
],
// 第二个任务分支方案
[
{
ID: 'agent7',
ActionType: 'Critique',
AgentName: '实验材料学家',
Description: '深入分析用户需求和技术约束',
ImportantInput: ['agent2'],
},
{
ID: 'agent8',
ActionType: 'Critique',
AgentName: '防护工程专家',
Description: '设计系统技术架构和数据流',
ImportantInput: ['agent8'],
},
{
ID: 'agent9',
ActionType: 'Improve',
AgentName: '腐蚀机理研究员',
Description: '评估系统安全性',
ImportantInput: ['agent4'],
},
{
ID: 'agent10',
ActionType: 'Finalize',
AgentName: '实验材料学家',
Description: '完成系统安全测试',
ImportantInput: ['agent9'],
},
],
//第三个任务分支方案
[
{
ID: 'agent12',
ActionType: 'Critique',
AgentName: '腐蚀机理研究员',
Description: '设计系统整体架构',
ImportantInput: ['agent11'],
},
{
ID: 'agent13',
ActionType: 'Improve',
AgentName: '防护工程专家',
Description: '实现系统核心功能',
ImportantInput: ['agent12'],
},
{
ID: 'agent14',
ActionType: 'Finalize',
AgentName: '实验材料学家',
Description: '进行系统集成测试',
ImportantInput: ['agent13'],
},
],
]
/**
* Mock API模拟后端 branch_TaskProcess 接口调用
*
* @param branch_Number - 分支数量
* @param Modification_Requirement - 修改需求
* @param Existing_Steps - 现有步骤列表(包含完整信息的对象数组)
* @param Baseline_Completion - 基线完成度
* @param stepTaskExisting - 现有任务
* @param General_Goal - 总体目标
* @returns Promise<BranchAction[][]> - 返回 2D 数组,与后端格式完全一致
*/
export const mockBranchTaskProcessAPI = async (params: {
branch_Number: number
Modification_Requirement: string
Existing_Steps: BranchAction[]
Baseline_Completion: number
stepTaskExisting: any
General_Goal: string
}): Promise<BranchAction[][]> => {
// 模拟网络延迟 800ms
await new Promise((resolve) => setTimeout(resolve, 800))
console.log('[Mock API] branch_TaskProcess 调用参数:', params)
// 🆕 使用轮询方式选择分支方案(依次循环使用所有分支方案)
const totalBranches = mockBranchTaskProcessDataRaw.length
const sessionKey = `branch-task-process-index-${params.stepTaskExisting?.Id || 'default'}`
// 获取上一次的选择索引
let lastIndex = parseInt(sessionStorage.getItem(sessionKey) || '0')
// 计算本次的选择索引(轮询到下一个分支)
const selectedBranchIndex = (lastIndex + 1) % totalBranches
// 保存本次的选择索引
sessionStorage.setItem(sessionKey, selectedBranchIndex.toString())
const rawBranchData = mockBranchTaskProcessDataRaw[selectedBranchIndex] || []
console.log(
'[Mock API] branch_TaskProcess 选择分支方案:',
selectedBranchIndex + 1,
'/',
totalBranches,
)
console.log('[Mock API] branch_TaskProcess 返回数据:', rawBranchData)
// 直接返回 2D 数组,与后端格式完全一致
return [rawBranchData]
}
export default mockBranchTaskProcessAPI

View File

@@ -0,0 +1,617 @@
// 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

@@ -0,0 +1,115 @@
<script setup lang="ts">
import { computed } from 'vue'
import SvgIcon from '@/components/SvgIcon/index.vue'
import { useAgentsStore } from '@/stores'
const agentsStore = useAgentsStore()
const emit = defineEmits<{
(e: 'click'): void
}>()
// 判断是否禁用 - 必须先点击任务大纲中的卡片
const isDisabled = computed(() => {
return !agentsStore.currentTask
})
// 获取agent组合卡片数量
const agentGroupCount = computed(() => {
if (!agentsStore.currentTask?.Id) return 1
// 获取该任务的已确认agent组合
const confirmedGroups = agentsStore.getConfirmedAgentGroups(agentsStore.currentTask.Id)
// 当前任务agents(1) + 已确认的agent组合数量
return confirmedGroups.length || 1
})
const handleClick = () => {
if (isDisabled.value) return
emit('click')
}
</script>
<template>
<div
class="assignment-button"
:class="{ 'is-disabled': isDisabled, 'has-groups': agentGroupCount > 1 }"
@click="handleClick"
:title="isDisabled ? '请先点击任务大纲中的任务卡片' : `${agentGroupCount} 个agent组合`"
>
<!-- 智能体分配图标 -->
<SvgIcon icon-class="agent-change" size="24px" color="#fff" />
<!-- agent组合数量显示 -->
<span class="agent-group-count">
{{ agentGroupCount }}
</span>
</div>
</template>
<style scoped lang="scss">
.assignment-button {
position: absolute;
right: 0;
top: 0;
width: 40px;
height: 36px;
background-color: #43a8aa;
border-radius: 10px 0 0 0;
cursor: pointer;
user-select: none;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
position: relative;
transition: all 0.2s ease;
&:hover {
filter: brightness(0.9);
}
&:active {
filter: brightness(0.8);
}
// 多个agent组合时显示红点指示器
&.has-groups::after {
content: '';
position: absolute;
top: -2px;
right: -2px;
width: 8px;
height: 8px;
background: #ff6b6b;
border-radius: 50%;
border: 2px solid white;
}
// 禁用状态
&.is-disabled {
opacity: 0.5;
cursor: not-allowed;
&:hover {
filter: none;
}
&:active {
filter: none;
}
}
}
.agent-group-count {
position: absolute;
right: 1px;
bottom: 2px;
font-size: 12px;
color: white;
font-weight: 800;
text-align: right;
line-height: 1;
}
</style>

View File

@@ -0,0 +1,99 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useAgentsStore, useSelectionStore } from '@/stores'
const agentsStore = useAgentsStore()
const selectionStore = useSelectionStore()
const emit = defineEmits<{
(e: 'click'): void
}>()
// 获取分支数量
const branchCount = computed(() => {
// flowBranches 包含所有通过 Branch 创建的分支
const extraBranches = selectionStore.flowBranches?.length || 1
return extraBranches
})
const handleClick = () => {
emit('click')
// 触发打开分支窗口
agentsStore.openPlanModification()
}
</script>
<template>
<div
class="branch-button"
:class="{ 'has-branches': branchCount > 0 }"
@click="handleClick"
:title="`${branchCount} 个分支`"
>
<!-- 分支图标 -->
<svg-icon icon-class="branch" size="24px" class="branch-icon" />
<!-- 分支数量显示 -->
<span class="branch-count">
{{ branchCount }}
</span>
</div>
</template>
<style scoped lang="scss">
.branch-button {
position: absolute;
right: 0;
bottom: 0;
/* 尺寸 */
width: 36px;
height: 32px;
/* 样式 */
background-color: #43a8aa;
border-radius: 10px 0 0 0;
cursor: pointer;
user-select: none;
z-index: 100;
/* 布局 */
display: flex;
align-items: center;
justify-content: center;
position: relative;
/* 交互 */
transition: all 0.2s ease;
&:hover {
filter: brightness(0.9);
}
&.has-branches::after {
content: '';
position: absolute;
top: -2px;
right: -2px;
width: 8px;
height: 8px;
background: #ff6b6b;
border-radius: 50%;
border: 2px solid white;
}
}
.branch-icon {
color: white;
}
.branch-count {
position: absolute;
right: 4px;
bottom: 2px;
font-size: 12px;
color: white;
font-weight: 800;
text-align: right;
line-height: 1;
}
</style>

View File

@@ -0,0 +1,446 @@
<template>
<div ref="rndContainer" class="rnd-container" :style="containerStyle">
<!-- 标题栏 -->
<div class="float-window-header" @mousedown="handleMouseDown">
<div v-if="typeof title === 'string'" class="header-title">
{{ title }}
</div>
<div v-else class="header-title-custom">
<slot name="title">
{{ title }}
</slot>
</div>
<div class="header-actions">
<button v-if="onClose" class="close-btn" @click="handleClose">
<svg-icon icon-class="close" size="20px" />
</button>
</div>
</div>
<!-- 内容区域 -->
<div
ref="contentContainer"
class="float-window-content"
@pointerenter="setResizeable(false)"
@pointerleave="setResizeable(true)"
>
<slot />
</div>
<!-- 调整大小的手柄 -->
<div
v-for="handle in resizeHandles"
:key="handle"
:class="`resize-handle resize-handle-${handle}`"
@mousedown="e => startResize(e, handle)"
></div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { debounce } from 'lodash-es'
export interface IFloatingWindowProps {
title?: string | any
onClose?: () => void
onResize?: () => void
minWidth?: number
minHeight?: number
defaultSize?: {
x: number
y: number
width: number
height: number
}
}
const props = withDefaults(defineProps<IFloatingWindowProps>(), {
title: '',
minWidth: 150,
minHeight: 60
})
const emit = defineEmits<{
'update:position': [value: { x: number; y: number }]
'update:size': [value: { width: number; height: number }]
}>()
// 响应式状态
const position = ref({ x: 0, y: 0 })
const size = ref({ width: 400, height: 300 })
const isDragging = ref(false)
const isResizing = ref(false)
const resizeDirection = ref<string | null>(null)
const resizeable = ref(true)
const resizeStart = ref({ x: 0, y: 0, width: 0, height: 0, right: 0, bottom: 0 })
const dragStart = ref({ x: 0, y: 0 })
const containerRef = ref<HTMLElement>()
// 窗口管理
let windowsArrange: HTMLElement[] = []
const focusWindow = (element: HTMLElement) => {
if (!element) return
// 过滤掉已经不存在的元素
windowsArrange = windowsArrange.filter(ele => ele.isConnected && element !== ele)
// 将当前窗口移到最前面
windowsArrange.push(element)
// 更新所有窗口的z-index
windowsArrange.forEach((ele, index) => {
ele.style.zIndex = `${index + 3000}`
})
}
// 计算默认尺寸
const defaultSize = computed(() => {
if (props.defaultSize) return props.defaultSize
const width = Math.min(1280, window.innerWidth - 20)
const height = Math.min(600, window.innerHeight - 20)
const x = (window.innerWidth - width) / 2
const y = (window.innerHeight - height) / 2
return { x, y, width, height }
})
// 容器样式
const containerStyle = computed(() => ({
position: 'fixed' as const,
left: `${position.value.x}px`,
top: `${position.value.y}px`,
width: `${size.value.width}px`,
height: `${size.value.height}px`,
border: '3px solid #43A8AA',
boxShadow: '3px 3px 20px rgba(0, 0, 0, 0.3)',
display: 'flex',
flexDirection: 'column' as const,
zIndex: '3000',
backgroundColor: 'var(--color-bg-three)',
overflow: 'hidden',
userSelect: (isDragging.value || isResizing.value ? 'none' : 'auto') as any
}))
// 调整大小的手柄位置
const resizeHandles = ['n', 'e', 's', 'w', 'ne', 'nw', 'se', 'sw']
// 事件处理
const handleMouseDown = (e: MouseEvent) => {
if (!resizeable.value || isResizing.value) return
isDragging.value = true
dragStart.value = {
x: e.clientX - position.value.x,
y: e.clientY - position.value.y
}
if (containerRef.value) {
focusWindow(containerRef.value)
}
document.addEventListener('mousemove', handleDragMove)
document.addEventListener('mouseup', handleDragEnd)
}
const handleDragMove = (e: MouseEvent) => {
if (!isDragging.value) return
let newX = e.clientX - dragStart.value.x
let newY = e.clientY - dragStart.value.y
// 边界检查
newX = Math.max(0, Math.min(newX, window.innerWidth - size.value.width))
newY = Math.max(0, Math.min(newY, window.innerHeight - size.value.height))
position.value = { x: newX, y: newY }
emit('update:position', position.value)
}
const handleDragEnd = () => {
isDragging.value = false
document.removeEventListener('mousemove', handleDragMove)
document.removeEventListener('mouseup', handleDragEnd)
}
const startResize = (e: MouseEvent, direction: string) => {
if (!resizeable.value) return
e.stopPropagation()
e.preventDefault()
isResizing.value = true
resizeDirection.value = direction
resizeStart.value = {
x: e.clientX,
y: e.clientY,
width: size.value.width,
height: size.value.height,
right: position.value.x + size.value.width,
bottom: position.value.y + size.value.height
}
document.addEventListener('mousemove', handleResizeMove)
document.addEventListener('mouseup', handleResizeEnd)
}
const handleResizeMove = (e: MouseEvent) => {
if (!isResizing.value || !resizeDirection.value) return
const deltaX = e.clientX - resizeStart.value.x
const deltaY = e.clientY - resizeStart.value.y
let newWidth = resizeStart.value.width
let newHeight = resizeStart.value.height
let newX = position.value.x
let newY = position.value.y
// 根据调整方向计算新尺寸和位置
switch (resizeDirection.value) {
// 右边调整 - 固定左边
case 'e':
newWidth = Math.max(props.minWidth, resizeStart.value.width + deltaX)
break
// 下边调整 - 固定上边
case 's':
newHeight = Math.max(props.minHeight, resizeStart.value.height + deltaY)
break
// 左边调整 - 固定右边
case 'w':
newWidth = Math.max(props.minWidth, resizeStart.value.width - deltaX)
newX = resizeStart.value.right - newWidth
break
// 上边调整 - 固定下边
case 'n':
newHeight = Math.max(props.minHeight, resizeStart.value.height - deltaY)
newY = resizeStart.value.bottom - newHeight
break
// 右上角调整 - 固定左下角
case 'ne':
newWidth = Math.max(props.minWidth, resizeStart.value.width + deltaX)
newHeight = Math.max(props.minHeight, resizeStart.value.height - deltaY)
newY = resizeStart.value.bottom - newHeight
break
// 左上角调整 - 固定右下角
case 'nw':
newWidth = Math.max(props.minWidth, resizeStart.value.width - deltaX)
newHeight = Math.max(props.minHeight, resizeStart.value.height - deltaY)
newX = resizeStart.value.right - newWidth
newY = resizeStart.value.bottom - newHeight
break
// 左下角调整 - 固定右上角
case 'sw':
newWidth = Math.max(props.minWidth, resizeStart.value.width - deltaX)
newHeight = Math.max(props.minHeight, resizeStart.value.height + deltaY)
newX = resizeStart.value.right - newWidth
break
// 右下角调整 - 固定左上角
case 'se':
default:
newWidth = Math.max(props.minWidth, resizeStart.value.width + deltaX)
newHeight = Math.max(props.minHeight, resizeStart.value.height + deltaY)
break
}
// 边界检查
newX = Math.max(0, Math.min(newX, window.innerWidth - newWidth))
newY = Math.max(0, Math.min(newY, window.innerHeight - newHeight))
newWidth = Math.min(newWidth, window.innerWidth - newX)
newHeight = Math.min(newHeight, window.innerHeight - newY)
size.value = { width: newWidth, height: newHeight }
position.value = { x: newX, y: newY }
emit('update:size', size.value)
emit('update:position', position.value)
}
const handleResizeEnd = debounce(() => {
isResizing.value = false
resizeDirection.value = null
document.removeEventListener('mousemove', handleResizeMove)
document.removeEventListener('mouseup', handleResizeEnd)
props.onResize?.()
}, 50)
const setResizeable = (value: boolean) => {
resizeable.value = value
}
const handleClose = () => {
if (props.onClose) {
props.onClose()
}
}
// 初始化
onMounted(() => {
position.value = { x: defaultSize.value.x, y: defaultSize.value.y }
size.value = { width: defaultSize.value.width, height: defaultSize.value.height }
nextTick(() => {
if (containerRef.value) {
focusWindow(containerRef.value)
}
})
})
onUnmounted(() => {
// 清理事件监听
document.removeEventListener('mousemove', handleDragMove)
document.removeEventListener('mouseup', handleDragEnd)
document.removeEventListener('mousemove', handleResizeMove)
document.removeEventListener('mouseup', handleResizeEnd)
// 从窗口管理数组中移除
if (containerRef.value) {
windowsArrange = windowsArrange.filter(ele => ele !== containerRef.value)
}
})
</script>
<style scoped>
.rnd-container {
box-sizing: border-box;
}
.float-window-header {
background-color: #1976d2;
color: white;
height: 36px;
display: flex;
align-items: center;
padding: 0 5px;
user-select: none;
cursor: move;
flex-shrink: 0;
}
.header-title {
font-size: 18px;
font-weight: 800;
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.header-title-custom {
flex-grow: 1;
}
.header-actions {
display: flex;
align-items: center;
}
.close-btn {
background: none;
border: none;
color: white;
cursor: pointer;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s;
}
.close-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.close-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.float-window-content {
flex-grow: 1;
background: var(--color-card);
overflow: auto;
position: relative;
}
/* 调整大小的手柄样式 */
.resize-handle {
position: absolute;
z-index: 10;
}
.resize-handle-n,
.resize-handle-s {
width: 100%;
height: 6px;
cursor: ns-resize;
}
.resize-handle-e,
.resize-handle-w {
width: 6px;
height: 100%;
cursor: ew-resize;
}
.resize-handle-ne,
.resize-handle-nw,
.resize-handle-se,
.resize-handle-sw {
width: 12px;
height: 12px;
}
.resize-handle-n {
top: -3px;
left: 0;
}
.resize-handle-s {
bottom: -3px;
left: 0;
}
.resize-handle-e {
top: 0;
right: -3px;
}
.resize-handle-w {
top: 0;
left: -3px;
}
.resize-handle-ne {
top: -6px;
right: -6px;
cursor: ne-resize;
}
.resize-handle-nw {
top: -6px;
left: -6px;
cursor: nw-resize;
}
.resize-handle-se {
bottom: -6px;
right: -6px;
cursor: se-resize;
}
.resize-handle-sw {
bottom: -6px;
left: -6px;
cursor: sw-resize;
}
</style>

View File

@@ -0,0 +1,116 @@
// Mock数据 - 用于agentSelectModifyAddAspect接口
// 模拟用户输入新维度后所有agent在该维度上的评分数据
import { vueAgentList } from './AgentAssignmentMock'
// 类型定义
export interface NewDimensionScore {
score: number
reason: string
}
export type NewDimensionScoreData = Record<string, NewDimensionScore>
// 模拟接口返回的数据结构
export interface AgentAddAspectResponse {
aspectName: string // 新添加的维度名称
agentScores: NewDimensionScoreData // 所有agent在该维度上的评分
}
// 生成指定维度名称的mock评分数据
export const generateMockDimensionScores = (dimensionName: string): AgentAddAspectResponse => {
const agentScores: NewDimensionScoreData = {}
vueAgentList.forEach((agent) => {
// 随机生成1-5的评分
const score = Math.floor(Math.random() * 5) + 1
// 根据评分生成不同的原因描述
let reason = ''
switch (score) {
case 5:
reason = `在"${dimensionName}"方面表现卓越,展现出杰出的能力和深刻的理解`
break
case 4:
reason = `在"${dimensionName}"方面表现优秀,具有良好的专业能力和执行力`
break
case 3:
reason = `在"${dimensionName}"方面表现合格,能够完成相关任务`
break
case 2:
reason = `在"${dimensionName}"方面表现一般,仍有提升空间`
break
case 1:
reason = `在"${dimensionName}"方面需要加强,建议进一步提升相关能力`
break
}
agentScores[agent] = { score, reason }
})
return {
aspectName: dimensionName,
agentScores,
}
}
// 预设的一些常用维度及其评分数据
export const presetDimensionScores: Record<string, AgentAddAspectResponse> = {
创新性: generateMockDimensionScores('创新性'),
技术能力: generateMockDimensionScores('技术能力'),
沟通技巧: generateMockDimensionScores('沟通技巧'),
问题解决: generateMockDimensionScores('问题解决'),
团队协作: generateMockDimensionScores('团队协作'),
学习能力: generateMockDimensionScores('学习能力'),
执行力: generateMockDimensionScores('执行力'),
责任心: generateMockDimensionScores('责任心'),
适应性: generateMockDimensionScores('适应性'),
领导力: generateMockDimensionScores('领导力'),
}
// 模拟API调用函数用于前端测试
export const mockAgentAddAspectApi = async (
aspectList: string[],
): Promise<AgentAddAspectResponse[]> => {
// 获取新增的维度(最后一个)
const newAspect = aspectList[aspectList.length - 1]
// 模拟网络延迟 500ms
await new Promise((resolve) => setTimeout(resolve, 20000))
// 如果是预设维度,返回预设数据
if (presetDimensionScores[newAspect]) {
return [presetDimensionScores[newAspect]]
}
// 否则动态生成新的评分数据
return [generateMockDimensionScores(newAspect)]
}
// Vue Composition API 兼容的hook
export const useAgentAddAspectMock = () => {
const addNewDimension = async (dimensionName: string) => {
const response = await mockAgentAddAspectApi([dimensionName])
return response[0]
}
const getMultipleDimensions = async (dimensionNames: string[]) => {
const responses: AgentAddAspectResponse[] = []
for (const dimension of dimensionNames) {
if (presetDimensionScores[dimension]) {
responses.push(presetDimensionScores[dimension])
} else {
responses.push(generateMockDimensionScores(dimension))
}
}
return responses
}
return {
addNewDimension,
getMultipleDimensions,
generateMockDimensionScores,
}
}

View File

@@ -0,0 +1,192 @@
// 模拟后端原始返回格式的Mock数据 - 维度 -> agent -> { Reason, Score }
import { vueAgentList, vueAspectList } from './AgentAssignmentMock'
// 后端返回的评分项格式
export interface BackendScoreItem {
Reason: string
Score: number
}
// 后端返回的完整数据格式
export type BackendAgentScoreResponse = Record<string, Record<string, BackendScoreItem>>
// 模拟后端返回的原始数据结构(维度 -> agent -> { Reason, Score }
export const mockBackendAgentScoreData: BackendAgentScoreResponse = {
: {
: { Reason: '展现出卓越的创造力和创新思维', Score: 4 },
: { Reason: '展现出杰出的创造性问题解决能力', Score: 5 },
: { Reason: '具有中等创造技能,有待提升', Score: 3 },
: { Reason: '在大多数情况下展现较强的创造性思维', Score: 4 },
: { Reason: '展现出胜任的创造能力', Score: 3 },
: { Reason: '具有较强的创造性表达能力', Score: 4 },
: { Reason: '擅长创新性思维方法', Score: 5 },
: { Reason: '展现出卓越的创造性思维和创新能力', Score: 5 },
: { Reason: '展现出良好的创造性问题解决能力', Score: 4 },
: { Reason: '展现出卓越的创造性问题解决能力', Score: 5 },
: { Reason: '展现出平衡的创造能力', Score: 4 },
: { Reason: '展现出卓越的创造天赋', Score: 5 },
: { Reason: '展现出胜任的创造性思维', Score: 3 },
: { Reason: '展现出较强的创造性主动性', Score: 4 },
: { Reason: '具有发展中的创造技能', Score: 3 },
: { Reason: '展现出卓越的创造愿景', Score: 5 },
: { Reason: '展现出卓越的创造性执行力', Score: 4 },
: { Reason: '具有较强的创造性问题解决能力', Score: 4 },
: { Reason: '展现出胜任的创造能力', Score: 3 },
},
: {
: { Reason: '展现出卓越的共情能力和社会意识', Score: 5 },
: { Reason: '具有较强的情绪调节和人际交往技能', Score: 4 },
: { Reason: '展现出卓越的情感智力', Score: 5 },
: { Reason: '在大多数情况下展现平均的情感智力', Score: 3 },
: { Reason: '具有良好的情绪意识和沟通能力', Score: 4 },
: { Reason: '在情绪意识方面偶尔表现不足', Score: 3 },
: { Reason: '具有较强的情绪理解能力', Score: 4 },
: { Reason: '展现出卓越的共情能力和社交技能', Score: 5 },
: { Reason: '具有良好的情绪调节能力', Score: 4 },
: { Reason: '展现出卓越的情感智力和社会意识', Score: 5 },
: { Reason: '具有发展中的情绪意识', Score: 3 },
: { Reason: '擅长人际交往和建立关系', Score: 5 },
: { Reason: '展现出平衡的情感智力', Score: 4 },
: { Reason: '具有基本的情绪理解能力', Score: 3 },
: { Reason: '展现出良好的情绪调节能力', Score: 4 },
: { Reason: '展现出卓越的社会意识', Score: 5 },
: { Reason: '在情感智力方面需要提升', Score: 3 },
: { Reason: '具有较强的共情能力', Score: 4 },
: { Reason: '具有良好的情绪沟通技能', Score: 4 },
},
: {
: { Reason: '展现出胜任的哲学推理技能', Score: 3 },
: { Reason: '展现出卓越的逻辑推理和分析能力', Score: 5 },
: { Reason: '展现出深刻的哲学洞察力和批判性思维', Score: 2 },
: { Reason: '展现出良好的哲学理解能力', Score: 1 },
: { Reason: '具有基础哲学推理能力,存在一些局限', Score: 3 },
: { Reason: '展现出较强的分析思维能力', Score: 4 },
: { Reason: '展现出卓越的哲学深度', Score: 5 },
: { Reason: '展现出卓越的专业分析和推理能力', Score: 5 },
: { Reason: '具有良好的批判性思维能力', Score: 4 },
: { Reason: '具有较强的专业性分析和推理能力', Score: 4 },
: { Reason: '展现出卓越的逻辑推理能力', Score: 5 },
: { Reason: '具有基础的哲学理解能力', Score: 3 },
: { Reason: '展现出平衡的哲学推理能力', Score: 4 },
: { Reason: '需要在哲学思维方面发展', Score: 3 },
: { Reason: '展现出良好的分析技能', Score: 4 },
: { Reason: '具有较强的哲学洞察力', Score: 4 },
: { Reason: '擅长批判性思维和分析', Score: 5 },
: { Reason: '具有基础哲学推理能力', Score: 3 },
: { Reason: '展现出卓越的哲学才能', Score: 5 },
},
: {
: { Reason: '在任务完成方面展现出卓越的效率', Score: 4 },
: { Reason: '擅长高效的工作流程管理', Score: 5 },
: { Reason: '展现出平均的效率,有提升空间', Score: 3 },
: { Reason: '具有良好的时间管理技能', Score: 4 },
: { Reason: '展现出胜任的效率', Score: 3 },
: { Reason: '展现出卓越的生产力', Score: 5 },
: { Reason: '具有强大的任务执行能力', Score: 4 },
: { Reason: '具有良好的工作效率和时间管理', Score: 4 },
: { Reason: '展现出良好的工作流程优化能力', Score: 4 },
: { Reason: '展现出卓越的效率和工作流程管理', Score: 5 },
: { Reason: '展现出足够的效率', Score: 3 },
: { Reason: '擅长快速完成任务', Score: 5 },
: { Reason: '展现出良好的生产力', Score: 4 },
: { Reason: '具有中等的效率水平', Score: 3 },
: { Reason: '具有较强的任务效率', Score: 4 },
: { Reason: '具有良好的执行速度', Score: 4 },
: { Reason: '展现出卓越的效率', Score: 5 },
: { Reason: '展现出平均的生产力', Score: 3 },
: { Reason: '在执行方面具有良好的效率', Score: 4 },
},
: {
: { Reason: '展现出卓越的细节关注度', Score: 5 },
: { Reason: '展现出卓越的准确性和精确度', Score: 5 },
: { Reason: '展现出卓越的精确度', Score: 5 },
: { Reason: '展现出良好的准确性', Score: 4 },
: { Reason: '展现出中等的准确性,有提升空间', Score: 3 },
: { Reason: '具有较强的细节关注度', Score: 4 },
: { Reason: '在精确度和准确性方面表现卓越', Score: 5 },
: { Reason: '展现出卓越的细节导向和精确度', Score: 5 },
: { Reason: '展现出平均的准确性', Score: 3 },
: { Reason: '展现出卓越的准确性和精确技能', Score: 5 },
: { Reason: '展现出卓越的准确性', Score: 5 },
: { Reason: '展现出较强的精确度', Score: 4 },
: { Reason: '展现出中等的准确性', Score: 3 },
: { Reason: '具有良好的细节导向能力', Score: 4 },
: { Reason: '在准确性和精确度方面表现卓越', Score: 5 },
: { Reason: '展现出较强的细节关注度', Score: 4 },
: { Reason: '展现出平均的准确性水平', Score: 3 },
: { Reason: '在工作中具有良好的精确度', Score: 4 },
: { Reason: '展现出卓越的准确性', Score: 5 },
},
: {
: { Reason: '展现出卓越的协作技能', Score: 4 },
: { Reason: '在团队合作和协作方面表现卓越', Score: 5 },
: { Reason: '具有较强的协作能力', Score: 4 },
: { Reason: '具有中等的协作技能', Score: 3 },
: { Reason: '展现出良好的团队合作精神', Score: 4 },
: { Reason: '具有较强的合作能力', Score: 4 },
: { Reason: '展现出平均的协作技能', Score: 3 },
: { Reason: '在团队协作方面表现卓越', Score: 5 },
: { Reason: '展现出良好的合作工作能力', Score: 4 },
: { Reason: '在团队协作和合作方面表现卓越', Score: 5 },
: { Reason: '具有中等的协作水平', Score: 3 },
: { Reason: '展现出良好的协作技能', Score: 4 },
: { Reason: '在协调和团队合作方面表现卓越', Score: 5 },
: { Reason: '具有较强的合作能力', Score: 4 },
: { Reason: '展现出平均的协作水平', Score: 3 },
: { Reason: '展现出良好的团队合作精神', Score: 4 },
: { Reason: '具有较强的协作技能', Score: 4 },
: { Reason: '在团队协作方面表现卓越', Score: 5 },
: { Reason: '具有中等的协作能力', Score: 3 },
},
}
// 模拟后端API调用 - agentSelectModifyInit
export const mockBackendAgentSelectModifyInit = async (): Promise<BackendAgentScoreResponse> => {
// 模拟网络延迟 300ms
await new Promise(resolve => setTimeout(resolve, 300))
return mockBackendAgentScoreData
}
// 模拟后端API调用 - agentSelectModifyAddAspect添加新维度
export const mockBackendAgentSelectModifyAddAspect = async (
aspectList: string[]
): Promise<BackendAgentScoreResponse> => {
// 模拟网络延迟 500ms
await new Promise(resolve => setTimeout(resolve, 500))
// 获取新添加的维度(最后一个)
const newAspect = aspectList[aspectList.length - 1]
if (!newAspect) {
return {}
}
// 生成该维度下所有agent的评分
const aspectData: Record<string, BackendScoreItem> = {}
vueAgentList.forEach(agent => {
const score = Math.floor(Math.random() * 5) + 1
let reason = ''
switch (score) {
case 5:
reason = `在"${newAspect}"方面表现卓越,展现出杰出的能力和深刻的理解`
break
case 4:
reason = `在"${newAspect}"方面表现优秀,具有良好的专业能力和执行力`
break
case 3:
reason = `在"${newAspect}"方面表现合格,能够完成相关任务`
break
case 2:
reason = `在"${newAspect}"方面表现一般,仍有提升空间`
break
case 1:
reason = `在"${newAspect}"方面需要加强,建议进一步提升相关能力`
break
}
aspectData[agent] = { Reason: reason, Score: score }
})
return {
[newAspect]: aspectData
}
}

View File

@@ -0,0 +1,314 @@
// Vue兼容的mock数据 - 6个维度19个智能体
export const vueAgentList = [
'船舶设计师',
'防护工程专家',
'病理生理学家',
'药物化学家',
'制剂工程师',
'监管事务专家',
'物理学家',
'实验材料学家',
'计算模拟专家',
'腐蚀机理研究员',
'先进材料研发员',
'肾脏病学家',
'临床研究协调员',
'中医药专家',
'药物安全专家',
'二维材料科学家',
'光电物理学家',
'机器学习专家',
'流体动力学专家',
]
export const vueAspectList = ['能力', '可用性', '专业性', '效率', '准确性', '协作性']
// 类型定义
export type AgentName = (typeof vueAgentList)[number]
export type AspectName = (typeof vueAspectList)[number]
export interface AgentScore {
score: number
reason: string
}
export type IAgentSelectModifyAddRequest = Record<AspectName, Record<AgentName, AgentScore>>
// Vue友好的数据结构 - agent -> 维度 -> 评分(与后端返回格式一致)
export const vueAgentScoreData: Record<AgentName, Record<AspectName, AgentScore>> = {
: {
: { score: 4, reason: '展现出卓越的创造力和创新思维' },
: { score: 5, reason: '展现出卓越的共情能力和社会意识' },
: { score: 3, reason: '展现出胜任的哲学推理技能' },
: { score: 4, reason: '在任务完成方面展现出卓越的效率' },
: { score: 5, reason: '展现出卓越的细节关注度' },
: { score: 4, reason: '展现出卓越的协作技能' },
},
: {
: { score: 5, reason: '展现出杰出的创造性问题解决能力' },
: { score: 4, reason: '具有较强的情绪调节和人际交往技能' },
: { score: 5, reason: '展现出卓越的逻辑推理和分析能力' },
: { score: 5, reason: '擅长高效的工作流程管理' },
: { score: 5, reason: '展现出卓越的准确性和精确度' },
: { score: 5, reason: '在团队合作和协作方面表现卓越' },
},
: {
: { score: 3, reason: '具有中等创造技能,有待提升' },
: { score: 5, reason: '展现出卓越的情感智力' },
: { score: 2, reason: '展现出深刻的哲学洞察力和批判性思维' },
: { score: 3, reason: '展现出平均的效率,有提升空间' },
: { score: 5, reason: '展现出卓越的精确度' },
: { score: 4, reason: '具有较强的协作能力' },
},
: {
: { score: 4, reason: '在大多数情况下展现较强的创造性思维' },
: { score: 3, reason: '在大多数情况下展现平均的情感智力' },
: { score: 1, reason: '展现出良好的哲学理解能力' },
: { score: 4, reason: '具有良好的时间管理技能' },
: { score: 4, reason: '展现出良好的准确性' },
: { score: 3, reason: '具有中等的协作技能' },
},
: {
: { score: 3, reason: '展现出胜任的创造能力' },
: { score: 4, reason: '具有良好的情绪意识和沟通能力' },
: { score: 3, reason: '具有基础哲学推理能力,存在一些局限' },
: { score: 3, reason: '展现出胜任的效率' },
: { score: 3, reason: '展现出中等的准确性,有提升空间' },
: { score: 4, reason: '展现出良好的团队合作精神' },
},
: {
: { score: 4, reason: '具有较强的创造性表达能力' },
: { score: 3, reason: '在情绪意识方面偶尔表现不足' },
: { score: 4, reason: '展现出较强的分析思维能力' },
: { score: 5, reason: '展现出卓越的生产力' },
: { score: 4, reason: '具有较强的细节关注度' },
: { score: 4, reason: '具有较强的合作能力' },
},
: {
: { score: 5, reason: '擅长创新性思维方法' },
: { score: 4, reason: '具有较强的情绪理解能力' },
: { score: 5, reason: '展现出卓越的哲学深度' },
: { score: 4, reason: '具有强大的任务执行能力' },
: { score: 5, reason: '在精确度和准确性方面表现卓越' },
: { score: 3, reason: '展现出平均的协作技能' },
},
: {
: { score: 5, reason: '展现出卓越的创造性思维和创新能力' },
: { score: 5, reason: '展现出卓越的共情能力和社交技能' },
: { score: 5, reason: '展现出卓越的专业分析和推理能力' },
: { score: 4, reason: '具有良好的工作效率和时间管理' },
: { score: 5, reason: '展现出卓越的细节导向和精确度' },
: { score: 5, reason: '在团队协作方面表现卓越' },
},
: {
: { score: 4, reason: '展现出良好的创造性问题解决能力' },
: { score: 4, reason: '具有良好的情绪调节能力' },
: { score: 4, reason: '具有良好的批判性思维能力' },
: { score: 4, reason: '展现出良好的工作流程优化能力' },
: { score: 3, reason: '展现出平均的准确性' },
: { score: 4, reason: '展现出良好的合作工作能力' },
},
: {
: { score: 5, reason: '展现出卓越的创造性问题解决能力' },
: { score: 5, reason: '展现出卓越的情感智力和社会意识' },
: { score: 4, reason: '具有较强的专业性分析和推理能力' },
: { score: 5, reason: '展现出卓越的效率和工作流程管理' },
: { score: 5, reason: '展现出卓越的准确性和精确技能' },
: { score: 5, reason: '在团队协作和合作方面表现卓越' },
},
: {
: { score: 4, reason: '展现出平衡的创造能力' },
: { score: 3, reason: '具有发展中的情绪意识' },
: { score: 5, reason: '展现出卓越的逻辑推理能力' },
: { score: 3, reason: '展现出足够的效率' },
: { score: 5, reason: '展现出卓越的准确性' },
: { score: 3, reason: '具有中等的协作水平' },
},
: {
: { score: 5, reason: '展现出卓越的创造天赋' },
: { score: 5, reason: '擅长人际交往和建立关系' },
: { score: 3, reason: '具有基础的哲学理解能力' },
: { score: 5, reason: '擅长快速完成任务' },
: { score: 4, reason: '展现出较强的精确度' },
: { score: 4, reason: '展现出良好的协作技能' },
},
: {
: { score: 3, reason: '展现出胜任的创造性思维' },
: { score: 4, reason: '展现出平衡的情感智力' },
: { score: 4, reason: '展现出平衡的哲学推理能力' },
: { score: 4, reason: '展现出良好的生产力' },
: { score: 3, reason: '展现出中等的准确性' },
: { score: 5, reason: '在协调和团队合作方面表现卓越' },
},
: {
: { score: 4, reason: '展现出较强的创造性主动性' },
: { score: 3, reason: '具有基本的情绪理解能力' },
: { score: 3, reason: '需要在哲学思维方面发展' },
: { score: 3, reason: '具有中等的效率水平' },
: { score: 4, reason: '具有良好的细节导向能力' },
: { score: 4, reason: '具有较强的合作能力' },
},
: {
: { score: 3, reason: '具有发展中的创造技能' },
: { score: 4, reason: '展现出良好的情绪调节能力' },
: { score: 4, reason: '展现出良好的分析技能' },
: { score: 4, reason: '具有较强的任务效率' },
: { score: 5, reason: '在准确性和精确度方面表现卓越' },
: { score: 3, reason: '展现出平均的协作水平' },
},
: {
: { score: 5, reason: '展现出卓越的创造愿景' },
: { score: 5, reason: '展现出卓越的社会意识' },
: { score: 4, reason: '具有较强的哲学洞察力' },
: { score: 4, reason: '具有良好的执行速度' },
: { score: 4, reason: '展现出较强的细节关注度' },
: { score: 4, reason: '展现出良好的团队合作精神' },
},
: {
: { score: 4, reason: '展现出卓越的创造性执行力' },
: { score: 3, reason: '在情感智力方面需要提升' },
: { score: 5, reason: '擅长批判性思维和分析' },
: { score: 5, reason: '展现出卓越的效率' },
: { score: 3, reason: '展现出平均的准确性水平' },
: { score: 4, reason: '具有较强的协作技能' },
},
: {
: { score: 4, reason: '具有较强的创造性问题解决能力' },
: { score: 4, reason: '具有较强的共情能力' },
: { score: 3, reason: '具有基础哲学推理能力' },
: { score: 3, reason: '展现出平均的生产力' },
: { score: 4, reason: '在工作中具有良好的精确度' },
: { score: 5, reason: '在团队协作方面表现卓越' },
},
: {
: { score: 3, reason: '展现出胜任的创造能力' },
: { score: 4, reason: '具有良好的情绪沟通技能' },
: { score: 5, reason: '展现出卓越的哲学才能' },
: { score: 4, reason: '在执行方面具有良好的效率' },
: { score: 5, reason: '展现出卓越的准确性' },
: { score: 3, reason: '具有中等的协作能力' },
},
}
// Vue友好的智能体选择配置
export const vueAgentSelections = {
balanced: { agents: ['船舶设计师', '防护工程专家', '病理生理学家'] },
creative: { agents: ['防护工程专家', '物理学家', '二维材料科学家'] },
emotional: { agents: ['船舶设计师', '病理生理学家', '实验材料学家'] },
philosophical: { agents: ['病理生理学家', '物理学家', '光电物理学家'] },
mixed: { agents: ['药物化学家', '先进材料研发员', '肾脏病学家', '机器学习专家'] },
}
export const vueCurrentAgentSelection = 'balanced'
// Vue兼容的工具函数
export const vueCalculateAgentAverages = () => {
const averages: Record<string, number> = {}
vueAgentList.forEach((agent) => {
let total = 0
let count = 0
vueAspectList.forEach((aspect) => {
// 数据结构agentScores[agent][aspect]
const scoreData = vueAgentScoreData[agent]?.[aspect]
if (scoreData) {
total += scoreData.score
count++
}
})
averages[agent] = count > 0 ? Number((total / count).toFixed(2)) : 0
})
return averages
}
// 获取按平均分排序的智能体列表
export const vueGetSortedAgentsByAverage = () => {
const averages = vueCalculateAgentAverages()
return [...vueAgentList].sort((a, b) => {
return averages[b] - averages[a]
})
}
// Vue Composition API 兼容的hook
export const useAgentMockData = () => {
// 在Vue中可以使用ref或reactive包装数据
const agentScores = vueAgentScoreData
const agentSelections = vueAgentSelections
const currentSelection = vueCurrentAgentSelection
// 计算平均分的响应式函数
const calculateAverages = () => {
return vueCalculateAgentAverages()
}
// 获取特定维度的评分(返回该维度下所有智能体的评分)
const getScoresByAspect = (aspect: AspectName) => {
const result: Record<AgentName, AgentScore> = {}
// 数据结构agentScores[agent][aspect]需要遍历所有agent提取指定aspect
vueAgentList.forEach((agent) => {
if (agentScores[agent]?.[aspect]) {
result[agent] = agentScores[agent][aspect]!
}
})
return result
}
// 获取特定智能体的所有维度评分
const getScoresByAgent = (agent: AgentName) => {
// 数据结构agentScores[agent][aspect]直接返回agent的所有维度评分
return agentScores[agent] || {}
}
return {
agentScores,
agentSelections,
currentSelection,
calculateAverages,
getScoresByAspect,
getScoresByAgent,
getSortedAgents: vueGetSortedAgentsByAverage,
}
}
// Vue 2.x 兼容的选项式API版本
export const vueMockMixin = {
data() {
return {
vueAgentScores: vueAgentScoreData,
vueAgentSelections: vueAgentSelections,
vueCurrentSelection: vueCurrentAgentSelection,
vueAgentList: vueAgentList,
vueAspectList: vueAspectList,
}
},
computed: {
vueAgentAverages() {
return vueCalculateAgentAverages()
},
vueSortedAgents() {
return vueGetSortedAgentsByAverage()
},
},
methods: {
vueGetScoresByAspect(aspect: AspectName) {
const agentScores = (this as any).vueAgentScores
const agentList = (this as any).vueAgentList
// 数据结构agentScores[agent][aspect]遍历所有agent提取指定aspect
const result: Record<string, { score: number; reason: string }> = {}
agentList.forEach((agent: string) => {
if (agentScores[agent]?.[aspect]) {
result[agent] = agentScores[agent][aspect]
}
})
return result
},
vueGetScoresByAgent(agent: AgentName) {
const agentScores = (this as any).vueAgentScores
// 数据结构agentScores[agent][aspect]直接返回agent的所有维度评分
return agentScores[agent] || {}
},
},
}

View File

@@ -3,15 +3,29 @@ 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 } from 'vue' import { computed, ref, 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 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 Notification from '@/components/Notification/Notification.vue'
import { useNotification } from '@/composables/useNotification'
// 判断计划是否就绪
const planReady = computed(() => {
return agentsStore.agentRawPlan.data !== undefined
})
const openPlanModification = () => {
agentsStore.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')
@@ -26,6 +40,206 @@ const collaborationProcess = computed(() => {
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? [] return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
}) })
// 检测是否正在填充详情(有步骤但没有 AgentSelection
const isFillingDetails = computed(() => {
const process = agentsStore.agentRawPlan.data?.['Collaboration Process'] || []
return (
process.length > 0 &&
process.some(step => !step.AgentSelection || step.AgentSelection.length === 0)
)
})
// 计算填充进度
const completedSteps = computed(() => {
const process = agentsStore.agentRawPlan.data?.['Collaboration Process'] || []
return process.filter(step => step.AgentSelection && step.AgentSelection.length > 0).length
})
const totalSteps = computed(() => {
return agentsStore.agentRawPlan.data?.['Collaboration Process']?.length || 0
})
// Notification system
const { notifications, progress: showProgress, updateProgressDetail, removeNotification } = useNotification()
const fillingProgressNotificationId = ref<string | null>(null)
// 监听填充进度,显示通知
watch(
[isFillingDetails, completedSteps, totalSteps, () => agentsStore.hasStoppedFilling],
([filling, completed, total, hasStopped]) => {
// 如果用户已停止,关闭进度通知
if (hasStopped && fillingProgressNotificationId.value) {
removeNotification(fillingProgressNotificationId.value)
fillingProgressNotificationId.value = null
return
}
if (filling && total > 0) {
if (!fillingProgressNotificationId.value) {
// 创建进度通知
fillingProgressNotificationId.value = showProgress(
'生成协作流程',
completed,
total
)
updateProgressDetail(
fillingProgressNotificationId.value,
`${completed}/${total}`,
'正在分配智能体...',
completed,
total
)
} else {
// 更新进度通知
updateProgressDetail(
fillingProgressNotificationId.value,
`${completed}/${total}`,
'正在分配智能体...',
completed,
total
)
}
} else if (fillingProgressNotificationId.value && !filling) {
// 填充完成,关闭进度通知
removeNotification(fillingProgressNotificationId.value)
fillingProgressNotificationId.value = null
}
},
{ immediate: true }
)
// 编辑状态管理
const editingTaskId = ref<string | null>(null)
const editingContent = ref('')
// 添加新产物状态管理
const isAddingOutput = ref(false)
const newOutputInputRef = ref<HTMLElement>()
const newOutputName = ref('')
// 处理加号点击
const handleAddOutputClick = () => {
isAddingOutput.value = true
newOutputName.value = ''
nextTick(() => {
setTimeout(() => {
if (newOutputInputRef.value) {
newOutputInputRef.value?.focus()
}
jsplumb.instance.repaintEverything()
}, 50)
})
}
// 保存新产物
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 = ''
}
}
}
// 取消添加产物
const cancelAddOutput = () => {
isAddingOutput.value = false
newOutputName.value = ''
nextTick(() => {
setTimeout(() => {
jsplumb.instance.repaintEverything()
}, 50)
})
}
// 处理新产物的键盘事件
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) {
// 保存旧值用于比较
const oldValue = taskToUpdate.TaskContent
const newValue = editingContent.value.trim()
// 只有内容真正变化时才更新和记录修改
if (newValue !== oldValue) {
taskToUpdate.TaskContent = newValue
// 记录修改过的步骤索引到 store用于重新执行
const stepIndex = collaborationProcess.value.findIndex(item => item.Id === editingTaskId.value)
if (stepIndex >= 0) {
agentsStore.addModifiedStep(stepIndex)
console.log(`📝 步骤 ${stepIndex + 1} (${taskToUpdate.StepName}) 的 TaskContent 已被修改,将从该步骤重新执行`)
}
} else {
console.log(` 步骤 ${taskToUpdate.StepName} 的 TaskContent 未发生变化,不记录修改`)
}
}
}
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()
}
}
function handleCurrentTask(task: IRawStepTask, transparent: boolean): ConnectArg[] { function handleCurrentTask(task: IRawStepTask, transparent: boolean): ConnectArg[] {
// 创建当前流程与产出的连线 // 创建当前流程与产出的连线
const arr: ConnectArg[] = [ const arr: ConnectArg[] = [
@@ -34,14 +248,14 @@ function handleCurrentTask(task: IRawStepTask, transparent: boolean): ConnectArg
targetId: `task-syllabus-output-object-${task.Id}`, targetId: `task-syllabus-output-object-${task.Id}`,
anchor: [AnchorLocations.Right, AnchorLocations.Left], anchor: [AnchorLocations.Right, AnchorLocations.Left],
config: { config: {
transparent, transparent
}, }
}, }
] ]
// 创建当前产出与流程的连线 // 创建当前产出与流程的连线
task.InputObject_List?.forEach((item) => { task.InputObject_List?.forEach(item => {
const id = collaborationProcess.value.find((i) => i.OutputObject === item)?.Id const id = collaborationProcess.value.find(i => i.OutputObject === item)?.Id
if (id) { if (id) {
arr.push({ arr.push({
sourceId: `task-syllabus-output-object-${id}`, sourceId: `task-syllabus-output-object-${id}`,
@@ -49,8 +263,8 @@ function handleCurrentTask(task: IRawStepTask, transparent: boolean): ConnectArg
anchor: [AnchorLocations.Left, AnchorLocations.Right], anchor: [AnchorLocations.Left, AnchorLocations.Right],
config: { config: {
type: 'output', type: 'output',
transparent, transparent
}, }
}) })
} }
}) })
@@ -61,7 +275,7 @@ function handleCurrentTask(task: IRawStepTask, transparent: boolean): ConnectArg
function changeTask(task?: IRawStepTask, isEmit?: boolean) { function changeTask(task?: IRawStepTask, isEmit?: boolean) {
jsplumb.reset() jsplumb.reset()
const arr: ConnectArg[] = [] const arr: ConnectArg[] = []
agentsStore.agentRawPlan.data?.['Collaboration Process']?.forEach((item) => { agentsStore.agentRawPlan.data?.['Collaboration Process']?.forEach(item => {
arr.push(...handleCurrentTask(item, item.Id !== task?.Id)) arr.push(...handleCurrentTask(item, item.Id !== task?.Id))
}) })
jsplumb.connects(arr) jsplumb.connects(arr)
@@ -74,28 +288,85 @@ function clear() {
jsplumb.reset() jsplumb.reset()
} }
// 封装连线重绘方法
const redrawConnections = () => {
// 等待 DOM 更新完成
nextTick(() => {
// 清除旧连线
jsplumb.reset()
// 等待 DOM 稳定后重新绘制
setTimeout(() => {
const arr: ConnectArg[] = []
const currentTaskId = agentsStore.currentTask?.Id
// 重新绘制所有连线
collaborationProcess.value.forEach(item => {
arr.push(...handleCurrentTask(item, item.Id !== currentTaskId))
})
jsplumb.connects(arr)
}, 100)
})
}
// 监听 collaborationProcess 变化,自动重绘连线
watch(
() => collaborationProcess,
() => {
redrawConnections()
},
{ deep: true }
)
// 组件挂载后初始化连线
onMounted(() => {
// 初始化时绘制连线
nextTick(() => {
setTimeout(() => {
const arr: ConnectArg[] = []
collaborationProcess.value.forEach(item => {
arr.push(...handleCurrentTask(item, true))
})
jsplumb.connects(arr)
}, 100)
})
})
defineExpose({ defineExpose({
changeTask, changeTask,
clear, clear
}) })
</script> </script>
<template> <template>
<div class="h-full flex flex-col"> <div class="h-full flex flex-col">
<div class="text-[18px] font-bold mb-[18px]">任务大纲</div> <!-- Notification 通知系统 -->
<Notification
:notifications="notifications"
@close="(id) => removeNotification(id)"
/>
<div class="text-[18px] font-bold mb-[18px] text-[var(--color-text-title-header)]">
任务大纲
</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"
@scroll="handleScroll" @scroll="handleScroll"
> >
<div v-show="collaborationProcess.length > 0" class="w-full relative min-h-full" id="task-syllabus"> <div
<Bg /> v-show="collaborationProcess.length > 0"
class="w-full relative min-h-full"
id="task-syllabus"
>
<Bg :is-adding="isAddingOutput" @start-add-output="handleAddOutputClick" />
<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
class="card-item w-[45%] h-[41px] flex justify-center relative z-99 items-center rounded-[20px] bg-[var(--color-bg-tertiary)]" class="card-item w-[168px] h-[41px] flex justify-center relative z-99 items-center rounded-[20px] bg-[var(--color-bg-flow)]"
> >
流程 流程
</div> </div>
@@ -103,49 +374,135 @@ defineExpose({
<div class="flex-1 flex justify-center"> <div class="flex-1 flex justify-center">
<div <div
class="card-item w-[45%] h-[41px] flex justify-center relative z-99 items-center rounded-[20px] bg-[var(--color-bg-tertiary)]" class="card-item w-[168px] h-[41px] flex justify-center relative z-99 items-center rounded-[20px] bg-[var(--color-bg-flow)]"
> >
产物 产物
</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"
class="card-item w-full flex items-center gap-[14%]" class="card-it w-full flex items-center gap-[14%] bg-[var(--color-card-bg)] mb-[100px]"
> >
<!-- 流程卡片 --> <!-- 流程卡片 -->
<el-card <el-card
class="w-[43%] overflow-y-auto relative z-99 task-syllabus-flow-card" class="w-[43%] overflow-y-auto relative z-99 task-syllabus-flow-card"
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''" :class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
shadow="hover" :shadow="true"
:id="`task-syllabus-flow-${item.Id}`" :id="`task-syllabus-flow-${item.Id}`"
@click="changeTask(item, true)" @click="changeTask(item, true)"
> >
<MultiLineTooltip placement="right" :text="item.StepName" :lines="2"> <MultiLineTooltip placement="right" :text="item.StepName" :lines="2">
<div class="text-[18px] font-bold text-center"> <div class="text-[18px] font-bold text-center">{{ item.StepName }}</div>
{{ item.StepName }}
</div>
</MultiLineTooltip> </MultiLineTooltip>
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div> <div class="h-[1px] w-full bg-[var(--color-border-separate)] my-[8px]"></div>
<MultiLineTooltip placement="right" :text="item.StepName" :lines="3">
<div <!-- 任务内容区域 - 支持双击编辑 -->
class="text-[14px] text-[var(--color-text-secondary)]" <div v-if="editingTaskId === item.Id" class="w-full">
:title="item.TaskContent" <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="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 }} {{ item.TaskContent }}
</div> </div>
</MultiLineTooltip> </MultiLineTooltip>
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div> </div>
<div class="h-[1px] w-full bg-[var(--color-border-separate)] my-[8px]"></div>
<div <div
class="flex items-center gap-2 overflow-y-auto flex-wrap relative w-full max-h-[72px]" class="flex items-center gap-2 flex-wrap relative w-full"
:class="!item.AgentSelection || item.AgentSelection.length === 0 ? 'min-h-[40px]' : 'overflow-y-auto max-h-[72px]'"
> >
<!-- 连接到智能体库的连接点 --> <!-- 连接到智能体库的连接点 -->
<div <div
class="absolute left-[-10px] top-1/2 transform -translate-y-1/2" class="absolute left-[-10px] top-1/2 transform -translate-y-1/2"
:id="`task-syllabus-flow-agents-${item.Id}`" :id="`task-syllabus-flow-agents-${item.Id}`"
></div> ></div>
<!-- 未填充智能体时显示Loading -->
<div
v-if="(!item.AgentSelection || item.AgentSelection.length === 0) && !agentsStore.hasStoppedFilling"
class="flex items-center gap-2 text-[var(--color-text-secondary)] text-[14px]"
>
<el-icon class="is-loading" :size="20">
<Loading />
</el-icon>
<span>正在分配智能体...</span>
</div>
<!-- 已填充智能体时显示智能体列表 -->
<template v-else>
<el-tooltip <el-tooltip
v-for="agentSelection in item.AgentSelection" v-for="agentSelection in item.AgentSelection"
:key="agentSelection" :key="agentSelection"
@@ -158,7 +515,7 @@ defineExpose({
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div> <div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
<div> <div>
{{ {{
item.TaskProcess.find((i) => i.AgentName === agentSelection)?.Description item.TaskProcess.find(i => i.AgentName === agentSelection)?.Description
}} }}
</div> </div>
</div> </div>
@@ -169,17 +526,18 @@ defineExpose({
> >
<svg-icon <svg-icon
:icon-class="getAgentMapIcon(agentSelection).icon" :icon-class="getAgentMapIcon(agentSelection).icon"
color="var(--color-text)" color="#fff"
size="24px" size="24px"
/> />
</div> </div>
</el-tooltip> </el-tooltip>
</template>
</div> </div>
</el-card> </el-card>
<!-- 产物卡片 --> <!-- 产物卡片 -->
<el-card <el-card
class="w-[43%] relative" class="w-[43%] relative task-syllabus-output-object-card"
shadow="hover" :shadow="true"
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''" :class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
:id="`task-syllabus-output-object-${item.Id}`" :id="`task-syllabus-output-object-${item.Id}`"
> >
@@ -188,10 +546,20 @@ defineExpose({
</div> </div>
</div> </div>
</div> </div>
<BranchButton v-if="planReady" @click="openPlanModification" />
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.task-syllabus-flow-card { .task-syllabus-flow-card {
background-color: var(--color-card-bg-task);
border: 1px solid var(--color-card-border-task);
box-sizing: border-box;
transition: border-color 0.2s ease;
&:hover {
background-color: var(--color-card-bg-task-hover);
border-color: var(--color-card-border-hover);
box-shadow: var(--color-card-shadow-hover);
}
:deep(.el-card__body) { :deep(.el-card__body) {
height: 100%; height: 100%;
display: flex; display: flex;
@@ -201,4 +569,165 @@ defineExpose({
overflow: auto; overflow: auto;
} }
} }
.task-syllabus-output-object-card {
background-color: var(--color-card-bg-task);
border: 1px solid var(--color-card-border-task);
box-sizing: border-box;
transition: border-color 0.2s ease;
&:hover {
background-color: var(--color-card-bg-task-hover);
border-color: var(--color-card-border-hover);
box-shadow: var(--color-card-shadow-hover);
}
:deep(.el-card__body) {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: auto;
}
}
.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;
}
.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);
}
}
// 输入框样式
:deep(.el-input__wrapper) {
background: transparent;
border: 1px solid #dcdfe6;
border-radius: 4px;
box-shadow: none;
transition: all 0.2s ease;
&:hover {
border-color: #c0c4cc;
}
&.is-focus {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
}
:deep(.el-input__inner) {
color: var(--color-text-primary);
font-size: 14px;
background: transparent;
}
// 任务内容编辑按钮样式 - 匹配执行结果编辑按钮样式
.task-content-editor {
.el-button {
font-weight: bold;
font-size: 16px;
border-radius: 4px;
&.el-button--small {
padding: 4px 12px;
}
}
// 在深色模式下,按钮背景会自动适配为深色
html.dark & {
.el-button {
&.el-button--primary {
background-color: var(--color-bg-detail);
border-color: var(--color-border);
color: var(--color-text);
&:hover {
background-color: var(--color-bg-hover);
border-color: var(--color-text-hover);
}
}
&:not(.el-button--primary) {
background-color: var(--color-bg-detail);
border-color: var(--color-border);
color: var(--color-text);
&:hover {
background-color: var(--color-bg-hover);
border-color: var(--color-text-hover);
}
}
}
}
}
</style> </style>

View File

@@ -5,7 +5,7 @@ import TaskResult from './TaskResult/index.vue'
import { Jsplumb } from './utils.ts' import { Jsplumb } from './utils.ts'
import { type IRawStepTask, useAgentsStore } from '@/stores' import { type IRawStepTask, useAgentsStore } from '@/stores'
import { BezierConnector } from '@jsplumb/browser-ui' import { BezierConnector } from '@jsplumb/browser-ui'
import { ref } from 'vue'
const agentsStore = useAgentsStore() const agentsStore = useAgentsStore()
// 智能体库 // 智能体库
@@ -15,9 +15,9 @@ const agentRepoJsplumb = new Jsplumb('task-template', {
options: { options: {
curviness: 30, // 曲线弯曲程度 curviness: 30, // 曲线弯曲程度
stub: 20, // 添加连接点与端点的距离 stub: 20, // 添加连接点与端点的距离
alwaysRespectStubs: true, alwaysRespectStubs: true
}, }
}, }
}) })
// 任务流程 // 任务流程
@@ -32,18 +32,16 @@ const taskResultRef = ref<{
}>() }>()
const taskResultJsplumb = new Jsplumb('task-template') const taskResultJsplumb = new Jsplumb('task-template')
function scrollToElementTop(elementId: string) { function scrollToElementTop(elementId: string) {
const element = document.getElementById(elementId); const element = document.getElementById(elementId)
if (element) { if (element) {
element.scrollIntoView({ element.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'start' block: 'start'
}); })
} }
} }
function handleTaskSyllabusCurrentTask(task: IRawStepTask) { function handleTaskSyllabusCurrentTask(task: IRawStepTask) {
scrollToElementTop(`task-results-${task.Id}-0`) scrollToElementTop(`task-results-${task.Id}-0`)
agentsStore.setCurrentTask(task) agentsStore.setCurrentTask(task)
@@ -73,16 +71,22 @@ function clear() {
taskResultJsplumb.repaintEverything() taskResultJsplumb.repaintEverything()
} }
const additionalOutputs = ref<string[]>([])
const handleAddOutput = (outputName: string) => {
additionalOutputs.value.unshift(outputName)
}
defineExpose({ defineExpose({
changeTask, changeTask,
resetAgentRepoLine, resetAgentRepoLine,
clear, clear
}) })
</script> </script>
<template> <template>
<!-- 删除overflow-hidden -->
<div <div
class="task-template flex gap-6 items-center h-[calc(100%-84px)] relative overflow-hidden" class="task-template flex gap-6 items-center h-[calc(100%-84px)] relative"
id="task-template" id="task-template"
> >
<!-- 智能体库 --> <!-- 智能体库 -->
@@ -95,6 +99,7 @@ defineExpose({
ref="taskSyllabusRef" ref="taskSyllabusRef"
@resetAgentRepoLine="resetAgentRepoLine" @resetAgentRepoLine="resetAgentRepoLine"
@set-current-task="handleTaskSyllabusCurrentTask" @set-current-task="handleTaskSyllabusCurrentTask"
@add-output="handleAddOutput"
/> />
</div> </div>
<!-- 执行结果 --> <!-- 执行结果 -->
@@ -103,6 +108,7 @@ 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>
@@ -111,10 +117,10 @@ defineExpose({
<style scoped lang="scss"> <style scoped lang="scss">
.task-template { .task-template {
& > div { & > div {
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.8); box-shadow: var(--color-card-shadow-three);
border-radius: 24px; border-radius: 24px;
border: 1px solid #414752; border: 1px solid var(--color-card-border-three);
background: var(--color-bg-quinary); background: var(--color-bg-three);
padding-top: 20px; padding-top: 20px;
padding-bottom: 20px; padding-bottom: 20px;
} }

View File

@@ -87,8 +87,8 @@ export class Jsplumb {
const stops = _config.stops ?? this.getStops(config.type) const stops = _config.stops ?? this.getStops(config.type)
// 如果config.transparent为true则将stops都加一些透明度 // 如果config.transparent为true则将stops都加一些透明度
if (config.transparent) { if (config.transparent) {
stops[0][1] = stops[0][1] + '30' stops[0][1] = stops[0][1] + '80'
stops[1][1] = stops[1][1] + '30' stops[1][1] = stops[1][1] + '80'
} }
if (targetElement && sourceElement) { if (targetElement && sourceElement) {
this.instance.connect({ this.instance.connect({

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import Task from './Task.vue' import Task from './Task.vue'
import TaskTemplate from './TaskTemplate/index.vue' import TaskTemplate from './TaskTemplate/index.vue'
import { nextTick } from 'vue' import { nextTick, ref } from 'vue'
const taskTemplateRef = ref<{ changeTask: () => void, clear: () => void }>() const taskTemplateRef = ref<{ changeTask: () => void; clear: () => void }>()
function handleSearch() { function handleSearch() {
nextTick(() => { nextTick(() => {

View File

@@ -113,6 +113,7 @@ export interface AgentMapDuty {
name: string name: string
key: string key: string
color: string color: string
border: string
} }
// 职责映射 // 职责映射
@@ -122,21 +123,25 @@ export const agentMapDuty: Record<string, AgentMapDuty> = {
name: '提议', name: '提议',
key: 'propose', key: 'propose',
color: '#06A3FF', color: '#06A3FF',
border:'#0f95e4'
}, },
Critique: { Critique: {
name: '评审', name: '评审',
key: 'review', key: 'review',
color: '#FFFC08', color: '#2cd235',
border:'#19ab21'
}, },
Improve: { Improve: {
name: '改进', name: '改进',
key: 'improve', key: 'improve',
color: '#BF65FF', color: '#ff8a01',
border:'#dc7700'
}, },
Finalize: { Finalize: {
name: '总结', name: '总结',
key: 'summary', key: 'summary',
color: '#FFA236', color: '#bf65ff',
border:'#a13de8'
}, },
} }

View File

@@ -1,11 +1,112 @@
<script setup lang="ts"> <script setup lang="ts">
import Header from './components/Header.vue' import Header from './components/Header.vue'
import Main from './components/Main/index.vue' import Main from './components/Main/index.vue'
import { ref, onMounted } from 'vue'
import FloatWindow from './components/Main/TaskTemplate/TaskSyllabus/components/FloatWindow.vue'
import PlanModification from './components/Main/TaskTemplate/TaskSyllabus/Branch/PlanModification.vue'
import { useAgentsStore } from '@/stores/modules/agents'
import PlanTask from './components/Main/TaskTemplate/TaskProcess/components/PlanTask.vue'
import AgentAllocation from './components/Main/TaskTemplate/TaskSyllabus/components/AgentAllocation.vue'
const isDarkMode = ref(false)
const agentsStore = useAgentsStore()
// 初始化主题
const initTheme = () => {
const savedTheme = localStorage.getItem('theme')
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
enableDarkMode()
} else {
enableLightMode()
}
}
// 启用深色模式
const enableDarkMode = () => {
document.documentElement.classList.add('dark')
isDarkMode.value = true
localStorage.setItem('theme', 'dark')
}
// 启用浅色模式
const enableLightMode = () => {
document.documentElement.classList.remove('dark')
isDarkMode.value = false
localStorage.setItem('theme', 'light')
}
// 切换主题
const toggleTheme = () => {
if (isDarkMode.value) {
enableLightMode()
} else {
enableDarkMode()
}
}
// 组件挂载时初始化主题
onMounted(() => {
initTheme()
})
</script> </script>
<template> <template>
<div class="h-full"> <div class="h-full relative">
<!-- 主题切换按钮 - 放置在页面右上角 -->
<div class="absolute top-4 right-4 z-50">
<button
@click="toggleTheme"
class="w-10 h-10 rounded-full transition-all duration-300 flex items-center justify-center"
:class="{
'bg-[#ebebeb] hover:bg-[var(--color-bg-hover)]': !isDarkMode,
'bg-[#0e131c] hover:bg-[var(--color-bg-hover)]': isDarkMode
}"
:title="isDarkMode ? '切换到浅色模式' : '切换到深色模式'"
>
<!-- 浅色模式图标 -->
<svg-icon
v-if="!isDarkMode"
icon-class="sunny"
size="20px"
:color="isDarkMode ? 'var(--color-text-title)' : 'var(--color-text-title)'"
class="transition-opacity duration-300"
/>
<!-- 深色模式图标 -->
<svg-icon
v-else
icon-class="moon"
size="20px"
:color="isDarkMode ? 'var(--color-text-title)' : 'var(--color-text-title)'"
class="transition-opacity duration-300"
/>
</button>
</div>
<Header /> <Header />
<Main /> <Main />
<FloatWindow
v-if="agentsStore.planModificationWindow"
title="任务大纲探索"
@close="agentsStore.closePlanModification"
>
<PlanModification />
</FloatWindow>
<FloatWindow
v-if="agentsStore.planTaskWindow"
title="任务过程探索"
@close="agentsStore.closePlanTask"
>
<PlanTask />
</FloatWindow>
<FloatWindow
v-if="agentsStore.agentAllocationDialog"
title="智能体探索"
@close="agentsStore.closeAgentAllocationDialog"
>
<AgentAllocation />
</FloatWindow>
</div> </div>
</template> </template>

View File

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

View File

@@ -10,3 +10,4 @@ export function setupStore(app: App<Element>) {
export * from './modules/agents.ts' export * from './modules/agents.ts'
export * from './modules/config.ts' export * from './modules/config.ts'
export * from './modules/selection.ts'

View File

@@ -1,17 +1,19 @@
import { ref } from 'vue' import { ref, watch, computed } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { getAgentMapIcon } from '@/layout/components/config'
import { store } from '../index' import { store } from '../index'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import type { IExecuteRawResponse } from '@/api' import type { IExecuteRawResponse } from '@/api'
import { useConfigStore } from '@/stores/modules/config.ts' import { useConfigStore } from '@/stores/modules/config.ts'
export interface Agent { export interface Agent {
Name: string Name: string
Profile: string Profile: string
Icon: string Icon: string
Classification: string Classification: string
apiUrl?: string
apiKey?: string
apiModel?: string
} }
type HslColorVector = [number, number, number] type HslColorVector = [number, number, number]
@@ -43,10 +45,51 @@ export interface IRawStepTask {
InputObject_List?: string[] InputObject_List?: string[]
OutputObject?: string OutputObject?: string
AgentSelection?: string[] AgentSelection?: string[]
Collaboration_Brief_FrontEnd: IRichText Collaboration_Brief_frontEnd: IRichText
TaskProcess: TaskProcess[] TaskProcess: TaskProcess[]
} }
export interface IApiAgentAction {
id: string
type: string
agent: string
description: string
inputs: string[]
}
export interface IApiStepTask {
name: string
content: string
inputs: string[]
output: string
agents: string[]
brief: IRichText
process: IApiAgentAction[]
}
// 智能体评分数据类型
export interface IScoreItem {
score: number
reason: string
}
// 单个任务的评分数据结构
export interface ITaskScoreData {
aspectList: string[] // 维度列表
agentScores: Record<string, Record<string, IScoreItem>> // agent -> 维度 -> 评分
timestamp: number // 缓存时间戳
}
export interface IAgentSelectModifyAddRequest {
aspectList: string[] // 维度列表
agentScores: Record<string, Record<string, IScoreItem>> // agent -> 维度 -> 评分(与后端返回格式一致)
}
export interface IAgentSelectModifyInitRequest {
goal: string
stepTask: IApiStepTask
}
export interface IRawPlanResponse { export interface IRawPlanResponse {
'Initial Input Object'?: string[] | string 'Initial Input Object'?: string[] | string
'General Goal'?: string 'General Goal'?: string
@@ -64,19 +107,199 @@ function clearStorageByVersion() {
export const useAgentsStore = defineStore('agents', () => { export const useAgentsStore = defineStore('agents', () => {
const configStore = useConfigStore() const configStore = useConfigStore()
const agents = useStorage<Agent[]>(`${storageKey}-repository`, []) const agents = useStorage<Agent[]>(`${storageKey}-repository`, [])
function setAgents(agent: Agent[]) { function setAgents(agent: Agent[]) {
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>(
`${storageKey}-score-data`,
null,
)
// 设置智能体评分数据(兼容旧版本)
function setAgentScoreData(data: IAgentSelectModifyAddRequest) {
IAgentSelectModifyAddRequest.value = data
}
// 添加新维度的评分数据(追加模式)
function addAgentScoreAspect(aspectName: string, scores: Record<string, IScoreItem>) {
if (!IAgentSelectModifyAddRequest.value) {
IAgentSelectModifyAddRequest.value = {
aspectList: [],
agentScores: {},
}
}
// 检查维度是否已存在
if (!IAgentSelectModifyAddRequest.value.aspectList.includes(aspectName)) {
IAgentSelectModifyAddRequest.value.aspectList.push(aspectName)
}
// 添加该维度的评分数据
for (const [agentName, scoreItem] of Object.entries(scores)) {
if (!IAgentSelectModifyAddRequest.value.agentScores[agentName]) {
IAgentSelectModifyAddRequest.value.agentScores[agentName] = {}
}
IAgentSelectModifyAddRequest.value.agentScores[agentName][aspectName] = scoreItem
}
}
// 清除智能体评分数据(兼容旧版本)
function clearAgentScoreData() {
IAgentSelectModifyAddRequest.value = null
}
// 智能体分配确认的组合列表存储 - 按任务ID分别存储不持久化到localStorage
const confirmedAgentGroupsMap = ref<Map<string, string[][]>>(new Map())
// 获取指定任务的确认的agent组合列表
function getConfirmedAgentGroups(taskId: string): string[][] {
return confirmedAgentGroupsMap.value.get(taskId) || []
}
// 添加确认的agent组合到指定任务
function addConfirmedAgentGroup(taskId: string, group: string[]) {
const groups = confirmedAgentGroupsMap.value.get(taskId) || []
groups.push(group)
confirmedAgentGroupsMap.value.set(taskId, groups)
}
// 清除指定任务的确认的agent组合列表
function clearConfirmedAgentGroups(taskId: string) {
confirmedAgentGroupsMap.value.delete(taskId)
}
// 清除所有任务的确认的agent组合列表
function clearAllConfirmedAgentGroups() {
confirmedAgentGroupsMap.value.clear()
}
const planModificationWindow = ref(false)
const planTaskWindow = ref(false)
const agentAllocationDialog = ref(false)
function openPlanModification() {
planModificationWindow.value = true
}
function closePlanModification() {
planModificationWindow.value = false
}
function openPlanTask() {
planTaskWindow.value = true
}
function closePlanTask() {
planTaskWindow.value = false
}
function openAgentAllocationDialog() {
agentAllocationDialog.value = true
}
function closeAgentAllocationDialog() {
agentAllocationDialog.value = false
}
// 任务搜索的内容 // 任务搜索的内容
const searchValue = useStorage<string>(`${storageKey}-search-value`, '') const searchValue = useStorage<string>(`${storageKey}-search-value`, '')
function setSearchValue(value: string) { function setSearchValue(value: string) {
searchValue.value = value searchValue.value = value
} }
const storageVersionIdentifier = useStorage<string>(`${storageKey}-storage-version-identifier`, '') const storageVersionIdentifier = useStorage<string>(
`${storageKey}-storage-version-identifier`,
'',
)
// 监听 configStore.config.agentRepository.storageVersionIdentifier 改变 // 监听 configStore.config.agentRepository.storageVersionIdentifier 改变
watch( watch(
() => configStore.config.agentRepository.storageVersionIdentifier, () => configStore.config.agentRepository.storageVersionIdentifier,
@@ -89,13 +312,132 @@ export const useAgentsStore = defineStore('agents', () => {
}, },
{ {
immediate: true, immediate: true,
} },
) )
// 当前的展示的任务流程 // 当前的展示的任务流程
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
// 🆕 智能判断如果是同一个任务保留用户修改过的数据AgentSelection、TaskProcess、Collaboration_Brief_frontEnd
if (existingTask && existingTask.Id === task.Id) {
currentTask.value = {
...task,
AgentSelection: existingTask.AgentSelection || task.AgentSelection,
TaskProcess: existingTask.TaskProcess || task.TaskProcess,
Collaboration_Brief_frontEnd:
existingTask.Collaboration_Brief_frontEnd || task.Collaboration_Brief_frontEnd,
}
// 🆕 同步更新主流程数据(让执行结果卡片和任务大纲都能联动)
syncCurrentTaskToMainProcess(currentTask.value)
console.log('🔄 setCurrentTask: 保留同一任务的分支数据', {
taskId: task.Id,
taskName: task.StepName,
preservedAgentSelection: currentTask.value.AgentSelection,
preservedTaskProcessLength: currentTask.value.TaskProcess?.length || 0,
})
} else {
// 不同任务:使用新任务数据
currentTask.value = task currentTask.value = task
console.log('🔄 setCurrentTask: 切换到不同任务', {
taskId: task.Id,
taskName: task.StepName,
agentSelection: task.AgentSelection,
taskProcessLength: task.TaskProcess?.length || 0,
})
}
}
// 🆕 同步 currentTask 到主流程数据
function syncCurrentTaskToMainProcess(updatedTask: IRawStepTask) {
const collaborationProcess = agentRawPlan.value.data?.['Collaboration Process']
if (!collaborationProcess) {
console.warn('⚠️ syncCurrentTaskToMainProcess: collaborationProcess 不存在')
return
}
const taskIndex = collaborationProcess.findIndex((t) => t.Id === updatedTask.Id)
if (taskIndex === -1) {
console.warn('⚠️ syncCurrentTaskToMainProcess: 未找到对应任务', updatedTask.Id)
return
}
// 使用 splice 确保 Vue 响应式更新
collaborationProcess.splice(taskIndex, 1, {
...collaborationProcess[taskIndex]!,
AgentSelection: updatedTask.AgentSelection || [],
TaskProcess: updatedTask.TaskProcess || [],
Collaboration_Brief_frontEnd: updatedTask.Collaboration_Brief_frontEnd,
})
console.log('✅ syncCurrentTaskToMainProcess: 已同步到主流程', {
taskName: collaborationProcess[taskIndex]!.StepName,
agentSelection: collaborationProcess[taskIndex]!.AgentSelection,
taskProcessLength: collaborationProcess[taskIndex]!.TaskProcess?.length || 0,
})
}
// 🆕 设置当前任务的 TaskProcess 数据(用于切换分支时更新显示)
function setCurrentTaskProcess(taskProcess: TaskProcess[]) {
if (currentTask.value) {
// 创建新的对象引用,确保响应式更新
currentTask.value = {
...currentTask.value,
TaskProcess: JSON.parse(JSON.stringify(taskProcess)),
}
// 🆕 同步到主流程数据(让执行结果卡片和任务大纲都能联动)
syncCurrentTaskToMainProcess(currentTask.value)
console.log('✅ 已更新 currentTask.TaskProcess 并同步到主流程:', {
taskId: currentTask.value.Id,
agent数量: taskProcess.length,
})
}
}
// 🆕 更新当前任务的 AgentSelection 和 TaskProcess用于在 AgentAllocation 中切换 agent 组合)
// 此函数专门用于强制更新,不会被 setCurrentTask 的"智能保留"逻辑阻止
function updateCurrentAgentSelection(
agentSelection: string[],
taskProcess: TaskProcess[],
collaborationBrief: any,
) {
if (currentTask.value) {
// 直接更新 currentTask不保留旧数据
currentTask.value = {
...currentTask.value,
AgentSelection: agentSelection,
TaskProcess: taskProcess,
Collaboration_Brief_frontEnd: collaborationBrief,
}
// 同步到主流程数据
syncCurrentTaskToMainProcess(currentTask.value)
console.log('✅ 已强制更新 currentTask 的 AgentSelection 并同步到主流程:', {
taskId: currentTask.value.Id,
taskName: currentTask.value.StepName,
newAgentSelection: agentSelection,
taskProcessLength: taskProcess.length,
})
}
} }
const agentRawPlan = ref<{ data?: IRawPlanResponse; loading?: boolean }>({ loading: false }) const agentRawPlan = ref<{ data?: IRawPlanResponse; loading?: boolean }>({ loading: false })
@@ -104,7 +446,8 @@ export const useAgentsStore = defineStore('agents', () => {
if (plan.data) { if (plan.data) {
plan.data['Collaboration Process'] = plan.data['Collaboration Process']?.map((item) => ({ plan.data['Collaboration Process'] = plan.data['Collaboration Process']?.map((item) => ({
...item, ...item,
Id: uuidv4(), // ✅ 只在任务没有 Id 时才生成新的 ID保持已存在任务的 Id 不变
Id: item.Id || uuidv4(),
})) }))
} }
agentRawPlan.value = { agentRawPlan.value = {
@@ -125,6 +468,69 @@ 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分别存储
const selectedAgentGroupMap = ref<Map<string, string[]>>(new Map())
// 设置指定任务的选择的 agent 组合
function setSelectedAgentGroup(taskId: string, agents: string[]) {
selectedAgentGroupMap.value.set(taskId, agents)
console.log('💾 保存任务选择的 agent 组合:', { taskId, agents })
}
// 获取指定任务的选择的 agent 组合
function getSelectedAgentGroup(taskId: string): string[] | undefined {
return selectedAgentGroupMap.value.get(taskId)
}
// 清除指定任务的选择的 agent 组合
function clearSelectedAgentGroup(taskId: string) {
selectedAgentGroupMap.value.delete(taskId)
}
// 清除所有任务的选择的 agent 组合
function clearAllSelectedAgentGroups() {
selectedAgentGroupMap.value.clear()
}
// 添加新产物
function addNewOutput(outputObject: string) {
if (!outputObject.trim()) return false
const trimmedOutput = outputObject.trim()
if (!additionalOutputs.value.includes(trimmedOutput)) {
additionalOutputs.value.unshift(trimmedOutput)
return true
}
return false
}
// 删除额外产物
function removeAdditionalOutput(outputObject: string) {
const index = additionalOutputs.value.indexOf(outputObject)
if (index > -1) {
additionalOutputs.value.splice(index, 1)
return true
}
return false
}
// 清除额外产物
function clearAdditionalOutputs() {
additionalOutputs.value = []
}
// 标记是否用户已停止智能体分配过程
const hasStoppedFilling = ref(false)
// 设置停止状态
function setHasStoppedFilling(value: boolean) {
hasStoppedFilling.value = value
} }
return { return {
@@ -134,11 +540,64 @@ export const useAgentsStore = defineStore('agents', () => {
setSearchValue, setSearchValue,
currentTask, currentTask,
setCurrentTask, setCurrentTask,
setCurrentTaskProcess, // 🆕 设置当前任务的 TaskProcess
updateCurrentAgentSelection, // 🆕 强制更新 AgentSelection用于 AgentAllocation 切换组合)
syncCurrentTaskToMainProcess, // 🆕 同步 currentTask 到主流程
agentRawPlan, agentRawPlan,
setAgentRawPlan, setAgentRawPlan,
executePlan, executePlan,
setExecutePlan, setExecutePlan,
resetAgent, resetAgent,
additionalOutputs,
addNewOutput,
removeAdditionalOutput,
clearAdditionalOutputs,
// 用户选择的 agent 组合
selectedAgentGroupMap,
setSelectedAgentGroup,
getSelectedAgentGroup,
clearSelectedAgentGroup,
clearAllSelectedAgentGroups,
planModificationWindow,
openPlanModification,
closePlanModification,
planTaskWindow,
openPlanTask,
closePlanTask,
agentAllocationDialog,
openAgentAllocationDialog,
closeAgentAllocationDialog,
// 智能体评分数据
IAgentSelectModifyAddRequest,
setAgentScoreData,
addAgentScoreAspect,
clearAgentScoreData,
// 🆕 按任务ID存储的评分数据
taskScoreDataMap,
getTaskScoreData,
setTaskScoreData,
hasTaskScoreData,
getPreloadedTaskIds,
clearTaskScoreData,
clearAllTaskScoreData,
preloadingTaskIds,
markTaskPreloading,
markTaskPreloadComplete,
isTaskPreloading,
// 确认的agent组合列表按任务ID分别存储
confirmedAgentGroupsMap,
getConfirmedAgentGroups,
addConfirmedAgentGroup,
clearConfirmedAgentGroups,
clearAllConfirmedAgentGroups,
// 停止填充状态
hasStoppedFilling,
setHasStoppedFilling,
// 重新执行相关
modifiedSteps,
addModifiedStep,
clearModifiedSteps,
hasModifiedSteps,
} }
}) })

View File

@@ -1,4 +1,5 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue'
import { readConfig } from '@/utils/readJson.ts' import { readConfig } from '@/utils/readJson.ts'
import { store } from '@/stores' import { store } from '@/stores'
@@ -10,15 +11,25 @@ export interface Config {
agentRepository: { agentRepository: {
storageVersionIdentifier: string storageVersionIdentifier: string
} }
// 是否是开发环境
dev?: boolean
// api base url
apiBaseUrl?: string
} }
const defaultConfig: Config = {
apiBaseUrl: `${import.meta.env.BASE_URL || '/'}api`
} as Config
export const useConfigStore = defineStore('config', () => { export const useConfigStore = defineStore('config', () => {
const config = ref<Config>({} as Config) const config = ref<Config>({} as Config)
// 异步调用readConfig // 异步调用readConfig
async function initConfig() { async function initConfig() {
config.value = await readConfig<Config>('config.json') const data = await readConfig<Config>('config.json')
config.value = {
...defaultConfig,
...data
}
} }

View File

@@ -0,0 +1,463 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { v4 as uuidv4 } from 'uuid'
import { store } from '../index'
import type { IRawStepTask, IApiStepTask } from './agents'
import type { Node, Edge } from '@vue-flow/core'
/**
* 分支数据接口
* @description 用于存储流程图中的分支节点和边数据
*/
export interface IBranchData {
/** 分支唯一 ID */
id: string
/** 父节点 ID根节点或任务节点 */
parentNodeId: string
/** 分支需求内容 */
branchContent: string
/** 分支类型 */
branchType: 'root' | 'task'
/** 分支包含的所有节点 */
nodes: Node[]
/** 分支包含的所有边 */
edges: Edge[]
/** 分支的任务数据 */
tasks: IRawStepTask[]
/** 创建时间 */
createdAt: number
}
export const useSelectionStore = defineStore('selection', () => {
// ==================== 任务大纲探索分支数据存储 ====================
/** 流程图分支列表 */
const flowBranches = ref<IBranchData[]>([])
/**
* 添加流程图分支
* @param data 分支数据
* @returns 分支 ID
*/
function addFlowBranch(data: {
parentNodeId: string
branchContent: string
branchType: 'root' | 'task'
nodes: Node[]
edges: Edge[]
tasks: IRawStepTask[]
}): string {
const branchId = `flow-branch-${uuidv4()}`
const newBranch: IBranchData = {
id: branchId,
parentNodeId: data.parentNodeId,
branchContent: data.branchContent,
branchType: data.branchType,
nodes: data.nodes,
edges: data.edges,
tasks: data.tasks,
createdAt: Date.now(),
}
flowBranches.value.push(newBranch)
return branchId
}
/**
* 获取所有流程图分支
* @returns 所有流程图分支数据
*/
function getAllFlowBranches(): IBranchData[] {
return flowBranches.value
}
/**
* 根据父节点 ID 获取流程图分支
* @param parentNodeId 父节点 ID
* @returns 匹配的流程图分支列表
*/
function getFlowBranchesByParent(parentNodeId: string): IBranchData[] {
return flowBranches.value.filter((branch) => branch.parentNodeId === parentNodeId)
}
/**
* 删除流程图分支
* @param branchId 分支 ID
* @returns 是否删除成功
*/
function removeFlowBranch(branchId: string): boolean {
const index = flowBranches.value.findIndex((branch) => branch.id === branchId)
if (index > -1) {
flowBranches.value.splice(index, 1)
return true
}
return false
}
/** 清除所有流程图分支 */
function clearFlowBranches() {
flowBranches.value = []
}
/**
* 根据父节点 ID 清除流程图分支
* @param parentNodeId 父节点 ID
*/
function clearFlowBranchesByParent(parentNodeId: string) {
flowBranches.value = flowBranches.value.filter((branch) => branch.parentNodeId !== parentNodeId)
}
// ==================== 任务过程探索分支数据存储 ====================
/**
* 任务过程分支数据映射
* @description 存储结构: Map<taskStepId, Map<agentGroupKey, IBranchData[]>>
* - taskStepId: 任务步骤 ID
* - agentGroupKey: agent 组合的唯一标识(排序后的 JSON 字符串)
* - IBranchData[]: 该 agent 组合在该任务下的所有分支数据
*/
const taskProcessBranchesMap = ref<Map<string, Map<string, IBranchData[]>>>(new Map())
/**
* 添加任务过程分支
* @param taskStepId 任务步骤 ID
* @param agents Agent 列表
* @param data 分支数据
* @returns 分支 ID
*/
function addTaskProcessBranch(
taskStepId: string,
agents: string[],
data: {
parentNodeId: string
branchContent: string
branchType: 'root' | 'task'
nodes: Node[]
edges: Edge[]
tasks: IRawStepTask[]
},
): string {
const branchId = `task-process-branch-${uuidv4()}`
const agentGroupKey = getAgentGroupKey(agents)
const newBranch: IBranchData = {
id: branchId,
parentNodeId: data.parentNodeId,
branchContent: data.branchContent,
branchType: data.branchType,
nodes: data.nodes,
edges: data.edges,
tasks: data.tasks,
createdAt: Date.now(),
}
// 获取或创建该任务步骤的 Map
if (!taskProcessBranchesMap.value.has(taskStepId)) {
taskProcessBranchesMap.value.set(taskStepId, new Map())
}
// 获取或创建该 agent 组合的分支列表
const agentMap = taskProcessBranchesMap.value.get(taskStepId)!
if (!agentMap.has(agentGroupKey)) {
agentMap.set(agentGroupKey, [])
}
agentMap.get(agentGroupKey)!.push(newBranch)
return branchId
}
/**
* 获取指定任务步骤和 agent 组合的所有分支
* @param taskStepId 任务步骤 ID
* @param agents Agent 列表
* @returns 匹配的任务过程分支列表
*/
function getTaskProcessBranches(taskStepId: string, agents: string[]): IBranchData[] {
const agentGroupKey = getAgentGroupKey(agents)
return taskProcessBranchesMap.value.get(taskStepId)?.get(agentGroupKey) || []
}
/**
* 获取所有任务过程分支
* @returns 所有任务过程分支数据映射
*/
function getAllTaskProcessBranches(): Map<string, Map<string, IBranchData[]>> {
return taskProcessBranchesMap.value
}
/**
* 根据父节点 ID 获取任务过程分支
* @param taskStepId 任务步骤 ID
* @param agents Agent 列表
* @param parentNodeId 父节点 ID
* @returns 匹配的任务过程分支列表
*/
function getTaskProcessBranchesByParent(
taskStepId: string,
agents: string[],
parentNodeId: string,
): IBranchData[] {
const agentGroupKey = getAgentGroupKey(agents)
const branches = taskProcessBranchesMap.value.get(taskStepId)?.get(agentGroupKey) || []
return branches.filter((branch) => branch.parentNodeId === parentNodeId)
}
/**
* 删除任务过程分支
* @param taskStepId 任务步骤 ID
* @param agents Agent 列表
* @param branchId 分支 ID
* @returns 是否删除成功
*/
function removeTaskProcessBranch(
taskStepId: string,
agents: string[],
branchId: string,
): boolean {
const agentGroupKey = getAgentGroupKey(agents)
const branches = taskProcessBranchesMap.value.get(taskStepId)?.get(agentGroupKey)
if (branches) {
const index = branches.findIndex((branch) => branch.id === branchId)
if (index > -1) {
branches.splice(index, 1)
return true
}
}
return false
}
/**
* 清除指定任务步骤和 agent 组合的所有分支
* @param taskStepId 任务步骤 ID
* @param agents Agent 列表(可选,不传则清除该任务步骤的所有分支)
*/
function clearTaskProcessBranches(taskStepId: string, agents?: string[]) {
if (agents) {
// 清除指定 agent 组合的分支
const agentGroupKey = getAgentGroupKey(agents)
const agentMap = taskProcessBranchesMap.value.get(taskStepId)
if (agentMap) {
agentMap.delete(agentGroupKey)
}
} else {
// 清除该任务步骤的所有分支(所有 agent 组合)
taskProcessBranchesMap.value.delete(taskStepId)
}
}
// ==================== Agent 组合 TaskProcess 数据存储 ====================
/**
* Agent 组合 TaskProcess 数据映射
* @description 用于存储 fill_stepTask_TaskProcess 接口返回的数据
* 存储结构: Map<taskId, Map<agentGroupKey, IApiStepTask>>
* - taskId: 任务 ID
* - agentGroupKey: agent 组合的唯一标识(排序后的 JSON 字符串)
* - IApiStepTask: 该 agent 组合的完整 TaskProcess 数据
*/
const agentTaskProcessMap = ref<Map<string, Map<string, IApiStepTask>>>(new Map())
/**
* 生成 agent 组合的唯一 key
* @description 排序后保证一致性
* @param agents Agent 列表
* @returns Agent 组合的唯一标识
*/
function getAgentGroupKey(agents: string[]): string {
// 处理 undefined 或 null 的情况
if (!agents || !Array.isArray(agents)) {
return JSON.stringify([])
}
return JSON.stringify([...agents].sort())
}
/**
* 存储 agent 组合的 TaskProcess 数据
* @param taskId 任务 ID
* @param agents Agent 列表
* @param taskProcess TaskProcess 数据
*/
function setAgentTaskProcess(taskId: string, agents: string[], taskProcess: IApiStepTask) {
const groupKey = getAgentGroupKey(agents)
// 获取或创建该任务的 Map
if (!agentTaskProcessMap.value.has(taskId)) {
agentTaskProcessMap.value.set(taskId, new Map())
}
// 存储该 agent 组合的 TaskProcess 数据
agentTaskProcessMap.value.get(taskId)!.set(groupKey, taskProcess)
}
/**
* 获取 agent 组合的 TaskProcess 数据
* @param taskId 任务 ID
* @param agents Agent 列表
* @returns TaskProcess 数据
*/
function getAgentTaskProcess(taskId: string, agents: string[]): IApiStepTask | undefined {
const groupKey = getAgentGroupKey(agents)
return agentTaskProcessMap.value.get(taskId)?.get(groupKey)
}
/**
* 检查 agent 组合是否已有 TaskProcess 数据
* @param taskId 任务 ID
* @param agents Agent 列表
* @returns 是否存在数据
*/
function hasAgentTaskProcess(taskId: string, agents: string[]): boolean {
const groupKey = getAgentGroupKey(agents)
return agentTaskProcessMap.value.get(taskId)?.has(groupKey) || false
}
/**
* 清除指定任务的所有 agent 组合 TaskProcess 数据
* @param taskId 任务 ID
*/
function clearAgentTaskProcess(taskId: string) {
agentTaskProcessMap.value.delete(taskId)
}
/** 清除所有任务的 agent 组合 TaskProcess 数据 */
function clearAllAgentTaskProcess() {
agentTaskProcessMap.value.clear()
}
// ==================== 当前生效的任务过程分支 ====================
/**
* 当前生效的任务过程分支映射
* @description 记录每个任务步骤和 agent 组合当前生效的分支 ID持久化选中状态
* 存储结构: Map<taskStepId, Map<agentGroupKey, branchId>>
* - taskStepId: 任务步骤 ID
* - agentGroupKey: agent 组合的唯一标识
* - branchId: 当前选中的分支 ID
*/
const activeTaskProcessBranchMap = ref<Map<string, Map<string, string>>>(new Map())
/**
* 当前生效的 TaskProcess 数据映射
* @description 用于外部组件显示职责分配
* 存储结构: Map<taskStepId, Map<agentGroupKey, TaskProcess[]>>
*/
const activeTaskProcessDataMap = ref<Map<string, Map<string, any[]>>>(new Map())
/**
* 设置当前生效的分支
* @param taskStepId 任务步骤 ID
* @param agents Agent 列表
* @param branchId 分支 ID
*/
function setActiveTaskProcessBranch(taskStepId: string, agents: string[], branchId: string) {
const agentGroupKey = getAgentGroupKey(agents)
// 获取或创建该任务步骤的 Map
if (!activeTaskProcessBranchMap.value.has(taskStepId)) {
activeTaskProcessBranchMap.value.set(taskStepId, new Map())
}
activeTaskProcessBranchMap.value.get(taskStepId)!.set(agentGroupKey, branchId)
}
/**
* 设置当前生效的 TaskProcess 数据
* @param taskStepId 任务步骤 ID
* @param agents Agent 列表
* @param taskProcess TaskProcess 数据
*/
function setActiveTaskProcessData(taskStepId: string, agents: string[], taskProcess: any[]) {
const agentGroupKey = getAgentGroupKey(agents)
// 获取或创建该任务步骤的 Map
if (!activeTaskProcessDataMap.value.has(taskStepId)) {
activeTaskProcessDataMap.value.set(taskStepId, new Map())
}
activeTaskProcessDataMap.value.get(taskStepId)!.set(agentGroupKey, taskProcess)
}
/**
* 获取当前生效的分支 ID
* @param taskStepId 任务步骤 ID
* @param agents Agent 列表
* @returns 分支 ID
*/
function getActiveTaskProcessBranch(taskStepId: string, agents: string[]): string | undefined {
const agentGroupKey = getAgentGroupKey(agents)
return activeTaskProcessBranchMap.value.get(taskStepId)?.get(agentGroupKey)
}
/**
* 获取当前生效的 TaskProcess 数据
* @param taskStepId 任务步骤 ID
* @param agents Agent 列表
* @returns TaskProcess 数据
*/
function getActiveTaskProcessData(taskStepId: string, agents: string[]): any[] | undefined {
const agentGroupKey = getAgentGroupKey(agents)
return activeTaskProcessDataMap.value.get(taskStepId)?.get(agentGroupKey)
}
/**
* 清除生效分支
* @param taskStepId 任务步骤 ID
* @param agents Agent 列表(可选,不传则清除该任务步骤的所有生效分支)
*/
function clearActiveTaskProcessBranch(taskStepId: string, agents?: string[]) {
if (agents) {
// 清除指定 agent 组合的生效分支
const agentGroupKey = getAgentGroupKey(agents)
activeTaskProcessBranchMap.value.get(taskStepId)?.delete(agentGroupKey)
activeTaskProcessDataMap.value.get(taskStepId)?.delete(agentGroupKey)
} else {
// 清除该任务步骤的所有生效分支(所有 agent 组合)
activeTaskProcessBranchMap.value.delete(taskStepId)
activeTaskProcessDataMap.value.delete(taskStepId)
}
}
return {
// ==================== 状态 ====================
flowBranches,
taskProcessBranchesMap,
agentTaskProcessMap,
activeTaskProcessBranchMap,
activeTaskProcessDataMap,
// ==================== 任务大纲分支管理方法 ====================
addFlowBranch,
getAllFlowBranches,
getFlowBranchesByParent,
removeFlowBranch,
clearFlowBranches,
clearFlowBranchesByParent,
// ==================== 任务过程分支管理方法 ====================
addTaskProcessBranch,
getTaskProcessBranches,
getAllTaskProcessBranches,
getTaskProcessBranchesByParent,
removeTaskProcessBranch,
clearTaskProcessBranches,
// ==================== 任务过程分支生效状态管理方法 ====================
setActiveTaskProcessBranch,
setActiveTaskProcessData,
getActiveTaskProcessBranch,
getActiveTaskProcessData,
clearActiveTaskProcessBranch,
// ==================== Agent 组合 TaskProcess 数据管理方法 ====================
getAgentGroupKey,
setAgentTaskProcess,
getAgentTaskProcess,
hasAgentTaskProcess,
clearAgentTaskProcess,
clearAllAgentTaskProcess,
}
})
/**
* 用于在组件外部使用 Selection Store
* @returns Selection Store 实例
*/
export function useSelectionStoreHook() {
return useSelectionStore(store)
}

View File

@@ -1,16 +1,222 @@
// 浅色模式
:root { :root {
--color-bg: #fff; --color-bg: #ffffff;
--color-bg-secondary: #fafafa; --color-bg-secondary: #ffffff;
--color-bg-tertiary: #f5f5f5; --color-bg-tertiary: #f0f0f0; //正确
--color-text: #000; --color-bg-quaternary: rgba(0, 0, 0, 11%);
--color-bg-quinary: #f7f7f7;
--color-text: #36404F;
// 标题文字颜色
--color-text-title-header: #262626;
--color-text-secondary: #4f4f4f;
--color-border-default: #e0e0e0;
--color-accent: #222222;
--color-accent-secondary: #315ab4;
// 鼠标悬浮背景色
--color-bg-hover: #f0f0f0;
// 鼠标悬浮字体颜色
--color-text-hover: #222222;
// 分割线颜色
--color-border: #D8D8d8;
// 内容区鼠标悬浮颜色
--color-bg-content-hover: #f0f0f0;
//列表背景颜色
--color-bg-list: #E1E1E1;
// 详情卡文字内容
--color-text-detail: #4f4f4f;
// 详情卡背景颜色
--color-bg-detail: #f7f7f7;
// 任务栏背景颜色
--color-bg-taskbar: #ffffff;
// 详情列表背景颜色
--color-bg-detail-list: rgba(230, 230, 230, 0.6) ;//#E6E6E6 60%
// 详情列表运行后背景颜色
--color-bg-detail-list-run: #e6e6e6; //运行后背景色#e6e6e6 100%
// 旋转图标背景颜色
--color-bg-icon-rotate: #a7a7a7;
// 智能体列表背景颜色
--color-agent-list-bg: #ededed; //背景色#fbfcff
// 智能体边框颜色
--color-agent-list-border: #E0E0E070; //边框#FFFFFF60 60%
//智能体鼠标悬浮颜色
--color-agent-list-hover-bg: #f7f7f7; //hover背景色#FFFFFF 100%
// 智能体鼠标悬浮边框颜色
--color-agent-list-hover-border: #e0e0e0; //hover边框#E0E0E0 100%
//智能体鼠标悬浮阴影
--color-agent-list-hover-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.2); //hover阴影 #FFFFFF 100%
//任务框下拉阴影
--color-task-shadow: 0px 10px 10px 5px rgba(0, 0, 0, 0.2);
// 卡片边框颜色
--color-card-border: #ececec;
// 卡片鼠标悬浮边框颜色
--color-card-border-hover: #ececec;
// 任务大纲卡片颜色
--color-card-bg-task: #ededed;
// 任务大纲卡片悬浮颜色
--color-card-bg-task-hover: #f7f7f7;
// 任务大纲卡片悬浮阴影效果
--color-card-shadow-hover: 0px 2px 4px 0px rgba(0, 0, 0, 0.2);
// 卡片阴影效果
--color-card-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.17);
// 任务大纲边框颜色
--color-card-border-task: #E0E0E0; //边框颜色#FFFFFF60
// 执行结果卡片颜色
--color-card-bg-result: #ececec;
// 执行结果边框颜色
--color-card-border-result: #ececec;
// 执行结果卡片悬浮颜色
--color-card-bg-result-hover: #ededed;
// 头部背景颜色
--color-header-bg: #ffffff;
// 头部字体颜色
--color-text-title: #2a3342;
//结果卡背景颜色
--color-bg-result: #eaedee;
// 任务字体背景颜色
--color-text-task: #36404F;
// 指示灯背景颜色
--color-bg-indicator: #f7f7f7;
// 三个卡片背景颜色
--color-bg-three: #fbfcff;
// 三个卡片边框颜色
--color-card-border-three: #FFFFFF60;
// 流程卡片背景颜色
--color-bg-flow: #f0f0f0;
// 三个卡片阴影
--color-card-shadow-three: 0px 0px 5px 0px rgba(0, 0, 0, 0.17);
// 头部阴影
--color-header-shadow: 0px 0px 5px 0px rgba(161, 161, 161, 0.5);
// 执行结果详情背景颜色
--color-bg-result-detail: #f7f7f7;
// 智能体库字体颜色
--color-text-agent-list: #636364;
// 智能体库字体悬浮颜色
--color-text-agent-list-hover: #222222;
// 任务栏字体颜色
--color-text-taskbar: #222222;
// 智能体卡片被选中后背景颜色
--color-agent-list-selected-bg: #f7f7f7; //选中后背景色#171b22 100%
// 结果卡片运行前颜色
--color-text-result-detail: rgba(0,0,0,0.6);
// 结果卡片运行后颜色
--color-text-result-detail-run: #000;
// 分割线
--color-border-separate: #D6D6D6;
// url颜色
--color-text-url: #FF0000;
// model颜色
--color-text-model: #0580ff;
} }
// 深色模式
html.dark { html.dark {
--color-bg: #131A27; --color-bg: #131A27;
--color-bg-secondary: #050505; --color-bg-secondary: rgba(5, 5, 5, 0.4);
--color-bg-tertiary: #20222A; --color-bg-tertiary: #20222A;
--color-bg-quaternary: #24252A; --color-bg-quaternary: #24252A;
--color-bg-quinary: #29303c; --color-bg-quinary: #29303c;
--color-text: #fff; --color-text: #fff;
// 标题文字颜色
--color-text-title-header: #ffffff;
--color-text-secondary: #C9C9C9; --color-text-secondary: #C9C9C9;
--color-border-default: #494B51;
--color-accent: #00F3FF;
--color-accent-secondary: #315ab4;
// 鼠标悬浮背景色
--color-bg-hover: #1c1e25;
// 鼠标悬浮字体颜色
--color-text-hover: #00f3ff;
// 分割线颜色
--color-border: #494B51;
// 内容区鼠标悬浮颜色
--color-bg-content-hover: #171b22;
// 列表背景颜色
--color-bg-list: #171B22;
// 详情卡文字内容
--color-text-detail: #ffffff;
// 详情卡背景颜色
--color-bg-detail: #20222a;
// 任务栏背景颜色
--color-bg-taskbar: #20222a;
// 详情列表背景颜色
--color-bg-detail-list: #050505; //运行后背景色#18191f 60%
//详情列表运行后背景颜色
--color-bg-detail-list-run: #050505;
// 旋转图标背景颜色
--color-bg-icon-rotate: #151619;
// 智能体列表背景颜色
--color-agent-list-bg: #1d222b; //背景色#181b20 100%
// 智能体边框颜色
--color-agent-list-border: rgba(8,8,8,0.6);
//智能体鼠标悬浮颜色
--color-agent-list-hover-bg: rgba(23,27,34,1); //hover背景色#171b22 100%
// 智能体鼠标悬浮边框颜色
--color-agent-list-hover-border: rgba(8,8,8,1); //hover边框#080808 100%
//智能体鼠标悬浮阴影
--color-agent-list-hover-shadow: 0 2 4 #00000050; //hover阴影 0 2 4 #00000050
//任务框下拉阴影
--color-task-shadow: 0px 10px 10px 5px rgba(0, 0, 0, 0.2);
// 卡片边框颜色
--color-card-border: #1a1a1a;
// 卡片鼠标悬浮边框颜色
--color-card-border-hover: #151515;
// 任务大纲卡片颜色
--color-card-bg-task: #20222a;
// 任务大纲卡片悬浮颜色
--color-card-bg-task-hover: #171b22;
// 任务大纲边框颜色
--color-card-border-task: #151515;
// 执行结果卡片颜色
--color-card-bg-result: #1a1a1a;
// 执行结果边框颜色
--color-card-border-result: #151515;
// 执行结果卡片悬浮颜色
--color-card-bg-result-hover: #171b22;
// 头部背景颜色
--color-header-bg: #050505;
// 标题头部字体颜色
--color-text-title: #2c72e7;
// 卡片阴影效果
--color-card-shadow: 0px 0px 5px 5px rgba(0, 0, 0, 0.17);
// 任务大纲卡片悬浮阴影效果
--color-card-shadow-hover: 0px 2px 4px 0px rgba(0, 0, 0, 0.2);
// 结果卡背景颜色
--color-bg-result: #0d1119;
// 任务字体背景颜色
--color-text-task: #d7d7d7;
// 指示灯背景颜色
--color-bg-indicator: #17181a;
// 三个卡片背景颜色
--color-bg-three: #29303c;
// 三个卡片边框颜色
--color-card-border-three: #393d42;
// 流程卡片背景颜色
--color-bg-flow: #1C222a;
// 三个卡片阴影
--color-card-shadow-three: 0px 0px 6px 0px rgba(0, 0, 0, 0.8);
// 头部阴影
--color-header-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.5);
// 执行结果详情背景颜色
--color-bg-result-detail: #24252a;
// 智能体库字体颜色
--color-text-agent-list: #969696;
// 智能体库字体悬浮颜色
--color-text-agent-list-hover: #b8b8b8;
// 任务栏字体颜色
--color-text-taskbar: #ffffff;
// 智能体卡片被选中后背景颜色
--color-agent-list-selected-bg: #20222a; //选中后背景色#171b22 100%
// 结果卡片运行前颜色
--color-text-result-detail: #6c6e72;
// 结果卡片运行后颜色
--color-text-result-detail-run: #fff;
// 分割线
--color-border-separate: rgba(255,255,255,0.18);
// url颜色
--color-text-url: #2cd235;
// model颜色
--color-text-model: #00c8d2;
--el-fill-color-blank: transparent !important;
} }

View File

@@ -4,6 +4,100 @@ $bg-tertiary: var(--color-bg-tertiary);
$bg-quaternary: var(--color-bg-quaternary); $bg-quaternary: var(--color-bg-quaternary);
$text: var(--color-text); $text: var(--color-text);
$text-secondary: var(--color-text-secondary); $text-secondary: var(--color-text-secondary);
// 鼠标悬浮
$bg-hover: var(--color-bg-hover);
// 字体悬浮
$text-hover: var(--color-text-hover);
// 分割线
$border: var(--color-border);
// 内容区鼠标悬浮
$bg-content-hover: var(--color-bg-content-hover);
// 列表背景颜色
$bg-list: var(--color-bg-list);
// 详情卡文字内容
$text-detail: var(--color-text-detail);
// 详情卡背景颜色
$bg-detail: var(--color-bg-detail);
// 任务栏背景颜色
$bg-taskbar: var(--color-bg-taskbar);
// 详情列表背景颜色
$bg-detail-list: var(--color-bg-detail-list);
// 旋转图标背景颜色
$bg-icon-rotate: var(--color-bg-icon-rotate);
// 智能体列表背景颜色
$agent-list-bg: var(--color-agent-list-bg);
// 智能体边框颜色
$agent-list-border: var(--color-agent-list-border);
//智能体鼠标悬浮颜色
$agent-list-hover-bg: var(--color-agent-list-hover-bg);
// 智能体鼠标悬浮边框颜色
$agent-list-hover-border: var(--color-agent-list-hover-border);
//智能体鼠标悬浮阴影
$agent-list-hover-shadow: var(--color-agent-list-hover-shadow);
//任务框下拉阴影
$task-shadow: var(--color-task-shadow);
// 卡片边框颜色
$card-border: var(--color-card-border);
// 卡片鼠标悬浮边框颜色
$card-border-hover: var(--color-card-border-hover);
// 标题文字颜色
$text-title: var(--color-text-title);
// 任务大纲卡片颜色
$card-bg-task: var(--color-card-bg-task);
// 任务大纲边框颜色
$card-border-task: var(--color-card-border-task);
// 执行结果卡片颜色
$card-bg-result: var(--color-card-bg-result);
// 执行结果边框颜色
$card-border-result: var(--color-card-border-result);
// 头部背景颜色
$header-bg: var(--color-header-bg);
// 头部字体颜色
$header-text-title: var(--color-text-title);
// 卡片阴影效果
$card-shadow: var(--color-card-shadow);
// 任务大纲卡片悬浮颜色
$card-bg-task-hover: var(--color-card-bg-task-hover);
// 任务大纲卡片悬浮阴影效果
$card-shadow-hover: var(--color-card-shadow-hover);
// 结果卡背景颜色
$bg-result: var(--color-bg-result);
// 执行结果卡片悬浮颜色
$card-bg-result-hover: var(--color-card-bg-result-hover);
// 详情列表运行后背景颜色
$bg-detail-list-run: var(--color-bg-detail-list-run);
// 任务字体背景颜色
$text-task: var(--color-text-task);
// 指示灯背景颜色
$bg-indicator: var(--color-bg-indicator);
// 三个卡片背景颜色
$bg-three: var(--color-bg-three);
// 流程卡片背景颜色
$bg-flow: var(--color-bg-flow);
// 三个卡片边框颜色
$card-border-three: var(--color-card-border-three);
// 三个卡片阴影
$card-shadow-three: var(--color-card-shadow-three);
// 头部阴影
$header-shadow: var(--color-header-shadow);
// 执行结果详情背景颜色
$bg-result-detail: var(--color-bg-result-detail);
// 智能体库字体颜色
$text-agent-list: var(--color-text-agent-list);
// 智能体库字体悬浮颜色
$text-agent-list-hover: var(--color-text-agent-list-hover);
// 任务栏字体颜色
$text-taskbar: var(--color-text-taskbar);
// 智能体卡片被选中后背景颜色
$agent-list-selected-bg: var(--color-agent-list-selected-bg); //选中后背景色#171b22 100%
// 结果卡片运行前颜色
$text-result-detail: var(--color-text-result-detail);
// url和model颜色
$text-url: var(--color-text-url);
$text-model: var(--color-text-model);
:export { :export {
bg: $bg; bg: $bg;
@@ -12,4 +106,51 @@ $text-secondary: var(--color-text-secondary);
bg-quaternary: $bg-quaternary; bg-quaternary: $bg-quaternary;
text: $text; text: $text;
text-secondary: $text-secondary; text-secondary: $text-secondary;
bg-hover:$bg-hover;
text-hover:$text-hover;
border: $border;
bg-content-hover:$bg-content-hover;
bg-list: $bg-list;
text-detail: $text-detail;
bg-detail: $bg-detail;
bg-taskbar: $bg-taskbar;
bg-detail-list: $bg-detail-list;
bg-icon-rotate:$bg-icon-rotate;
agent-list-bg: $agent-list-bg;
agent-list-border: $agent-list-border;
agent-list-hover-bg: $agent-list-hover-bg;
agent-list-hover-border: $agent-list-hover-border;
agent-list-hover-shadow: $agent-list-hover-shadow;
task-shadow: $task-shadow;
card-border: $card-border;
card-border-hover: $card-border-hover;
text-title: $text-title;
card-border: $card-border;
card-bg-task: $card-bg-task;
card-border-task: $card-border-task;
card-bg-result: $card-bg-result;
card-border-result: $card-border-result;
header-bg: $header-bg;
header-text-title: $header-text-title;
card-shadow: $card-shadow;
card-bg-task-hover: $card-bg-task-hover;
card-shadow-hover: $card-shadow-hover;
bg-result: $bg-result;
card-bg-result-hover: $card-bg-result-hover;
bg-detail-list-run: $bg-detail-list-run;
text-task: $text-task;
bg-indicator: $bg-indicator;
bg-three: $bg-three;
bg-flow: $bg-flow;
card-border-three: $card-border-three;
card-shadow-three: $card-shadow-three;
header-shadow: $header-shadow;
bg-result-detail: $bg-result-detail;
text-agent-list: $text-agent-list;
text-agent-list-hover: $text-agent-list-hover;
text-taskbar: $text-taskbar;
agent-list-selected-bg: $agent-list-selected-bg;
text-result-detail: $text-result-detail;
text-url: $text-url;
text-model: $text-model;
} }

View File

@@ -20,7 +20,7 @@ export function changeBriefs(task?: IRawStepTask[]): IRawStepTask[] {
return task.map((item) => { return task.map((item) => {
const record = { const record = {
...item, ...item,
Collaboration_Brief_FrontEnd: changeBrief(item), Collaboration_Brief_frontEnd: changeBrief(item),
} }
return record return record
}) })
@@ -30,10 +30,10 @@ function changeBrief(task: IRawStepTask): IRichText {
// 如果不存在AgentSelection直接返回 // 如果不存在AgentSelection直接返回
const agents = task.AgentSelection ?? [] const agents = task.AgentSelection ?? []
if (agents.length === 0) { if (agents.length === 0) {
return task.Collaboration_Brief_FrontEnd return task.Collaboration_Brief_frontEnd
} }
const data: IRichText['data'] = {}; const data: IRichText['data'] = {}
let indexOffset = 0; let indexOffset = 0
// 根据InputObject_List修改 // 根据InputObject_List修改
const inputs = task.InputObject_List ?? [] const inputs = task.InputObject_List ?? []
@@ -41,63 +41,62 @@ function changeBrief(task: IRawStepTask): IRichText {
data[(index + indexOffset).toString()] = { data[(index + indexOffset).toString()] = {
text, text,
style: { background: '#ACDBA0' }, style: { background: '#ACDBA0' },
}; }
return `!<${index + indexOffset}>!`; return `!<${index + indexOffset}>!`
}); })
const inputSentence = nameJoin(inputPlaceHolders); const inputSentence = nameJoin(inputPlaceHolders)
indexOffset += inputs.length; indexOffset += inputs.length
// 根据AgentSelection修改 // 根据AgentSelection修改
const namePlaceholders = agents.map((text, index) => { const namePlaceholders = agents.map((text, index) => {
data[(index + indexOffset).toString()] = { data[(index + indexOffset).toString()] = {
text, text,
style: { background: '#E5E5E5', boxShadow: '1px 1px 4px 1px #0003' }, style: { background: '#E5E5E5', boxShadow: '1px 1px 4px 1px #0003' },
}; }
return `!<${index + indexOffset}>!`; return `!<${index + indexOffset}>!`
}); })
const nameSentence = nameJoin(namePlaceholders); const nameSentence = nameJoin(namePlaceholders)
indexOffset += agents.length; indexOffset += agents.length
let actionSentence = task.TaskContent ?? ''
let actionSentence = task.TaskContent ?? '';
// delete the last '.' of actionSentence // delete the last '.' of actionSentence
if (actionSentence[actionSentence.length - 1] === '.') { if (actionSentence[actionSentence.length - 1] === '.') {
actionSentence = actionSentence.slice(0, -1); actionSentence = actionSentence.slice(0, -1)
} }
const actionIndex = indexOffset++; const actionIndex = indexOffset++
data[actionIndex.toString()] = { data[actionIndex.toString()] = {
text: actionSentence, text: actionSentence,
style: { background: '#DDD', border: '1.5px solid #ddd' }, style: { background: '#DDD', border: '1.5px solid #ddd' },
}; }
let outputSentence = ''; let outputSentence = ''
const output = task.OutputObject ?? ''; const output = task.OutputObject ?? ''
if (output) { if (output) {
data[indexOffset.toString()] = { data[indexOffset.toString()] = {
text: output, text: output,
style: { background: '#FFCA8C' }, style: { background: '#FFCA8C' },
}; }
outputSentence = `得到 !<${indexOffset}>!`; outputSentence = `得到 !<${indexOffset}>!`
} }
// Join them togeter // Join them togeter
let content = inputSentence; let content = inputSentence
if (content) { if (content) {
content = `基于${content}, ${nameSentence} 执行任务 !<${actionIndex}>!`; content = `基于${content}, ${nameSentence} 执行任务 !<${actionIndex}>!`
} else { } else {
content = `${nameSentence} 执行任务 !<${actionIndex}>!`; content = `${nameSentence} 执行任务 !<${actionIndex}>!`
} }
if (outputSentence) { if (outputSentence) {
content = `${content}, ${outputSentence}.`; content = `${content}, ${outputSentence}.`
} else { } else {
content = `${content}.`; content = `${content}.`
} }
content = content.trim(); content = content.trim()
return { return {
template: content, template: content,
data, data,
}; }
} }

View File

@@ -1,9 +1,16 @@
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios' import type {
AxiosError,
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios'
import axios from 'axios' import axios from 'axios'
import qs from 'qs' import qs from 'qs'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { ElNotification } from 'element-plus' import { ElNotification } from 'element-plus'
import { ref } from 'vue' import { ref } from 'vue'
import { useConfigStoreHook } from '@/stores'
// 创建 axios 实例 // 创建 axios 实例
let service: AxiosInstance let service: AxiosInstance
@@ -14,9 +21,9 @@ export interface AxiosResponseData {
} }
export function initService() { export function initService() {
const configStore = useConfigStoreHook()
service = axios.create({ service = axios.create({
baseURL: '/api', baseURL: configStore.config.apiBaseUrl,
timeout: 50000,
headers: { 'Content-Type': 'application/json;charset=utf-8' }, headers: { 'Content-Type': 'application/json;charset=utf-8' },
paramsSerializer: (params) => { paramsSerializer: (params) => {
return qs.stringify(params) return qs.stringify(params)

View File

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

View File

@@ -14,6 +14,7 @@ const pathSrc = resolve(__dirname, 'src')
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
base: '',
plugins: [ plugins: [
vue(), vue(),
tailwindcss(), tailwindcss(),
@@ -38,7 +39,7 @@ export default defineConfig({
], ],
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url)),
}, },
}, },
server: { server: {
@@ -46,13 +47,14 @@ export default defineConfig({
'/api': { '/api': {
changeOrigin: true, changeOrigin: true,
// 接口地址 // 接口地址
target: 'http://localhost:8000', // target: 'http://82.157.183.212:21092',
rewrite: (path: string) => target: 'http://82.157.183.212:21097',
path.replace(/^\/api/, ''), // target: 'http://localhost:8000',
configure: (proxy, options) => { // rewrite: (path: string) => path.replace(/^\/api/, ''),
console.log('Proxy configured:', options) // configure: (proxy, options) => {
} // console.log('Proxy configured:', options)
// },
},
}, },
}, },
}
}) })