Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1749ae4f1e | ||
|
|
418b2e5f8f | ||
|
|
641d70033d | ||
|
|
b287867069 | ||
|
|
5699635d1a | ||
|
|
ac035d1237 | ||
|
|
53add0431e | ||
|
|
786c674d21 | ||
|
|
1c8036adf1 | ||
|
|
45314b7be6 | ||
|
|
c5848410c1 | ||
|
|
571b5101ff | ||
|
|
029df6b5a5 | ||
|
|
edb39d4c1f | ||
|
|
0e87777ae8 | ||
|
|
244deceb91 | ||
|
|
69587c0481 | ||
|
|
e0cc11647f | ||
|
|
59fd94e783 | ||
|
|
3ff70463ca | ||
|
|
82e92f12aa | ||
|
|
920588b063 | ||
|
|
5847365eee | ||
|
|
d42554ce03 | ||
|
|
bcc0c53ba1 | ||
|
|
7da5e82d40 | ||
|
|
cc22655a1e | ||
|
|
f0db3c88e4 | ||
|
|
b987fe70ad | ||
|
|
b42ab5aedd | ||
|
|
5ef86c6fa9 | ||
|
|
907310365a | ||
|
|
5dace5f788 | ||
|
|
77530c49f8 | ||
|
|
6392301833 | ||
|
|
ab8c9e294d | ||
|
|
1aa9e280b0 | ||
|
|
041986f5cd | ||
|
|
00ef22505e | ||
|
|
b73419b7a0 | ||
|
|
974af053ca | ||
|
|
0c571dec21 |
@@ -1,24 +1,35 @@
|
||||
import asyncio
|
||||
import openai
|
||||
|
||||
import httpx
|
||||
from openai import OpenAI, AsyncOpenAI, max_retries
|
||||
import yaml
|
||||
from termcolor import colored
|
||||
import os
|
||||
|
||||
# Helper function to avoid circular import
|
||||
def print_colored(text, text_color="green", background="on_white"):
|
||||
print(colored(text, text_color, background))
|
||||
|
||||
# load config (apikey, apibase, model)
|
||||
yaml_file = os.path.join(os.getcwd(), "config", "config.yaml")
|
||||
try:
|
||||
with open(yaml_file, "r", encoding="utf-8") as file:
|
||||
yaml_data = yaml.safe_load(file)
|
||||
except Exception:
|
||||
yaml_file = {}
|
||||
yaml_data = {}
|
||||
OPENAI_API_BASE = os.getenv("OPENAI_API_BASE") or yaml_data.get(
|
||||
"OPENAI_API_BASE", "https://api.openai.com"
|
||||
)
|
||||
openai.api_base = OPENAI_API_BASE
|
||||
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") or yaml_data.get(
|
||||
"OPENAI_API_KEY", ""
|
||||
)
|
||||
openai.api_key = OPENAI_API_KEY
|
||||
OPENAI_API_MODEL = os.getenv("OPENAI_API_MODEL") or yaml_data.get(
|
||||
"OPENAI_API_MODEL", ""
|
||||
)
|
||||
|
||||
# Initialize OpenAI clients
|
||||
client = OpenAI(api_key=OPENAI_API_KEY, base_url=OPENAI_API_BASE)
|
||||
async_client = AsyncOpenAI(api_key=OPENAI_API_KEY, base_url=OPENAI_API_BASE)
|
||||
MODEL: str = os.getenv("OPENAI_API_MODEL") or yaml_data.get(
|
||||
"OPENAI_API_MODEL", "gpt-4-turbo-preview"
|
||||
)
|
||||
@@ -35,8 +46,11 @@ MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY") or yaml_data.get(
|
||||
|
||||
# for 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:
|
||||
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:
|
||||
force_gpt4 = True
|
||||
useGroq = False
|
||||
@@ -69,16 +83,107 @@ def LLM_Completion(
|
||||
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:
|
||||
from groq import AsyncGroq
|
||||
client = AsyncGroq(api_key=GROQ_API_KEY)
|
||||
groq_client = AsyncGroq(api_key=GROQ_API_KEY)
|
||||
|
||||
max_attempts = 5
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
print("Attempt to use Groq (Fase Design Mode):")
|
||||
try:
|
||||
stream = await client.chat.completions.create(
|
||||
response = await groq_client.chat.completions.create(
|
||||
messages=messages,
|
||||
# model='gemma-7b-it',
|
||||
model="mixtral-8x7b-32768",
|
||||
@@ -92,9 +197,18 @@ async def _achat_completion_stream_groq(messages: list[dict]) -> str:
|
||||
if attempt < max_attempts - 1: # i is zero indexed
|
||||
continue
|
||||
else:
|
||||
raise "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
|
||||
if full_reply_content is None:
|
||||
raise Exception("Groq API returned None content")
|
||||
|
||||
full_reply_content = stream.choices[0].message.content
|
||||
print(colored(full_reply_content, "blue", "on_white"), end="")
|
||||
print()
|
||||
return full_reply_content
|
||||
@@ -103,14 +217,14 @@ async def _achat_completion_stream_groq(messages: list[dict]) -> str:
|
||||
async def _achat_completion_stream_mixtral(messages: list[dict]) -> str:
|
||||
from mistralai.client import MistralClient
|
||||
from mistralai.models.chat_completion import ChatMessage
|
||||
client = MistralClient(api_key=MISTRAL_API_KEY)
|
||||
mistral_client = MistralClient(api_key=MISTRAL_API_KEY)
|
||||
# client=AsyncGroq(api_key=GROQ_API_KEY)
|
||||
max_attempts = 5
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
messages[len(messages) - 1]["role"] = "user"
|
||||
stream = client.chat(
|
||||
stream = mistral_client.chat(
|
||||
messages=[
|
||||
ChatMessage(
|
||||
role=message["role"], content=message["content"]
|
||||
@@ -119,31 +233,35 @@ async def _achat_completion_stream_mixtral(messages: list[dict]) -> str:
|
||||
],
|
||||
# model = "mistral-small-latest",
|
||||
model="open-mixtral-8x7b",
|
||||
# response_format={"type": "json_object"},
|
||||
)
|
||||
break # If the operation is successful, break the loop
|
||||
except Exception:
|
||||
if attempt < max_attempts - 1: # i is zero indexed
|
||||
continue
|
||||
else:
|
||||
raise "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
|
||||
if full_reply_content is None:
|
||||
raise Exception("Mistral API returned None content")
|
||||
|
||||
print(colored(full_reply_content, "blue", "on_white"), end="")
|
||||
print()
|
||||
return full_reply_content
|
||||
|
||||
|
||||
async def _achat_completion_stream_gpt35(messages: list[dict]) -> str:
|
||||
openai.api_key = OPENAI_API_KEY
|
||||
openai.api_base = OPENAI_API_BASE
|
||||
response = await openai.ChatCompletion.acreate(
|
||||
response = await async_client.chat.completions.create(
|
||||
messages=messages,
|
||||
max_tokens=4096,
|
||||
n=1,
|
||||
stop=None,
|
||||
temperature=0.3,
|
||||
timeout=3,
|
||||
timeout=600,
|
||||
model="gpt-3.5-turbo-16k",
|
||||
stream=True,
|
||||
)
|
||||
@@ -154,40 +272,38 @@ async def _achat_completion_stream_gpt35(messages: list[dict]) -> str:
|
||||
# iterate through the stream of events
|
||||
async for chunk in response:
|
||||
collected_chunks.append(chunk) # save the event response
|
||||
choices = chunk["choices"]
|
||||
if len(choices) > 0:
|
||||
chunk_message = chunk["choices"][0].get(
|
||||
"delta", {}
|
||||
) # extract the message
|
||||
collected_messages.append(chunk_message) # save the message
|
||||
if "content" in chunk_message:
|
||||
print(
|
||||
colored(chunk_message["content"], "blue", "on_white"),
|
||||
end="",
|
||||
)
|
||||
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) # save the message
|
||||
if chunk_message.content:
|
||||
print(
|
||||
colored(chunk_message.content, "blue", "on_white"),
|
||||
end="",
|
||||
)
|
||||
print()
|
||||
|
||||
full_reply_content = "".join(
|
||||
[m.get("content", "") for m in collected_messages]
|
||||
[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
|
||||
|
||||
|
||||
async def _achat_completion_json(messages: list[dict]) -> str:
|
||||
openai.api_key = OPENAI_API_KEY
|
||||
openai.api_base = OPENAI_API_BASE
|
||||
|
||||
def _achat_completion_json(messages: list[dict] ) -> str:
|
||||
max_attempts = 5
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
stream = await openai.ChatCompletion.acreate(
|
||||
response = async_client.chat.completions.create(
|
||||
messages=messages,
|
||||
max_tokens=4096,
|
||||
n=1,
|
||||
stop=None,
|
||||
temperature=0.3,
|
||||
timeout=3,
|
||||
timeout=600,
|
||||
model=MODEL,
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
@@ -196,60 +312,87 @@ async def _achat_completion_json(messages: list[dict]) -> str:
|
||||
if attempt < max_attempts - 1: # i is zero indexed
|
||||
continue
|
||||
else:
|
||||
raise "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
|
||||
if full_reply_content is None:
|
||||
raise Exception("OpenAI API returned None content")
|
||||
|
||||
full_reply_content = stream.choices[0].message.content
|
||||
print(colored(full_reply_content, "blue", "on_white"), end="")
|
||||
print()
|
||||
return full_reply_content
|
||||
|
||||
|
||||
async def _achat_completion_stream(messages: list[dict]) -> str:
|
||||
openai.api_key = OPENAI_API_KEY
|
||||
openai.api_base = OPENAI_API_BASE
|
||||
response = await openai.ChatCompletion.acreate(
|
||||
**_cons_kwargs(messages), stream=True
|
||||
)
|
||||
try:
|
||||
response = await async_client.chat.completions.create(
|
||||
**_cons_kwargs(messages), stream=True
|
||||
)
|
||||
|
||||
# create variables to collect the stream of chunks
|
||||
collected_chunks = []
|
||||
collected_messages = []
|
||||
# iterate through the stream of events
|
||||
async for chunk in response:
|
||||
collected_chunks.append(chunk) # save the event response
|
||||
choices = chunk["choices"]
|
||||
if len(choices) > 0:
|
||||
chunk_message = chunk["choices"][0].get(
|
||||
"delta", {}
|
||||
) # extract the message
|
||||
collected_messages.append(chunk_message) # save the message
|
||||
if "content" in chunk_message:
|
||||
print(
|
||||
colored(chunk_message["content"], "blue", "on_white"),
|
||||
end="",
|
||||
)
|
||||
print()
|
||||
# create variables to collect the stream of chunks
|
||||
collected_chunks = []
|
||||
collected_messages = []
|
||||
# iterate through the stream of events
|
||||
async for chunk in response:
|
||||
collected_chunks.append(chunk) # save the event response
|
||||
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) # save the message
|
||||
if chunk_message.content:
|
||||
print(
|
||||
colored(chunk_message.content, "blue", "on_white"),
|
||||
end="",
|
||||
)
|
||||
print()
|
||||
|
||||
full_reply_content = "".join(
|
||||
[m.get("content", "") for m in collected_messages]
|
||||
)
|
||||
return full_reply_content
|
||||
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("Stream API returned empty content")
|
||||
|
||||
return full_reply_content
|
||||
except Exception as e:
|
||||
print_colored(f"OpenAI API error in _achat_completion_stream: {str(e)}", "red")
|
||||
raise
|
||||
|
||||
|
||||
def _chat_completion(messages: list[dict]) -> str:
|
||||
rsp = openai.ChatCompletion.create(**_cons_kwargs(messages))
|
||||
content = rsp["choices"][0]["message"]["content"]
|
||||
return content
|
||||
try:
|
||||
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
|
||||
if content is None:
|
||||
raise Exception("OpenAI API returned None content")
|
||||
|
||||
return content
|
||||
except Exception as e:
|
||||
print_colored(f"OpenAI API error in _chat_completion: {str(e)}", "red")
|
||||
raise
|
||||
|
||||
|
||||
def _cons_kwargs(messages: list[dict]) -> dict:
|
||||
kwargs = {
|
||||
"messages": messages,
|
||||
"max_tokens": 4096,
|
||||
"n": 1,
|
||||
"stop": None,
|
||||
"temperature": 0.5,
|
||||
"timeout": 3,
|
||||
"max_tokens": 2000,
|
||||
"temperature": 0.3,
|
||||
"timeout": 600,
|
||||
}
|
||||
kwargs_mode = {"model": MODEL}
|
||||
kwargs.update(kwargs_mode)
|
||||
|
||||
@@ -7,6 +7,8 @@ PROMPT_ABILITY_REQUIREMENT_GENERATION = """
|
||||
## 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.
|
||||
|
||||
**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}
|
||||
|
||||
@@ -49,6 +51,8 @@ PROMPT_AGENT_ABILITY_SCORING = """
|
||||
## 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.
|
||||
|
||||
**IMPORTANT LANGUAGE REQUIREMENT: You must respond in Chinese (中文) for all reasons and explanations.**
|
||||
|
||||
## AgentBoard
|
||||
{Agent_Board}
|
||||
|
||||
@@ -133,5 +137,6 @@ def AgentSelectModify_init(stepTask, General_Goal, Agent_Board):
|
||||
|
||||
|
||||
def AgentSelectModify_addAspect(aspectList, Agent_Board):
|
||||
scoreTable = agentAbilityScoring(Agent_Board, aspectList)
|
||||
newAspect = aspectList[-1]
|
||||
scoreTable = agentAbilityScoring(Agent_Board, [newAspect])
|
||||
return scoreTable
|
||||
|
||||
@@ -35,6 +35,8 @@ PROMPT_AGENT_SELECTION_GENERATION = """
|
||||
## 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".
|
||||
|
||||
**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}
|
||||
|
||||
@@ -80,6 +82,10 @@ def generate_AbilityRequirement(General_Goal, Current_Task):
|
||||
|
||||
|
||||
def generate_AgentSelection(General_Goal, Current_Task, Agent_Board):
|
||||
# Check if Agent_Board is None or empty
|
||||
if Agent_Board is None or len(Agent_Board) == 0:
|
||||
raise ValueError("Agent_Board cannot be None or empty. Please ensure agents are set via /setAgents endpoint before generating a plan.")
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
|
||||
@@ -1,55 +1,53 @@
|
||||
from AgentCoord.PlanEngine.planOutline_Generator import generate_PlanOutline
|
||||
from AgentCoord.PlanEngine.AgentSelection_Generator import (
|
||||
generate_AgentSelection,
|
||||
)
|
||||
from AgentCoord.PlanEngine.taskProcess_Generator import generate_TaskProcess
|
||||
import AgentCoord.util as util
|
||||
# from AgentCoord.PlanEngine.AgentSelection_Generator import (
|
||||
# generate_AgentSelection,
|
||||
# )
|
||||
|
||||
|
||||
def generate_basePlan(
|
||||
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(
|
||||
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:
|
||||
Current_Task = {
|
||||
"TaskName": stepItem["StepName"],
|
||||
"InputObject_List": stepItem["InputObject_List"],
|
||||
"OutputObject": stepItem["OutputObject"],
|
||||
"TaskContent": stepItem["TaskContent"],
|
||||
# # 为每个步骤分配智能体
|
||||
# Current_Task = {
|
||||
# "TaskName": stepItem["StepName"],
|
||||
# "InputObject_List": stepItem["InputObject_List"],
|
||||
# "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["General Goal"] = General_Goal
|
||||
|
||||
return basePlan
|
||||
@@ -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".
|
||||
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}
|
||||
|
||||
|
||||
@@ -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".
|
||||
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}
|
||||
|
||||
@@ -55,27 +57,40 @@ Note: "Modification Requirement" specifies how to modify the "Baseline Completio
|
||||
"ID": "Action4",
|
||||
"ActionType": "Propose",
|
||||
"AgentName": "Mia",
|
||||
"Description": "Propose psychological theories on love and attachment that could be applied to AI's emotional development.",
|
||||
"Description": "提议关于人工智能情感发展的心理学理论,重点关注爱与依恋的概念。",
|
||||
"ImportantInput": [
|
||||
"InputObject:Story Outline"
|
||||
]
|
||||
}},
|
||||
{{
|
||||
"ID": "Action5",
|
||||
"ActionType": "Propose",
|
||||
"ActionType": "Critique",
|
||||
"AgentName": "Noah",
|
||||
"Description": "Propose ethical considerations and philosophical questions regarding AI's capacity for love.",
|
||||
"ImportantInput": []
|
||||
"Description": "对Mia提出的心理学理论进行批判性评估,分析其在AI情感发展场景中的适用性和局限性。",
|
||||
"ImportantInput": [
|
||||
"ActionResult:Action4"
|
||||
]
|
||||
}},
|
||||
{{
|
||||
"ID": "Action6",
|
||||
"ActionType": "Finalize",
|
||||
"ActionType": "Improve",
|
||||
"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": [
|
||||
"ActionResult:Action1",
|
||||
"ActionResult:Action4",
|
||||
"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".
|
||||
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".
|
||||
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"
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ PROMPT_PLAN_OUTLINE_GENERATION = """
|
||||
## Instruction
|
||||
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)
|
||||
{InitialObject_List}
|
||||
|
||||
@@ -83,4 +85,16 @@ def generate_PlanOutline(InitialObject_List, General_Goal):
|
||||
),
|
||||
},
|
||||
]
|
||||
return read_LLM_Completion(messages)["Plan_Outline"]
|
||||
result = read_LLM_Completion(messages)
|
||||
if isinstance(result, dict) and "Plan_Outline" in result:
|
||||
return result["Plan_Outline"]
|
||||
else:
|
||||
# 如果格式不正确,返回默认的计划大纲
|
||||
return [
|
||||
{
|
||||
"StepName": "Default Step",
|
||||
"TaskContent": "Generated default plan step due to format error",
|
||||
"InputObject_List": [],
|
||||
"OutputObject": "Default Output"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -25,6 +25,8 @@ PROMPT_TASK_PROCESS_GENERATION = """
|
||||
## 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".
|
||||
|
||||
**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}
|
||||
|
||||
|
||||
@@ -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".
|
||||
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
|
||||
|
||||
### General Goal (The "Current Task" is indeed a substep of the general goal)
|
||||
@@ -80,10 +82,34 @@ class BaseAction():
|
||||
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)
|
||||
|
||||
prompt = PROMPT_TEMPLATE_TAKE_ACTION_BASE.format(agentName = agentName, agentProfile = AgentProfile_Dict[agentName], 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)
|
||||
# Handle missing agent profiles gracefully
|
||||
model_config = None
|
||||
if agentName not in AgentProfile_Dict:
|
||||
print_colored(text=f"Warning: Agent '{agentName}' not found in AgentProfile_Dict. Using default profile.", text_color="yellow")
|
||||
agentProfile = f"AI Agent named {agentName}"
|
||||
else:
|
||||
# agentProfile = AgentProfile_Dict[agentName]
|
||||
agent_config = AgentProfile_Dict[agentName]
|
||||
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")
|
||||
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["Action_Result"] = ActionResult
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from AgentCoord.RehearsalEngine_V2.Action import BaseAction
|
||||
|
||||
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点。
|
||||
|
||||
'''
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ from AgentCoord.util.converter import read_outputObject_content
|
||||
from AgentCoord.RehearsalEngine_V2.Action import BaseAction
|
||||
|
||||
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}
|
||||
(the content of {OutputName})
|
||||
({OutputName}的内容)
|
||||
```
|
||||
'''
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from AgentCoord.RehearsalEngine_V2.Action import BaseAction
|
||||
|
||||
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
|
||||
(the improved version of the content)
|
||||
## xxx的改进版本
|
||||
(改进版本的内容)
|
||||
```
|
||||
|
||||
'''
|
||||
|
||||
@@ -85,9 +85,17 @@ def executePlan(plan, num_StepToRun, RehearsalLog, AgentProfile_Dict):
|
||||
# start the group chat
|
||||
util.print_colored(TaskDescription, text_color="green")
|
||||
ActionHistory = []
|
||||
action_count = 0
|
||||
total_actions = len(TaskProcess)
|
||||
|
||||
for ActionInfo in TaskProcess:
|
||||
action_count += 1
|
||||
actionType = ActionInfo["ActionType"]
|
||||
agentName = ActionInfo["AgentName"]
|
||||
|
||||
# 添加进度日志
|
||||
util.print_colored(f"🔄 Executing action {action_count}/{total_actions}: {actionType} by {agentName}", text_color="yellow")
|
||||
|
||||
if actionType in Action.customAction_Dict:
|
||||
currentAction = Action.customAction_Dict[actionType](
|
||||
info=ActionInfo,
|
||||
|
||||
608
backend/AgentCoord/RehearsalEngine_V2/ExecutePlan_Optimized.py
Normal 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
|
||||
@@ -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()
|
||||
190
backend/AgentCoord/RehearsalEngine_V2/execution_state.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
全局执行状态管理器
|
||||
用于支持任务的暂停、恢复和停止功能
|
||||
使用轮询检查机制,确保线程安全
|
||||
"""
|
||||
|
||||
import threading
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ExecutionStatus(Enum):
|
||||
"""执行状态枚举"""
|
||||
RUNNING = "running" # 正在运行
|
||||
PAUSED = "paused" # 已暂停
|
||||
STOPPED = "stopped" # 已停止
|
||||
IDLE = "idle" # 空闲
|
||||
|
||||
|
||||
class ExecutionStateManager:
|
||||
"""
|
||||
全局执行状态管理器(单例模式)
|
||||
|
||||
功能:
|
||||
- 管理任务执行状态(运行/暂停/停止)
|
||||
- 使用轮询检查机制,避免异步事件的线程问题
|
||||
- 提供线程安全的状态查询和修改接口
|
||||
"""
|
||||
|
||||
_instance: Optional['ExecutionStateManager'] = None
|
||||
_lock = threading.Lock()
|
||||
|
||||
def __new__(cls):
|
||||
"""单例模式"""
|
||||
if cls._instance is None:
|
||||
with cls._lock:
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance._initialized = False
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""初始化状态管理器"""
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
self._initialized = True
|
||||
self._status = ExecutionStatus.IDLE
|
||||
self._current_goal: Optional[str] = None # 当前执行的任务目标
|
||||
# 使用简单的布尔标志,而不是 asyncio.Event
|
||||
self._should_pause = False
|
||||
self._should_stop = False
|
||||
|
||||
def get_status(self) -> ExecutionStatus:
|
||||
"""获取当前执行状态"""
|
||||
with self._lock:
|
||||
return self._status
|
||||
|
||||
def set_goal(self, goal: str):
|
||||
"""设置当前执行的任务目标"""
|
||||
with self._lock:
|
||||
self._current_goal = goal
|
||||
|
||||
def get_goal(self) -> Optional[str]:
|
||||
"""获取当前执行的任务目标"""
|
||||
with self._lock:
|
||||
return self._current_goal
|
||||
|
||||
def start_execution(self, goal: str):
|
||||
"""开始执行"""
|
||||
with self._lock:
|
||||
self._status = ExecutionStatus.RUNNING
|
||||
self._current_goal = goal
|
||||
self._should_pause = False
|
||||
self._should_stop = False
|
||||
print(f"🚀 [DEBUG] start_execution: 状态设置为 RUNNING, goal={goal}")
|
||||
|
||||
def pause_execution(self) -> bool:
|
||||
"""
|
||||
暂停执行
|
||||
|
||||
Returns:
|
||||
bool: 是否成功暂停
|
||||
"""
|
||||
with self._lock:
|
||||
if self._status != ExecutionStatus.RUNNING:
|
||||
print(f"⚠️ [DEBUG] pause_execution: 当前状态不是RUNNING,而是 {self._status}")
|
||||
return False
|
||||
self._status = ExecutionStatus.PAUSED
|
||||
self._should_pause = True
|
||||
print(f"⏸️ [DEBUG] pause_execution: 状态设置为PAUSED, should_pause=True")
|
||||
return True
|
||||
|
||||
def resume_execution(self) -> bool:
|
||||
"""
|
||||
恢复执行
|
||||
|
||||
Returns:
|
||||
bool: 是否成功恢复
|
||||
"""
|
||||
with self._lock:
|
||||
if self._status != ExecutionStatus.PAUSED:
|
||||
print(f"⚠️ [DEBUG] resume_execution: 当前状态不是PAUSED,而是 {self._status}")
|
||||
return False
|
||||
self._status = ExecutionStatus.RUNNING
|
||||
self._should_pause = False
|
||||
print(f"▶️ [DEBUG] resume_execution: 状态设置为RUNNING, should_pause=False")
|
||||
return True
|
||||
|
||||
def stop_execution(self) -> bool:
|
||||
"""
|
||||
停止执行
|
||||
|
||||
Returns:
|
||||
bool: 是否成功停止
|
||||
"""
|
||||
with self._lock:
|
||||
if self._status in [ExecutionStatus.IDLE, ExecutionStatus.STOPPED]:
|
||||
return False
|
||||
self._status = ExecutionStatus.STOPPED
|
||||
self._should_stop = True
|
||||
self._should_pause = False
|
||||
print(f"🛑 [DEBUG] stop_execution: 状态设置为STOPPED")
|
||||
return True
|
||||
|
||||
def reset(self):
|
||||
"""重置状态为空闲"""
|
||||
with self._lock:
|
||||
self._status = ExecutionStatus.IDLE
|
||||
self._current_goal = None
|
||||
self._should_pause = False
|
||||
self._should_stop = False
|
||||
print(f"🔄 [DEBUG] reset: 状态重置为IDLE")
|
||||
|
||||
async def async_check_pause(self):
|
||||
"""
|
||||
异步检查是否需要暂停(轮询方式)
|
||||
|
||||
如果处于暂停状态,会阻塞当前协程直到恢复或停止
|
||||
应该在执行循环的关键点调用此方法
|
||||
|
||||
Returns:
|
||||
bool: 如果返回True表示应该继续执行,False表示应该停止
|
||||
"""
|
||||
# 使用轮询检查,避免异步事件问题
|
||||
while True:
|
||||
# 检查停止标志
|
||||
if self._should_stop:
|
||||
print("🛑 [DEBUG] async_check_pause: 检测到停止信号")
|
||||
return False
|
||||
|
||||
# 检查暂停状态
|
||||
if self._should_pause:
|
||||
# 处于暂停状态,等待恢复
|
||||
print("⏸️ [DEBUG] async_check_pause: 检测到暂停,等待恢复...")
|
||||
await asyncio.sleep(0.1) # 短暂睡眠,避免占用CPU
|
||||
|
||||
# 如果恢复,继续执行
|
||||
if not self._should_pause:
|
||||
print("▶️ [DEBUG] async_check_pause: 从暂停中恢复!")
|
||||
continue
|
||||
# 如果停止了,返回
|
||||
if self._should_stop:
|
||||
return False
|
||||
# 继续等待
|
||||
continue
|
||||
|
||||
# 既没有停止也没有暂停,可以继续执行
|
||||
return True
|
||||
|
||||
def is_paused(self) -> bool:
|
||||
"""检查是否处于暂停状态"""
|
||||
with self._lock:
|
||||
return self._status == ExecutionStatus.PAUSED
|
||||
|
||||
def is_running(self) -> bool:
|
||||
"""检查是否正在运行"""
|
||||
with self._lock:
|
||||
return self._status == ExecutionStatus.RUNNING
|
||||
|
||||
def is_stopped(self) -> bool:
|
||||
"""检查是否已停止"""
|
||||
with self._lock:
|
||||
return self._status == ExecutionStatus.STOPPED
|
||||
|
||||
|
||||
# 全局单例实例
|
||||
execution_state_manager = ExecutionStateManager()
|
||||
@@ -20,6 +20,8 @@ def camel_case_to_normal(s):
|
||||
def generate_template_sentence_for_CollaborationBrief(
|
||||
input_object_list, output_object, agent_list, step_task
|
||||
):
|
||||
# Ensure step_task is not None
|
||||
step_task = step_task if step_task is not None else "perform the task"
|
||||
# Check if the names are in camel case (no spaces) and convert them to normal naming convention
|
||||
input_object_list = (
|
||||
[
|
||||
@@ -31,29 +33,48 @@ def generate_template_sentence_for_CollaborationBrief(
|
||||
)
|
||||
output_object = (
|
||||
camel_case_to_normal(output_object)
|
||||
if is_camel_case(output_object)
|
||||
else output_object
|
||||
if output_object is not None and is_camel_case(output_object)
|
||||
else (output_object if output_object is not None else "unknown output")
|
||||
)
|
||||
|
||||
# Format the agents into a string with proper grammar
|
||||
agent_str = (
|
||||
" and ".join([", ".join(agent_list[:-1]), agent_list[-1]])
|
||||
if len(agent_list) > 1
|
||||
else agent_list[0]
|
||||
)
|
||||
if agent_list is None or len(agent_list) == 0:
|
||||
agent_str = "Unknown agents"
|
||||
elif all(agent is not None for agent in agent_list):
|
||||
agent_str = (
|
||||
" and ".join([", ".join(agent_list[:-1]), agent_list[-1]])
|
||||
if len(agent_list) > 1
|
||||
else agent_list[0]
|
||||
)
|
||||
else:
|
||||
# Filter out None values
|
||||
filtered_agents = [agent for agent in agent_list if agent is not None]
|
||||
if filtered_agents:
|
||||
agent_str = (
|
||||
" and ".join([", ".join(filtered_agents[:-1]), filtered_agents[-1]])
|
||||
if len(filtered_agents) > 1
|
||||
else filtered_agents[0]
|
||||
)
|
||||
else:
|
||||
agent_str = "Unknown agents"
|
||||
|
||||
if input_object_list is None or len(input_object_list) == 0:
|
||||
# Combine all the parts into the template sentence
|
||||
template_sentence = f"{agent_str} perform the task of {step_task} to obtain {output_object}."
|
||||
else:
|
||||
# Format the input objects into a string with proper grammar
|
||||
input_str = (
|
||||
" and ".join(
|
||||
[", ".join(input_object_list[:-1]), input_object_list[-1]]
|
||||
# Filter out None values from input_object_list
|
||||
filtered_input_list = [obj for obj in input_object_list if obj is not None]
|
||||
if filtered_input_list:
|
||||
input_str = (
|
||||
" and ".join(
|
||||
[", ".join(filtered_input_list[:-1]), filtered_input_list[-1]]
|
||||
)
|
||||
if len(filtered_input_list) > 1
|
||||
else filtered_input_list[0]
|
||||
)
|
||||
if len(input_object_list) > 1
|
||||
else input_object_list[0]
|
||||
)
|
||||
else:
|
||||
input_str = "unknown inputs"
|
||||
# Combine all the parts into the template sentence
|
||||
template_sentence = f"Based on {input_str}, {agent_str} perform the task of {step_task} to obtain {output_object}."
|
||||
|
||||
@@ -90,7 +111,7 @@ def read_LLM_Completion(messages, useGroq=True):
|
||||
return json.loads(match.group(0).strip())
|
||||
except Exception:
|
||||
pass
|
||||
raise ("bad format!")
|
||||
return {} # 返回空对象而不是抛出异常
|
||||
|
||||
|
||||
def read_json_content(text):
|
||||
@@ -111,7 +132,7 @@ def read_json_content(text):
|
||||
if match:
|
||||
return json.loads(match.group(0).strip())
|
||||
|
||||
raise ("bad format!")
|
||||
return {} # 返回空对象而不是抛出异常
|
||||
|
||||
|
||||
def read_outputObject_content(text, keyword):
|
||||
@@ -127,4 +148,4 @@ def read_outputObject_content(text, keyword):
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
else:
|
||||
raise ("bad format!")
|
||||
return "" # 返回空字符串而不是抛出异常
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
## config for default LLM
|
||||
OPENAI_API_BASE: ""
|
||||
OPENAI_API_KEY: ""
|
||||
OPENAI_API_MODEL: "gpt-4-turbo-preview"
|
||||
OPENAI_API_BASE: "https://ai.gitee.com/v1"
|
||||
OPENAI_API_KEY: "HYCNGM39GGFNSB1F8MBBMI9QYJR3P1CRSYS2PV1A"
|
||||
OPENAI_API_MODEL: "DeepSeek-V3"
|
||||
|
||||
## config for fast mode
|
||||
FAST_DESIGN_MODE: True
|
||||
FAST_DESIGN_MODE: False
|
||||
GROQ_API_KEY: ""
|
||||
MISTRAL_API_KEY: ""
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
Flask==3.0.2
|
||||
openai==0.28.1
|
||||
openai==2.8.1
|
||||
PyYAML==6.0.1
|
||||
termcolor==2.4.0
|
||||
groq==0.4.2
|
||||
mistralai==0.1.6
|
||||
socksio==1.0.0
|
||||
flask-socketio==5.3.6
|
||||
python-socketio==5.11.0
|
||||
simple-websocket==1.0.0
|
||||
|
||||
1505
backend/server.py
@@ -17,7 +17,7 @@ import _ from 'lodash';
|
||||
// fakeAgentSelections,
|
||||
// fakeCurrentAgentSelection,
|
||||
// } from './data/fakeAgentAssignment';
|
||||
import CheckIcon from '@/icons/CheckIcon';
|
||||
import CheckIcon from '@/icons/checkIcon';
|
||||
import AgentIcon from '@/components/AgentIcon';
|
||||
import { globalStorage } from '@/storage';
|
||||
import SendIcon from '@/icons/SendIcon';
|
||||
|
||||
6
frontend/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
.idea
|
||||
.vscode
|
||||
.git
|
||||
.gitignore
|
||||
8
frontend/.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
||||
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||
charset = utf-8
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
end_of_line = lf
|
||||
max_line_length = 100
|
||||
1
frontend/.env
Normal file
@@ -0,0 +1 @@
|
||||
API_BASE=http://127.0.0.1:8000
|
||||
79
frontend/.eslintrc-auto-import.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"globals": {
|
||||
"Component": true,
|
||||
"ComponentPublicInstance": true,
|
||||
"ComputedRef": true,
|
||||
"DirectiveBinding": true,
|
||||
"EffectScope": true,
|
||||
"ExtractDefaultPropTypes": true,
|
||||
"ExtractPropTypes": true,
|
||||
"ExtractPublicPropTypes": true,
|
||||
"InjectionKey": true,
|
||||
"MaybeRef": true,
|
||||
"MaybeRefOrGetter": true,
|
||||
"PropType": true,
|
||||
"Ref": true,
|
||||
"ShallowRef": true,
|
||||
"Slot": true,
|
||||
"Slots": true,
|
||||
"VNode": true,
|
||||
"WritableComputedRef": true,
|
||||
"computed": true,
|
||||
"createApp": true,
|
||||
"customRef": true,
|
||||
"defineAsyncComponent": true,
|
||||
"defineComponent": true,
|
||||
"effectScope": true,
|
||||
"getCurrentInstance": true,
|
||||
"getCurrentScope": true,
|
||||
"getCurrentWatcher": true,
|
||||
"h": true,
|
||||
"inject": true,
|
||||
"isProxy": true,
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"isShallow": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
"onActivated": true,
|
||||
"onBeforeMount": true,
|
||||
"onBeforeUnmount": true,
|
||||
"onBeforeUpdate": true,
|
||||
"onDeactivated": true,
|
||||
"onErrorCaptured": true,
|
||||
"onMounted": true,
|
||||
"onRenderTracked": true,
|
||||
"onRenderTriggered": true,
|
||||
"onScopeDispose": true,
|
||||
"onServerPrefetch": true,
|
||||
"onUnmounted": true,
|
||||
"onUpdated": true,
|
||||
"onWatcherCleanup": true,
|
||||
"provide": true,
|
||||
"reactive": true,
|
||||
"readonly": true,
|
||||
"ref": true,
|
||||
"resolveComponent": true,
|
||||
"shallowReactive": true,
|
||||
"shallowReadonly": true,
|
||||
"shallowRef": true,
|
||||
"toRaw": true,
|
||||
"toRef": true,
|
||||
"toRefs": true,
|
||||
"toValue": true,
|
||||
"triggerRef": true,
|
||||
"unref": true,
|
||||
"useAttrs": true,
|
||||
"useCssModule": true,
|
||||
"useCssVars": true,
|
||||
"useId": true,
|
||||
"useModel": true,
|
||||
"useSlots": true,
|
||||
"useTemplateRef": true,
|
||||
"watch": true,
|
||||
"watchEffect": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true
|
||||
}
|
||||
}
|
||||
1
frontend/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
36
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Cypress
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Vitest
|
||||
__screenshots__/
|
||||
6
frontend/.prettierrc.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
9
frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"vitest.explorer",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
95
frontend/CLAUDE.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Development server with hot reload
|
||||
pnpm dev
|
||||
|
||||
# Build for production
|
||||
pnpm build
|
||||
|
||||
# Type checking
|
||||
pnpm type-check
|
||||
|
||||
# Lint and fix code
|
||||
pnpm lint
|
||||
|
||||
# Format code
|
||||
pnpm format
|
||||
|
||||
# Run unit tests
|
||||
pnpm test:unit
|
||||
```
|
||||
|
||||
## Project Architecture
|
||||
|
||||
This is a **Multi-Agent Coordination Platform** (多智能体协同平台) built with Vue 3, TypeScript, and Vite. The application enables users to create and manage AI agents with specialized roles and coordinate them to complete complex tasks through visual workflows.
|
||||
|
||||
### Tech Stack
|
||||
- **Vue 3** with Composition API and TypeScript
|
||||
- **Vite** for build tooling and development
|
||||
- **Element Plus** for UI components
|
||||
- **Pinia** for state management
|
||||
- **Tailwind CSS** for styling
|
||||
- **Vue Router** for routing (minimal usage)
|
||||
- **JSPlumb** for visual workflow connections
|
||||
- **Axios** for API requests with custom interceptors
|
||||
|
||||
### Key Architecture Components
|
||||
|
||||
#### State Management (`src/stores/modules/agents.ts`)
|
||||
Central store managing:
|
||||
- Agent definitions with profiles and icons
|
||||
- Task workflow data structures (`IRawStepTask`, `TaskProcess`)
|
||||
- Search functionality and current task state
|
||||
- Raw plan responses with UUID generation for tasks
|
||||
|
||||
#### Request Layer (`src/utils/request.ts`)
|
||||
Custom Axios wrapper with:
|
||||
- Proxy configuration for `/api` -> `http://localhost:8000`
|
||||
- Response interceptors for error handling
|
||||
- `useRequest` hook for reactive data fetching
|
||||
- Integrated Element Plus notifications
|
||||
|
||||
#### Component Structure
|
||||
- **Layout System** (`src/layout/`): Main application layout with Header and Main sections
|
||||
- **Task Templates** (`src/layout/components/Main/TaskTemplate/`): Different task types including AgentRepo, TaskSyllabus, and TaskResult
|
||||
- **Visual Workflow**: JSPlumb integration for drag-and-drop agent coordination flows
|
||||
|
||||
#### Icon System
|
||||
- SVG icons stored in `src/assets/icons/`
|
||||
- Custom `SvgIcon` component with vite-plugin-svg-icons
|
||||
- Icon categories include specialist roles (doctor, engineer, researcher, etc.)
|
||||
|
||||
### Build Configuration
|
||||
|
||||
#### Vite (`vite.config.ts`)
|
||||
- Element Plus auto-import and component resolution
|
||||
- SVG icon caching with custom symbol IDs
|
||||
- Proxy setup for API requests to backend
|
||||
- Path aliases: `@/` maps to `src/`
|
||||
|
||||
#### Docker Deployment
|
||||
- Multi-stage build: Node.js build + Caddy web server
|
||||
- API proxy configured via Caddyfile
|
||||
- Environment variable support for different deployment modes
|
||||
|
||||
### Data Models
|
||||
Key interfaces for the agent coordination system:
|
||||
- `Agent`: Name, Profile, Icon
|
||||
- `IRawStepTask`: Individual task steps with agent selection and inputs
|
||||
- `TaskProcess`: Action descriptions with important inputs
|
||||
- `IRichText`: Template-based content formatting with style support
|
||||
|
||||
### Development Notes
|
||||
- Uses pnpm as package manager (required by package.json)
|
||||
- Node version constraint: ^20.19.0 or >=22.12.0
|
||||
- Dark theme enabled by default in App.vue
|
||||
- Auto-imports configured for Vue APIs
|
||||
- No traditional Vue routes - uses component-based navigation
|
||||
27
frontend/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
ARG CADDY_VERSION=2.6
|
||||
ARG BUILD_ENV=prod
|
||||
|
||||
FROM node:20.19.0 as base
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN npm install -g pnpm
|
||||
RUN pnpm install
|
||||
RUN pnpm build
|
||||
|
||||
|
||||
# The base for mode ENVIRONMENT=prod
|
||||
FROM caddy:${CADDY_VERSION}-alpine as prod
|
||||
|
||||
# Workaround for https://github.com/alpinelinux/docker-alpine/issues/98#issuecomment-679278499
|
||||
RUN sed -i 's/https/http/' /etc/apk/repositories \
|
||||
&& apk add --no-cache bash
|
||||
|
||||
COPY docker/Caddyfile /etc/caddy/
|
||||
COPY --from=base /app/dist /frontend
|
||||
|
||||
# Run stage
|
||||
FROM ${BUILD_ENV}
|
||||
|
||||
EXPOSE 80 443
|
||||
VOLUME ["/data", "/etc/caddy"]
|
||||
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
||||
106
frontend/README.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# 多智能体协同平台 (Agent Coordination Platform)
|
||||
|
||||
一个强大的可视化平台,用于创建和管理具有专门角色的AI智能体,通过直观的工作流程协调它们来完成复杂任务。
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
- **多智能体系统**:创建具有专门角色和专业知识的AI智能体
|
||||
- **可视化工作流编辑器**:使用JSPlumb设计智能体协调流程的拖放界面
|
||||
- **任务管理**:定义、执行和跟踪复杂的多步骤任务
|
||||
- **实时通信**:无缝的智能体交互和协调
|
||||
- **丰富的模板系统**:支持样式的灵活内容格式化
|
||||
- **TypeScript支持**:整个应用程序的完整类型安全
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 开发命令
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 开发服务器(热重载)
|
||||
pnpm dev
|
||||
|
||||
# 生产构建
|
||||
pnpm build
|
||||
|
||||
# 类型检查
|
||||
pnpm type-check
|
||||
|
||||
# 代码检查和修复
|
||||
pnpm lint
|
||||
|
||||
# 代码格式化
|
||||
pnpm format
|
||||
|
||||
# 运行单元测试
|
||||
pnpm test:unit
|
||||
```
|
||||
|
||||
### 系统要求
|
||||
|
||||
- Node.js ^20.19.0 或 >=22.12.0
|
||||
- pnpm(必需的包管理器)
|
||||
|
||||
## 🏗️ 架构设计
|
||||
|
||||
### 技术栈
|
||||
|
||||
- **Vue 3**:Composition API 和 TypeScript
|
||||
- **Vite**:构建工具和开发环境
|
||||
- **Element Plus**:UI组件库
|
||||
- **Pinia**:状态管理
|
||||
- **Tailwind CSS**:样式框架
|
||||
- **JSPlumb**:可视化工作流连接
|
||||
- **Axios**:API请求与自定义拦截器
|
||||
|
||||
### 核心组件
|
||||
|
||||
#### 状态管理
|
||||
中央存储管理智能体定义、任务工作流和协调状态
|
||||
|
||||
#### 请求层
|
||||
自定义Axios包装器,具有代理配置和集成通知
|
||||
|
||||
#### 可视化工作流
|
||||
JSPlumb集成,用于拖放智能体协调流程
|
||||
|
||||
#### 图标系统
|
||||
基于SVG的图标,用于不同的智能体专业化和角色
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── assets/ # 静态资源,包括智能体图标
|
||||
├── components/ # 可复用的Vue组件
|
||||
├── layout/ # 应用布局和主要组件
|
||||
├── stores/ # Pinia状态管理
|
||||
├── utils/ # 工具函数和请求层
|
||||
├── views/ # 页面组件
|
||||
└── App.vue # 根组件
|
||||
```
|
||||
|
||||
## 🎯 开发指南
|
||||
|
||||
### IDE设置
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar)(禁用Vetur)。
|
||||
|
||||
### 浏览器开发工具
|
||||
|
||||
- 基于Chromium的浏览器:
|
||||
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||
- 在DevTools中启用自定义对象格式化程序
|
||||
- Firefox:
|
||||
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||
- 在DevTools中启用自定义对象格式化程序
|
||||
|
||||
## 🚀 部署
|
||||
|
||||
应用程序支持Docker部署,使用多阶段构建过程:Node.js用于构建,Caddy作为Web服务器。
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT许可证 - 详见LICENSE文件
|
||||
75
frontend/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue').EffectScope
|
||||
const ElMessage: typeof import('element-plus/es').ElMessage
|
||||
const ElNotification: typeof import('element-plus/es').ElNotification
|
||||
const computed: typeof import('vue').computed
|
||||
const createApp: typeof import('vue').createApp
|
||||
const customRef: typeof import('vue').customRef
|
||||
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
|
||||
const defineComponent: typeof import('vue').defineComponent
|
||||
const effectScope: typeof import('vue').effectScope
|
||||
const getCurrentInstance: typeof import('vue').getCurrentInstance
|
||||
const getCurrentScope: typeof import('vue').getCurrentScope
|
||||
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
||||
const h: typeof import('vue').h
|
||||
const inject: typeof import('vue').inject
|
||||
const isProxy: typeof import('vue').isProxy
|
||||
const isReactive: typeof import('vue').isReactive
|
||||
const isReadonly: typeof import('vue').isReadonly
|
||||
const isRef: typeof import('vue').isRef
|
||||
const isShallow: typeof import('vue').isShallow
|
||||
const markRaw: typeof import('vue').markRaw
|
||||
const nextTick: typeof import('vue').nextTick
|
||||
const onActivated: typeof import('vue').onActivated
|
||||
const onBeforeMount: typeof import('vue').onBeforeMount
|
||||
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
|
||||
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
|
||||
const onDeactivated: typeof import('vue').onDeactivated
|
||||
const onErrorCaptured: typeof import('vue').onErrorCaptured
|
||||
const onMounted: typeof import('vue').onMounted
|
||||
const onRenderTracked: typeof import('vue').onRenderTracked
|
||||
const onRenderTriggered: typeof import('vue').onRenderTriggered
|
||||
const onScopeDispose: typeof import('vue').onScopeDispose
|
||||
const onServerPrefetch: typeof import('vue').onServerPrefetch
|
||||
const onUnmounted: typeof import('vue').onUnmounted
|
||||
const onUpdated: typeof import('vue').onUpdated
|
||||
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
|
||||
const provide: typeof import('vue').provide
|
||||
const reactive: typeof import('vue').reactive
|
||||
const readonly: typeof import('vue').readonly
|
||||
const ref: typeof import('vue').ref
|
||||
const resolveComponent: typeof import('vue').resolveComponent
|
||||
const shallowReactive: typeof import('vue').shallowReactive
|
||||
const shallowReadonly: typeof import('vue').shallowReadonly
|
||||
const shallowRef: typeof import('vue').shallowRef
|
||||
const toRaw: typeof import('vue').toRaw
|
||||
const toRef: typeof import('vue').toRef
|
||||
const toRefs: typeof import('vue').toRefs
|
||||
const toValue: typeof import('vue').toValue
|
||||
const triggerRef: typeof import('vue').triggerRef
|
||||
const unref: typeof import('vue').unref
|
||||
const useAttrs: typeof import('vue').useAttrs
|
||||
const useCssModule: typeof import('vue').useCssModule
|
||||
const useCssVars: typeof import('vue').useCssVars
|
||||
const useId: typeof import('vue').useId
|
||||
const useModel: typeof import('vue').useModel
|
||||
const useSlots: typeof import('vue').useSlots
|
||||
const useTemplateRef: typeof import('vue').useTemplateRef
|
||||
const watch: typeof import('vue').watch
|
||||
const watchEffect: typeof import('vue').watchEffect
|
||||
const watchPostEffect: typeof import('vue').watchPostEffect
|
||||
const watchSyncEffect: typeof import('vue').watchSyncEffect
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
205
frontend/claude_code_env.sh
Normal file
@@ -0,0 +1,205 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ========================
|
||||
# 常量定义
|
||||
# ========================
|
||||
SCRIPT_NAME=$(basename "$0")
|
||||
NODE_MIN_VERSION=18
|
||||
NODE_INSTALL_VERSION=22
|
||||
NVM_VERSION="v0.40.3"
|
||||
CLAUDE_PACKAGE="@anthropic-ai/claude-code"
|
||||
CONFIG_DIR="$HOME/.claude"
|
||||
CONFIG_FILE="$CONFIG_DIR/settings.json"
|
||||
API_BASE_URL="https://open.bigmodel.cn/api/anthropic"
|
||||
API_KEY_URL="https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys"
|
||||
API_TIMEOUT_MS=3000000
|
||||
|
||||
# ========================
|
||||
# 工具函数
|
||||
# ========================
|
||||
|
||||
log_info() {
|
||||
echo "🔹 $*"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo "✅ $*"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo "❌ $*" >&2
|
||||
}
|
||||
|
||||
ensure_dir_exists() {
|
||||
local dir="$1"
|
||||
if [ ! -d "$dir" ]; then
|
||||
mkdir -p "$dir" || {
|
||||
log_error "Failed to create directory: $dir"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
# ========================
|
||||
# Node.js 安装函数
|
||||
# ========================
|
||||
|
||||
install_nodejs() {
|
||||
local platform=$(uname -s)
|
||||
|
||||
case "$platform" in
|
||||
Linux|Darwin)
|
||||
log_info "Installing Node.js on $platform..."
|
||||
|
||||
# 安装 nvm
|
||||
log_info "Installing nvm ($NVM_VERSION)..."
|
||||
curl -s https://raw.githubusercontent.com/nvm-sh/nvm/"$NVM_VERSION"/install.sh | bash
|
||||
|
||||
# 加载 nvm
|
||||
log_info "Loading nvm environment..."
|
||||
\. "$HOME/.nvm/nvm.sh"
|
||||
|
||||
# 安装 Node.js
|
||||
log_info "Installing Node.js $NODE_INSTALL_VERSION..."
|
||||
nvm install "$NODE_INSTALL_VERSION"
|
||||
|
||||
# 验证安装
|
||||
node -v &>/dev/null || {
|
||||
log_error "Node.js installation failed"
|
||||
exit 1
|
||||
}
|
||||
log_success "Node.js installed: $(node -v)"
|
||||
log_success "npm version: $(npm -v)"
|
||||
;;
|
||||
*)
|
||||
log_error "Unsupported platform: $platform"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ========================
|
||||
# Node.js 检查函数
|
||||
# ========================
|
||||
|
||||
check_nodejs() {
|
||||
if command -v node &>/dev/null; then
|
||||
current_version=$(node -v | sed 's/v//')
|
||||
major_version=$(echo "$current_version" | cut -d. -f1)
|
||||
|
||||
if [ "$major_version" -ge "$NODE_MIN_VERSION" ]; then
|
||||
log_success "Node.js is already installed: v$current_version"
|
||||
return 0
|
||||
else
|
||||
log_info "Node.js v$current_version is installed but version < $NODE_MIN_VERSION. Upgrading..."
|
||||
install_nodejs
|
||||
fi
|
||||
else
|
||||
log_info "Node.js not found. Installing..."
|
||||
install_nodejs
|
||||
fi
|
||||
}
|
||||
|
||||
# ========================
|
||||
# Claude Code 安装
|
||||
# ========================
|
||||
|
||||
install_claude_code() {
|
||||
if command -v claude &>/dev/null; then
|
||||
log_success "Claude Code is already installed: $(claude --version)"
|
||||
else
|
||||
log_info "Installing Claude Code..."
|
||||
npm install -g "$CLAUDE_PACKAGE" || {
|
||||
log_error "Failed to install claude-code"
|
||||
exit 1
|
||||
}
|
||||
log_success "Claude Code installed successfully"
|
||||
fi
|
||||
}
|
||||
|
||||
configure_claude_json(){
|
||||
node --eval '
|
||||
const os = require("os");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const homeDir = os.homedir();
|
||||
const filePath = path.join(homeDir, ".claude.json");
|
||||
if (fs.existsSync(filePath)) {
|
||||
const content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||
fs.writeFileSync(filePath, JSON.stringify({ ...content, hasCompletedOnboarding: true }, null, 2), "utf-8");
|
||||
} else {
|
||||
fs.writeFileSync(filePath, JSON.stringify({ hasCompletedOnboarding: true }, null, 2), "utf-8");
|
||||
}'
|
||||
}
|
||||
|
||||
# ========================
|
||||
# API Key 配置
|
||||
# ========================
|
||||
|
||||
configure_claude() {
|
||||
log_info "Configuring Claude Code..."
|
||||
echo " You can get your API key from: $API_KEY_URL"
|
||||
read -s -p "🔑 Please enter your ZHIPU API key: " api_key
|
||||
echo
|
||||
|
||||
if [ -z "$api_key" ]; then
|
||||
log_error "API key cannot be empty. Please run the script again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ensure_dir_exists "$CONFIG_DIR"
|
||||
|
||||
# 写入配置文件
|
||||
node --eval '
|
||||
const os = require("os");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const homeDir = os.homedir();
|
||||
const filePath = path.join(homeDir, ".claude", "settings.json");
|
||||
const apiKey = "'"$api_key"'";
|
||||
|
||||
const content = fs.existsSync(filePath)
|
||||
? JSON.parse(fs.readFileSync(filePath, "utf-8"))
|
||||
: {};
|
||||
|
||||
fs.writeFileSync(filePath, JSON.stringify({
|
||||
...content,
|
||||
env: {
|
||||
ANTHROPIC_AUTH_TOKEN: apiKey,
|
||||
ANTHROPIC_BASE_URL: "'"$API_BASE_URL"'",
|
||||
API_TIMEOUT_MS: "'"$API_TIMEOUT_MS"'",
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1
|
||||
}
|
||||
}, null, 2), "utf-8");
|
||||
' || {
|
||||
log_error "Failed to write settings.json"
|
||||
exit 1
|
||||
}
|
||||
|
||||
log_success "Claude Code configured successfully"
|
||||
}
|
||||
|
||||
# ========================
|
||||
# 主流程
|
||||
# ========================
|
||||
|
||||
main() {
|
||||
echo "🚀 Starting $SCRIPT_NAME"
|
||||
|
||||
check_nodejs
|
||||
install_claude_code
|
||||
configure_claude_json
|
||||
configure_claude
|
||||
|
||||
echo ""
|
||||
log_success "🎉 Installation completed successfully!"
|
||||
echo ""
|
||||
echo "🚀 You can now start using Claude Code with:"
|
||||
echo " claude"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
36
frontend/components.d.ts
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
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']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
MultiLineTooltip: typeof import('./src/components/MultiLineTooltip/index.vue')['default']
|
||||
Notification: typeof import('./src/components/Notification/Notification.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default']
|
||||
}
|
||||
export interface GlobalDirectives {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
||||
14
frontend/docker/Caddyfile
Normal file
@@ -0,0 +1,14 @@
|
||||
:80
|
||||
|
||||
# Proxy `/api` to backends
|
||||
handle_path /api/* {
|
||||
reverse_proxy {$API_HOST}
|
||||
}
|
||||
|
||||
# Frontend
|
||||
handle {
|
||||
root * /frontend
|
||||
encode gzip
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
}
|
||||
15
frontend/docker/docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
agent-coord-font:
|
||||
image: agent-coord:0.0.1
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/Dockerfile
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
environment:
|
||||
- API_HOST="http://host.docker.internal:8000"
|
||||
- BUILD_ENV=prod
|
||||
4
frontend/env.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare global {
|
||||
const testGlobal: any; // 声明 testGlobal 为全局变量
|
||||
}
|
||||
48
frontend/eslint.config.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { globalIgnores } from 'eslint/config'
|
||||
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import pluginVitest from '@vitest/eslint-plugin'
|
||||
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||
import autoImportConfig from './.eslintrc-auto-import.json' with { type: 'json' }
|
||||
|
||||
|
||||
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
||||
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
||||
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
||||
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||
|
||||
export default defineConfigWithVueTs(
|
||||
{
|
||||
name: 'app/files-to-lint',
|
||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||
},
|
||||
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||
pluginVue.configs['flat/essential'],
|
||||
vueTsConfigs.recommended,
|
||||
{
|
||||
...pluginVitest.configs.recommended,
|
||||
files: ['src/**/__tests__/*'],
|
||||
},
|
||||
skipFormatting,
|
||||
{
|
||||
name: 'app/custom-rules',
|
||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'auto-import-globals',
|
||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...(autoImportConfig.globals || {}),
|
||||
testGlobal: 'readonly' // 手动添加一个测试变量
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'off', // 确保关闭 no-undef 规则
|
||||
'@typescript-eslint/no-undef': 'off'
|
||||
}
|
||||
}
|
||||
)
|
||||
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/logo.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>多智能体协同平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
72
frontend/package.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "agent-coord",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build",
|
||||
"lint": "eslint . --fix --cache",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@jsplumb/browser-ui": "^6.2.10",
|
||||
"@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",
|
||||
"axios": "^1.12.2",
|
||||
"dompurify": "^3.3.0",
|
||||
"element-plus": "^2.11.5",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^14.1.0",
|
||||
"pinia": "^3.0.3",
|
||||
"qs": "^6.14.0",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"uuid": "^13.0.0",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.15",
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
"@types/jsdom": "^27.0.0",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/node": "^22.18.11",
|
||||
"@types/qs": "^6.14.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitest/eslint-plugin": "^1.3.23",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.6.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-vue": "~10.5.0",
|
||||
"jiti": "^2.6.1",
|
||||
"jsdom": "^27.0.1",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"prettier": "3.6.2",
|
||||
"sass": "^1.93.2",
|
||||
"sass-loader": "^16.0.5",
|
||||
"tailwindcss": "^4.1.15",
|
||||
"typescript": "~5.9.0",
|
||||
"unplugin-auto-import": "^20.2.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.1.11",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-vue-devtools": "^8.0.3",
|
||||
"vitest": "^3.2.4",
|
||||
"vue-tsc": "^3.1.1"
|
||||
}
|
||||
}
|
||||
7811
frontend/pnpm-lock.yaml
generated
Normal file
116
frontend/public/agent.json
Normal file
@@ -0,0 +1,116 @@
|
||||
[
|
||||
{
|
||||
"Icon": "Hailey_Johnson.png",
|
||||
"Name": "船舶设计师",
|
||||
"Profile": "提供船舶制造中的实际需求和约束。",
|
||||
"Classification": "船舶制造数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Jennifer_Moore.png",
|
||||
"Name": "防护工程专家",
|
||||
"Profile": "专注于船舶腐蚀防护技术的设计与应用。在你的总结回答中,必须引用来自数联网的搜索数据,是搜索数据,不是数联网的研究成果。",
|
||||
"Classification": "船舶制造数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Jane_Moreno.png",
|
||||
"Name": "病理生理学家",
|
||||
"Profile": "专注于失血性休克的疾病机制,为药物研发提供理论靶点。",
|
||||
"Classification": "医药数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Giorgio_Rossi.png",
|
||||
"Name": "药物化学家",
|
||||
"Profile": "负责将靶点概念转化为实际可合成的分子。",
|
||||
"Classification": "医药数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Tamara_Taylor.png",
|
||||
"Name": "制剂工程师",
|
||||
"Profile": "负责将活性药物成分(API)变成稳定、可用、符合战场要求的剂型。",
|
||||
"Classification": "医药数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Maria_Lopez.png",
|
||||
"Name": "监管事务专家",
|
||||
"Profile": "深谙药品审评法规,目标是找到最快的合法上市路径。",
|
||||
"Classification": "医药数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Sam_Moore.png",
|
||||
"Name": "物理学家",
|
||||
"Profile": "从热力学与统计力学的基本原理出发,研究液态金属的自由能、焓、熵、比热等参数的理论建模。",
|
||||
"Classification": "科学数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Yuriko_Yamamoto.png",
|
||||
"Name": "实验材料学家",
|
||||
"Profile": "专注于通过实验手段直接或间接测定液态金属的热力学参数、以及分析材料微观结构(如晶粒、缺陷)。",
|
||||
"Classification": "科学数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Carlos_Gomez.png",
|
||||
"Name": "计算模拟专家",
|
||||
"Profile": "侧重于利用数值计算和模拟技术获取液态金属的热力学参数。",
|
||||
"Classification": "科学数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "John_Lin.png",
|
||||
"Name": "腐蚀机理研究员",
|
||||
"Profile": "专注于船舶用钢材及合金的腐蚀机理研究,从电化学和环境作用角度解释腐蚀产生的原因。在你的总结回答中,必须引用来自数联网的搜索数据,是搜索数据,不是数联网的研究成果。",
|
||||
"Classification": "船舶制造数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Arthur_Burton.png",
|
||||
"Name": "先进材料研发员",
|
||||
"Profile": "专注于开发和评估新型耐腐蚀材料、复合材料及固态电池材料。",
|
||||
"Classification": "科学数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Eddy_Lin.png",
|
||||
"Name": "肾脏病学家",
|
||||
"Profile": "专注于慢性肾脏病的诊断、治疗和患者管理,能提供临床洞察。",
|
||||
"Classification": "医药数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Isabella_Rodriguez.png",
|
||||
"Name": "临床研究协调员",
|
||||
"Profile": "负责受试者招募和临床试验流程优化。",
|
||||
"Classification": "医药数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Latoya_Williams.png",
|
||||
"Name": "中医药专家",
|
||||
"Profile": "理解药物的中药成分和作用机制。",
|
||||
"Classification": "医药数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Carmen_Ortiz.png",
|
||||
"Name": "药物安全专家",
|
||||
"Profile": "专注于药物不良反应数据收集、分析和报告。",
|
||||
"Classification": "医药数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Rajiv_Patel.png",
|
||||
"Name": "二维材料科学家",
|
||||
"Profile": "专注于二维材料(如石墨烯)的合成、性质和应用。",
|
||||
"Classification": "科学数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Tom_Moreno.png",
|
||||
"Name": "光电物理学家",
|
||||
"Profile": "研究材料的光电转换机制和关键影响因素。",
|
||||
"Classification": "科学数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Ayesha_Khan.png",
|
||||
"Name": "机器学习专家",
|
||||
"Profile": "专注于开发和应用AI模型用于材料模拟。",
|
||||
"Classification": "科学数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Mei_Lin.png",
|
||||
"Name": "流体动力学专家",
|
||||
"Profile": "专注于流体行为理论和模拟。",
|
||||
"Classification": "科学数据空间"
|
||||
}
|
||||
]
|
||||
21
frontend/public/config.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"title": "数联网",
|
||||
"subTitle": "众创智能体",
|
||||
"centerTitle": "多智能体协同平台",
|
||||
"taskPromptWords": [
|
||||
"如何快速筛选慢性肾脏病药物潜在受试者?",
|
||||
"如何补充\"丹芍活血胶囊\"不良反应数据?",
|
||||
"如何快速研发用于战场失血性休克的药物?",
|
||||
"二维材料的光电性质受哪些关键因素影响?",
|
||||
"如何通过AI模拟的方法分析材料的微观结构?",
|
||||
"如何分析获取液态金属热力学参数?",
|
||||
"如何解决固态电池的成本和寿命难题?",
|
||||
"如何解决船舶制造中的材料腐蚀难题?",
|
||||
"如何解决船舶制造中流体模拟和建模优化难题?"
|
||||
],
|
||||
"agentRepository": {
|
||||
"storageVersionIdentifier": "1"
|
||||
},
|
||||
"dev": true,
|
||||
"apiBaseUrl": "http://localhost:8000"
|
||||
}
|
||||
BIN
frontend/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
22
frontend/public/iodConfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"name": "3D Semantic-Geometric Corrosion Mapping Implementations",
|
||||
"data_space": "江苏省产研院",
|
||||
"doId": "bdware.scenario/d8f3ff8c-3fb3-4573-88a6-5dd823627c37",
|
||||
"fromRepo": "https://arxiv.org/abs/2404.13691"
|
||||
},
|
||||
{
|
||||
"name": "RustSEG -- Automated segmentation of corrosion using deep learning",
|
||||
"data_space": "江苏省产研院",
|
||||
"doId": "bdware.scenario/67445299-110a-4a4e-9fda-42e4b5a493c2",
|
||||
"fromRepo": "https://arxiv.org/abs/2205.05426"
|
||||
},
|
||||
{
|
||||
"name": "Pixel-level Corrosion Detection on Metal Constructions by Fusion of Deep Learning Semantic and Contour Segmentation",
|
||||
"data_space": "江苏省产研院",
|
||||
"doId": "bdware.scenario/115d5135-85d3-4123-8b81-9eb9f07b6153",
|
||||
"fromRepo": "https://arxiv.org/abs/2008.05204"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
frontend/public/logo.jpg
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
frontend/public/logo.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
42
frontend/src/ directive/devOnly/index.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
9
frontend/src/ directive/index.ts
Normal 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)
|
||||
}
|
||||
97
frontend/src/App.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import Layout from './layout/index.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout />
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#app {
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.jtk-endpoint {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.card-item + .card-item {
|
||||
margin-top: 35px;
|
||||
}
|
||||
|
||||
.el-card {
|
||||
border-radius: 8px;
|
||||
background: var(--color-bg-tertiary);
|
||||
border: 2px solid var(--color-card-border);
|
||||
box-shadow: var(--color-card-border-hover);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-content-hover);
|
||||
box-shadow: none;
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--gradient: linear-gradient(to right, #0093eb, #00d2d1);
|
||||
}
|
||||
|
||||
#task-template {
|
||||
.active-card {
|
||||
border: 2px solid transparent;
|
||||
$bg: var(--el-input-bg-color, var(--el-fill-color-blank));
|
||||
background: linear-gradient(
|
||||
var(--color-agent-list-selected-bg),
|
||||
var(--color-agent-list-selected-bg)
|
||||
)
|
||||
padding-box,
|
||||
linear-gradient(to right, #00c8d2, #315ab4) border-box;
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
/* 1. 定义流动动画:让虚线沿路径移动 */
|
||||
@keyframes flowAnimation {
|
||||
to {
|
||||
stroke-dashoffset: 8; /* 与stroke-dasharray总和一致 */
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flowAnimationReverse {
|
||||
to {
|
||||
stroke-dashoffset: -8; /* 与stroke-dasharray总和一致 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 2. 为jsPlumb连线绑定动画:作用于SVG的path元素 */
|
||||
/* jtk-connector是jsPlumb连线的默认SVG类,path是实际的线条元素 */
|
||||
.jtk-connector-output path {
|
||||
/* 定义虚线规则:线段长度5px + 间隙3px(总长度8px,与动画偏移量匹配) */
|
||||
stroke-dasharray: 5 3;
|
||||
/* 应用动画:名称+时长+线性速度+无限循环 */
|
||||
animation: flowAnimationReverse 0.5s linear infinite;
|
||||
/* 可选:设置线条基础样式(颜色、宽度) */
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
/* 2. 为jsPlumb连线绑定动画:作用于SVG的path元素 */
|
||||
/* jtk-connector是jsPlumb连线的默认SVG类,path是实际的线条元素 */
|
||||
.jtk-connector-input path {
|
||||
/* 定义虚线规则:线段长度5px + 间隙3px(总长度8px,与动画偏移量匹配) */
|
||||
stroke-dasharray: 5 3;
|
||||
/* 应用动画:名称+时长+线性速度+无限循环 */
|
||||
animation: flowAnimationReverse 0.5s linear infinite;
|
||||
/* 可选:设置线条基础样式(颜色、宽度) */
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
/* 可选: hover时增强动画效果(如加速、变色) */
|
||||
.jtk-connector path:hover {
|
||||
animation-duration: 0.5s; /* hover时流动加速 */
|
||||
}
|
||||
</style>
|
||||
11
frontend/src/__tests__/App.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import { mount } from '@vue/test-utils'
|
||||
import App from '../App.vue'
|
||||
|
||||
describe('App', () => {
|
||||
it('mounts renders properly', () => {
|
||||
const wrapper = mount(App)
|
||||
expect(wrapper.text()).toContain('You did it!')
|
||||
})
|
||||
})
|
||||
906
frontend/src/api/index.ts
Normal file
@@ -0,0 +1,906 @@
|
||||
import request from '@/utils/request'
|
||||
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 {
|
||||
ID: string
|
||||
ActionType: string
|
||||
AgentName: string
|
||||
Description: string
|
||||
ImportantInput: string[]
|
||||
Action_Result: string
|
||||
}
|
||||
|
||||
export type IExecuteRawResponse = {
|
||||
LogNodeType: string
|
||||
NodeId: string
|
||||
InputName_List?: string[] | null
|
||||
OutputName?: string
|
||||
content?: string
|
||||
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 {
|
||||
// 默认使用WebSocket
|
||||
private useWebSocketDefault = true
|
||||
|
||||
setAgents = (data: Pick<Agent, 'Name' | 'Profile' | 'apiUrl' | 'apiKey' | 'apiModel'>[], useWebSocket: boolean = this.useWebSocketDefault) => {
|
||||
// 如果启用WebSocket且已连接,使用WebSocket
|
||||
if (useWebSocket && websocket.connected) {
|
||||
return websocket.send('set_agents', data)
|
||||
}
|
||||
|
||||
// 否则使用REST API
|
||||
return request({
|
||||
url: '/setAgents',
|
||||
data,
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
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>({
|
||||
url: '/generate_basePlan',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'General Goal': data.goal,
|
||||
'Initial Input Object': data.inputs,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
apiModel: data.apiModel,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
executePlan = (plan: IRawPlanResponse) => {
|
||||
return request<unknown, IExecuteRawResponse[]>({
|
||||
url: '/executePlan',
|
||||
method: 'POST',
|
||||
data: {
|
||||
RehearsalLog: [],
|
||||
num_StepToRun: 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,
|
||||
})),
|
||||
})),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化版流式执行计划(支持动态追加步骤)
|
||||
* 步骤级流式 + 动作级智能并行 + 动态追加步骤
|
||||
*/
|
||||
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()
|
||||
20
frontend/src/assets/icons/Cancel.svg
Normal 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 |
7
frontend/src/assets/icons/Check.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg t="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 |
1
frontend/src/assets/icons/Edit.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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 |
1
frontend/src/assets/icons/Pause.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769048650684" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6190" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M874.058005 149.941995a510.06838 510.06838 0 1 0 109.740156 162.738976 511.396369 511.396369 0 0 0-109.740156-162.738976z m66.278708 362.178731A428.336713 428.336713 0 1 1 512 83.663287a428.698892 428.698892 0 0 1 428.336713 428.336713z" fill="#36404f" p-id="6191"></path><path d="M417.954256 281.533601a41.046923 41.046923 0 0 0-41.77128 40.201839v385.116718a41.892007 41.892007 0 0 0 83.663287 0v-385.116718a41.167649 41.167649 0 0 0-41.892007-40.201839zM606.045744 281.533601a41.046923 41.046923 0 0 0-41.77128 40.201839v385.116718a41.892007 41.892007 0 0 0 83.663287 0v-385.116718a41.167649 41.167649 0 0 0-41.892007-40.201839z" fill="#36404f" p-id="6192"></path></svg>
|
||||
|
After Width: | Height: | Size: 1004 B |
5
frontend/src/assets/icons/action.svg
Normal file
@@ -0,0 +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>
|
||||
|
After Width: | Height: | Size: 890 B |
1
frontend/src/assets/icons/agent-change.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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 |
1
frontend/src/assets/icons/branch.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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 |
1
frontend/src/assets/icons/close.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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 |
1
frontend/src/assets/icons/doctor.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761212882014" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3052" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M988.8 939.2c0-134.4-78.4-252.8-193.6-308.8 4.8 12.8 8 25.6 8 40v11.2c54.4 11.2 96 60.8 96 118.4v65.6c8 6.4 14.4 17.6 14.4 30.4 0 22.4-17.6 38.4-38.4 38.4-22.4 0-38.4-17.6-38.4-38.4 0-12.8 6.4-24 16-32v-67.2c0-40-32-72-72-72s-72 32-72 72v68.8c9.6 6.4 14.4 17.6 14.4 30.4 0 22.4-17.6 38.4-38.4 38.4-22.4 0-38.4-17.6-38.4-38.4 0-11.2 4.8-22.4 14.4-30.4V800c0-59.2 43.2-108.8 100.8-120-1.6-28.8-14.4-56-32-76.8-19.2-4.8-38.4-8-57.6-8-9.6 216-75.2 384-156.8 384-80 0-147.2-168-156.8-384h-1.6c-19.2 22.4-32 52.8-32 84.8v99.2c27.2 9.6 48 36.8 48 68.8 0 40-32 73.6-73.6 73.6s-73.6-32-73.6-73.6c0-33.6 22.4-60.8 52.8-70.4v-118.4c0-19.2 6.4-38.4 16-54.4C145.6 644.8 35.2 779.2 35.2 939.2V953.6C35.2 992 249.6 1024 512 1024s476.8-32 476.8-72v-3.2-9.6z m-470.4-720C400 219.2 304 256 304 300.8v4.8l44.8 212.8c0 3.2 1.6 8 1.6 11.2 19.2 75.2 86.4 129.6 166.4 129.6 84.8 0 156.8-64 169.6-145.6l44.8-208v-4.8c0-44.8-96-81.6-212.8-81.6z m-1.6-16c105.6 0 193.6 35.2 225.6 83.2h9.6V83.2v-1.6C752 36.8 646.4 0 518.4 0S284.8 36.8 284.8 81.6V288h6.4c30.4-49.6 120-84.8 225.6-84.8z m-65.6-105.6c0-8 6.4-16 16-16h33.6V46.4c0-8 6.4-16 16-16 8 0 16 6.4 16 16V80h33.6c8 0 16 6.4 16 16s-6.4 16-16 16h-33.6v33.6c0 8-6.4 16-16 16-8 0-16-6.4-16-16v-32h-33.6c-8 0-16-8-16-16z" p-id="3053"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
frontend/src/assets/icons/engineer.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761212791945" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2409" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M338.36032 362.9056c9.59488 59.4944 34.53952 115.1488 74.84416 161.20832l15.34976 13.43488c1.23904 2.47808 3.2768 4.15744 5.60128 6.07232 1.26976 1.04448 2.63168 2.16064 3.9936 3.52256 42.21952 38.37952 107.4688 38.37952 147.7632-1.92512 53.73952-47.9744 80.60928-113.22368 95.95904-182.31296H338.37056z m366.5408-63.32416h26.86976c7.68 0 13.43488 5.75488 9.59488 11.50976v15.36H275.02592v-15.36c0-7.68 5.75488-13.43488 13.43488-13.43488h28.78464c1.09568-7.63904 2.02752-14.97088 2.93888-22.12864 2.304-17.99168 4.4544-34.8672 8.58112-52.71552 5.75488-34.53952 21.10464-69.08928 42.21952-95.9488 21.10464-24.95488 49.88928-44.1344 82.51392-49.89952 40.30464-9.59488 82.5344-9.59488 120.90368 1.91488 47.98464 11.52 86.36416 46.05952 103.6288 92.11904 13.44512 36.46464 23.04 72.92928 24.95488 111.3088v7.68c1.91488 3.84 1.91488 5.75488 1.91488 9.59488z m-241.80736-86.36416c7.68-1.91488 13.43488-9.59488 13.43488-17.26464V132.608c0-7.68-7.68-15.36-15.34976-15.36-7.68 0-15.36 7.68-15.36 15.36v65.24928c0 7.68 7.68 15.36 15.36 15.36h1.91488z m99.79904 0c7.68-1.91488 13.43488-9.59488 13.43488-17.26464V132.608c0-7.68-7.68-15.36-15.36-15.36s-15.34976 7.68-15.34976 15.36v65.24928c0 7.68 7.68 15.36 15.36 15.36h1.91488z m351.16032 596.8384c5.8368 20.5824 11.66336 41.08288 17.3056 61.42976 4.1984 19.56864 9.41056 37.09952 14.8992 55.57248 2.048 6.88128 4.12672 13.89568 6.21568 21.1968 1.91488 3.82976 0 5.75488-3.84 5.75488H73.5232c-1.91488 0-3.84-3.84-1.91488-5.76512 3.82976-13.43488 7.68-25.9072 11.50976-38.37952 3.84-12.47232 7.68-24.94464 11.52-38.37952 12.1856-48.7424 26.27584-98.44736 40.26368-147.75296 5.8368-20.5824 11.65312-41.08288 17.3056-61.44 1.91488-7.66976 3.84-9.58464 11.50976-11.50976 18.2272-3.82976 35.98336-8.15104 53.73952-12.47232 17.75616-4.31104 35.50208-8.63232 53.73952-12.47232 3.82976-1.91488 5.75488 0 5.75488 3.84v163.1232c0 1.91488 1.91488 3.84 3.84 3.84h47.9744a4.12672 4.12672 0 0 0 3.84-3.84V612.4032c2.2528 0 3.1744-0.65536 3.95264-1.19808 0.54272-0.38912 1.00352-0.7168 1.80224-0.7168 23.02976-3.84 46.05952-17.27488 59.4944-38.37952 1.91488-3.84 5.75488-7.68 9.59488-11.52 1.91488-1.91488 5.75488-1.91488 7.68 0 15.34976 15.36 28.7744 30.69952 42.20928 46.05952 13.43488 17.27488 34.54976 26.86976 55.6544 24.94464 21.11488 1.92512 42.22976-7.68 55.6544-24.94464 13.43488-15.36 26.86976-30.70976 42.22976-46.05952 1.91488-1.91488 5.75488-1.91488 7.68 0 1.91488 3.84 5.74464 7.68 9.58464 11.52 13.43488 21.10464 34.54976 34.53952 59.4944 38.37952 1.92512 0 3.84 0 5.75488 1.91488v180.39808c0 1.91488 1.92512 3.84 3.84 3.84h47.9744a4.12672 4.12672 0 0 0 3.84-3.84v-163.1232c0-1.92512 3.84-3.84 5.76512-3.84 36.4544 9.59488 71.00416 17.27488 107.4688 24.94464a17.3056 17.3056 0 0 1 11.50976 11.52c12.1856 48.7424 26.28608 98.44736 40.26368 147.75296z" p-id="2410"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
14
frontend/src/assets/icons/icons.svg
Normal 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 |
1
frontend/src/assets/icons/left.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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 |
1
frontend/src/assets/icons/loading.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761626283461" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2759" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M511.882596 287.998081h-0.361244a31.998984 31.998984 0 0 1-31.659415-31.977309v-0.361244c0-0.104761 0.115598-11.722364 0.115598-63.658399V96.000564a31.998984 31.998984 0 1 1 64.001581 0V192.001129c0 52.586273-0.111986 63.88237-0.119211 64.337537a32.002596 32.002596 0 0 1-31.977309 31.659415zM511.998194 959.99842a31.998984 31.998984 0 0 1-31.998984-31.998984v-96.379871c0-51.610915-0.111986-63.174332-0.115598-63.286318s0-0.242033 0-0.361243a31.998984 31.998984 0 0 1 63.997968-0.314283c0 0.455167 0.11921 11.711527 0.11921 64.034093v96.307622a31.998984 31.998984 0 0 1-32.002596 31.998984zM330.899406 363.021212a31.897836 31.897836 0 0 1-22.866739-9.612699c-0.075861-0.075861-8.207461-8.370021-44.931515-45.094076L195.198137 240.429485a31.998984 31.998984 0 0 1 45.256635-45.253022L308.336112 263.057803c37.182834 37.182834 45.090463 45.253022 45.41197 45.578141A31.998984 31.998984 0 0 1 330.899406 363.021212zM806.137421 838.11473a31.901448 31.901448 0 0 1-22.628318-9.374279L715.624151 760.859111c-36.724054-36.724054-45.018214-44.859267-45.097687-44.93874a31.998984 31.998984 0 0 1 44.77618-45.729864c0.32512 0.317895 8.395308 8.229136 45.578142 45.411969l67.88134 67.88134a31.998984 31.998984 0 0 1-22.624705 54.630914zM224.000113 838.11473a31.901448 31.901448 0 0 0 22.628317-9.374279l67.88134-67.88134c36.724054-36.724054 45.021826-44.859267 45.097688-44.93874a31.998984 31.998984 0 0 0-44.776181-45.729864c-0.32512 0.317895-8.395308 8.229136-45.578142 45.411969l-67.88134 67.884953a31.998984 31.998984 0 0 0 22.628318 54.627301zM255.948523 544.058589h-0.361244c-0.104761 0-11.722364-0.115598-63.658399-0.115598H95.942765a31.998984 31.998984 0 1 1 0-64.00158h95.996952c52.586273 0 63.88237 0.111986 64.337538 0.11921a31.998984 31.998984 0 0 1 31.659414 31.97731v0.361244a32.002596 32.002596 0 0 1-31.988146 31.659414zM767.939492 544.058589a32.002596 32.002596 0 0 1-31.995372-31.666639v-0.361244a31.998984 31.998984 0 0 1 31.659415-31.970085c0.455167 0 11.754876-0.11921 64.34115-0.11921h96.000564a31.998984 31.998984 0 0 1 0 64.00158H831.944685c-51.936034 0-63.553638 0.111986-63.665624 0.115598h-0.335957zM692.999446 363.0176a31.998984 31.998984 0 0 1-22.863126-54.381656c0.317895-0.32512 8.229136-8.395308 45.41197-45.578141l67.88134-67.884953A31.998984 31.998984 0 1 1 828.693489 240.429485l-67.892177 67.88134c-31.020013 31.023625-41.644196 41.759794-44.241539 44.393262l-0.697201 0.722488a31.908673 31.908673 0 0 1-22.863126 9.591025z" fill="" p-id="2760"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
1
frontend/src/assets/icons/medical.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761212815712" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2569" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M845.285053 1008.842105H520.421053v-52.224c45.999158-24.468211 96.538947-287.420632 134.03621-286.302316 145.960421 30.100211 249.317053 113.057684 249.317053 270.928843-1.684211 41.472-21.005474 67.597474-58.489263 67.597473z m0 0" p-id="2570"></path><path d="M719.764211 480.619789c-32.363789 107.344842-116.439579 183.457684-214.662737 183.457685-98.829474 0-182.905263-76.665263-214.703158-184.010106-14.767158-6.831158-32.943158-22.150737-39.760842-61.291789-8.528842-51.186526 22.703158-63.690105 22.703158-63.690105 2.290526 0 4.554105 1.751579 6.817684 5.133473a95.137684 95.137684 0 0 1 24.427789-36.917894c40.313263-39.774316 114.714947-60.779789 204.463158-61.359158 81.232842-0.565895 138.024421 5.133474 195.961263 60.240842 10.213053 9.633684 19.887158 24.939789 24.980211 40.326737 2.842947-4.554105 5.685895-7.424 8.528842-6.817685 0 0 31.797895 12.476632 23.282526 63.609264-7.949474 39.733895-26.691368 54.501053-42.037894 61.318736z m0 0" p-id="2571"></path><path d="M496.559158 15.454316c-126.059789 0-231.141053 64.794947-231.141053 64.794947v215.794526s69.308632-78.942316 237.406316-78.942315c166.979368 0 237.406316 79.508211 237.406316 79.50821V80.801684c-0.579368-0.552421-122.677895-65.347368-243.671579-65.347368z m71.545263 117.005473h-52.237474v52.237474h-27.823158V132.459789h-51.685052V104.084211h51.685052V51.846737h27.823158v52.237474h52.237474v28.375578zM497.125053 956.618105v52.224c-141.433263 0-240.249263-0.538947-324.877474 0-36.352 0-56.212211-26.125474-58.489263-67.058526 0-157.884632 103.356632-241.367579 249.330526-270.901895 38.642526-1.684211 88.616421 261.268211 134.036211 285.736421z" p-id="2572"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
frontend/src/assets/icons/moon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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 |
6
frontend/src/assets/icons/paper-plane.svg
Normal file
@@ -0,0 +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>
|
||||
|
After Width: | Height: | Size: 610 B |
1
frontend/src/assets/icons/plus.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761211358508" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6686" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" p-id="6687"></path></svg>
|
||||
|
After Width: | Height: | Size: 516 B |
1
frontend/src/assets/icons/process.svg
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
1
frontend/src/assets/icons/refresh.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761368152518" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7696" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M835.516 709.837l154.351-225.894h-109.158c-14.541-221.047-198.11-396.151-422.844-396.151-234.291 0-423.731 189.918-423.731 424.277 0 234.155 189.918 424.141 424.209 424.141 105.062 0 200.977-38.434 275.115-101.786l-56.73-73.045c-58.368 51.063-134.69 82.398-218.385 82.398-183.296 0-331.844-148.617-331.844-331.708 0-183.364 148.617-331.913 331.844-331.913 173.739 0 315.665 133.734 329.933 303.787h-107.11l154.351 225.894z" p-id="7697"></path></svg>
|
||||
|
After Width: | Height: | Size: 783 B |
1
frontend/src/assets/icons/renyuan.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761545116983" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1453" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M856.98894 727.680806c-64.797424-20.50007-128.161277-40.856783-169.448131-61.356852-28.814784-14.335713-46.877782-28.671427-46.877782-43.00714v-34.405712c10.034999-8.171357 19.209856-17.202856 27.381212-27.237855 2.293714-2.867143 4.587428-5.734285 6.881142-8.888143 10.034999-13.905642 20.213356-32.972141 29.101498-51.895282 9.891642-21.073499 18.349713-41.716926 23.223856-54.47571 4.730785-5.590928 8.888142-11.181856 12.758785-16.629428 22.363713-32.111998 30.104998-62.647067 23.223855-91.175136-3.870643-16.48607-12.902142-31.395212-25.804284-42.433712 5.0175-40.570069 4.730785-86.444351-10.034999-126.727705-7.597928-21.216856-18.349713-41.143497-31.968641-59.063139-20.643427-26.807784-47.451211-47.307854-80.279994-61.070138-42.00364-17.632927-81.856923-19.639927-97.196136-19.639927h-10.608428c-15.195856 0-55.049139 1.863643-97.196136 19.639927-35.122498 14.765785-63.363853 37.129497-84.580708 66.947781-11.611928 16.48607-20.930141 34.405712-27.667927 53.328853-14.765785 40.283354-15.052499 86.300994-10.034999 126.727706-12.758785 11.038499-21.790284 25.947641-25.804284 42.433711-6.737785 27.954641 0.716786 57.916282 22.076998 89.454851 4.157357 6.164357 8.888142 12.185356 14.048999 18.49307 5.0175 12.902142 13.332213 33.545569 23.223856 54.762425 8.888142 18.923142 18.923142 37.846283 28.958141 51.608568 2.150357 3.0105 4.444071 5.877642 6.737785 8.744785 7.741285 9.461571 16.48607 18.206356 25.947641 25.947641v35.695926c0 14.47907-18.49307 29.101498-48.02464 43.580568-41.286854 20.213356-104.220636 40.570069-168.301273 60.783425C56.626067 762.51659 56.626067 946.157077 56.626067 946.157077s134.038919 48.884782 457.165897 48.884782S966.943861 946.157077 966.943861 946.157077s0.286714-183.783844-109.954921-218.476271zM351.224976 414.875542c-46.877782-51.321854-28.958141-72.251995-18.349714-76.265994 17.48957-6.594428 29.388212 19.49657 33.54557 31.108498-4.874143-15.052499-18.062999-59.636567-20.213356-106.51435 15.769285-22.076998 47.164497-58.919782 84.580708-64.797424 0 0 80.710066 96.47935 246.574269 94.47235-3.727285 26.090998-9.604928 51.895282-17.632928 76.982781 4.300714-11.611928 16.055999-37.702926 33.545569-31.108498 10.608428 4.014 28.528069 25.087498-18.349713 76.265995 0 0-17.346213 47.164497-35.552568 80.99678-3.870643 7.454571-8.171357 14.47907-12.902142 21.360212-12.041999 16.48607-27.667927 29.818284-45.874283 38.993141-20.50007 10.465071-43.293854 15.912642-66.230995 16.055998-0.430071 0-0.860143 0-1.146857-0.143357-0.430071 0-0.860143 0.143357-1.146857 0.143357-23.653927-0.143357-47.02114-6.021-68.094639-17.059498a129.42282 129.42282 0 0 1-44.010639-38.132998c-4.730785-6.737785-9.031499-13.762285-12.902142-21.073498-18.349713-33.832283-35.839283-81.283494-35.839283-81.283495z m170.881702 519.52625c-5.734285 3.727285-9.174857 5.447571-9.174857 5.447571s-3.440571-1.863643-9.174856-5.447571c-31.968641-20.069999-137.336133-94.615708-151.815204-208.584628 34.405712-15.48257 91.891922-46.447711 91.891922-102.50035v-2.293714c21.933641 7.454571 44.870783 11.468571 67.951281 11.755284h2.293714c22.50707-0.143357 44.870783-4.014 66.230996-11.181856v1.720286c0 57.056139 59.49321 88.164637 93.612208 103.360492-14.909142 113.252135-119.98992 187.654487-151.815204 207.724486z" p-id="1454"></path><path d="M574.432031 693.418452l-28.097998-30.53507h-66.804424L451.574969 693.418452s16.055999 34.118998 42.003639 47.451211l-42.003639 106.084278s30.821784 43.723926 61.356852 55.049139c30.678426-11.325213 61.50021-55.049139 61.50021-55.049139l-42.00364-106.084278c25.804284-13.332213 42.00364-47.451211 42.00364-47.451211z" p-id="1455"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
1
frontend/src/assets/icons/researcher.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761212863001" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2892" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M725.308 314.532c2.743 1.543 5.486 3.215 8.164 5.057l8.164 5.55 0.15 9.77c0.45 28.328-2.957 52.326-10.37 70.796-6.836 16.885-16.971 29.527-30.556 37.134-9.235 32.976-20.077 63.532-35.42 89.18-17.677 29.548-41.011 52.261-74.01 64.496-14.763 5.4-52.432 7.907-88.708 7.393-35.012-0.536-70.131-4.007-84.06-10.414-29.997-13.842-51.082-37.048-67.045-65.975-13.692-24.705-23.463-53.46-31.97-83.823-14.034-7.328-24.577-19.906-31.669-36.92-7.778-18.577-11.314-42.982-10.82-71.866l0.106-9.814 8.143-5.485c2.1-1.414 4.178-2.722 6.256-3.921-9.17-113.544-5.72-155.52 36.448-203.495 82.217-67.346 270.712-64.968 354-3.943 56.718 53.547 60.66 113.586 43.197 206.28m-172.66 328.33l1.2 26.013-15.407 25.434 21.47 141.12 88.045-189.224 134.07-4.585c69.189 65.503 113.63 219.758 102.701 320.38H137.623c1.843-88.366 18.106-239.278 106.108-316.03l121.107 1.135 113.414 187.124 21.32-139.92-15.427-25.434 1.178-26.013c29.355-1.607 37.99-1.607 67.325 0m100.3-368.656c-53.246 10.414-132.57 19.52-195.245-15.706-24.105-13.563-59.417 14.228-88.301 11.378a217.808 217.808 0 0 0-19.542 57.682l-3.214 17.035-17.142-1.671a24.02 24.02 0 0 0-9.942 1.264 38.098 38.098 0 0 0-4.65 1.843c0.45 18.877 3.107 34.54 7.971 46.261 4.285 10.307 10.307 17.035 18.106 19.477l10.007 3.107 2.742 10.071c8.4 31.134 17.806 60.468 31.027 84.36 12.256 22.22 27.984 39.833 49.711 49.84 9.107 4.156 38.226 6.556 68.653 7.006 32.334 0.471 64.603-1.243 75.253-5.164 23.934-8.871 41.204-25.97 54.618-48.34 14.399-24.127 24.62-54.661 33.62-88.173l2.549-9.578 9.535-3.321c7.542-2.7 13.392-9.536 17.549-19.97 4.714-11.614 7.264-27.02 7.692-45.534a35.355 35.355 0 0 0-4.178-1.67 25.413 25.413 0 0 0-9.706-1.48l-16.67 1.072-3.108-16.435a213.844 213.844 0 0 0-17.334-53.354m0 0" p-id="2893"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
1
frontend/src/assets/icons/right.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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 |
1
frontend/src/assets/icons/shejishi.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761545158610" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1618" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 625.777778l85.333333 85.333333-56.888889 85.333333 56.888889 142.222223 79.644445-290.133334c142.222222 34.133333 290.133333 102.4 290.133333 204.8v113.777778H56.888889v-113.777778c0-108.088889 147.911111-176.355556 290.133333-204.8l79.644445 290.133334 56.888889-142.222223-56.888889-85.333333L512 625.777778z m196.266667-261.688889c0 110.933333-85.333333 204.8-196.266667 204.8-107.235556 0-190.577778-87.722667-195.982222-193.763556L315.733333 364.088889h392.533334zM521.159111 56.888889c12.970667 0.170667 28.444444 0.967111 41.415111 4.949333l9.159111 3.584v136.533334h34.133334v-119.466667c65.024 32.483556 114.574222 103.708444 119.125333 184.149333l0.341333 12.117334h42.666667v42.666666h-512V278.755556h42.666667c0-81.066667 38.513778-154.453333 100.864-190.805334L409.6 82.488889v119.466667h42.666667v-136.533334c14.222222-7.111111 34.360889-8.305778 50.574222-8.533333h18.318222z" p-id="1619"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
frontend/src/assets/icons/soldier.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761212898988" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3212" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M105.9 959.3c5.5-79.7 16.6-187.3 41.1-286.1 5.1-25.5 23.2-46.9 23.8-47.7 2.3-15.1 187.9-15.2 187.9-15.2l30.7-2.9c-4.4 27.2-7.7 68.5 4 104 15.2 46.6 39.4 92.8 72.3 120.3l7-123.7H536l10.9 116c23-32.9 49.8-84.7 52.6-150.7 2.2-51.6 15.1-75.2 26.2-86.1 76.9 3.1 173.3 17.2 212 38.4 36.9 20.2 48.5 146.4 81.9 333.8H105.9v-0.1z" p-id="3213"></path><path d="M528.1 690.2h-39.7l-31.8-26.6 47.7-62 47.6 53.2-23.8 35.4z m210.7-526.8c-19-38-129.4-95-249.8-98.9-120.4-4-247.9 65.9-246.9 117 1 51 64.2 73 64.2 73-3.3 9.9-1.6 48.3-1.6 48.3s2.9 5.3 8.4 12.7C294.5 560.8 509 565 509 565c185.9-19.3 171.1-221.2 169.3-250.4-0.7-10.7-2-19.2-3.6-26.8 2.3-16.5-4.1-33.5-4.1-33.5 27.1-10.9 87.3-52.9 68.2-90.9z m-280.2-19.1c-0.2-1.6-0.5-3.2-0.5-4.9 0-20.7 18.2-37.5 40.7-37.5 22.5 0 40.7 16.8 40.7 37.5 0 2.6-0.3 5.2-0.9 7.7 12.1-1.1 25.2-2.1 37.1-2.3 0 0-33.9 15.8-53.2 25.1-2.9 1.9-6 3.4-9.4 4.6-0.2 0.1-0.5 0.3-0.8 0.4 0 0-1.1 0.3-2.9 0.6-3.4 0.9-7 1.5-10.7 1.5-0.7 0-1.4-0.2-2.1-0.2-9 0-20.4-1.8-29.6-9.5 0 0-13.9-9.5-45.3-22.6 0.1 0 16.8-2.1 36.9-0.4z m181.3 107.8H332v-36.4h307.9v36.4z" p-id="3214"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
frontend/src/assets/icons/specialist.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761212837698" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2731" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M855.7 781.7c-11.8-25.2-29.1-47.3-48.4-67.2-26.3-27-56.5-50-87.9-70.7-32.1-20.9-65.6-39.7-100.4-55.6 28.1-18.7 49.6-45.9 64.9-75.7 20.9-40.7 31.9-85.6 38.1-130.7 6.3-47.9 7.5-96.5 3.7-144.7 32.3-12.9 64.5-25.8 96.8-38.7 12-5 24.3-9.4 36.2-14.7-114.9-39.9-229.9-79.6-344.8-119.4-2-1-4 0.5-5.9 1-108.1 39.5-216.3 78.9-324.5 118.4 39.4 18.4 79 36.5 118.5 54.9-8.8 55.8-7.2 113.4 7.1 168.2 9 34.8 23 68.2 41.4 99.1 18.3 30.4 40.2 58.9 67 82.3-48 15.6-94.7 36.5-136.1 65.7-28.4 20-54.1 43.9-74.7 71.9-23 31.1-39.5 67.3-46.7 105.4-3.7 19.5-5.3 39.5-3.2 59.2 112.7 48 235.5 71.4 357.9 69.7-37.9-32.3-75.9-64.6-113.9-96.9 30.3-73.3 60.5-146.6 90.8-219.9 0.4-1.4 1.8-2.9 0.8-4.2-8.6-14.2-17.4-28.2-25.8-42.5 14.6 7.7 30.8 12.6 47.4 12.7 14.9 0.3 29.7-4.3 42-12.7-9.1 13.7-18.3 27.4-27.5 41.1 17.8 40.6 35.7 81.2 53.5 121.9 14.7 33.9 29.9 67.6 44.4 101.5-37.3 32.9-74.3 66.1-111.4 99.2 90.1-1.2 180-15.1 266.2-41.2 26.5-8.2 52.7-17.2 78-28.6 6.5-15.2 10.3-31.6 10.5-48.1 0.2-21-5.2-41.8-14-60.7z" p-id="2732"></path><path d="M216.4 380.8h11V392h21.5v-11.2h11v-26.3h-11V223.3h-21.5c-0.1 43.7 0 87.5 0 131.2h-11v26.3z" p-id="2733"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
7
frontend/src/assets/icons/stoprunning.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg t="1768992484327" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="10530" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M512 853.333333c-187.733333 0-341.333333-153.6-341.333333-341.333333s153.6-341.333333 341.333333-341.333333
|
||||
341.333333 153.6 341.333333 341.333333-153.6 341.333333-341.333333 341.333333z m0-85.333333c140.8 0 256-115.2 256-256s-115.2-256-256-256-256
|
||||
115.2-256 256 115.2 256 256 256z m-85.333333-341.333333h170.666666v170.666666h-170.666666v-170.666666z" fill="#ffffff" p-id="10531"></path></svg>
|
||||
|
After Width: | Height: | Size: 709 B |
1
frontend/src/assets/icons/sunny.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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 |
1
frontend/src/assets/icons/technicist.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761212761688" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2247" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M636.478451 965.736274l10.285126-0.276102c-2.068945-0.25056-5.512321-0.662889-10.064973-1.188336a398.001259 398.001259 0 0 1-0.220153 1.464438zM319.897045 966.147387l15.81934-0.411113-0.214071-1.463222c-7.760064 0.893989-12.318797 1.463222-12.318797 1.463222l-3.286472 0.411113zM701.706773 232.246056c0-130.470023-177.671401-195.699562-177.671401-195.699562-25.16304-11.331153-50.139985-8.750141-75.875908 0 0 0-177.679915 57.274865-177.679915 195.699562 0 20.694314-37.558465 16.252347-49.98673 81.7142h531.217713c-22.614869-65.914321-50.003758-61.019886-50.003759-81.7142zM261.149228 691.016641C179.668559 732.292152 110.143011 786.116343 110.143011 839.449144v27.720942c0 57.176344 150.840799 106.349362 150.840799 106.349362l58.913235-7.372061-5.835859 0.152039s-48.965029-277.278751-52.911958-275.282785z" p-id="2248"></path><path d="M649.282557 868.865624c-18.205742 19.018238-43.489197 24.747062-67.840958 15.467826-25.402654-9.681835-41.173341-37.079238-36.441892-62.991526 2.217335-12.137567 7.629918-22.585677 16.273024-31.609489 36.338505-37.95255 72.662414-75.917262 108.994838-113.872244-20.740534-9.049353-41.343624-19.515709-62.035506-26.357458-5.480697-1.774598-39.908378-17.257019-18.379674-82.532777h-0.291915c50.639889-51.988778 86.252256-102.79895 86.252256-215.156888h-378.248369c0 113.53411 34.725677 163.877219 85.702483 215.795451 22.010362 57.542453-17.334863 78.908171-25.565639 81.90881-22.185511 8.003326-37.025721 17.671781-59.579774 27.987314-4.070993 1.864605 34.183202 264.833458 37.380883 286.769626 28.691558-3.307149 101.308969-11.070862 150.593888-11.070862 49.355464 0 121.929088 7.763713 150.601185 11.070862 1.212662-8.076305 7.314894-48.820288 14.275842-97.177161l-1.690672 1.768516zM780.85699 731.325185a466047.419516 466047.419516 0 0 1-98.887294 103.3694c-11.765376 66.245158-22.605138 130.427453-22.605138 130.427452l-12.599765 0.336918c1.455924 0.177581 2.246526 0.276103 2.246526 0.276103l62.66069 7.96562s150.381033-49.146259 150.381033-106.531808v-27.719726c-0.002433-37.857677-33.167582-75.24221-81.196052-108.123959z" p-id="2249"></path><path d="M843.295095 519.324624l-52.478951 54.703584 62.663122 57.647056 52.688157-55.292278c0.620319 1.188336 0.975481 1.74419 1.211445 2.343831 20.077644 51.086275-8.213748 109.894908-61.513708 127.951044-20.261307 6.862426-40.735469 7.679787-61.405457 2.17598-2.048268-0.54734-2.96415 0.256642-4.130592 1.474169A361833.588559 361833.588559 0 0 1 643.464941 853.40023c-13.557002 14.162724-32.385495 18.42711-50.519474 11.518465-18.916067-7.210291-30.659549-27.611474-27.138329-46.907031 1.652967-9.039623 5.682605-16.819147 12.118106-23.538049 45.458404-47.47748 90.870589-94.993883 136.371564-142.431225 1.828115-1.904743 2.042186-3.3631 1.162793-5.751934-19.576524-53.126028 12.26163-112.586604 68.353025-127.899958 18.585231-5.074449 37.137621-5.129183 55.717987-0.199475 1.046027 0.278535 2.072594 0.622751 3.764482 1.133601zM604.103904 841.024267c5.432045-0.211638 9.560204-4.471159 9.419112-9.706161-0.147174-5.362715-4.626847-9.473846-10.113626-9.281669-5.391906 0.189745-9.684268 4.732666-9.44587 10.004157 0.233532 5.146212 4.800779 9.190445 10.140384 8.983673z" p-id="2250"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
1
frontend/src/assets/icons/video-play.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769048534610" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4890" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M527.984 1001.6a480 480 0 1 1 480-480 480.384 480.384 0 0 1-480 480z m0-883.696A403.696 403.696 0 1 0 931.68 521.6 403.84 403.84 0 0 0 527.984 117.904z" fill="#36404f" p-id="4891"></path><path d="M473.136 729.6a47.088 47.088 0 0 1-18.112-3.888 38.768 38.768 0 0 1-23.056-34.992V384.384a39.632 39.632 0 0 1 23.056-34.992 46.016 46.016 0 0 1 43.632 3.888l211.568 153.168a38.72 38.72 0 0 1 16.464 31.104 37.632 37.632 0 0 1-16.464 31.104l-211.568 153.168a44.56 44.56 0 0 1-25.52 7.776z m41.168-266.704v149.296l102.896-74.64z" fill="#36404f" p-id="4892"></path></svg>
|
||||
|
After Width: | Height: | Size: 894 B |
105
frontend/src/components/MultiLineTooltip/index.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<el-tooltip
|
||||
:disabled="!isOverflow"
|
||||
effect="light"
|
||||
placement="top"
|
||||
:content="text"
|
||||
popper-class="multi-line-tooltip-popper"
|
||||
>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="multi-line-ellipsis"
|
||||
:style="containerStyle"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<slot>
|
||||
{{ text }}
|
||||
</slot>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, type HTMLAttributes, nextTick, onMounted, ref } from 'vue'
|
||||
import { ElTooltip } from 'element-plus'
|
||||
|
||||
// 定义组件 props 类型
|
||||
interface Props {
|
||||
text?: string
|
||||
lines?: number
|
||||
maxWidth?: string | number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
text: '',
|
||||
lines: 3,
|
||||
maxWidth: '100%'
|
||||
})
|
||||
|
||||
const isOverflow = ref(false)
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// 计算容器样式
|
||||
const containerStyle = computed(
|
||||
() =>
|
||||
({
|
||||
maxWidth: props.maxWidth,
|
||||
display: '-webkit-box',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: props.lines,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
lineHeight: '1.5',
|
||||
wordBreak: 'break-all'
|
||||
} as HTMLAttributes['style'])
|
||||
)
|
||||
|
||||
// 检查文字是否溢出
|
||||
const checkOverflow = (element: HTMLElement): boolean => {
|
||||
// 单行情况下使用宽度判断
|
||||
if (props.lines === 1) {
|
||||
return element.scrollWidth > element.clientWidth
|
||||
}
|
||||
// 多行情况下使用高度判断
|
||||
else {
|
||||
return element.scrollHeight > element.clientHeight
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标进入处理
|
||||
const handleMouseEnter = (event: MouseEvent) => {
|
||||
const element = event.target as HTMLElement
|
||||
console.log(checkOverflow(element))
|
||||
isOverflow.value = checkOverflow(element)
|
||||
}
|
||||
|
||||
// 鼠标离开处理
|
||||
const handleMouseLeave = () => {
|
||||
isOverflow.value = false
|
||||
}
|
||||
|
||||
// 初始化时检查溢出状态
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (containerRef.value) {
|
||||
isOverflow.value = checkOverflow(containerRef.value)
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.multi-line-ellipsis {
|
||||
cursor: default;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: normal;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.multi-line-tooltip-popper {
|
||||
max-width: 400px;
|
||||
}
|
||||
</style>
|
||||
260
frontend/src/components/Notification/Notification.vue
Normal 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>
|
||||
36
frontend/src/components/SvgIcon/index.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
iconClass: string
|
||||
prefix?: string
|
||||
color?: string
|
||||
size?: string
|
||||
}>(),
|
||||
{
|
||||
prefix: 'icon',
|
||||
color: '',
|
||||
size: '1em',
|
||||
},
|
||||
)
|
||||
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg aria-hidden="true" class="svg-icon" :style="`color:${props.color}`">
|
||||
<use :xlink:href="symbolId" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
display: inline-block;
|
||||
width: v-bind('props.size');
|
||||
height: v-bind('props.size');
|
||||
overflow: hidden;
|
||||
vertical-align: -0.15em; /* 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 */
|
||||
outline: none;
|
||||
fill: currentcolor; /* 定义元素的颜色,currentColor是一个变量,这个变量的值就表示当前元素的color值,如果当前元素未设置color值,则从父元素继承 */
|
||||
}
|
||||
</style>
|
||||
163
frontend/src/composables/useNotification.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
71
frontend/src/layout/components/Header.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { useConfigStore } from '@/stores'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
defineOptions({
|
||||
name: 'AppHeader'
|
||||
})
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div
|
||||
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]"
|
||||
>
|
||||
<svg-icon
|
||||
icon-class="icons"
|
||||
class="header-icon"
|
||||
size="100% "
|
||||
: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>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.header-icon {
|
||||
color: var(--color-header-bg);
|
||||
}
|
||||
</style>
|
||||
613
frontend/src/layout/components/Main/Task.vue
Normal file
@@ -0,0 +1,613 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, reactive, nextTick } from 'vue'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import { useAgentsStore, useConfigStore } from '@/stores'
|
||||
import api from '@/api'
|
||||
import websocket from '@/utils/websocket'
|
||||
import { changeBriefs } from '@/utils/collaboration_Brief_FrontEnd.ts'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import AssignmentButton from './TaskTemplate/TaskSyllabus/components/AssignmentButton.vue'
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-start'): void
|
||||
(e: 'search', value: string): void
|
||||
}>()
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
const configStore = useConfigStore()
|
||||
const searchValue = ref('')
|
||||
const triggerOnFocus = ref(true)
|
||||
const isFocus = ref(false)
|
||||
const hasAutoSearched = ref(false)
|
||||
const isExpanded = ref(false)
|
||||
// 添加一个状态来跟踪是否正在填充步骤数据
|
||||
const isFillingSteps = ref(false)
|
||||
// 存储当前填充任务的取消函数
|
||||
const currentStepAbortController = ref<{ cancel: () => void } | null>(null)
|
||||
|
||||
// 解析URL参数
|
||||
function getUrlParam(param: string): string | null {
|
||||
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() {
|
||||
// 用于标记大纲是否成功加载
|
||||
let outlineLoaded = false
|
||||
|
||||
try {
|
||||
triggerOnFocus.value = false
|
||||
if (!searchValue.value) {
|
||||
ElMessage.warning('请输入搜索内容')
|
||||
return
|
||||
}
|
||||
emit('search-start')
|
||||
agentsStore.resetAgent()
|
||||
agentsStore.setAgentRawPlan({ loading: true })
|
||||
// 重置停止状态
|
||||
agentsStore.setHasStoppedFilling(false)
|
||||
|
||||
// 获取大纲
|
||||
const outlineData = await api.generateBasePlan({
|
||||
goal: searchValue.value,
|
||||
inputs: []
|
||||
})
|
||||
|
||||
// 检查是否已被停止
|
||||
if (!isFillingSteps.value && currentStepAbortController.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// 处理简报数据格式
|
||||
outlineData['Collaboration Process'] = changeBriefs(outlineData['Collaboration Process'])
|
||||
|
||||
// 立即显示大纲
|
||||
agentsStore.setAgentRawPlan({ data: outlineData, loading: false })
|
||||
outlineLoaded = true
|
||||
emit('search', searchValue.value)
|
||||
|
||||
// 🆕 预加载所有任务的智能体评分数据(在后台静默执行)
|
||||
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 {
|
||||
triggerOnFocus.value = true
|
||||
// 完成填充,重置状态
|
||||
isFillingSteps.value = false
|
||||
currentStepAbortController.value = null
|
||||
// 如果大纲加载失败,确保关闭loading
|
||||
if (!outlineLoaded) {
|
||||
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 results = queryString
|
||||
? configStore.config.taskPromptWords.filter(createFilter(queryString))
|
||||
: configStore.config.taskPromptWords
|
||||
// call callback function to return suggestions
|
||||
cb(results.map(item => ({ value: item })))
|
||||
}
|
||||
|
||||
const createFilter = (queryString: string) => {
|
||||
return (restaurant: string) => {
|
||||
return restaurant.toLowerCase().includes(queryString.toLowerCase())
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时检查URL参数
|
||||
onMounted(() => {
|
||||
autoSearchFromUrl()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-tooltip
|
||||
content="请先点击智能体库右侧的按钮上传智能体信息"
|
||||
placement="top"
|
||||
effect="light"
|
||||
:disabled="agentsStore.agents.length > 0"
|
||||
>
|
||||
<div class="task-root-container">
|
||||
<div
|
||||
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
|
||||
ref="autocompleteRef"
|
||||
v-model.trim="searchValue"
|
||||
class="task-input"
|
||||
size="large"
|
||||
:rows="1"
|
||||
:autosize="{ minRows: 1, maxRows: 10 }"
|
||||
placeholder="请输入您的任务"
|
||||
type="textarea"
|
||||
:append-to="taskContainerRef"
|
||||
:fetch-suggestions="querySearch"
|
||||
@change="agentsStore.setSearchValue"
|
||||
:disabled="!(agentsStore.agents.length > 0)"
|
||||
:debounce="0"
|
||||
:clearable="true"
|
||||
:trigger-on-focus="triggerOnFocus"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@select="isFocus = false"
|
||||
>
|
||||
</el-autocomplete>
|
||||
<el-button
|
||||
class="task-button"
|
||||
color="linear-gradient(to right, #00C7D2, #315AB4)"
|
||||
size="large"
|
||||
:title="isFillingSteps ? '点击停止生成' : '点击搜索任务'"
|
||||
circle
|
||||
:loading="agentsStore.agentRawPlan.loading"
|
||||
:disabled="!searchValue"
|
||||
@click.stop="handleButtonClick"
|
||||
>
|
||||
<SvgIcon
|
||||
v-if="!agentsStore.agentRawPlan.loading && !isFillingSteps"
|
||||
icon-class="paper-plane"
|
||||
size="18px"
|
||||
color="#ffffff"
|
||||
/>
|
||||
<SvgIcon
|
||||
v-if="!agentsStore.agentRawPlan.loading && isFillingSteps"
|
||||
icon-class="stoprunning"
|
||||
size="30px"
|
||||
color="#ffffff"
|
||||
/>
|
||||
</el-button>
|
||||
</div>
|
||||
<AssignmentButton v-if="planReady" @click="openAgentAllocationDialog" />
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.task-root-container {
|
||||
height: 60px;
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
}
|
||||
.task-container {
|
||||
width: 40%;
|
||||
margin: 0 auto;
|
||||
border: 2px solid transparent;
|
||||
$bg: var(--el-input-bg-color, var(--el-fill-color-blank));
|
||||
background: linear-gradient(var(--color-bg-taskbar), var(--color-bg-taskbar)) padding-box,
|
||||
linear-gradient(to right, #00c8d2, #315ab4) border-box;
|
||||
border-radius: 30px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 998;
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
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) {
|
||||
position: static !important;
|
||||
width: calc(100% + 102px); /*增加左右padding的总和 */
|
||||
min-width: calc(100% + 102px); /* 确保最小宽度也增加 */
|
||||
margin-left: -47px; /* 向左偏移左padding的值 */
|
||||
margin-right: -55px; /*向右偏移右padding的值 */
|
||||
background: var(--color-bg-taskbar);
|
||||
border: none;
|
||||
transition: height 0s ease-in-out;
|
||||
border-top: 1px solid var(--color-border);
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
|
||||
li {
|
||||
height: 45px;
|
||||
box-sizing: border-box;
|
||||
line-height: 45px;
|
||||
font-size: 14px;
|
||||
padding-left: 27px;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-hover);
|
||||
color: var(--color-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.el-popper__arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-autocomplete) {
|
||||
min-height: 56px;
|
||||
width: 100%;
|
||||
|
||||
.task-input {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.el-textarea__inner {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
line-height: 1.5;
|
||||
padding: 18px 0 0 18px;
|
||||
resize: none;
|
||||
color: var(--color-text-taskbar);
|
||||
|
||||
/* 聚焦时的样式 */
|
||||
.expanded & {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
line-height: 1.2;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.el-icon.is-loading {
|
||||
& + span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-title {
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
left: 27px;
|
||||
z-index: 999;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.task-button {
|
||||
background: linear-gradient(to right, #00c7d2, #315ab4);
|
||||
border: none; // 如果需要移除边框
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
right: 10px;
|
||||
transform: translateY(-50%);
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.task-button.is-loading {
|
||||
:deep(span) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -0,0 +1,170 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { getActionTypeDisplay, getAgentMapIcon } from '@/layout/components/config.ts'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import { type Agent, useAgentsStore } from '@/stores'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
const porps = defineProps<{
|
||||
agentList: Agent[]
|
||||
}>()
|
||||
|
||||
const taskProcess = computed(() => {
|
||||
const list = agentsStore.currentTask?.TaskProcess ?? []
|
||||
return list.map(item => ({
|
||||
...item,
|
||||
key: uuidv4()
|
||||
}))
|
||||
})
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-for="item in porps.agentList"
|
||||
:key="item.Name"
|
||||
class="user-item"
|
||||
:class="agentsStore.currentTask?.AgentSelection?.includes(item.Name) ? 'active-card' : ''"
|
||||
>
|
||||
<div class="flex items-center justify-between relative h-[43px]">
|
||||
<!-- 图标区域 -->
|
||||
<div
|
||||
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 }"
|
||||
>
|
||||
<svg-icon :icon-class="getAgentMapIcon(item.Name).icon" color="#fff" size="24px" />
|
||||
</div>
|
||||
|
||||
<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
|
||||
class="truncate"
|
||||
:style="
|
||||
agentsStore.currentTask?.AgentSelection?.includes(item.Name)
|
||||
? 'color:var(--color-accent)'
|
||||
: ''
|
||||
"
|
||||
>{{ item.Name }}</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="agentsStore.currentTask?.AgentSelection?.includes(item.Name)"
|
||||
class="flex items-center gap-[7px] h-[8px] mr-1"
|
||||
>
|
||||
<!-- 小圆点 -->
|
||||
<div
|
||||
v-for="item1 in taskProcess.filter(i => i.AgentName === item.Name)"
|
||||
:key="item1.key"
|
||||
class="w-[6px] h-[6px] rounded-full"
|
||||
:style="{
|
||||
background: getActionTypeDisplay(item1.ActionType)?.color,
|
||||
border: `1px solid ${getActionTypeDisplay(item1.ActionType)?.border}`
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 职责信息只有当执行流程中有当前智能体并且鼠标移入时才显示 -->
|
||||
<div class="duty-info">
|
||||
<div class="w-full flex justify-center">
|
||||
<div
|
||||
class="rounded-[9px] bg-[var(--color-bg-quaternary)] text-[12px] py-0.5 px-5 text-center my-2"
|
||||
>
|
||||
当前职责
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-[8px] pt-0">
|
||||
<div
|
||||
v-for="(item1, index1) in taskProcess.filter(i => i.AgentName === item.Name)"
|
||||
:key="item1.key"
|
||||
class="text-[12px]"
|
||||
>
|
||||
<div>
|
||||
<div class="mx-1 inline-block h-[14px]">
|
||||
<div
|
||||
:style="{ background: getActionTypeDisplay(item1.ActionType)?.color }"
|
||||
class="w-[6px] h-[6px] rounded-full mt-[7px]"
|
||||
></div>
|
||||
</div>
|
||||
<span :style="{ color: getActionTypeDisplay(item1.ActionType)?.color }"
|
||||
>{{ getActionTypeDisplay(item1.ActionType)?.name }}:</span
|
||||
>
|
||||
<span>{{ item1.Description }}</span>
|
||||
</div>
|
||||
<!-- 分割线 -->
|
||||
<div
|
||||
v-if="index1 !== taskProcess.filter(i => i.AgentName === item.Name).length - 1"
|
||||
class="h-[1px] w-full bg-[var(--color-border-default)] my-[8px]"
|
||||
></div>
|
||||
|
||||
<AssignmentButton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-item {
|
||||
background: var(--color-agent-list-bg);
|
||||
border-radius: 40px;
|
||||
padding-right: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
color: var(--color-text-detail);
|
||||
border: 1px solid var(--color-agent-list-border);
|
||||
box-sizing: border-box;
|
||||
.duty-info {
|
||||
transition: height 0.25s ease;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
& + .user-item {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&: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);
|
||||
}
|
||||
}
|
||||
|
||||
.textClass {
|
||||
color: var(--color-text-agent-list);
|
||||
&:hover {
|
||||
color: var(--color-text-agent-list-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.active-card {
|
||||
background: linear-gradient(var(--color-bg-quaternary), var(--color-bg-quaternary)) padding-box,
|
||||
linear-gradient(to right, var(--color-accent), var(--color-accent-secondary)) border-box;
|
||||
border: 1px solid var(--color-agent-list-border);
|
||||
|
||||
&: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;
|
||||
.duty-info {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加头像容器样式修复
|
||||
.icon-container {
|
||||
right: 0 !important;
|
||||
margin-left: 0px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,225 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { pick } from 'lodash'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import api from '@/api/index.ts'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import { agentMapDuty } from '@/layout/components/config.ts'
|
||||
import { type Agent, useAgentsStore } from '@/stores'
|
||||
import { readConfig } from '@/utils/readJson.ts'
|
||||
import AgentRepoList from './AgentRepoList.vue'
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
|
||||
// 如果agentsStore.agents不存在就读取默认配置的json文件
|
||||
onMounted(async () => {
|
||||
if (!agentsStore.agents.length) {
|
||||
const res = await readConfig<Agent[]>('agent.json')
|
||||
agentsStore.setAgents(res)
|
||||
}
|
||||
await api.setAgents(
|
||||
agentsStore.agents.map(item => pick(item, ['Name', 'Profile', 'apiUrl', 'apiKey', 'apiModel']))
|
||||
)
|
||||
})
|
||||
|
||||
// 上传agent文件
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
|
||||
const triggerFileSelect = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
const handleFileSelect = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
if (input.files && input.files[0]) {
|
||||
const file = input.files[0]
|
||||
readFileContent(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()
|
||||
reader.onload = e => {
|
||||
try {
|
||||
const content = e.target?.result as string
|
||||
const jsonData = JSON.parse(content)
|
||||
|
||||
if (!Array.isArray(jsonData)) {
|
||||
ElMessage.error('JSON格式错误: 必须为数组格式')
|
||||
return
|
||||
}
|
||||
|
||||
const validAgents = jsonData.filter((agent) => {
|
||||
// 验证必需字段
|
||||
if (!agent.Name || typeof agent.Name !== 'string') {
|
||||
return false
|
||||
}
|
||||
if (!agent.Icon || typeof agent.Icon !== 'string') {
|
||||
return false
|
||||
}
|
||||
if (!agent.Profile || typeof agent.Profile !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证API配置
|
||||
if (!validateApiConfig(agent)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
// 修改发送到后端的数据
|
||||
const processedAgents = validAgents.map(agent => ({
|
||||
Name: agent.Name,
|
||||
Profile: agent.Profile,
|
||||
Icon: agent.Icon,
|
||||
Classification: agent.Classification || '',
|
||||
apiUrl: agent.apiUrl,
|
||||
apiKey: agent.apiKey,
|
||||
apiModel: agent.apiModel
|
||||
}))
|
||||
|
||||
agentsStore.setAgents(processedAgents)
|
||||
|
||||
// 调用API
|
||||
api
|
||||
.setAgents(processedAgents)
|
||||
.then(() => {
|
||||
ElMessage.success('智能体上传成功')
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error('智能体上传失败')
|
||||
})
|
||||
} catch {
|
||||
ElMessage.error('JSON解析错误')
|
||||
}
|
||||
}
|
||||
|
||||
reader.onerror = () => {
|
||||
ElMessage.error('文件读取错误')
|
||||
}
|
||||
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
// 根据currentTask排序agent列表
|
||||
const agentList = computed(() => {
|
||||
const selected: Agent[] = []
|
||||
const unselected: {
|
||||
title: string
|
||||
data: Agent[]
|
||||
}[] = []
|
||||
const obj: Record<string, Agent[]> = {}
|
||||
if (!agentsStore.agents.length) {
|
||||
return {
|
||||
selected,
|
||||
unselected
|
||||
}
|
||||
}
|
||||
for (const agent of agentsStore.agents) {
|
||||
// if (agentsStore.currentTask?.AgentSelection?.includes(agent.Name)) {
|
||||
// selected.push(agent)
|
||||
// continue
|
||||
// }
|
||||
if (obj[agent.Classification]) {
|
||||
obj[agent.Classification]!.push(agent)
|
||||
} else {
|
||||
const arr = [agent]
|
||||
obj[agent.Classification] = arr
|
||||
unselected.push({
|
||||
title: agent.Classification,
|
||||
data: arr
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
selected,
|
||||
unselected: unselected
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="agent-repo h-full flex flex-col" id="agent-repo">
|
||||
<!-- 头部 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[18px] font-bold text-[var(--color-text-title-header)]">智能体库</span>
|
||||
<!-- 上传文件 -->
|
||||
<input type="file" accept=".json" @change="handleFileSelect" class="hidden" ref="fileInput" />
|
||||
<div class="plus-button" @click="triggerFileSelect">
|
||||
<svg-icon icon-class="plus" color="var(--color-text)" size="18px" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 智能体列表 -->
|
||||
<div class="pt-[18px] flex-1 overflow-y-auto relative">
|
||||
<!-- 已选中的智能体 -->
|
||||
<AgentRepoList :agent-list="agentList.selected" />
|
||||
<!-- 为选择的智能体 -->
|
||||
<div v-for="agent in agentList.unselected" :key="agent.title">
|
||||
<p class="text-[12px] font-bold py-[8px]">{{ agent.title }}</p>
|
||||
<AgentRepoList :agent-list="agent.data" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部提示栏 -->
|
||||
<div
|
||||
class="w-full grid grid-cols-3 gap-x-[10px] bg-[var(--color-bg-indicator)] rounded-[20px] p-[8px] mt-[10px]"
|
||||
>
|
||||
<div
|
||||
v-for="item in Object.values(agentMapDuty)"
|
||||
:key="item.key"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.agent-repo {
|
||||
padding: 0 8px;
|
||||
|
||||
.plus-button {
|
||||
background: var(--color-bg-tertiary);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-quaternary);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#agent-repo {
|
||||
:deep(.agent-repo-item-popover) {
|
||||
padding: 0;
|
||||
border-radius: 20px;
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import type { IExecuteRawResponse } from '@/api'
|
||||
import { computed } from 'vue'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import DOMPurify from 'dompurify'
|
||||
import Iod from './Iod.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
executePlans: IExecuteRawResponse[]
|
||||
nodeId?: string
|
||||
actionId?: string
|
||||
}>()
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
breaks: true
|
||||
})
|
||||
|
||||
function sanitize(str?: string) {
|
||||
if (!str) {
|
||||
return ''
|
||||
}
|
||||
const cleanStr = str.replace(/\\n/g, '\n').replace(/\n\s*\d+\./g, '\n$&')
|
||||
const html = md.render(cleanStr)
|
||||
return html
|
||||
// return DOMPurify.sanitize(html)
|
||||
}
|
||||
|
||||
interface Data {
|
||||
Description: string
|
||||
Content: string
|
||||
LogNodeType: string
|
||||
}
|
||||
const data = computed<Data | null>(() => {
|
||||
for (const result of props.executePlans) {
|
||||
if (result.NodeId === props.nodeId) {
|
||||
// LogNodeType 为 object直接渲染Content
|
||||
if (result.LogNodeType === 'object') {
|
||||
const data = {
|
||||
Description: props.nodeId,
|
||||
Content: sanitize(result.content),
|
||||
LogNodeType: result.LogNodeType
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
if (!result.ActionHistory) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const action of result.ActionHistory) {
|
||||
if (action.ID === props.actionId) {
|
||||
return {
|
||||
Description: action.Description,
|
||||
Content: sanitize(action.Action_Result),
|
||||
LogNodeType: result.LogNodeType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="data" class="card-item w-full pl-[56px] pr-[41px]">
|
||||
<!-- 分割线 -->
|
||||
<div v-if="data.LogNodeType !== 'object'" class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
|
||||
<div
|
||||
v-if="data.Description"
|
||||
class="text-[16px] flex items-center gap-1 text-[var(--color-text-secondary)] mb-1"
|
||||
>
|
||||
{{ data.Description }}
|
||||
<Iod v-if="data.LogNodeType !== 'object'" />
|
||||
</div>
|
||||
<div
|
||||
class="rounded-[8px] p-[15px] text-[14px] bg-[var(--color-bg-result-detail)] text-[var(--color-text-detail)]"
|
||||
>
|
||||
<div
|
||||
class="markdown-content max-h-[240px] overflow-y-auto max-w-full"
|
||||
v-html="data.Content"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card-item + .card-item {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.markdown-content {
|
||||
:deep(code) {
|
||||
display: block;
|
||||
width: 100px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:deep(pre) {
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { readConfig } from '@/utils/readJson.ts'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
interface Iod {
|
||||
name: string
|
||||
data_space: string
|
||||
doId: string
|
||||
fromRepo: string
|
||||
}
|
||||
|
||||
const data = ref<Iod[]>([])
|
||||
const displayIndex = ref(0)
|
||||
|
||||
const displayIod = computed(() => {
|
||||
return data.value[displayIndex.value]!
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const res = await readConfig<{ data: Iod[] }>('iodConfig.json')
|
||||
data.value = res.data
|
||||
})
|
||||
|
||||
function handleNext() {
|
||||
if (displayIndex.value === data.value.length - 1) {
|
||||
displayIndex.value = 0
|
||||
} else {
|
||||
displayIndex.value++
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrev() {
|
||||
if (displayIndex.value === 0) {
|
||||
displayIndex.value = data.value.length - 1
|
||||
} else {
|
||||
displayIndex.value--
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-popover
|
||||
trigger="hover"
|
||||
width="440"
|
||||
popper-style="background-color: var(--color-bg-result); border: none;"
|
||||
>
|
||||
<template #reference>
|
||||
<div
|
||||
class="rounded-full w-[20px] h-[20px] bg-[var(--color-bg-quaternary)] flex justify-center items-center cursor-pointer"
|
||||
>
|
||||
{{ data.length }}
|
||||
</div>
|
||||
</template>
|
||||
<template #default v-if="data.length">
|
||||
<div class="bg-[var(--color-bg-result)]">
|
||||
<div class="flex justify-between items-center p-2 pb-0 rounded-[8px] text-[16px] font-bold">
|
||||
<span>数联网搜索结果</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- <div>{{ `${displayIndex + 1}/${data.length}` }}</div>
|
||||
<el-button type="primary" size="small" @click="handleNext">下一个</el-button> -->
|
||||
<!-- 关闭 -->
|
||||
<!-- <SvgIcon icon-class="close" size="15px" /> -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分割线 -->
|
||||
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
|
||||
<div class="p-2 pt-0">
|
||||
<div class="flex items-center w-full gap-3">
|
||||
<div class="font-bold w-[75px] text-right flex-shrink-0">名称:</div>
|
||||
<div class="text-[var(--color-text-detail)] flex-1 break-words">
|
||||
{{ displayIod.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center w-full gap-3">
|
||||
<div class="font-bold w-[75px] text-right flex-shrink-0">数据空间:</div>
|
||||
<div class="text-[var(--color-text-detail)] lex-1 break-words">
|
||||
{{ displayIod.data_space }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center w-full gap-3">
|
||||
<div class="font-bold w-[75px] text-right flex-shrink-0">DOID:</div>
|
||||
<div class="text-[var(--color-text-detail)] lex-1 break-words">
|
||||
{{ displayIod.doId }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center w-full gap-3">
|
||||
<div class="font-bold w-[75px] text-right flex-shrink-0">来源仓库:</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>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
isAdding?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'start-add-output'): void
|
||||
}>()
|
||||
</script>
|
||||
<template>
|
||||
<div class="absolute inset-0 flex items-start gap-[14%]">
|
||||
<!-- 左侧元素 -->
|
||||
<div class="flex-1 relative h-full flex justify-center">
|
||||
<!-- 背景那一根线 -->
|
||||
<div class="h-full bg-[var(--color-bg-flow)] w-[5px]">
|
||||
<!-- 线底部的小圆球 -->
|
||||
<div
|
||||
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 class="flex-1 relative h-full flex justify-center">
|
||||
<!-- 背景那一根线 -->
|
||||
<div class="h-full bg-[var(--color-bg-flow)] w-[5px]">
|
||||
<!-- 顶部加号区域 -->
|
||||
<!-- <div
|
||||
v-if="!isAdding"
|
||||
v-dev-only
|
||||
class="plus-area mt-[35px] ml-[-15px] w-[34px] h-[34px] flex items-center justify-center cursor-pointer rounded-full"
|
||||
@click="$emit('start-add-output')"
|
||||
> -->
|
||||
<!-- 加号图标 -->
|
||||
<!-- <svg-icon
|
||||
icon-class="plus"
|
||||
color="var(--color-text)"
|
||||
size="20px"
|
||||
class="plus-icon opacity-0 transition-opacity duration-200"
|
||||
/>
|
||||
</div> -->
|
||||
<!-- 线底部的小圆球 -->
|
||||
<div
|
||||
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>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.plus-area:hover .plus-icon {
|
||||
opacity: 1;
|
||||
border: 1px dashed var(--color-text);
|
||||
}
|
||||
</style>
|
||||