Compare commits
1 Commits
web
...
23db6fc4a1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23db6fc4a1 |
@@ -1,434 +0,0 @@
|
||||
"""
|
||||
Word 文档 LLM 报告导出器
|
||||
调用大模型生成专业的任务执行报告
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class DocxLLMExporter:
|
||||
"""Word 文档 LLM 报告导出器 - 调用大模型生成报告"""
|
||||
|
||||
# LLM 配置(从 config.yaml 加载)
|
||||
LLM_CONFIG = {
|
||||
'OPENAI_API_BASE': None,
|
||||
'OPENAI_API_KEY': None,
|
||||
'OPENAI_API_MODEL': None,
|
||||
}
|
||||
|
||||
# Prompt 模板
|
||||
PROMPT_TEMPLATE = """你是一位专业的项目管理顾问和报告分析师。你的任务是将以下任务执行数据生成一份详细、专业、结构化的执行报告。
|
||||
|
||||
## 任务基本信息
|
||||
- 任务名称:{task_name}
|
||||
|
||||
## 任务大纲(规划阶段)
|
||||
{task_outline}
|
||||
|
||||
## 执行结果
|
||||
{rehearsal_log}
|
||||
|
||||
## 参与智能体
|
||||
{agents}
|
||||
|
||||
## 智能体评分
|
||||
{agent_scores}
|
||||
|
||||
---
|
||||
|
||||
## 报告要求
|
||||
|
||||
请生成一份完整的任务执行报告,包含以下章节:
|
||||
|
||||
### 1. 执行摘要
|
||||
用 2-3 句话概括本次任务的整体执行情况。
|
||||
|
||||
### 2. 任务概述
|
||||
- 任务背景与目标
|
||||
- 任务范围与边界
|
||||
|
||||
### 3. 任务规划分析
|
||||
- 任务拆解的合理性
|
||||
- 智能体角色分配的优化建议
|
||||
- 工作流程设计
|
||||
|
||||
### 4. 执行过程回顾
|
||||
- 各阶段的完成情况
|
||||
- 关键决策点
|
||||
- 遇到的问题及解决方案
|
||||
|
||||
### 5. 成果产出分析
|
||||
- 产出物的质量评估
|
||||
- 产出与预期目标的匹配度
|
||||
|
||||
### 6. 团队协作分析
|
||||
- 智能体之间的协作模式
|
||||
- 信息传递效率
|
||||
|
||||
### 7. 质量评估
|
||||
- 整体完成质量评分(1-10分)
|
||||
- 各维度的具体评分及理由
|
||||
|
||||
### 8. 经验教训与改进建议
|
||||
- 成功经验
|
||||
- 存在的问题与不足
|
||||
- 改进建议
|
||||
|
||||
---
|
||||
|
||||
## 输出格式要求
|
||||
- 使用 Markdown 格式输出
|
||||
- 语言:简体中文
|
||||
- 适当使用列表、表格增强可读性
|
||||
- 报告长度必须达到 4000-6000 字,每个章节都要详细展开,不要遗漏任何章节
|
||||
- 每个章节的内容要充实,提供具体的分析和建议
|
||||
- 注意:所有加粗标记必须成对出现,如 **文本**,不要单独使用 ** 或缺少结束标记
|
||||
- 禁止使用 mermaid、graph TD、flowchart 等图表代码,如果需要描述流程请用纯文字描述
|
||||
- 不要生成附录章节(如有关键参数对照表、工艺流程图等),如果确实需要附录再生成
|
||||
- 不要在报告中显示"报告总字数"这样的统计信息
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._load_llm_config()
|
||||
|
||||
def _load_llm_config(self):
|
||||
"""从配置文件加载 LLM 配置"""
|
||||
try:
|
||||
import yaml
|
||||
# 尝试多个可能的配置文件路径
|
||||
possible_paths = [
|
||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'config.yaml'),
|
||||
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'backend', 'config', 'config.yaml'),
|
||||
os.path.join(os.getcwd(), 'config', 'config.yaml'),
|
||||
]
|
||||
|
||||
for config_path in possible_paths:
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
if config:
|
||||
self.LLM_CONFIG['OPENAI_API_BASE'] = config.get('OPENAI_API_BASE')
|
||||
self.LLM_CONFIG['OPENAI_API_KEY'] = config.get('OPENAI_API_KEY')
|
||||
self.LLM_CONFIG['OPENAI_API_MODEL'] = config.get('OPENAI_API_MODEL')
|
||||
print(f"已加载 LLM 配置: {self.LLM_CONFIG['OPENAI_API_MODEL']}")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"加载 LLM 配置失败: {e}")
|
||||
|
||||
def generate(self, task_data: Dict[str, Any], file_path: str) -> bool:
|
||||
"""生成 Word 文档(调用 LLM 生成报告)"""
|
||||
try:
|
||||
# 1. 准备数据
|
||||
task_name = task_data.get('task_name', '未命名任务')
|
||||
task_outline = task_data.get('task_outline')
|
||||
rehearsal_log = task_data.get('rehearsal_log')
|
||||
agent_scores = task_data.get('agent_scores')
|
||||
|
||||
# 2. 提取参与智能体(从 task_outline 的 Collaboration Process 中提取)
|
||||
agents = self._extract_agents(task_outline)
|
||||
|
||||
# 3. 过滤 agent_scores(只保留参与当前任务的智能体评分)
|
||||
filtered_agent_scores = self._filter_agent_scores(agent_scores, agents)
|
||||
|
||||
# 4. 格式化数据为 JSON 字符串
|
||||
task_outline_str = json.dumps(task_outline, ensure_ascii=False, indent=2) if task_outline else '无'
|
||||
rehearsal_log_str = json.dumps(rehearsal_log, ensure_ascii=False, indent=2) if rehearsal_log else '无'
|
||||
agents_str = ', '.join(agents) if agents else '无'
|
||||
agent_scores_str = json.dumps(filtered_agent_scores, ensure_ascii=False, indent=2) if filtered_agent_scores else '无'
|
||||
|
||||
# 5. 构建 Prompt
|
||||
prompt = self.PROMPT_TEMPLATE.format(
|
||||
task_name=task_name,
|
||||
task_outline=task_outline_str,
|
||||
rehearsal_log=rehearsal_log_str,
|
||||
agents=agents_str,
|
||||
agent_scores=agent_scores_str
|
||||
)
|
||||
|
||||
# 6. 调用 LLM 生成报告
|
||||
print("正在调用大模型生成报告...")
|
||||
report_content = self._call_llm(prompt)
|
||||
|
||||
if not report_content:
|
||||
print("LLM 生成报告失败")
|
||||
return False
|
||||
|
||||
# 7. 清理报告内容:去掉开头的"任务执行报告"标题(如果存在)
|
||||
report_content = self._clean_report_title(report_content)
|
||||
|
||||
print(f"报告生成成功,长度: {len(report_content)} 字符")
|
||||
|
||||
# 8. 将 Markdown 转换为 Word 文档
|
||||
self._save_as_word(report_content, file_path)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Word LLM 导出失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _clean_report_title(self, content: str) -> str:
|
||||
"""清理报告开头的重复标题"""
|
||||
lines = content.split('\n')
|
||||
if not lines:
|
||||
return content
|
||||
|
||||
# 检查第一行是否是"任务执行报告"
|
||||
first_line = lines[0].strip()
|
||||
if first_line == '任务执行报告' or first_line == '# 任务执行报告':
|
||||
# 去掉第一行
|
||||
lines = lines[1:]
|
||||
# 去掉可能的空行
|
||||
while lines and not lines[0].strip():
|
||||
lines.pop(0)
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def _extract_agents(self, task_outline: Any) -> list:
|
||||
"""从 task_outline 中提取参与智能体列表"""
|
||||
agents = set()
|
||||
if not task_outline or not isinstance(task_outline, dict):
|
||||
return []
|
||||
|
||||
collaboration_process = task_outline.get('Collaboration Process', [])
|
||||
if not collaboration_process or not isinstance(collaboration_process, list):
|
||||
return []
|
||||
|
||||
for step in collaboration_process:
|
||||
if isinstance(step, dict):
|
||||
agent_selection = step.get('AgentSelection', [])
|
||||
if isinstance(agent_selection, list):
|
||||
for agent in agent_selection:
|
||||
if agent:
|
||||
agents.add(agent)
|
||||
|
||||
return list(agents)
|
||||
|
||||
def _filter_agent_scores(self, agent_scores: Any, agents: list) -> dict:
|
||||
"""过滤 agent_scores,只保留参与当前任务的智能体评分"""
|
||||
if not agent_scores or not isinstance(agent_scores, dict):
|
||||
return {}
|
||||
|
||||
if not agents:
|
||||
return {}
|
||||
|
||||
filtered = {}
|
||||
for step_id, step_data in agent_scores.items():
|
||||
if not isinstance(step_data, dict):
|
||||
continue
|
||||
|
||||
aspect_list = step_data.get('aspectList', [])
|
||||
agent_scores_data = step_data.get('agentScores', {})
|
||||
|
||||
if not agent_scores_data:
|
||||
continue
|
||||
|
||||
# 只保留在 agents 列表中的智能体评分
|
||||
filtered_scores = {}
|
||||
for agent_name, scores in agent_scores_data.items():
|
||||
if agent_name in agents and isinstance(scores, dict):
|
||||
filtered_scores[agent_name] = scores
|
||||
|
||||
if filtered_scores:
|
||||
filtered[step_id] = {
|
||||
'aspectList': aspect_list,
|
||||
'agentScores': filtered_scores
|
||||
}
|
||||
|
||||
return filtered
|
||||
|
||||
def _call_llm(self, prompt: str) -> str:
|
||||
"""调用大模型 API 生成报告"""
|
||||
try:
|
||||
import openai
|
||||
|
||||
# 验证配置
|
||||
if not self.LLM_CONFIG['OPENAI_API_KEY']:
|
||||
print("错误: OPENAI_API_KEY 未配置")
|
||||
return ""
|
||||
if not self.LLM_CONFIG['OPENAI_API_BASE']:
|
||||
print("错误: OPENAI_API_BASE 未配置")
|
||||
return ""
|
||||
if not self.LLM_CONFIG['OPENAI_API_MODEL']:
|
||||
print("错误: OPENAI_API_MODEL 未配置")
|
||||
return ""
|
||||
|
||||
# 配置 OpenAI 客户端
|
||||
client = openai.OpenAI(
|
||||
api_key=self.LLM_CONFIG['OPENAI_API_KEY'],
|
||||
base_url=self.LLM_CONFIG['OPENAI_API_BASE']
|
||||
)
|
||||
|
||||
# 调用 API
|
||||
response = client.chat.completions.create(
|
||||
model=self.LLM_CONFIG['OPENAI_API_MODEL'],
|
||||
messages=[
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=12000,
|
||||
)
|
||||
|
||||
if response and response.choices:
|
||||
return response.choices[0].message.content
|
||||
|
||||
return ""
|
||||
|
||||
except ImportError:
|
||||
print("请安装 openai 库: pip install openai")
|
||||
return ""
|
||||
except Exception as e:
|
||||
print(f"调用 LLM 失败: {e}")
|
||||
return ""
|
||||
|
||||
def _save_as_word(self, markdown_content: str, file_path: str):
|
||||
"""将 Markdown 内容保存为 Word 文档"""
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
|
||||
doc = Document()
|
||||
|
||||
# 提取文档标题(从第一个 # 标题获取)
|
||||
lines = markdown_content.split('\n')
|
||||
first_title = None
|
||||
content_start = 0
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
line = line.strip()
|
||||
if line.startswith('# '):
|
||||
first_title = line[2:].strip()
|
||||
content_start = i + 1
|
||||
break
|
||||
|
||||
# 添加文档标题
|
||||
if first_title:
|
||||
title = doc.add_heading(first_title, level=0)
|
||||
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
|
||||
# 解析剩余的 Markdown 内容
|
||||
remaining_content = '\n'.join(lines[content_start:])
|
||||
self._parse_markdown_to_doc(remaining_content, doc)
|
||||
|
||||
# 添加时间戳
|
||||
doc.add_paragraph(f"\n\n导出时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
doc.save(file_path)
|
||||
print(f"Word 文档已保存: {file_path}")
|
||||
|
||||
except ImportError:
|
||||
print("请安装 python-docx 库: pip install python-docx")
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"保存 Word 文档失败: {e}")
|
||||
raise
|
||||
|
||||
def _parse_markdown_to_doc(self, markdown_content: str, doc):
|
||||
"""解析 Markdown 内容并添加到 Word 文档"""
|
||||
lines = markdown_content.split('\n')
|
||||
i = 0
|
||||
table_rows = []
|
||||
in_table = False
|
||||
|
||||
while i < len(lines):
|
||||
line = lines[i].rstrip()
|
||||
|
||||
# 空行处理
|
||||
if not line:
|
||||
in_table = False
|
||||
if table_rows:
|
||||
self._add_table_to_doc(table_rows, doc)
|
||||
table_rows = []
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 表格分隔线检测(跳过 |---| 或 |:---| 等格式的行)
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('|') and stripped.endswith('|') and '---' in stripped:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 表格检测:检查是否是表格行
|
||||
if '|' in line and line.strip().startswith('|'):
|
||||
# 收集表格行
|
||||
cells = [cell.strip() for cell in line.split('|')[1:-1]]
|
||||
if cells and any(cells): # 跳过空行
|
||||
table_rows.append(cells)
|
||||
in_table = True
|
||||
i += 1
|
||||
continue
|
||||
else:
|
||||
# 如果之前在表格中,现在不是表格行了,添加表格
|
||||
if in_table and table_rows:
|
||||
self._add_table_to_doc(table_rows, doc)
|
||||
table_rows = []
|
||||
in_table = False
|
||||
|
||||
# 标题处理
|
||||
if line.startswith('### '):
|
||||
doc.add_heading(line[4:].strip(), level=3)
|
||||
elif line.startswith('## '):
|
||||
doc.add_heading(line[3:].strip(), level=1)
|
||||
elif line.startswith('# '):
|
||||
doc.add_heading(line[2:].strip(), level=0)
|
||||
# 无序列表处理(去掉 • 或 - 符号)
|
||||
elif line.startswith('- ') or line.startswith('* ') or line.startswith('• '):
|
||||
# 去掉列表符号,保留内容
|
||||
text = line[2:].strip() if line.startswith(('- ', '* ')) else line[1:].strip()
|
||||
self._add_formatted_paragraph(text, doc, 'List Bullet')
|
||||
# 普通段落(处理加粗)
|
||||
else:
|
||||
# 使用格式化方法处理加粗
|
||||
self._add_formatted_paragraph(line, doc)
|
||||
|
||||
i += 1
|
||||
|
||||
# 处理最后的表格
|
||||
if table_rows:
|
||||
self._add_table_to_doc(table_rows, doc)
|
||||
|
||||
def _add_table_to_doc(self, table_rows: list, doc):
|
||||
"""将表格行添加到 Word 文档"""
|
||||
if not table_rows:
|
||||
return
|
||||
|
||||
# 创建表格
|
||||
table = doc.add_table(rows=len(table_rows), cols=len(table_rows[0]))
|
||||
table.style = 'Light Grid Accent 1'
|
||||
|
||||
for i, row_data in enumerate(table_rows):
|
||||
row = table.rows[i]
|
||||
for j, cell_text in enumerate(row_data):
|
||||
cell = row.cells[j]
|
||||
cell.text = ''
|
||||
|
||||
# 处理加粗
|
||||
parts = re.split(r'(\*\*.+?\*\*)', cell_text)
|
||||
for part in parts:
|
||||
if part.startswith('**') and part.endswith('**'):
|
||||
run = cell.paragraphs[0].add_run(part[2:-2])
|
||||
run.bold = True
|
||||
elif part:
|
||||
cell.paragraphs[0].add_run(part)
|
||||
|
||||
def _add_formatted_paragraph(self, text: str, doc, style: str = None):
|
||||
"""添加带格式的段落"""
|
||||
# 处理加粗文本
|
||||
para = doc.add_paragraph(style=style)
|
||||
|
||||
# 分割文本处理加粗
|
||||
parts = re.split(r'(\*\*.+?\*\*)', text)
|
||||
for part in parts:
|
||||
if part.startswith('**') and part.endswith('**'):
|
||||
# 加粗文本
|
||||
run = para.add_run(part[2:-2])
|
||||
run.bold = True
|
||||
else:
|
||||
para.add_run(part)
|
||||
@@ -1,269 +0,0 @@
|
||||
"""
|
||||
信息图 LLM 报告导出器
|
||||
调用大模型生成信息图展示的格式化内容
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class InfographicLLMExporter:
|
||||
"""信息图 LLM 报告导出器 - 调用大模型生成信息图内容"""
|
||||
|
||||
LLM_CONFIG = {
|
||||
'OPENAI_API_BASE': None,
|
||||
'OPENAI_API_KEY': None,
|
||||
'OPENAI_API_MODEL': None,
|
||||
}
|
||||
|
||||
PROMPT_TEMPLATE = """你是一位专业的可视化设计师和数据分析专家。你的任务是将以下任务执行数据生成适合信息图展示的格式化内容。
|
||||
|
||||
## 任务基本信息
|
||||
- 任务名称:{task_name}
|
||||
|
||||
## 任务大纲(规划阶段)
|
||||
{task_outline}
|
||||
|
||||
## 执行结果
|
||||
{rehearsal_log}
|
||||
|
||||
## 参与智能体
|
||||
{agents}
|
||||
|
||||
## 智能体评分
|
||||
{agent_scores}
|
||||
|
||||
---
|
||||
|
||||
## 信息图内容要求
|
||||
|
||||
请生成以下信息图展示内容(JSON 格式输出):
|
||||
|
||||
{{
|
||||
"summary": "执行摘要 - 2-3句话概括整体执行情况",
|
||||
"highlights": [
|
||||
"亮点1 - 取得的显著成果",
|
||||
"亮点2 - 关键突破或创新",
|
||||
"亮点3 - 重要的里程碑"
|
||||
],
|
||||
"statistics": {{
|
||||
"total_steps": 执行总步骤数,
|
||||
"agent_count": 参与智能体数量,
|
||||
"completion_rate": 完成率(百分比),
|
||||
"quality_score": 质量评分(1-10)
|
||||
}},
|
||||
"key_insights": [
|
||||
"关键洞察1 - 从执行过程中总结的洞见",
|
||||
"关键洞察2 - 值得关注的趋势或模式",
|
||||
"关键洞察3 - 对未来工作的建议"
|
||||
],
|
||||
"timeline": [
|
||||
{{"step": "步骤名称", "status": "完成/进行中/未完成", "key_result": "关键产出"}},
|
||||
...
|
||||
],
|
||||
"agent_performance": [
|
||||
{{"name": "智能体名称", "score": 评分, "contribution": "主要贡献"}},
|
||||
...
|
||||
]
|
||||
}}
|
||||
|
||||
---
|
||||
|
||||
## 输出要求
|
||||
- 输出必须是有效的 JSON 格式
|
||||
- 语言:简体中文
|
||||
- 所有字符串值使用中文
|
||||
- statistics 中的数值必须是整数或浮点数
|
||||
- 确保 JSON 格式正确,不要有语法错误
|
||||
- 不要输出 JSON 之外的任何内容
|
||||
- **重要**:"执行结果"(rehearsal_log)只是参考数据,用于帮助你分析整体执行情况生成摘要、亮点、统计数据等。**不要在任何输出字段中直接复制或输出原始执行结果数据**,而应该对这些数据进行分析和提炼,生成适合信息图展示的格式化内容。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._load_llm_config()
|
||||
|
||||
def _load_llm_config(self):
|
||||
"""从配置文件加载 LLM 配置"""
|
||||
try:
|
||||
import yaml
|
||||
possible_paths = [
|
||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'config.yaml'),
|
||||
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'backend', 'config', 'config.yaml'),
|
||||
os.path.join(os.getcwd(), 'config', 'config.yaml'),
|
||||
]
|
||||
|
||||
for config_path in possible_paths:
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
if config:
|
||||
self.LLM_CONFIG['OPENAI_API_BASE'] = config.get('OPENAI_API_BASE')
|
||||
self.LLM_CONFIG['OPENAI_API_KEY'] = config.get('OPENAI_API_KEY')
|
||||
self.LLM_CONFIG['OPENAI_API_MODEL'] = config.get('OPENAI_API_MODEL')
|
||||
print(f"已加载 LLM 配置: {self.LLM_CONFIG['OPENAI_API_MODEL']}")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"加载 LLM 配置失败: {e}")
|
||||
|
||||
def generate(self, task_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""生成信息图内容(调用 LLM 生成)"""
|
||||
try:
|
||||
task_name = task_data.get('task_name', '未命名任务')
|
||||
task_outline = task_data.get('task_outline')
|
||||
rehearsal_log = task_data.get('rehearsal_log')
|
||||
agent_scores = task_data.get('agent_scores')
|
||||
|
||||
agents = self._extract_agents(task_outline)
|
||||
filtered_agent_scores = self._filter_agent_scores(agent_scores, agents)
|
||||
|
||||
task_outline_str = json.dumps(task_outline, ensure_ascii=False, indent=2) if task_outline else '无'
|
||||
rehearsal_log_str = json.dumps(rehearsal_log, ensure_ascii=False, indent=2) if rehearsal_log else '无'
|
||||
agents_str = ', '.join(agents) if agents else '无'
|
||||
agent_scores_str = json.dumps(filtered_agent_scores, ensure_ascii=False, indent=2) if filtered_agent_scores else '无'
|
||||
|
||||
prompt = self.PROMPT_TEMPLATE.format(
|
||||
task_name=task_name,
|
||||
task_outline=task_outline_str,
|
||||
rehearsal_log=rehearsal_log_str,
|
||||
agents=agents_str,
|
||||
agent_scores=agent_scores_str
|
||||
)
|
||||
|
||||
print("正在调用大模型生成信息图内容...")
|
||||
llm_result = self._call_llm(prompt)
|
||||
|
||||
if not llm_result:
|
||||
print("LLM 生成信息图内容失败")
|
||||
return None
|
||||
|
||||
infographic_data = self._parse_llm_result(llm_result)
|
||||
if not infographic_data:
|
||||
print("解析 LLM 结果失败")
|
||||
return None
|
||||
|
||||
print(f"信息图内容生成成功")
|
||||
return infographic_data
|
||||
|
||||
except Exception as e:
|
||||
print(f"信息图 LLM 生成失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def _extract_agents(self, task_outline: Any) -> list:
|
||||
"""从 task_outline 中提取参与智能体列表"""
|
||||
agents = set()
|
||||
if not task_outline or not isinstance(task_outline, dict):
|
||||
return []
|
||||
|
||||
collaboration_process = task_outline.get('Collaboration Process', [])
|
||||
if not collaboration_process or not isinstance(collaboration_process, list):
|
||||
return []
|
||||
|
||||
for step in collaboration_process:
|
||||
if isinstance(step, dict):
|
||||
agent_selection = step.get('AgentSelection', [])
|
||||
if isinstance(agent_selection, list):
|
||||
for agent in agent_selection:
|
||||
if agent:
|
||||
agents.add(agent)
|
||||
|
||||
return list(agents)
|
||||
|
||||
def _filter_agent_scores(self, agent_scores: Any, agents: list) -> dict:
|
||||
"""过滤 agent_scores,只保留参与当前任务的智能体评分"""
|
||||
if not agent_scores or not isinstance(agent_scores, dict):
|
||||
return {}
|
||||
|
||||
if not agents:
|
||||
return {}
|
||||
|
||||
filtered = {}
|
||||
for step_id, step_data in agent_scores.items():
|
||||
if not isinstance(step_data, dict):
|
||||
continue
|
||||
|
||||
aspect_list = step_data.get('aspectList', [])
|
||||
agent_scores_data = step_data.get('agentScores', {})
|
||||
|
||||
if not agent_scores_data:
|
||||
continue
|
||||
|
||||
filtered_scores = {}
|
||||
for agent_name, scores in agent_scores_data.items():
|
||||
if agent_name in agents and isinstance(scores, dict):
|
||||
filtered_scores[agent_name] = scores
|
||||
|
||||
if filtered_scores:
|
||||
filtered[step_id] = {
|
||||
'aspectList': aspect_list,
|
||||
'agentScores': filtered_scores
|
||||
}
|
||||
|
||||
return filtered
|
||||
|
||||
def _call_llm(self, prompt: str) -> str:
|
||||
"""调用大模型 API 生成内容"""
|
||||
try:
|
||||
import openai
|
||||
|
||||
if not self.LLM_CONFIG['OPENAI_API_KEY']:
|
||||
print("错误: OPENAI_API_KEY 未配置")
|
||||
return ""
|
||||
if not self.LLM_CONFIG['OPENAI_API_BASE']:
|
||||
print("错误: OPENAI_API_BASE 未配置")
|
||||
return ""
|
||||
if not self.LLM_CONFIG['OPENAI_API_MODEL']:
|
||||
print("错误: OPENAI_API_MODEL 未配置")
|
||||
return ""
|
||||
|
||||
client = openai.OpenAI(
|
||||
api_key=self.LLM_CONFIG['OPENAI_API_KEY'],
|
||||
base_url=self.LLM_CONFIG['OPENAI_API_BASE']
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=self.LLM_CONFIG['OPENAI_API_MODEL'],
|
||||
messages=[
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=8000,
|
||||
)
|
||||
|
||||
if response and response.choices:
|
||||
return response.choices[0].message.content
|
||||
|
||||
return ""
|
||||
|
||||
except ImportError:
|
||||
print("请安装 openai 库: pip install openai")
|
||||
return ""
|
||||
except Exception as e:
|
||||
print(f"调用 LLM 失败: {e}")
|
||||
return ""
|
||||
|
||||
def _parse_llm_result(self, llm_result: str) -> Optional[Dict[str, Any]]:
|
||||
"""解析 LLM 返回的 JSON 字符串"""
|
||||
try:
|
||||
json_str = llm_result.strip()
|
||||
if json_str.startswith("```json"):
|
||||
json_str = json_str[7:]
|
||||
if json_str.startswith("```"):
|
||||
json_str = json_str[3:]
|
||||
if json_str.endswith("```"):
|
||||
json_str = json_str[:-3]
|
||||
json_str = json_str.strip()
|
||||
|
||||
return json.loads(json_str)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"JSON 解析失败: {e}")
|
||||
print(f"原始结果: {llm_result[:500]}...")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"解析失败: {e}")
|
||||
return None
|
||||
@@ -1,315 +0,0 @@
|
||||
"""
|
||||
Markdown LLM 报告导出器
|
||||
调用大模型生成专业的任务执行报告(Markdown 格式)
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class MarkdownLLMExporter:
|
||||
"""Markdown LLM 报告导出器 - 调用大模型生成报告"""
|
||||
|
||||
# LLM 配置(从 config.yaml 加载)
|
||||
LLM_CONFIG = {
|
||||
'OPENAI_API_BASE': None,
|
||||
'OPENAI_API_KEY': None,
|
||||
'OPENAI_API_MODEL': None,
|
||||
}
|
||||
|
||||
# Prompt 模板
|
||||
PROMPT_TEMPLATE = """你是一位专业的项目管理顾问和报告分析师。你的任务是将以下任务执行数据生成一份详细、专业、结构化的执行报告。
|
||||
|
||||
## 任务基本信息
|
||||
- 任务名称:{task_name}
|
||||
|
||||
## 任务大纲(规划阶段)
|
||||
{task_outline}
|
||||
|
||||
## 执行结果
|
||||
{rehearsal_log}
|
||||
|
||||
## 参与智能体
|
||||
{agents}
|
||||
|
||||
## 智能体评分
|
||||
{agent_scores}
|
||||
|
||||
---
|
||||
|
||||
## 报告要求
|
||||
|
||||
请生成一份完整的任务执行报告,包含以下章节:
|
||||
|
||||
### 1. 执行摘要
|
||||
用 2-3 句话概括本次任务的整体执行情况。
|
||||
|
||||
### 2. 任务概述
|
||||
- 任务背景与目标
|
||||
- 任务范围与边界
|
||||
|
||||
### 3. 任务规划分析
|
||||
- 任务拆解的合理性
|
||||
- 智能体角色分配的优化建议
|
||||
- 工作流程设计
|
||||
|
||||
### 4. 执行过程回顾
|
||||
- 各阶段的完成情况
|
||||
- 关键决策点
|
||||
- 遇到的问题及解决方案
|
||||
|
||||
### 5. 成果产出分析
|
||||
- 产出物的质量评估
|
||||
- 产出与预期目标的匹配度
|
||||
|
||||
### 6. 团队协作分析
|
||||
- 智能体之间的协作模式
|
||||
- 信息传递效率
|
||||
|
||||
### 7. 质量评估
|
||||
- 整体完成质量评分(1-10分)
|
||||
- 各维度的具体评分及理由
|
||||
|
||||
### 8. 经验教训与改进建议
|
||||
- 成功经验
|
||||
- 存在的问题与不足
|
||||
- 改进建议
|
||||
|
||||
---
|
||||
|
||||
## 输出格式要求
|
||||
- 使用 Markdown 格式输出
|
||||
- 语言:简体中文
|
||||
- 适当使用列表、表格增强可读性
|
||||
- 报告长度必须达到 4000-6000 字,每个章节都要详细展开,不要遗漏任何章节
|
||||
- 每个章节的内容要充实,提供具体的分析和建议
|
||||
- 注意:所有加粗标记必须成对出现,如 **文本**,不要单独使用 ** 或缺少结束标记
|
||||
- 禁止使用 mermaid、graph TD、flowchart 等图表代码,如果需要描述流程请用纯文字描述
|
||||
- 不要生成附录章节(如有关键参数对照表、工艺流程图等),如果确实需要附录再生成
|
||||
- 不要在报告中显示"报告总字数"这样的统计信息
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._load_llm_config()
|
||||
|
||||
def _load_llm_config(self):
|
||||
"""从配置文件加载 LLM 配置"""
|
||||
try:
|
||||
import yaml
|
||||
# 尝试多个可能的配置文件路径
|
||||
possible_paths = [
|
||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'config.yaml'),
|
||||
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'backend', 'config', 'config.yaml'),
|
||||
os.path.join(os.getcwd(), 'config', 'config.yaml'),
|
||||
]
|
||||
|
||||
for config_path in possible_paths:
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
if config:
|
||||
self.LLM_CONFIG['OPENAI_API_BASE'] = config.get('OPENAI_API_BASE')
|
||||
self.LLM_CONFIG['OPENAI_API_KEY'] = config.get('OPENAI_API_KEY')
|
||||
self.LLM_CONFIG['OPENAI_API_MODEL'] = config.get('OPENAI_API_MODEL')
|
||||
print(f"已加载 LLM 配置: {self.LLM_CONFIG['OPENAI_API_MODEL']}")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"加载 LLM 配置失败: {e}")
|
||||
|
||||
def generate(self, task_data: Dict[str, Any], file_path: str) -> bool:
|
||||
"""生成 Markdown 文件(调用 LLM 生成报告)"""
|
||||
try:
|
||||
# 1. 准备数据
|
||||
task_name = task_data.get('task_name', '未命名任务')
|
||||
task_outline = task_data.get('task_outline')
|
||||
rehearsal_log = task_data.get('rehearsal_log')
|
||||
agent_scores = task_data.get('agent_scores')
|
||||
|
||||
# 2. 提取参与智能体(从 task_outline 的 Collaboration Process 中提取)
|
||||
agents = self._extract_agents(task_outline)
|
||||
|
||||
# 3. 过滤 agent_scores(只保留参与当前任务的智能体评分)
|
||||
filtered_agent_scores = self._filter_agent_scores(agent_scores, agents)
|
||||
|
||||
# 4. 格式化数据为 JSON 字符串
|
||||
task_outline_str = json.dumps(task_outline, ensure_ascii=False, indent=2) if task_outline else '无'
|
||||
rehearsal_log_str = json.dumps(rehearsal_log, ensure_ascii=False, indent=2) if rehearsal_log else '无'
|
||||
agents_str = ', '.join(agents) if agents else '无'
|
||||
agent_scores_str = json.dumps(filtered_agent_scores, ensure_ascii=False, indent=2) if filtered_agent_scores else '无'
|
||||
|
||||
# 5. 构建 Prompt
|
||||
prompt = self.PROMPT_TEMPLATE.format(
|
||||
task_name=task_name,
|
||||
task_outline=task_outline_str,
|
||||
rehearsal_log=rehearsal_log_str,
|
||||
agents=agents_str,
|
||||
agent_scores=agent_scores_str
|
||||
)
|
||||
|
||||
# 6. 调用 LLM 生成报告
|
||||
print("正在调用大模型生成 Markdown 报告...")
|
||||
report_content = self._call_llm(prompt)
|
||||
|
||||
if not report_content:
|
||||
print("LLM 生成报告失败")
|
||||
return False
|
||||
|
||||
# 7. 清理报告内容:去掉开头的重复标题(如果存在)
|
||||
report_content = self._clean_report_title(report_content)
|
||||
|
||||
print(f"Markdown 报告生成成功,长度: {len(report_content)} 字符")
|
||||
|
||||
# 8. 保存为 Markdown 文件
|
||||
self._save_as_markdown(report_content, task_name, file_path)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Markdown LLM 导出失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _clean_report_title(self, content: str) -> str:
|
||||
"""清理报告开头的重复标题"""
|
||||
lines = content.split('\n')
|
||||
if not lines:
|
||||
return content
|
||||
|
||||
# 检查第一行是否是"任务执行报告"
|
||||
first_line = lines[0].strip()
|
||||
if first_line == '任务执行报告' or first_line == '# 任务执行报告':
|
||||
# 去掉第一行
|
||||
lines = lines[1:]
|
||||
# 去掉可能的空行
|
||||
while lines and not lines[0].strip():
|
||||
lines.pop(0)
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def _extract_agents(self, task_outline: Any) -> list:
|
||||
"""从 task_outline 中提取参与智能体列表"""
|
||||
agents = set()
|
||||
if not task_outline or not isinstance(task_outline, dict):
|
||||
return []
|
||||
|
||||
collaboration_process = task_outline.get('Collaboration Process', [])
|
||||
if not collaboration_process or not isinstance(collaboration_process, list):
|
||||
return []
|
||||
|
||||
for step in collaboration_process:
|
||||
if isinstance(step, dict):
|
||||
agent_selection = step.get('AgentSelection', [])
|
||||
if isinstance(agent_selection, list):
|
||||
for agent in agent_selection:
|
||||
if agent:
|
||||
agents.add(agent)
|
||||
|
||||
return list(agents)
|
||||
|
||||
def _filter_agent_scores(self, agent_scores: Any, agents: list) -> dict:
|
||||
"""过滤 agent_scores,只保留参与当前任务的智能体评分"""
|
||||
if not agent_scores or not isinstance(agent_scores, dict):
|
||||
return {}
|
||||
|
||||
if not agents:
|
||||
return {}
|
||||
|
||||
filtered = {}
|
||||
for step_id, step_data in agent_scores.items():
|
||||
if not isinstance(step_data, dict):
|
||||
continue
|
||||
|
||||
aspect_list = step_data.get('aspectList', [])
|
||||
agent_scores_data = step_data.get('agentScores', {})
|
||||
|
||||
if not agent_scores_data:
|
||||
continue
|
||||
|
||||
# 只保留在 agents 列表中的智能体评分
|
||||
filtered_scores = {}
|
||||
for agent_name, scores in agent_scores_data.items():
|
||||
if agent_name in agents and isinstance(scores, dict):
|
||||
filtered_scores[agent_name] = scores
|
||||
|
||||
if filtered_scores:
|
||||
filtered[step_id] = {
|
||||
'aspectList': aspect_list,
|
||||
'agentScores': filtered_scores
|
||||
}
|
||||
|
||||
return filtered
|
||||
|
||||
def _call_llm(self, prompt: str) -> str:
|
||||
"""调用大模型 API 生成报告"""
|
||||
try:
|
||||
import openai
|
||||
|
||||
# 验证配置
|
||||
if not self.LLM_CONFIG['OPENAI_API_KEY']:
|
||||
print("错误: OPENAI_API_KEY 未配置")
|
||||
return ""
|
||||
if not self.LLM_CONFIG['OPENAI_API_BASE']:
|
||||
print("错误: OPENAI_API_BASE 未配置")
|
||||
return ""
|
||||
if not self.LLM_CONFIG['OPENAI_API_MODEL']:
|
||||
print("错误: OPENAI_API_MODEL 未配置")
|
||||
return ""
|
||||
|
||||
# 配置 OpenAI 客户端
|
||||
client = openai.OpenAI(
|
||||
api_key=self.LLM_CONFIG['OPENAI_API_KEY'],
|
||||
base_url=self.LLM_CONFIG['OPENAI_API_BASE']
|
||||
)
|
||||
|
||||
# 调用 API
|
||||
response = client.chat.completions.create(
|
||||
model=self.LLM_CONFIG['OPENAI_API_MODEL'],
|
||||
messages=[
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=12000,
|
||||
)
|
||||
|
||||
if response and response.choices:
|
||||
return response.choices[0].message.content
|
||||
|
||||
return ""
|
||||
|
||||
except ImportError:
|
||||
print("请安装 openai 库: pip install openai")
|
||||
return ""
|
||||
except Exception as e:
|
||||
print(f"调用 LLM 失败: {e}")
|
||||
return ""
|
||||
|
||||
def _save_as_markdown(self, markdown_content: str, task_name: str, file_path: str):
|
||||
"""将 Markdown 内容保存为文件"""
|
||||
try:
|
||||
# 在内容前添加标题(如果还没有的话)
|
||||
if not markdown_content.strip().startswith('# '):
|
||||
markdown_content = f"# {task_name}\n\n{markdown_content}"
|
||||
|
||||
# 添加时间戳
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
footer = f"\n\n---\n*导出时间: {timestamp}*"
|
||||
|
||||
# 如果内容末尾没有明显的分隔符,添加分隔符
|
||||
if not markdown_content.rstrip().endswith('---'):
|
||||
markdown_content = markdown_content.rstrip() + footer
|
||||
else:
|
||||
# 已有分隔符,在它之前添加时间戳
|
||||
markdown_content = markdown_content.rstrip() + footer
|
||||
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
print(f"Markdown 文件已保存: {file_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"保存 Markdown 文件失败: {e}")
|
||||
raise
|
||||
@@ -1,374 +0,0 @@
|
||||
"""
|
||||
思维导图 LLM 导出器
|
||||
调用大模型生成专业的任务执行思维导图
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
class MindmapLLMExporter:
|
||||
"""思维导图 LLM 导出器 - 调用大模型生成思维导图"""
|
||||
|
||||
LLM_CONFIG = {
|
||||
'OPENAI_API_BASE': None,
|
||||
'OPENAI_API_KEY': None,
|
||||
'OPENAI_API_MODEL': None,
|
||||
}
|
||||
|
||||
PROMPT_TEMPLATE = """你是一位专业的项目管理顾问和可视化设计师。你的任务是将以下任务执行数据生成一份结构清晰、层次分明的思维导图。
|
||||
|
||||
## 任务基本信息
|
||||
- 任务名称:{task_name}
|
||||
|
||||
## 任务大纲(规划阶段)
|
||||
{task_outline}
|
||||
|
||||
## 执行结果
|
||||
{rehearsal_log}
|
||||
|
||||
## 参与智能体
|
||||
{agents}
|
||||
|
||||
## 智能体评分
|
||||
{agent_scores}
|
||||
|
||||
---
|
||||
|
||||
## 思维导图要求
|
||||
|
||||
请生成一份完整的思维导图,包含以下核心分支:
|
||||
|
||||
### 1. 任务概述
|
||||
- 任务背景与目标
|
||||
- 任务范围
|
||||
|
||||
### 2. 任务规划
|
||||
- 任务拆解
|
||||
- 智能体角色分配
|
||||
- 工作流程设计
|
||||
|
||||
### 3. 执行过程
|
||||
- 各阶段的完成情况
|
||||
- 关键决策点
|
||||
- 遇到的问题及解决方案
|
||||
|
||||
### 4. 成果产出
|
||||
- 产出物列表
|
||||
- 质量评估
|
||||
|
||||
### 5. 团队协作
|
||||
- 智能体之间的协作模式
|
||||
- 信息传递效率
|
||||
|
||||
### 6. 质量评估
|
||||
- 整体评分
|
||||
- 各维度评分
|
||||
|
||||
### 7. 经验与改进
|
||||
- 成功经验
|
||||
- 改进建议
|
||||
|
||||
---
|
||||
|
||||
## 输出格式要求
|
||||
请直接输出 JSON 格式,不要包含任何 Markdown 标记。JSON 结构如下:
|
||||
```json
|
||||
{{
|
||||
"title": "思维导图标题",
|
||||
"root": "中心主题",
|
||||
"branches": [
|
||||
{{
|
||||
"name": "分支主题名称",
|
||||
"children": [
|
||||
{{
|
||||
"name": "子主题名称",
|
||||
"children": [
|
||||
{{"name": "具体内容1"}},
|
||||
{{"name": "具体内容2"}}
|
||||
]
|
||||
}}
|
||||
]
|
||||
}},
|
||||
{{
|
||||
"name": "另一个分支主题",
|
||||
"children": [
|
||||
{{"name": "子主题A"}},
|
||||
{{"name": "子主题B"}}
|
||||
]
|
||||
}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
|
||||
注意:
|
||||
- 思维导图层次分明,每个分支至少有 2-3 层深度
|
||||
- 内容简洁,每个节点字数控制在 50-100 字以内
|
||||
- 使用简体中文
|
||||
- 确保 JSON 格式正确,不要有语法错误
|
||||
- 不要生成"报告总字数"这样的统计信息
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._load_llm_config()
|
||||
|
||||
def _load_llm_config(self):
|
||||
"""从配置文件加载 LLM 配置"""
|
||||
try:
|
||||
import yaml
|
||||
possible_paths = [
|
||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'config.yaml'),
|
||||
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'backend', 'config', 'config.yaml'),
|
||||
os.path.join(os.getcwd(), 'config', 'config.yaml'),
|
||||
]
|
||||
|
||||
for config_path in possible_paths:
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
if config:
|
||||
self.LLM_CONFIG['OPENAI_API_BASE'] = config.get('OPENAI_API_BASE')
|
||||
self.LLM_CONFIG['OPENAI_API_KEY'] = config.get('OPENAI_API_KEY')
|
||||
self.LLM_CONFIG['OPENAI_API_MODEL'] = config.get('OPENAI_API_MODEL')
|
||||
print(f"已加载 LLM 配置: {self.LLM_CONFIG['OPENAI_API_MODEL']}")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"加载 LLM 配置失败: {e}")
|
||||
|
||||
def generate(self, task_data: Dict[str, Any], file_path: str) -> bool:
|
||||
"""生成思维导图(调用 LLM 生成)"""
|
||||
try:
|
||||
task_name = task_data.get('task_name', '未命名任务')
|
||||
task_outline = task_data.get('task_outline')
|
||||
rehearsal_log = task_data.get('rehearsal_log')
|
||||
agent_scores = task_data.get('agent_scores')
|
||||
|
||||
agents = self._extract_agents(task_outline)
|
||||
filtered_agent_scores = self._filter_agent_scores(agent_scores, agents)
|
||||
|
||||
task_outline_str = json.dumps(task_outline, ensure_ascii=False, indent=2) if task_outline else '无'
|
||||
rehearsal_log_str = json.dumps(rehearsal_log, ensure_ascii=False, indent=2) if rehearsal_log else '无'
|
||||
agents_str = ', '.join(agents) if agents else '无'
|
||||
agent_scores_str = json.dumps(filtered_agent_scores, ensure_ascii=False, indent=2) if filtered_agent_scores else '无'
|
||||
|
||||
prompt = self.PROMPT_TEMPLATE.format(
|
||||
task_name=task_name,
|
||||
task_outline=task_outline_str,
|
||||
rehearsal_log=rehearsal_log_str,
|
||||
agents=agents_str,
|
||||
agent_scores=agent_scores_str
|
||||
)
|
||||
|
||||
print("正在调用大模型生成思维导图...")
|
||||
mindmap_json_str = self._call_llm(prompt)
|
||||
|
||||
if not mindmap_json_str:
|
||||
print("LLM 生成思维导图失败")
|
||||
return False
|
||||
|
||||
print(f"思维导图内容生成成功,长度: {len(mindmap_json_str)} 字符")
|
||||
self._create_mindmap_from_json(mindmap_json_str, file_path, task_name)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"思维导图 LLM 导出失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _extract_agents(self, task_outline: Any) -> list:
|
||||
"""从 task_outline 中提取参与智能体列表"""
|
||||
agents = set()
|
||||
if not task_outline or not isinstance(task_outline, dict):
|
||||
return []
|
||||
|
||||
collaboration_process = task_outline.get('Collaboration Process', [])
|
||||
if not collaboration_process or not isinstance(collaboration_process, list):
|
||||
return []
|
||||
|
||||
for step in collaboration_process:
|
||||
if isinstance(step, dict):
|
||||
agent_selection = step.get('AgentSelection', [])
|
||||
if isinstance(agent_selection, list):
|
||||
for agent in agent_selection:
|
||||
if agent:
|
||||
agents.add(agent)
|
||||
return list(agents)
|
||||
|
||||
def _filter_agent_scores(self, agent_scores: Any, agents: list) -> dict:
|
||||
"""过滤 agent_scores,只保留参与当前任务的智能体评分"""
|
||||
if not agent_scores or not isinstance(agent_scores, dict):
|
||||
return {}
|
||||
if not agents:
|
||||
return {}
|
||||
|
||||
filtered = {}
|
||||
for step_id, step_data in agent_scores.items():
|
||||
if not isinstance(step_data, dict):
|
||||
continue
|
||||
aspect_list = step_data.get('aspectList', [])
|
||||
agent_scores_data = step_data.get('agentScores', {})
|
||||
if not agent_scores_data:
|
||||
continue
|
||||
filtered_scores = {}
|
||||
for agent_name, scores in agent_scores_data.items():
|
||||
if agent_name in agents and isinstance(scores, dict):
|
||||
filtered_scores[agent_name] = scores
|
||||
if filtered_scores:
|
||||
filtered[step_id] = {'aspectList': aspect_list, 'agentScores': filtered_scores}
|
||||
return filtered
|
||||
|
||||
def _call_llm(self, prompt: str) -> str:
|
||||
"""调用大模型 API 生成思维导图"""
|
||||
try:
|
||||
import openai
|
||||
|
||||
if not self.LLM_CONFIG['OPENAI_API_KEY']:
|
||||
print("错误: OPENAI_API_KEY 未配置")
|
||||
return ""
|
||||
if not self.LLM_CONFIG['OPENAI_API_BASE']:
|
||||
print("错误: OPENAI_API_BASE 未配置")
|
||||
return ""
|
||||
if not self.LLM_CONFIG['OPENAI_API_MODEL']:
|
||||
print("错误: OPENAI_API_MODEL 未配置")
|
||||
return ""
|
||||
|
||||
client = openai.OpenAI(
|
||||
api_key=self.LLM_CONFIG['OPENAI_API_KEY'],
|
||||
base_url=self.LLM_CONFIG['OPENAI_API_BASE']
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=self.LLM_CONFIG['OPENAI_API_MODEL'],
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
temperature=0.7,
|
||||
max_tokens=8000,
|
||||
)
|
||||
|
||||
if response and response.choices:
|
||||
return response.choices[0].message.content
|
||||
return ""
|
||||
|
||||
except ImportError:
|
||||
print("请安装 openai 库: pip install openai")
|
||||
return ""
|
||||
except Exception as e:
|
||||
print(f"调用 LLM 失败: {e}")
|
||||
return ""
|
||||
|
||||
def _fix_json_string(self, json_str: str) -> str:
|
||||
"""修复常见的 JSON 语法错误"""
|
||||
import re
|
||||
|
||||
json_str = json_str.strip()
|
||||
|
||||
json_str = re.sub(r',\s*}', '}', json_str)
|
||||
json_str = re.sub(r',\s*]', ']', json_str)
|
||||
|
||||
json_str = re.sub(r'""', '"', json_str)
|
||||
|
||||
single_quotes = re.findall(r"'[^']*'", json_str)
|
||||
for sq in single_quotes:
|
||||
if '"' not in sq:
|
||||
json_str = json_str.replace(sq, sq.replace("'", '"'))
|
||||
|
||||
trailing_comma_pattern = re.compile(r',(\s*[}\]])')
|
||||
json_str = trailing_comma_pattern.sub(r'\1', json_str)
|
||||
|
||||
unquoted_key_pattern = re.compile(r'([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:')
|
||||
def fix_unquoted_key(match):
|
||||
prefix = match.group(1)
|
||||
key = match.group(2)
|
||||
return f'{prefix}"{key}":'
|
||||
json_str = unquoted_key_pattern.sub(fix_unquoted_key, json_str)
|
||||
|
||||
unquoted_value_pattern = re.compile(r':\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*([,}\]])')
|
||||
def replace_unquoted(match):
|
||||
value = match.group(1)
|
||||
end = match.group(2)
|
||||
if value.lower() in ('true', 'false', 'null'):
|
||||
return f': {value}{end}'
|
||||
else:
|
||||
return f': "{value}"{end}'
|
||||
json_str = unquoted_value_pattern.sub(replace_unquoted, json_str)
|
||||
|
||||
print(f"JSON 修复后长度: {len(json_str)} 字符")
|
||||
return json_str
|
||||
|
||||
def _create_mindmap_from_json(self, json_str: str, file_path: str, task_name: str):
|
||||
"""从 JSON 字符串创建思维导图(Markdown 格式)"""
|
||||
try:
|
||||
json_str = json_str.strip()
|
||||
if '```json' in json_str:
|
||||
json_str = json_str.split('```json')[1].split('```')[0]
|
||||
elif '```' in json_str:
|
||||
json_str = json_str.split('```')[1].split('```')[0]
|
||||
|
||||
json_str = self._fix_json_string(json_str)
|
||||
|
||||
mindmap_data = json.loads(json_str)
|
||||
|
||||
# 生成 Markdown 格式
|
||||
markdown_content = self._generate_markdown(mindmap_data, task_name)
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
print(f"思维导图已保存: {file_path}")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"JSON 解析失败: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"创建思维导图失败: {e}")
|
||||
raise
|
||||
|
||||
def _generate_markdown(self, mindmap_data: dict, task_name: str) -> str:
|
||||
"""将思维导图数据转换为 Markdown 格式(markmap 兼容)"""
|
||||
lines = []
|
||||
|
||||
# 直接使用无序列表,不需要标题和代码块包裹
|
||||
# 根节点使用 2 个空格缩进
|
||||
lines.append(f" - {task_name}")
|
||||
|
||||
branches = mindmap_data.get('branches', [])
|
||||
for branch in branches:
|
||||
# 一级分支使用 4 个空格缩进(比根节点多 2 个空格,是根节点的子节点)
|
||||
self._add_branch(branch, lines, indent=4)
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def _add_branch(self, branch: dict, lines: list, indent: int = 2):
|
||||
"""递归添加分支到思维导图"""
|
||||
name = branch.get('name', '')
|
||||
if not name:
|
||||
return
|
||||
|
||||
prefix = ' ' * indent
|
||||
lines.append(f"{prefix}- {name}")
|
||||
|
||||
children = branch.get('children', [])
|
||||
if children:
|
||||
for child in children:
|
||||
if isinstance(child, dict):
|
||||
self._add_child_node(child, lines, indent + 2)
|
||||
else:
|
||||
lines.append(f"{' ' * (indent + 2)}- {child}")
|
||||
|
||||
def _add_child_node(self, child: dict, lines: list, indent: int):
|
||||
"""添加子节点"""
|
||||
name = child.get('name', '')
|
||||
if not name:
|
||||
return
|
||||
|
||||
prefix = ' ' * indent
|
||||
lines.append(f"{prefix}- {name}")
|
||||
|
||||
children = child.get('children', [])
|
||||
if children:
|
||||
for grandchild in children:
|
||||
if isinstance(grandchild, dict):
|
||||
self._add_child_node(grandchild, lines, indent + 2)
|
||||
else:
|
||||
lines.append(f"{' ' * (indent + 2)}- {grandchild}")
|
||||
@@ -1,772 +0,0 @@
|
||||
"""
|
||||
PowerPoint 演示文稿 LLM 导出器
|
||||
调用大模型生成专业的任务执行演示文稿
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches, Pt
|
||||
from pptx.enum.text import PP_ALIGN
|
||||
from pptx.chart.data import CategoryChartData
|
||||
from pptx.enum.chart import XL_CHART_TYPE
|
||||
|
||||
|
||||
class PptLLMExporter:
|
||||
"""PowerPoint 演示文稿 LLM 导出器 - 调用大模型生成 PPT"""
|
||||
|
||||
# LLM 配置(从 config.yaml 加载)
|
||||
LLM_CONFIG = {
|
||||
'OPENAI_API_BASE': None,
|
||||
'OPENAI_API_KEY': None,
|
||||
'OPENAI_API_MODEL': None,
|
||||
}
|
||||
|
||||
# Prompt 模板 - 与 docx_llm.py 内容对齐
|
||||
PROMPT_TEMPLATE = """你是一位专业的项目管理顾问和演示文稿设计师。你的任务是将以下任务执行数据生成一份详细、专业的 PowerPoint 演示文稿内容。
|
||||
|
||||
## 任务基本信息
|
||||
- 任务名称:{task_name}
|
||||
|
||||
## 任务大纲(规划阶段)
|
||||
{task_outline}
|
||||
|
||||
## 执行结果
|
||||
{execution_results}
|
||||
|
||||
## 参与智能体
|
||||
{agents}
|
||||
|
||||
## 智能体评分
|
||||
{agent_scores}
|
||||
|
||||
---
|
||||
|
||||
## 演示文稿要求
|
||||
|
||||
请生成一份完整的演示文稿,包含以下幻灯片(共11页):
|
||||
|
||||
1. 封面页:标题、用户ID、日期
|
||||
2. 目录页:5个主要章节(其他作为小点)
|
||||
3. 执行摘要:任务整体执行情况概述
|
||||
4. 任务概述:任务背景与目标、任务范围与边界
|
||||
5. 任务规划分析:任务拆解的合理性、智能体角色分配的优化建议、工作流程设计
|
||||
6. 执行过程回顾:各阶段的完成情况、关键决策点、遇到的问题及解决方案
|
||||
7. 成果产出分析:产出物的质量评估、产出与预期目标的匹配度
|
||||
8. 团队协作分析:智能体之间的协作模式、信息传递效率
|
||||
9. 质量评估:整体完成质量评分(1-10分)、各维度的具体评分及理由
|
||||
10. 经验教训与改进建议:成功经验、存在的问题与不足、改进建议
|
||||
11. 结束页:感谢语
|
||||
|
||||
---
|
||||
|
||||
## 输出格式要求
|
||||
请直接输出 JSON 格式,不要包含任何 Markdown 标记。JSON 结构如下:
|
||||
```json
|
||||
{{
|
||||
"title": "{task_name}",
|
||||
"user_id": "{user_id}",
|
||||
"date": "{date}",
|
||||
"slides": [
|
||||
{{
|
||||
"type": "title",
|
||||
"title": "主标题",
|
||||
"user_id": "用户ID",
|
||||
"date": "日期"
|
||||
}},
|
||||
{{
|
||||
"type": "toc",
|
||||
"title": "目录",
|
||||
"sections": [
|
||||
{{"title": "一、执行摘要", "subs": []}},
|
||||
{{"title": "二、任务概述与规划", "subs": ["任务背景与目标", "任务范围与边界", "任务拆解", "智能体角色"]}},
|
||||
{{"title": "三、执行过程与成果", "subs": ["阶段完成情况", "关键决策点", "产出质量", "目标匹配度"]}},
|
||||
{{"title": "四、团队协作与质量评估", "subs": ["协作模式", "信息传递", "质量评分"]}},
|
||||
{{"title": "五、经验建议与总结", "subs": ["成功经验", "问题与不足", "改进建议"]}}
|
||||
]
|
||||
}},
|
||||
{{
|
||||
"type": "content",
|
||||
"title": "执行摘要",
|
||||
"bullets": ["要点1", "要点2", "要点3"]
|
||||
}},
|
||||
{{
|
||||
"type": "content",
|
||||
"title": "任务概述",
|
||||
"bullets": ["任务背景与目标", "任务范围与边界"]
|
||||
}},
|
||||
{{
|
||||
"type": "content",
|
||||
"title": "任务规划分析",
|
||||
"bullets": ["任务拆解的合理性", "智能体角色分配", "工作流程设计"]
|
||||
}},
|
||||
{{
|
||||
"type": "content",
|
||||
"title": "执行过程回顾",
|
||||
"bullets": ["各阶段完成情况", "关键决策点", "遇到的问题及解决方案"]
|
||||
}},
|
||||
{{
|
||||
"type": "content",
|
||||
"title": "成果产出分析",
|
||||
"bullets": ["产出物质量评估", "与预期目标匹配度"]
|
||||
}},
|
||||
{{
|
||||
"type": "content",
|
||||
"title": "团队协作分析",
|
||||
"bullets": ["智能体协作模式", "信息传递效率"]
|
||||
}},
|
||||
{{
|
||||
"type": "content",
|
||||
"title": "质量评估",
|
||||
"bullets": ["整体评分(1-10分)", "各维度评分及理由"]
|
||||
}},
|
||||
{{
|
||||
"type": "content",
|
||||
"title": "经验教训与改进建议",
|
||||
"bullets": ["成功经验", "存在的问题与不足", "改进建议"]
|
||||
}},
|
||||
{{
|
||||
"type": "ending",
|
||||
"title": "感谢聆听"
|
||||
}},
|
||||
{{
|
||||
"type": "table",
|
||||
"title": "质量评分表",
|
||||
"headers": ["维度", "评分", "说明"],
|
||||
"rows": [["整体质量", "8.5", "表现优秀"], ["协作效率", "7.5", "有待提升"]]
|
||||
}},
|
||||
{{
|
||||
"type": "chart",
|
||||
"title": "任务完成进度",
|
||||
"chart_type": "bar",
|
||||
"categories": ["规划阶段", "执行阶段", "评估阶段"],
|
||||
"series": [
|
||||
{{"name": "完成度", "values": [100, 85, 90]}}
|
||||
]
|
||||
}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
|
||||
**重要说明:**
|
||||
1. **幻灯片顺序**:slides 数组中的幻灯片顺序必须严格按照以下顺序:
|
||||
- 第1页:type="title" 封面页
|
||||
- 第2页:type="toc" 目录页
|
||||
- 第3-10页:type="content" 内容页(8页左右)
|
||||
- 第11-14页:type="table" 或 "chart" 表格/图表页
|
||||
- 最后一页:type="ending" 结束页
|
||||
|
||||
2. **每张幻灯片必须包含完整的字段**:
|
||||
- `content` 类型必须有 `title` 和 `bullets` 数组
|
||||
- `table` 类型必须有 `title`、`headers` 数组和 `rows` 数组
|
||||
- `chart` 类型必须有 `title`、`chart_type`、`categories` 数组和 `series` 数组
|
||||
- `ending` 类型必须有 `title`
|
||||
|
||||
3. **禁止出现"单击此处添加标题"**:所有幻灯片都必须填写完整内容,不能留空
|
||||
|
||||
注意:
|
||||
- 幻灯片数量为11-15 页
|
||||
- 每页内容详细一点,使用要点式呈现,需要表格或者图表增强可读性
|
||||
- 不要生成"报告总字数"这样的统计信息
|
||||
- 所有文字使用简体中文
|
||||
- 根据实际任务数据生成内容,不是编造
|
||||
- **禁止使用中文引号「」『』""** ,所有引号必须是英文双引号 ""
|
||||
- user_id 填写:{user_id}
|
||||
- date 填写:{date}
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._load_llm_config()
|
||||
|
||||
def _load_llm_config(self):
|
||||
"""从配置文件加载 LLM 配置"""
|
||||
try:
|
||||
import yaml
|
||||
possible_paths = [
|
||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'config.yaml'),
|
||||
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'backend', 'config', 'config.yaml'),
|
||||
os.path.join(os.getcwd(), 'config', 'config.yaml'),
|
||||
]
|
||||
|
||||
for config_path in possible_paths:
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
if config:
|
||||
self.LLM_CONFIG['OPENAI_API_BASE'] = config.get('OPENAI_API_BASE')
|
||||
self.LLM_CONFIG['OPENAI_API_KEY'] = config.get('OPENAI_API_KEY')
|
||||
self.LLM_CONFIG['OPENAI_API_MODEL'] = config.get('OPENAI_API_MODEL')
|
||||
print(f"已加载 LLM 配置: {self.LLM_CONFIG['OPENAI_API_MODEL']}")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"加载 LLM 配置失败: {e}")
|
||||
|
||||
def generate(self, task_data: Dict[str, Any], file_path: str) -> bool:
|
||||
"""生成 PowerPoint 演示文稿(调用 LLM 生成)"""
|
||||
try:
|
||||
task_name = task_data.get('task_name', '未命名任务')
|
||||
task_content = task_data.get('task_content', '')
|
||||
task_outline = task_data.get('task_outline')
|
||||
rehearsal_log = task_data.get('rehearsal_log')
|
||||
agent_scores = task_data.get('agent_scores')
|
||||
user_id = task_data.get('user_id', '')
|
||||
date = task_data.get('date', '')
|
||||
|
||||
agents = self._extract_agents(task_outline)
|
||||
filtered_agent_scores = self._filter_agent_scores(agent_scores, agents)
|
||||
|
||||
task_outline_str = json.dumps(task_outline, ensure_ascii=False, indent=2) if task_outline else '无'
|
||||
rehearsal_log_str = json.dumps(rehearsal_log, ensure_ascii=False, indent=2) if rehearsal_log else '无'
|
||||
agents_str = ', '.join(agents) if agents else '无'
|
||||
agent_scores_str = json.dumps(filtered_agent_scores, ensure_ascii=False, indent=2) if filtered_agent_scores else '无'
|
||||
|
||||
prompt = self.PROMPT_TEMPLATE.format(
|
||||
task_name=task_name,
|
||||
task_content=task_content,
|
||||
task_outline=task_outline_str,
|
||||
execution_results=rehearsal_log_str,
|
||||
agents=agents_str,
|
||||
agent_scores=agent_scores_str,
|
||||
user_id=user_id,
|
||||
date=date
|
||||
)
|
||||
|
||||
print("正在调用大模型生成 PPT 内容...")
|
||||
ppt_json_str = self._call_llm(prompt)
|
||||
|
||||
if not ppt_json_str:
|
||||
print("LLM 生成 PPT 失败")
|
||||
return False
|
||||
|
||||
print(f"PPT 内容生成成功,长度: {len(ppt_json_str)} 字符")
|
||||
self._create_ppt_from_json(ppt_json_str, file_path, task_name)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"PPT LLM 导出失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _extract_agents(self, task_outline: Any) -> list:
|
||||
agents = set()
|
||||
if not task_outline or not isinstance(task_outline, dict):
|
||||
return []
|
||||
|
||||
collaboration_process = task_outline.get('Collaboration Process', [])
|
||||
if not collaboration_process or not isinstance(collaboration_process, list):
|
||||
return []
|
||||
|
||||
for step in collaboration_process:
|
||||
if isinstance(step, dict):
|
||||
agent_selection = step.get('AgentSelection', [])
|
||||
if isinstance(agent_selection, list):
|
||||
for agent in agent_selection:
|
||||
if agent:
|
||||
agents.add(agent)
|
||||
return list(agents)
|
||||
|
||||
def _filter_agent_scores(self, agent_scores: Any, agents: list) -> dict:
|
||||
if not agent_scores or not isinstance(agent_scores, dict):
|
||||
return {}
|
||||
if not agents:
|
||||
return {}
|
||||
|
||||
filtered = {}
|
||||
for step_id, step_data in agent_scores.items():
|
||||
if not isinstance(step_data, dict):
|
||||
continue
|
||||
aspect_list = step_data.get('aspectList', [])
|
||||
agent_scores_data = step_data.get('agentScores', {})
|
||||
if not agent_scores_data:
|
||||
continue
|
||||
filtered_scores = {}
|
||||
for agent_name, scores in agent_scores_data.items():
|
||||
if agent_name in agents and isinstance(scores, dict):
|
||||
filtered_scores[agent_name] = scores
|
||||
if filtered_scores:
|
||||
filtered[step_id] = {'aspectList': aspect_list, 'agentScores': filtered_scores}
|
||||
return filtered
|
||||
|
||||
def _call_llm(self, prompt: str) -> str:
|
||||
try:
|
||||
import openai
|
||||
if not self.LLM_CONFIG['OPENAI_API_KEY']:
|
||||
print("错误: OPENAI_API_KEY 未配置")
|
||||
return ""
|
||||
if not self.LLM_CONFIG['OPENAI_API_BASE']:
|
||||
print("错误: OPENAI_API_BASE 未配置")
|
||||
return ""
|
||||
if not self.LLM_CONFIG['OPENAI_API_MODEL']:
|
||||
print("错误: OPENAI_API_MODEL 未配置")
|
||||
return ""
|
||||
|
||||
client = openai.OpenAI(
|
||||
api_key=self.LLM_CONFIG['OPENAI_API_KEY'],
|
||||
base_url=self.LLM_CONFIG['OPENAI_API_BASE']
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=self.LLM_CONFIG['OPENAI_API_MODEL'],
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
temperature=0.7,
|
||||
max_tokens=8000,
|
||||
)
|
||||
|
||||
if response and response.choices:
|
||||
return response.choices[0].message.content
|
||||
return ""
|
||||
|
||||
except ImportError:
|
||||
print("请安装 openai 库: pip install openai")
|
||||
return ""
|
||||
except Exception as e:
|
||||
print(f"调用 LLM 失败: {e}")
|
||||
return ""
|
||||
|
||||
def _fix_json_string(self, json_str: str) -> str:
|
||||
"""修复常见的 JSON 语法错误"""
|
||||
import re
|
||||
|
||||
json_str = json_str.strip()
|
||||
|
||||
# 首先尝试提取 JSON 代码块
|
||||
if '```json' in json_str:
|
||||
json_str = json_str.split('```json')[1].split('```')[0]
|
||||
elif '```' in json_str:
|
||||
# 检查是否有结束标记
|
||||
parts = json_str.split('```')
|
||||
if len(parts) >= 3:
|
||||
json_str = parts[1]
|
||||
else:
|
||||
# 没有结束标记,可能是代码块内容
|
||||
json_str = parts[1] if len(parts) > 1 else json_str
|
||||
|
||||
json_str = json_str.strip()
|
||||
|
||||
# 关键修复:处理中文双引号包含英文双引号的情况
|
||||
# 例如:"工作流程采用"提出-评估-改进"的模式"
|
||||
# 需要:找到中文引号对,去除它们,同时转义内部的英文引号
|
||||
import re
|
||||
|
||||
# 中文引号 Unicode
|
||||
CHINESE_LEFT = '\u201c' # "
|
||||
CHINESE_RIGHT = '\u201d' # "
|
||||
|
||||
# 使用正则匹配中文引号对之间的内容,并处理内部的英文引号
|
||||
# 匹配 "内容" 格式(中文引号对)
|
||||
pattern = re.compile(r'(\u201c)(.*?)(\u201d)')
|
||||
def replace_chinese_quotes(match):
|
||||
content = match.group(2)
|
||||
# 将内部未转义的英文双引号转义
|
||||
content = re.sub(r'(?<!\\)"', r'\\"', content)
|
||||
return content
|
||||
|
||||
json_str = pattern.sub(replace_chinese_quotes, json_str)
|
||||
|
||||
# 现在移除所有中文引号字符(已经被处理过了)
|
||||
json_str = json_str.replace(CHINESE_LEFT, '')
|
||||
json_str = json_str.replace(CHINESE_RIGHT, '')
|
||||
json_str = json_str.replace('\u2018', "'") # 中文左单引号 '
|
||||
json_str = json_str.replace('\u2019', "'") # 中文右单引号 '
|
||||
|
||||
# 移除行尾的逗号(trailing comma)
|
||||
json_str = re.sub(r',\s*}', '}', json_str)
|
||||
json_str = re.sub(r',\s*]', ']', json_str)
|
||||
|
||||
# 修复空字符串
|
||||
json_str = re.sub(r'""', '"', json_str)
|
||||
|
||||
# 修复单引号
|
||||
single_quotes = re.findall(r"'[^']*'", json_str)
|
||||
for sq in single_quotes:
|
||||
if '"' not in sq:
|
||||
json_str = json_str.replace(sq, sq.replace("'", '"'))
|
||||
|
||||
# 移除 trailing comma
|
||||
trailing_comma_pattern = re.compile(r',(\s*[}\]])')
|
||||
json_str = trailing_comma_pattern.sub(r'\1', json_str)
|
||||
|
||||
# 修复未加引号的 key
|
||||
unquoted_key_pattern = re.compile(r'([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:')
|
||||
def fix_unquoted_key(match):
|
||||
prefix = match.group(1)
|
||||
key = match.group(2)
|
||||
return f'{prefix}"{key}":'
|
||||
json_str = unquoted_key_pattern.sub(fix_unquoted_key, json_str)
|
||||
|
||||
# 修复未加引号的值(但保留 true/false/null)
|
||||
unquoted_value_pattern = re.compile(r':\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*([,}\]])')
|
||||
def replace_unquoted(match):
|
||||
value = match.group(1)
|
||||
end = match.group(2)
|
||||
if value.lower() in ('true', 'false', 'null'):
|
||||
return f': {value}{end}'
|
||||
else:
|
||||
return f': "{value}"{end}'
|
||||
json_str = unquoted_value_pattern.sub(replace_unquoted, json_str)
|
||||
|
||||
# 新增:修复多余的逗号(连续逗号)
|
||||
json_str = re.sub(r',,+,', ',', json_str)
|
||||
|
||||
# 新增:修复缺少冒号的情况(key 后面没有冒号)
|
||||
# 匹配 "key" value 而不是 "key": value
|
||||
json_str = re.sub(r'(\"[^\"]+\")\s+([\[{])', r'\1: \2', json_str)
|
||||
|
||||
# 新增:尝试修复换行符问题,将单个 \n 替换为 \\n
|
||||
# 但这可能会导致问题,所以先注释掉
|
||||
# json_str = json_str.replace('\n', '\\n')
|
||||
|
||||
# 新增:移除可能的 BOM 或不可见字符
|
||||
json_str = json_str.replace('\ufeff', '')
|
||||
|
||||
print(f"JSON 修复后长度: {len(json_str)} 字符")
|
||||
return json_str
|
||||
|
||||
def _create_ppt_from_json(self, json_str: str, file_path: str, task_name: str):
|
||||
"""从 JSON 字符串创建 PPT(直接创建,不使用模板)"""
|
||||
try:
|
||||
json_str = json_str.strip()
|
||||
if '```json' in json_str:
|
||||
json_str = json_str.split('```json')[1].split('```')[0]
|
||||
elif '```' in json_str:
|
||||
json_str = json_str.split('```')[1].split('```')[0]
|
||||
|
||||
json_str = self._fix_json_string(json_str)
|
||||
|
||||
# 尝试解析,失败时输出更多信息帮助调试
|
||||
try:
|
||||
replace_data = json.loads(json_str)
|
||||
except json.JSONDecodeError as e:
|
||||
# 输出错误位置附近的 JSON 内容帮助调试
|
||||
print(f"JSON 解析失败: {e}")
|
||||
print(f"错误位置: 第 {e.lineno} 行, 第 {e.colno} 列")
|
||||
start = max(0, e.pos - 100)
|
||||
end = min(len(json_str), e.pos + 100)
|
||||
context = json_str[start:end]
|
||||
print(f"错误上下文: ...{context}...")
|
||||
raise
|
||||
|
||||
# 直接创建空白演示文稿,不使用模板
|
||||
prs = Presentation()
|
||||
prs.slide_width = Inches(13.333)
|
||||
prs.slide_height = Inches(7.5)
|
||||
|
||||
slides_data = replace_data.get('slides', [])
|
||||
|
||||
# 分离 ending 幻灯片和其他幻灯片,确保 ending 在最后
|
||||
ending_slides = [s for s in slides_data if s.get('type') == 'ending']
|
||||
other_slides = [s for s in slides_data if s.get('type') != 'ending']
|
||||
|
||||
# 先创建其他幻灯片
|
||||
for slide_data in other_slides:
|
||||
slide_type = slide_data.get('type', 'content')
|
||||
|
||||
if slide_type == 'title':
|
||||
self._add_title_slide(prs, slide_data)
|
||||
elif slide_type == 'toc':
|
||||
self._add_toc_slide(prs, slide_data)
|
||||
elif slide_type == 'content':
|
||||
self._add_content_slide(prs, slide_data)
|
||||
elif slide_type == 'two_column':
|
||||
self._add_two_column_slide(prs, slide_data)
|
||||
elif slide_type == 'table':
|
||||
self._add_table_slide(prs, slide_data)
|
||||
elif slide_type == 'chart':
|
||||
self._add_chart_slide(prs, slide_data)
|
||||
|
||||
# 最后创建 ending 幻灯片
|
||||
for slide_data in ending_slides:
|
||||
self._add_ending_slide(prs, slide_data)
|
||||
|
||||
prs.save(file_path)
|
||||
print(f"PowerPoint 已保存: {file_path}")
|
||||
|
||||
except ImportError:
|
||||
print("请安装 python-pptx 库: pip install python-pptx")
|
||||
raise
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"JSON 解析失败: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"创建 PPT 失败: {e}")
|
||||
raise
|
||||
|
||||
def _add_title_slide(self, prs, data):
|
||||
"""添加封面页"""
|
||||
slide_layout = prs.slide_layouts[6] # 空白布局
|
||||
slide = prs.slides.add_slide(slide_layout)
|
||||
|
||||
# 删除默认的占位符形状
|
||||
for shape in list(slide.shapes):
|
||||
if shape.has_text_frame and shape.text_frame.text.strip() in ['单击此处添加标题', '单击此处添加内容', 'Click to add title', 'Click to add text']:
|
||||
slide.shapes.remove(shape)
|
||||
|
||||
# 主标题
|
||||
title_box = slide.shapes.add_textbox(Inches(0.5), Inches(2.5), Inches(12.333), Inches(1.5))
|
||||
tf = title_box.text_frame
|
||||
p = tf.paragraphs[0]
|
||||
p.text = data.get('title', '')
|
||||
p.font.size = Pt(44)
|
||||
p.font.bold = True
|
||||
p.alignment = PP_ALIGN.CENTER
|
||||
|
||||
# 用户ID
|
||||
user_id_box = slide.shapes.add_textbox(Inches(0.5), Inches(4.2), Inches(12.333), Inches(0.5))
|
||||
tf = user_id_box.text_frame
|
||||
p = tf.paragraphs[0]
|
||||
p.text = f"用户ID: {data.get('user_id', '')}"
|
||||
p.font.size = Pt(18)
|
||||
p.alignment = PP_ALIGN.CENTER
|
||||
|
||||
# 日期
|
||||
date_box = slide.shapes.add_textbox(Inches(0.5), Inches(4.8), Inches(12.333), Inches(0.5))
|
||||
tf = date_box.text_frame
|
||||
p = tf.paragraphs[0]
|
||||
p.text = f"日期: {data.get('date', '')}"
|
||||
p.font.size = Pt(18)
|
||||
p.alignment = PP_ALIGN.CENTER
|
||||
|
||||
def _add_toc_slide(self, prs, data):
|
||||
"""添加目录页"""
|
||||
slide_layout = prs.slide_layouts[6] # 空白布局
|
||||
slide = prs.slides.add_slide(slide_layout)
|
||||
|
||||
# 删除默认的占位符形状
|
||||
for shape in list(slide.shapes):
|
||||
if shape.has_text_frame and shape.text_frame.text.strip() in ['单击此处添加标题', '单击此处添加内容', 'Click to add title', 'Click to add text']:
|
||||
slide.shapes.remove(shape)
|
||||
|
||||
# 标题
|
||||
title_box = slide.shapes.add_textbox(Inches(0.5), Inches(0.4), Inches(12.333), Inches(0.8))
|
||||
tf = title_box.text_frame
|
||||
p = tf.paragraphs[0]
|
||||
p.text = data.get('title', '目录')
|
||||
p.font.size = Pt(36)
|
||||
p.font.bold = True
|
||||
|
||||
# 目录内容 - 使用两栏布局
|
||||
content_box = slide.shapes.add_textbox(Inches(0.5), Inches(1.5), Inches(12.333), Inches(5.5))
|
||||
tf = content_box.text_frame
|
||||
tf.word_wrap = True
|
||||
|
||||
sections = data.get('sections', [])
|
||||
|
||||
for i, section in enumerate(sections):
|
||||
section_title = section.get('title', '')
|
||||
subs = section.get('subs', [])
|
||||
|
||||
if i == 0:
|
||||
p = tf.paragraphs[0]
|
||||
else:
|
||||
p = tf.add_paragraph()
|
||||
|
||||
# 大章节标题
|
||||
p.text = section_title
|
||||
p.level = 0
|
||||
p.font.size = Pt(20)
|
||||
p.font.bold = True
|
||||
|
||||
# 小点
|
||||
for sub in subs:
|
||||
p_sub = tf.add_paragraph()
|
||||
p_sub.text = sub
|
||||
p_sub.level = 1 # 次级缩进
|
||||
p_sub.font.size = Pt(16)
|
||||
|
||||
def _add_ending_slide(self, prs, data):
|
||||
"""添加结束页"""
|
||||
slide_layout = prs.slide_layouts[6] # 空白布局
|
||||
slide = prs.slides.add_slide(slide_layout)
|
||||
|
||||
# 删除默认的占位符形状
|
||||
for shape in list(slide.shapes):
|
||||
if shape.has_text_frame and shape.text_frame.text.strip() in ['单击此处添加标题', '单击此处添加内容', 'Click to add title', 'Click to add text']:
|
||||
slide.shapes.remove(shape)
|
||||
|
||||
# 结束语
|
||||
title_box = slide.shapes.add_textbox(Inches(0.5), Inches(3), Inches(12.333), Inches(1.5))
|
||||
tf = title_box.text_frame
|
||||
p = tf.paragraphs[0]
|
||||
p.text = data.get('title', '感谢聆听')
|
||||
p.font.size = Pt(44)
|
||||
p.font.bold = True
|
||||
p.alignment = PP_ALIGN.CENTER
|
||||
|
||||
def _add_content_slide(self, prs, data):
|
||||
"""添加内容页"""
|
||||
slide_layout = prs.slide_layouts[6] # 空白布局
|
||||
slide = prs.slides.add_slide(slide_layout)
|
||||
|
||||
# 删除默认的占位符形状(避免出现"单击此处添加标题")
|
||||
for shape in list(slide.shapes):
|
||||
if shape.has_text_frame and shape.text_frame.text.strip() in ['单击此处添加标题', '单击此处添加内容', 'Click to add title', 'Click to add text']:
|
||||
slide.shapes.remove(shape)
|
||||
|
||||
# 标题
|
||||
title_box = slide.shapes.add_textbox(Inches(0.5), Inches(0.3), Inches(12.333), Inches(0.8))
|
||||
tf = title_box.text_frame
|
||||
p = tf.paragraphs[0]
|
||||
p.text = data.get('title', '')
|
||||
p.font.size = Pt(32)
|
||||
p.font.bold = True
|
||||
|
||||
# 内容
|
||||
content_box = slide.shapes.add_textbox(Inches(0.5), Inches(1.3), Inches(12.333), Inches(5.8))
|
||||
tf = content_box.text_frame
|
||||
tf.word_wrap = True
|
||||
|
||||
bullets = data.get('bullets', [])
|
||||
|
||||
for i, bullet in enumerate(bullets):
|
||||
if i == 0:
|
||||
p = tf.paragraphs[0]
|
||||
else:
|
||||
p = tf.add_paragraph()
|
||||
p.text = bullet
|
||||
p.level = 0
|
||||
p.font.size = Pt(18)
|
||||
|
||||
def _add_two_column_slide(self, prs, data):
|
||||
"""添加双栏内容页"""
|
||||
slide_layout = prs.slide_layouts[6] # 空白布局
|
||||
slide = prs.slides.add_slide(slide_layout)
|
||||
|
||||
# 删除默认的占位符形状
|
||||
for shape in list(slide.shapes):
|
||||
if shape.has_text_frame and shape.text_frame.text.strip() in ['单击此处添加标题', '单击此处添加内容', 'Click to add title', 'Click to add text']:
|
||||
slide.shapes.remove(shape)
|
||||
|
||||
# 标题
|
||||
title_box = slide.shapes.add_textbox(Inches(0.5), Inches(0.3), Inches(12.333), Inches(0.8))
|
||||
tf = title_box.text_frame
|
||||
p = tf.paragraphs[0]
|
||||
p.text = data.get('title', '')
|
||||
p.font.size = Pt(32)
|
||||
p.font.bold = True
|
||||
|
||||
# 左侧
|
||||
left_box = slide.shapes.add_textbox(Inches(0.5), Inches(1.3), Inches(5.8), Inches(5.8))
|
||||
tf = left_box.text_frame
|
||||
tf.word_wrap = True
|
||||
|
||||
left_title = data.get('left_title', '')
|
||||
p = tf.paragraphs[0]
|
||||
p.text = left_title
|
||||
p.font.size = Pt(22)
|
||||
p.font.bold = True
|
||||
|
||||
left_bullets = data.get('left_bullets', [])
|
||||
for bullet in left_bullets:
|
||||
p = tf.add_paragraph()
|
||||
p.text = bullet
|
||||
p.level = 1
|
||||
p.font.size = Pt(16)
|
||||
|
||||
# 右侧
|
||||
right_box = slide.shapes.add_textbox(Inches(7.0), Inches(1.3), Inches(5.8), Inches(5.8))
|
||||
tf = right_box.text_frame
|
||||
tf.word_wrap = True
|
||||
|
||||
right_title = data.get('right_title', '')
|
||||
p = tf.paragraphs[0]
|
||||
p.text = right_title
|
||||
p.font.size = Pt(22)
|
||||
p.font.bold = True
|
||||
|
||||
right_bullets = data.get('right_bullets', [])
|
||||
for bullet in right_bullets:
|
||||
p = tf.add_paragraph()
|
||||
p.text = bullet
|
||||
p.level = 1
|
||||
p.font.size = Pt(16)
|
||||
|
||||
def _add_table_slide(self, prs, data):
|
||||
"""添加表格页"""
|
||||
slide_layout = prs.slide_layouts[6] # 空白布局
|
||||
slide = prs.slides.add_slide(slide_layout)
|
||||
|
||||
# 删除默认的占位符形状
|
||||
for shape in list(slide.shapes):
|
||||
if shape.has_text_frame and shape.text_frame.text.strip() in ['单击此处添加标题', '单击此处添加内容', 'Click to add title', 'Click to add text']:
|
||||
slide.shapes.remove(shape)
|
||||
|
||||
# 标题
|
||||
title_box = slide.shapes.add_textbox(Inches(0.5), Inches(0.3), Inches(12), Inches(0.6))
|
||||
tf = title_box.text_frame
|
||||
p = tf.paragraphs[0]
|
||||
p.text = data.get('title', '')
|
||||
p.font.size = Pt(32)
|
||||
p.font.bold = True
|
||||
|
||||
# 表格
|
||||
headers = data.get('headers', [])
|
||||
rows = data.get('rows', [])
|
||||
|
||||
if headers and rows:
|
||||
x, y, cx, cy = Inches(0.5), Inches(1.5), Inches(12), Inches(3)
|
||||
table = slide.shapes.add_table(len(rows) + 1, len(headers), x, y, cx, cy).table
|
||||
|
||||
# 表头
|
||||
for i, header in enumerate(headers):
|
||||
table.cell(0, i).text = header
|
||||
|
||||
# 数据
|
||||
for row_idx, row_data in enumerate(rows):
|
||||
for col_idx, cell_data in enumerate(row_data):
|
||||
table.cell(row_idx + 1, col_idx).text = cell_data
|
||||
|
||||
def _add_chart_slide(self, prs, data):
|
||||
"""添加图表页"""
|
||||
slide_layout = prs.slide_layouts[6] # 空白布局
|
||||
slide = prs.slides.add_slide(slide_layout)
|
||||
|
||||
# 删除默认的占位符形状
|
||||
for shape in list(slide.shapes):
|
||||
if shape.has_text_frame and shape.text_frame.text.strip() in ['单击此处添加标题', '单击此处添加内容', 'Click to add title', 'Click to add text']:
|
||||
slide.shapes.remove(shape)
|
||||
|
||||
# 标题
|
||||
title_box = slide.shapes.add_textbox(Inches(0.5), Inches(0.3), Inches(12.333), Inches(0.6))
|
||||
tf = title_box.text_frame
|
||||
p = tf.paragraphs[0]
|
||||
p.text = data.get('title', '')
|
||||
p.font.size = Pt(32)
|
||||
p.font.bold = True
|
||||
|
||||
# 图表数据
|
||||
chart_type_str = data.get('chart_type', 'bar').lower()
|
||||
categories = data.get('categories', [])
|
||||
series_list = data.get('series', [])
|
||||
|
||||
# 映射图表类型
|
||||
chart_type_map = {
|
||||
'bar': XL_CHART_TYPE.COLUMN_CLUSTERED,
|
||||
'column': XL_CHART_TYPE.COLUMN_CLUSTERED,
|
||||
'line': XL_CHART_TYPE.LINE,
|
||||
'pie': XL_CHART_TYPE.PIE,
|
||||
'饼图': XL_CHART_TYPE.PIE,
|
||||
'柱状图': XL_CHART_TYPE.COLUMN_CLUSTERED,
|
||||
'折线图': XL_CHART_TYPE.LINE,
|
||||
}
|
||||
chart_type = chart_type_map.get(chart_type_str, XL_CHART_TYPE.COLUMN_CLUSTERED)
|
||||
|
||||
# 创建图表数据
|
||||
chart_data = CategoryChartData()
|
||||
chart_data.categories = categories
|
||||
|
||||
for series in series_list:
|
||||
series_name = series.get('name', '数据')
|
||||
values = series.get('values', [])
|
||||
chart_data.add_series(series_name, values)
|
||||
|
||||
# 添加图表
|
||||
x, y, cx, cy = Inches(0.5), Inches(1.2), Inches(12.333), Inches(5.5)
|
||||
chart = slide.shapes.add_chart(chart_type, x, y, cx, cy, chart_data).chart
|
||||
|
||||
# 兼容旧版本
|
||||
def _load_llm_config_old(self):
|
||||
self._load_llm_config()
|
||||
@@ -1,481 +0,0 @@
|
||||
"""
|
||||
Excel 文档 LLM 报告导出器
|
||||
调用大模型生成专业的任务执行报告,并保存为Excel格式
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
|
||||
try:
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class XlsxLLMExporter:
|
||||
"""Excel 文档 LLM 报告导出器 - 调用大模型生成报告"""
|
||||
|
||||
LLM_CONFIG = {
|
||||
'OPENAI_API_BASE': None,
|
||||
'OPENAI_API_KEY': None,
|
||||
'OPENAI_API_MODEL': None,
|
||||
}
|
||||
|
||||
PROMPT_TEMPLATE = """你是一位专业的项目管理顾问和数据分析专家。你的任务是将以下任务执行数据生成一份详细、专业、结构化的执行报告。
|
||||
|
||||
## 任务基本信息
|
||||
- 任务名称:{task_name}
|
||||
|
||||
## 任务大纲(规划阶段)
|
||||
{task_outline}
|
||||
|
||||
## 执行结果
|
||||
{rehearsal_log}
|
||||
|
||||
## 参与智能体
|
||||
{agents}
|
||||
|
||||
## 智能体评分
|
||||
{agent_scores}
|
||||
|
||||
---
|
||||
|
||||
## 报告要求
|
||||
|
||||
请生成一份完整的任务执行报告,包含以下章节:
|
||||
|
||||
### 1. 执行摘要
|
||||
用 2-3 句话概括本次任务的整体执行情况。
|
||||
|
||||
### 2. 任务概述
|
||||
- 任务背景与目标
|
||||
- 任务范围与边界
|
||||
|
||||
### 3. 任务规划分析
|
||||
- 任务拆解的合理性
|
||||
- 智能体角色分配的优化建议
|
||||
- 工作流程设计
|
||||
|
||||
### 4. 执行过程回顾
|
||||
- 各阶段的完成情况
|
||||
- 关键决策点
|
||||
- 遇到的问题及解决方案
|
||||
|
||||
### 5. 成果产出分析
|
||||
- 产出物的质量评估
|
||||
- 产出与预期目标的匹配度
|
||||
|
||||
### 6. 团队协作分析
|
||||
- 智能体之间的协作模式
|
||||
- 信息传递效率
|
||||
|
||||
### 7. 质量评估
|
||||
- 整体完成质量评分(1-10分)
|
||||
- 各维度的具体评分及理由
|
||||
|
||||
### 8. 经验教训与改进建议
|
||||
- 成功经验
|
||||
- 存在的问题与不足
|
||||
- 改进建议
|
||||
|
||||
---
|
||||
|
||||
## 输出格式要求
|
||||
- 使用 Markdown 格式输出
|
||||
- 语言:简体中文
|
||||
- 适当使用列表、表格增强可读性
|
||||
- 报告长度必须达到 3000-5000 字,每个章节都要详细展开,不要遗漏任何章节
|
||||
- 每个章节的内容要充实,提供具体的分析和建议
|
||||
- 注意:所有加粗标记必须成对出现,如 **文本**,不要单独使用 ** 或缺少结束标记
|
||||
- 禁止使用 mermaid、graph TD、flowchart 等图表代码,如果需要描述流程请用纯文字描述
|
||||
- 不要生成附录章节
|
||||
- 不要在报告中显示"报告总字数"这样的统计信息
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._load_llm_config()
|
||||
|
||||
def _load_llm_config(self):
|
||||
"""从配置文件加载 LLM 配置"""
|
||||
try:
|
||||
import yaml
|
||||
possible_paths = [
|
||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'config.yaml'),
|
||||
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'backend', 'config', 'config.yaml'),
|
||||
os.path.join(os.getcwd(), 'config', 'config.yaml'),
|
||||
]
|
||||
|
||||
for config_path in possible_paths:
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
if config:
|
||||
self.LLM_CONFIG['OPENAI_API_BASE'] = config.get('OPENAI_API_BASE')
|
||||
self.LLM_CONFIG['OPENAI_API_KEY'] = config.get('OPENAI_API_KEY')
|
||||
self.LLM_CONFIG['OPENAI_API_MODEL'] = config.get('OPENAI_API_MODEL')
|
||||
print(f"已加载 LLM 配置: {self.LLM_CONFIG['OPENAI_API_MODEL']}")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"加载 LLM 配置失败: {e}")
|
||||
|
||||
def generate(self, task_data: Dict[str, Any], file_path: str) -> bool:
|
||||
"""生成 Excel 文档(调用 LLM 生成报告)"""
|
||||
try:
|
||||
task_name = task_data.get('task_name', '未命名任务')
|
||||
task_outline = task_data.get('task_outline')
|
||||
rehearsal_log = task_data.get('rehearsal_log')
|
||||
agent_scores = task_data.get('agent_scores')
|
||||
|
||||
agents = self._extract_agents(task_outline)
|
||||
filtered_agent_scores = self._filter_agent_scores(agent_scores, agents)
|
||||
|
||||
task_outline_str = json.dumps(task_outline, ensure_ascii=False, indent=2) if task_outline else '无'
|
||||
rehearsal_log_str = json.dumps(rehearsal_log, ensure_ascii=False, indent=2) if rehearsal_log else '无'
|
||||
agents_str = ', '.join(agents) if agents else '无'
|
||||
agent_scores_str = json.dumps(filtered_agent_scores, ensure_ascii=False, indent=2) if filtered_agent_scores else '无'
|
||||
|
||||
prompt = self.PROMPT_TEMPLATE.format(
|
||||
task_name=task_name,
|
||||
task_outline=task_outline_str,
|
||||
rehearsal_log=rehearsal_log_str,
|
||||
agents=agents_str,
|
||||
agent_scores=agent_scores_str
|
||||
)
|
||||
|
||||
print("正在调用大模型生成 Excel 报告...")
|
||||
report_content = self._call_llm(prompt)
|
||||
|
||||
if not report_content:
|
||||
print("LLM 生成报告失败")
|
||||
return False
|
||||
|
||||
report_content = self._clean_report_title(report_content)
|
||||
print(f"报告生成成功,长度: {len(report_content)} 字符")
|
||||
|
||||
self._save_as_excel(report_content, file_path, task_name)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Excel LLM 导出失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _clean_report_title(self, content: str) -> str:
|
||||
"""清理报告开头的重复标题"""
|
||||
lines = content.split('\n')
|
||||
if not lines:
|
||||
return content
|
||||
|
||||
first_line = lines[0].strip()
|
||||
if first_line == '任务执行报告' or first_line == '# 任务执行报告':
|
||||
lines = lines[1:]
|
||||
while lines and not lines[0].strip():
|
||||
lines.pop(0)
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def _extract_agents(self, task_outline: Any) -> list:
|
||||
"""从 task_outline 中提取参与智能体列表"""
|
||||
agents = set()
|
||||
if not task_outline or not isinstance(task_outline, dict):
|
||||
return []
|
||||
|
||||
collaboration_process = task_outline.get('Collaboration Process', [])
|
||||
if not collaboration_process or not isinstance(collaboration_process, list):
|
||||
return []
|
||||
|
||||
for step in collaboration_process:
|
||||
if isinstance(step, dict):
|
||||
agent_selection = step.get('AgentSelection', [])
|
||||
if isinstance(agent_selection, list):
|
||||
for agent in agent_selection:
|
||||
if agent:
|
||||
agents.add(agent)
|
||||
|
||||
return list(agents)
|
||||
|
||||
def _filter_agent_scores(self, agent_scores: Any, agents: list) -> dict:
|
||||
"""过滤 agent_scores,只保留参与当前任务的智能体评分"""
|
||||
if not agent_scores or not isinstance(agent_scores, dict):
|
||||
return {}
|
||||
if not agents:
|
||||
return {}
|
||||
|
||||
filtered = {}
|
||||
for step_id, step_data in agent_scores.items():
|
||||
if not isinstance(step_data, dict):
|
||||
continue
|
||||
|
||||
aspect_list = step_data.get('aspectList', [])
|
||||
agent_scores_data = step_data.get('agentScores', {})
|
||||
|
||||
if not agent_scores_data:
|
||||
continue
|
||||
|
||||
filtered_scores = {}
|
||||
for agent_name, scores in agent_scores_data.items():
|
||||
if agent_name in agents and isinstance(scores, dict):
|
||||
filtered_scores[agent_name] = scores
|
||||
|
||||
if filtered_scores:
|
||||
filtered[step_id] = {
|
||||
'aspectList': aspect_list,
|
||||
'agentScores': filtered_scores
|
||||
}
|
||||
|
||||
return filtered
|
||||
|
||||
def _call_llm(self, prompt: str) -> str:
|
||||
"""调用大模型 API 生成报告"""
|
||||
try:
|
||||
import openai
|
||||
|
||||
if not self.LLM_CONFIG['OPENAI_API_KEY']:
|
||||
print("错误: OPENAI_API_KEY 未配置")
|
||||
return ""
|
||||
if not self.LLM_CONFIG['OPENAI_API_BASE']:
|
||||
print("错误: OPENAI_API_BASE 未配置")
|
||||
return ""
|
||||
if not self.LLM_CONFIG['OPENAI_API_MODEL']:
|
||||
print("错误: OPENAI_API_MODEL 未配置")
|
||||
return ""
|
||||
|
||||
client = openai.OpenAI(
|
||||
api_key=self.LLM_CONFIG['OPENAI_API_KEY'],
|
||||
base_url=self.LLM_CONFIG['OPENAI_API_BASE']
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=self.LLM_CONFIG['OPENAI_API_MODEL'],
|
||||
messages=[
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=10000,
|
||||
)
|
||||
|
||||
if response and response.choices:
|
||||
return response.choices[0].message.content
|
||||
|
||||
return ""
|
||||
|
||||
except ImportError:
|
||||
print("请安装 openai 库: pip install openai")
|
||||
return ""
|
||||
except Exception as e:
|
||||
print(f"调用 LLM 失败: {e}")
|
||||
return ""
|
||||
|
||||
def _save_as_excel(self, markdown_content: str, file_path: str, task_name: str):
|
||||
"""将 Markdown 内容保存为 Excel 文档"""
|
||||
try:
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "任务执行报告"
|
||||
|
||||
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
||||
header_font = Font(bold=True, color="FFFFFF", size=12)
|
||||
title_font = Font(bold=True, size=14)
|
||||
section_font = Font(bold=True, size=11)
|
||||
normal_font = Font(size=10)
|
||||
|
||||
thin_border = Border(
|
||||
left=Side(style='thin'),
|
||||
right=Side(style='thin'),
|
||||
top=Side(style='thin'),
|
||||
bottom=Side(style='thin')
|
||||
)
|
||||
|
||||
ws.column_dimensions['A'].width = 20
|
||||
ws.column_dimensions['B'].width = 80
|
||||
|
||||
row = 1
|
||||
ws[f'A{row}'] = task_name
|
||||
ws[f'A{row}'].font = Font(bold=True, size=16)
|
||||
ws[f'A{row}'].alignment = Alignment(horizontal='center', vertical='center')
|
||||
ws.merge_cells(f'A{row}:B{row}')
|
||||
row += 1
|
||||
|
||||
ws[f'A{row}'] = f"导出时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
ws[f'A{row}'].font = Font(size=9, italic=True)
|
||||
ws[f'A{row}'].alignment = Alignment(horizontal='center', vertical='center')
|
||||
ws.merge_cells(f'A{row}:B{row}')
|
||||
row += 2
|
||||
|
||||
lines = markdown_content.split('\n')
|
||||
current_section = ""
|
||||
table_data = []
|
||||
in_table = False
|
||||
|
||||
for line in lines:
|
||||
line = line.rstrip()
|
||||
|
||||
if not line:
|
||||
if in_table and table_data:
|
||||
row = self._write_table_to_excel(ws, table_data, row, header_fill, header_font, thin_border, normal_font)
|
||||
table_data = []
|
||||
in_table = False
|
||||
continue
|
||||
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('|') and stripped.endswith('|') and '---' in stripped:
|
||||
continue
|
||||
|
||||
if '|' in line and line.strip().startswith('|'):
|
||||
cells = [cell.strip() for cell in line.split('|')[1:-1]]
|
||||
if cells and any(cells):
|
||||
table_data.append(cells)
|
||||
in_table = True
|
||||
continue
|
||||
else:
|
||||
if in_table and table_data:
|
||||
row = self._write_table_to_excel(ws, table_data, row, header_fill, header_font, thin_border, normal_font)
|
||||
table_data = []
|
||||
in_table = False
|
||||
|
||||
if line.startswith('### '):
|
||||
if table_data:
|
||||
row = self._write_table_to_excel(ws, table_data, row, header_fill, header_font, thin_border, normal_font)
|
||||
table_data = []
|
||||
current_section = line[4:].strip()
|
||||
ws[f'A{row}'] = current_section
|
||||
ws[f'A{row}'].font = Font(bold=True, size=12, color="4472C4")
|
||||
ws[f'A{row}'].alignment = Alignment(horizontal='center', vertical='center')
|
||||
ws.merge_cells(f'A{row}:B{row}')
|
||||
row += 1
|
||||
elif line.startswith('## '):
|
||||
if table_data:
|
||||
row = self._write_table_to_excel(ws, table_data, row, header_fill, header_font, thin_border, normal_font)
|
||||
table_data = []
|
||||
section_title = line[2:].strip()
|
||||
ws[f'A{row}'] = section_title
|
||||
ws[f'A{row}'].font = Font(bold=True, size=13)
|
||||
ws[f'A{row}'].alignment = Alignment(horizontal='center', vertical='center')
|
||||
ws.merge_cells(f'A{row}:B{row}')
|
||||
row += 1
|
||||
elif line.startswith('# '):
|
||||
pass
|
||||
elif line.startswith('#### '):
|
||||
if table_data:
|
||||
row = self._write_table_to_excel(ws, table_data, row, header_fill, header_font, thin_border, normal_font)
|
||||
table_data = []
|
||||
current_section = line[5:].strip()
|
||||
ws[f'A{row}'] = current_section
|
||||
ws[f'A{row}'].font = Font(bold=True, size=11, color="4472C4")
|
||||
ws[f'A{row}'].alignment = Alignment(horizontal='center', vertical='center')
|
||||
ws.merge_cells(f'A{row}:B{row}')
|
||||
row += 1
|
||||
elif line.startswith('- ') or line.startswith('* ') or line.startswith('• '):
|
||||
text = line[2:].strip() if line.startswith(('- ', '* ')) else line[1:].strip()
|
||||
text = self._clean_markdown(text)
|
||||
ws[f'A{row}'] = "• " + text
|
||||
ws[f'A{row}'].font = normal_font
|
||||
ws[f'A{row}'].alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
|
||||
ws.merge_cells(f'A{row}:B{row}')
|
||||
row += 1
|
||||
elif line.startswith('**') and '**:' in line:
|
||||
parts = line.split(':', 1)
|
||||
if len(parts) == 2:
|
||||
key = self._clean_markdown(parts[0])
|
||||
value = self._clean_markdown(parts[1])
|
||||
ws[f'A{row}'] = f"{key}: {value}"
|
||||
ws[f'A{row}'].font = Font(bold=True, size=10)
|
||||
ws[f'A{row}'].alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
|
||||
ws.merge_cells(f'A{row}:B{row}')
|
||||
row += 1
|
||||
else:
|
||||
clean_line = self._clean_markdown(line)
|
||||
if clean_line:
|
||||
ws[f'A{row}'] = clean_line
|
||||
ws[f'A{row}'].font = normal_font
|
||||
ws[f'A{row}'].alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
|
||||
ws.merge_cells(f'A{row}:B{row}')
|
||||
row += 1
|
||||
|
||||
if table_data:
|
||||
row = self._write_table_to_excel(ws, table_data, row, header_fill, header_font, thin_border, normal_font)
|
||||
|
||||
ws.column_dimensions['B'].width = 80
|
||||
|
||||
# 设置自适应行高
|
||||
for r in range(1, row + 1):
|
||||
ws.row_dimensions[r].bestFit = True
|
||||
|
||||
for col in ['A', 'B']:
|
||||
for r in range(1, row + 1):
|
||||
cell = ws[f'{col}{r}']
|
||||
if cell.border is None or cell.border == Border():
|
||||
cell.border = Border(
|
||||
left=Side(style='none'),
|
||||
right=Side(style='none'),
|
||||
top=Side(style='none'),
|
||||
bottom=Side(style='none')
|
||||
)
|
||||
|
||||
wb.save(file_path)
|
||||
print(f"Excel 文档已保存: {file_path}")
|
||||
|
||||
except ImportError:
|
||||
print("请安装 openpyxl 库: pip install openpyxl")
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"保存 Excel 文档失败: {e}")
|
||||
raise
|
||||
|
||||
def _write_table_to_excel(self, ws, table_data, row, header_fill, header_font, border, normal_font):
|
||||
"""将表格数据写入 Excel"""
|
||||
if not table_data:
|
||||
return row
|
||||
|
||||
if len(table_data) == 1:
|
||||
ws[f'A{row}'] = table_data[0][0] if table_data[0] else ""
|
||||
ws[f'A{row}'].font = normal_font
|
||||
ws[f'A{row}'].alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
|
||||
ws.merge_cells(f'A{row}:B{row}')
|
||||
return row + 1
|
||||
|
||||
max_cols = max(len(row_data) for row_data in table_data)
|
||||
max_cols = min(max_cols, 2)
|
||||
|
||||
for col_idx in range(max_cols):
|
||||
col_letter = get_column_letter(col_idx + 1)
|
||||
ws.column_dimensions[col_letter].width = 25 if max_cols > 1 else 80
|
||||
|
||||
start_row = row
|
||||
for row_idx, row_data in enumerate(table_data):
|
||||
for col_idx in range(min(len(row_data), max_cols)):
|
||||
col_letter = get_column_letter(col_idx + 1)
|
||||
cell = ws[f'{col_letter}{row + row_idx}']
|
||||
cell.value = self._clean_markdown(row_data[col_idx])
|
||||
cell.font = header_font if row_idx == 0 else normal_font
|
||||
cell.fill = header_fill if row_idx == 0 else PatternFill()
|
||||
cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
|
||||
cell.border = border
|
||||
|
||||
if max_cols > 1:
|
||||
for col_idx in range(len(row_data), max_cols):
|
||||
col_letter = get_column_letter(col_idx + 1)
|
||||
ws[f'{col_letter}{row + row_idx}'].border = border
|
||||
|
||||
return row + len(table_data) + 1
|
||||
|
||||
def _clean_markdown(self, text: str) -> str:
|
||||
"""清理 Markdown 格式标记"""
|
||||
import re
|
||||
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
|
||||
text = re.sub(r'\*(.+?)\*', r'\1', text)
|
||||
text = re.sub(r'__(.+?)__', r'\1', text)
|
||||
text = re.sub(r'_(.+?)_', r'\1', text)
|
||||
text = re.sub(r'~~(.+?)~~', r'\1', text)
|
||||
text = re.sub(r'`(.+?)`', r'\1', text)
|
||||
text = text.replace('\\n', '\n').replace('\\t', '\t')
|
||||
return text.strip()
|
||||
@@ -1,17 +1,12 @@
|
||||
import asyncio
|
||||
|
||||
import httpx
|
||||
from openai import OpenAI, AsyncOpenAI, max_retries
|
||||
import openai
|
||||
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")
|
||||
yaml_data = {}
|
||||
try:
|
||||
with open(yaml_file, "r", encoding="utf-8") as file:
|
||||
yaml_data = yaml.safe_load(file)
|
||||
@@ -20,16 +15,11 @@ except Exception:
|
||||
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_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)
|
||||
openai.api_key = OPENAI_API_KEY
|
||||
MODEL: str = os.getenv("OPENAI_API_MODEL") or yaml_data.get(
|
||||
"OPENAI_API_MODEL", "gpt-4-turbo-preview"
|
||||
)
|
||||
@@ -46,11 +36,30 @@ 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,model_config: dict = None
|
||||
messages: list[dict], stream: bool = True, useGroq: bool = True
|
||||
) -> str:
|
||||
if model_config:
|
||||
return _call_with_custom_config(messages,stream,model_config)
|
||||
if not useGroq or not FAST_DESIGN_MODE:
|
||||
# 增强消息验证:确保所有消息的 role 和 content 非空且不是空白字符串
|
||||
if not messages or len(messages) == 0:
|
||||
raise ValueError("Messages list is empty")
|
||||
|
||||
# print(f"[DEBUG] LLM_Completion received {len(messages)} messages", flush=True)
|
||||
|
||||
for i, msg in enumerate(messages):
|
||||
if not isinstance(msg, dict):
|
||||
raise ValueError(f"Message at index {i} is not a dictionary")
|
||||
if not msg.get("role") or str(msg.get("role")).strip() == "":
|
||||
raise ValueError(f"Message at index {i} has empty 'role'")
|
||||
if not msg.get("content") or str(msg.get("content")).strip() == "":
|
||||
raise ValueError(f"Message at index {i} has empty 'content'")
|
||||
|
||||
# 额外验证:确保content不会因为格式化问题变成空
|
||||
content = str(msg.get("content")).strip()
|
||||
if len(content) < 10: # 设置最小长度阈值
|
||||
print(f"[WARNING] Message at index {i} has very short content: '{content}'", flush=True)
|
||||
# 修改1
|
||||
if not GROQ_API_KEY:
|
||||
useGroq = False
|
||||
elif not useGroq or not FAST_DESIGN_MODE:
|
||||
force_gpt4 = True
|
||||
useGroq = False
|
||||
else:
|
||||
@@ -82,106 +91,16 @@ 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"[API Error] Custom API error: {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"[API Warn] Stream connection interrupted, retrying in {wait_time}s...", "yellow")
|
||||
await asyncio.sleep(wait_time)
|
||||
continue
|
||||
except Exception as e:
|
||||
print_colored(f"[API Error] Custom API stream error: {str(e)}", "red")
|
||||
raise
|
||||
|
||||
|
||||
async def _achat_completion_stream_groq(messages: list[dict]) -> str:
|
||||
from groq import AsyncGroq
|
||||
groq_client = AsyncGroq(api_key=GROQ_API_KEY)
|
||||
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:
|
||||
response = await groq_client.chat.completions.create(
|
||||
stream = await client.chat.completions.create(
|
||||
messages=messages,
|
||||
# model='gemma-7b-it',
|
||||
model="mixtral-8x7b-32768",
|
||||
@@ -195,34 +114,25 @@ async def _achat_completion_stream_groq(messages: list[dict]) -> str:
|
||||
if attempt < max_attempts - 1: # i is zero indexed
|
||||
continue
|
||||
else:
|
||||
raise Exception("failed")
|
||||
raise "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")
|
||||
|
||||
# print(colored(full_reply_content, "blue", "on_white"), end="")
|
||||
# print()
|
||||
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_mixtral(messages: list[dict]) -> str:
|
||||
from mistralai.client import MistralClient
|
||||
from mistralai.models.chat_completion import ChatMessage
|
||||
mistral_client = MistralClient(api_key=MISTRAL_API_KEY)
|
||||
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 = mistral_client.chat(
|
||||
stream = client.chat(
|
||||
messages=[
|
||||
ChatMessage(
|
||||
role=message["role"], content=message["content"]
|
||||
@@ -231,38 +141,41 @@ 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 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")
|
||||
raise "failed"
|
||||
|
||||
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()
|
||||
print(colored(full_reply_content, "blue", "on_white"), end="")
|
||||
print()
|
||||
return full_reply_content
|
||||
|
||||
|
||||
async def _achat_completion_stream_gpt35(messages: list[dict]) -> str:
|
||||
response = await async_client.chat.completions.create(
|
||||
messages=messages,
|
||||
max_tokens=4096,
|
||||
temperature=0.3,
|
||||
timeout=600,
|
||||
model="gpt-3.5-turbo-16k",
|
||||
stream=True,
|
||||
)
|
||||
openai.api_key = OPENAI_API_KEY
|
||||
openai.api_base = OPENAI_API_BASE
|
||||
|
||||
kwargs = {
|
||||
"messages": messages,
|
||||
"max_tokens": 4096,
|
||||
"n": 1,
|
||||
"stop": None,
|
||||
"temperature": 0.3,
|
||||
"timeout": 3,
|
||||
"model": "gpt-3.5-turbo-16k",
|
||||
"stream": True,
|
||||
}
|
||||
# print("[DEBUG] about to call acreate with kwargs:", type(kwargs), kwargs)
|
||||
assert kwargs is not None, "kwargs is None right before acreate!"
|
||||
assert isinstance(kwargs, dict), "kwargs must be dict!"
|
||||
response = await openai.ChatCompletion.acreate(**kwargs)
|
||||
|
||||
|
||||
|
||||
# create variables to collect the stream of chunks
|
||||
collected_chunks = []
|
||||
@@ -270,38 +183,40 @@ 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 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()
|
||||
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()
|
||||
|
||||
full_reply_content = "".join(
|
||||
[m.content or "" for m in collected_messages if m is not None]
|
||||
[m.get("content", "") for m in collected_messages]
|
||||
)
|
||||
|
||||
# 检查最终结果是否为空
|
||||
if not full_reply_content or full_reply_content.strip() == "":
|
||||
raise Exception("Stream API (gpt-3.5) returned empty content")
|
||||
|
||||
return full_reply_content
|
||||
|
||||
|
||||
def _achat_completion_json(messages: list[dict] ) -> str:
|
||||
async def _achat_completion_json(messages: list[dict]) -> str:
|
||||
openai.api_key = OPENAI_API_KEY
|
||||
openai.api_base = OPENAI_API_BASE
|
||||
|
||||
max_attempts = 5
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
response = async_client.chat.completions.create(
|
||||
stream = await openai.ChatCompletion.acreate(
|
||||
messages=messages,
|
||||
max_tokens=4096,
|
||||
n=1,
|
||||
stop=None,
|
||||
temperature=0.3,
|
||||
timeout=600,
|
||||
timeout=3,
|
||||
model=MODEL,
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
@@ -310,88 +225,107 @@ def _achat_completion_json(messages: list[dict] ) -> str:
|
||||
if attempt < max_attempts - 1: # i is zero indexed
|
||||
continue
|
||||
else:
|
||||
raise Exception("failed")
|
||||
raise "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")
|
||||
|
||||
# print(colored(full_reply_content, "blue", "on_white"), end="")
|
||||
# print()
|
||||
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:
|
||||
try:
|
||||
response = await async_client.chat.completions.create(
|
||||
**_cons_kwargs(messages), stream=True
|
||||
)
|
||||
# print(">>>> _achat_completion_stream 被调用", flush=True)
|
||||
# print(">>>> messages 实参 =", messages, flush=True)
|
||||
# print(">>>> messages 类型 =", type(messages), flush=True)
|
||||
openai.api_key = OPENAI_API_KEY
|
||||
openai.api_base = OPENAI_API_BASE
|
||||
response = await openai.ChatCompletion.acreate(
|
||||
**_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 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()
|
||||
# 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()
|
||||
|
||||
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"[API Error] OpenAI API stream error: {str(e)}", "red")
|
||||
raise
|
||||
full_reply_content = "".join(
|
||||
[m.get("content", "") for m in collected_messages]
|
||||
)
|
||||
return full_reply_content
|
||||
|
||||
|
||||
def _chat_completion(messages: list[dict]) -> str:
|
||||
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"[API Error] OpenAI API error: {str(e)}", "red")
|
||||
raise
|
||||
rsp = openai.ChatCompletion.create(**_cons_kwargs(messages))
|
||||
content = rsp["choices"][0]["message"]["content"]
|
||||
return content
|
||||
|
||||
|
||||
def _cons_kwargs(messages: list[dict]) -> dict:
|
||||
kwargs = {
|
||||
"messages": messages,
|
||||
"max_tokens": 2000,
|
||||
"temperature": 0.3,
|
||||
"timeout": 600,
|
||||
"max_tokens": 4096,
|
||||
"temperature": 0.5,
|
||||
}
|
||||
kwargs_mode = {"model": MODEL}
|
||||
kwargs.update(kwargs_mode)
|
||||
return kwargs
|
||||
print("[DEBUG] kwargs =", kwargs)
|
||||
assert isinstance(kwargs, dict), f"_cons_kwargs returned {type(kwargs)}, must be dict"
|
||||
|
||||
# 添加调试信息
|
||||
print(f'[DEBUG] _cons_kwargs messages: {messages}', flush=True)
|
||||
|
||||
# 检查并修复消息中的null值
|
||||
for i, msg in enumerate(messages):
|
||||
# 确保msg是字典
|
||||
if not isinstance(msg, dict):
|
||||
print(f"[ERROR] Message {i} is not a dictionary: {msg}", flush=True)
|
||||
messages[i] = {"role": "user", "content": str(msg) if msg is not None else ""}
|
||||
continue
|
||||
|
||||
# 确保role和content存在且不为None
|
||||
if "role" not in msg or msg["role"] is None:
|
||||
print(f"[ERROR] Message {i} missing role, setting to 'user'", flush=True)
|
||||
msg["role"] = "user"
|
||||
else:
|
||||
msg["role"] = str(msg["role"]).strip()
|
||||
|
||||
if "content" not in msg or msg["content"] is None:
|
||||
print(f"[ERROR] Message {i} missing content, setting to empty string", flush=True)
|
||||
msg["content"] = ""
|
||||
else:
|
||||
msg["content"] = str(msg["content"]).strip()
|
||||
|
||||
# 根据不同的API提供商调整参数
|
||||
if "deepseek" in MODEL.lower():
|
||||
# DeepSeek API特殊处理
|
||||
print("[DEBUG] DeepSeek API detected, adjusting parameters", flush=True)
|
||||
kwargs.pop("n", None) # 移除n参数,DeepSeek可能不支持
|
||||
if "timeout" in kwargs:
|
||||
kwargs.pop("timeout", None)
|
||||
# DeepSeek可能不支持stop参数
|
||||
kwargs.pop("stop", None)
|
||||
else:
|
||||
# OpenAI兼容的API
|
||||
kwargs["n"] = 1
|
||||
kwargs["stop"] = None
|
||||
kwargs["timeout"] = 3
|
||||
|
||||
kwargs["model"] = MODEL
|
||||
|
||||
# 确保messages列表中的每个元素都有有效的role和content
|
||||
kwargs["messages"] = [msg for msg in messages if msg["role"] and msg["content"]]
|
||||
print(f"[DEBUG] Final kwargs for API call: {kwargs.keys()}", flush=True)
|
||||
|
||||
return kwargs
|
||||
@@ -7,8 +7,6 @@ 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}
|
||||
|
||||
@@ -43,16 +41,14 @@ def generate_AbilityRequirement(General_Goal, Current_Task):
|
||||
),
|
||||
},
|
||||
]
|
||||
#print(messages[1]["content"])
|
||||
return read_LLM_Completion(messages)["AbilityRequirement"]
|
||||
print(messages[1]["content"])
|
||||
return read_LLM_Completion(messages)["AbilityRequirement"]
|
||||
|
||||
|
||||
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}
|
||||
|
||||
@@ -87,7 +83,6 @@ class JSON_Agent(BaseModel):
|
||||
class JSON_AGENT_ABILITY_SCORING(RootModel):
|
||||
root: Dict[str, JSON_Agent]
|
||||
|
||||
|
||||
def agentAbilityScoring(Agent_Board, Ability_Requirement_List):
|
||||
scoreTable = {}
|
||||
for Ability_Requirement in Ability_Requirement_List:
|
||||
@@ -104,12 +99,12 @@ def agentAbilityScoring(Agent_Board, Ability_Requirement_List):
|
||||
),
|
||||
},
|
||||
]
|
||||
#print(messages[1]["content"])
|
||||
print(messages[1]["content"])
|
||||
scoreTable[Ability_Requirement] = read_LLM_Completion(messages)
|
||||
return scoreTable
|
||||
|
||||
|
||||
def AgentSelectModify_init(stepTask, General_Goal, Agent_Board):
|
||||
async def AgentSelectModify_init(stepTask, General_Goal, Agent_Board):
|
||||
Current_Task = {
|
||||
"TaskName": stepTask["StepName"],
|
||||
"InputObject_List": stepTask["InputObject_List"],
|
||||
@@ -129,14 +124,13 @@ def AgentSelectModify_init(stepTask, General_Goal, Agent_Board):
|
||||
),
|
||||
},
|
||||
]
|
||||
Ability_Requirement_List = read_LLM_Completion(messages)[
|
||||
Ability_Requirement_List = await read_LLM_Completion(messages)[
|
||||
"AbilityRequirement"
|
||||
]
|
||||
scoreTable = agentAbilityScoring(Agent_Board, Ability_Requirement_List)
|
||||
scoreTable = await agentAbilityScoring(Agent_Board, Ability_Requirement_List)
|
||||
return scoreTable
|
||||
|
||||
|
||||
def AgentSelectModify_addAspect(aspectList, Agent_Board):
|
||||
newAspect = aspectList[-1]
|
||||
scoreTable = agentAbilityScoring(Agent_Board, [newAspect])
|
||||
scoreTable = agentAbilityScoring(Agent_Board, aspectList)
|
||||
return scoreTable
|
||||
|
||||
@@ -35,8 +35,6 @@ 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}
|
||||
|
||||
@@ -77,14 +75,10 @@ def generate_AbilityRequirement(General_Goal, Current_Task):
|
||||
),
|
||||
},
|
||||
]
|
||||
return read_LLM_Completion(messages)["AbilityRequirement"]
|
||||
|
||||
print(messages[1]["content"])
|
||||
return read_LLM_Completion(messages)["AbilityRequirement"]
|
||||
|
||||
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",
|
||||
@@ -99,13 +93,12 @@ def generate_AgentSelection(General_Goal, Current_Task, Agent_Board):
|
||||
),
|
||||
},
|
||||
]
|
||||
#print(messages[1]["content"])
|
||||
print(messages[1]["content"])
|
||||
|
||||
agentboard_set = {agent["Name"] for agent in Agent_Board}
|
||||
|
||||
while True:
|
||||
candidate = read_LLM_Completion(messages)["AgentSelectionPlan"]
|
||||
# 添加调试打印
|
||||
candidate = read_LLM_Completion(messages)["AgentSelectionPlan"]
|
||||
if len(candidate) > MAX_TEAM_SIZE:
|
||||
teamSize = random.randint(2, MAX_TEAM_SIZE)
|
||||
candidate = candidate[0:teamSize]
|
||||
@@ -113,6 +106,7 @@ def generate_AgentSelection(General_Goal, Current_Task, Agent_Board):
|
||||
continue
|
||||
AgentSelectionPlan = sorted(candidate)
|
||||
AgentSelectionPlan_set = set(AgentSelectionPlan)
|
||||
|
||||
# Check if every item in AgentSelectionPlan is in agentboard
|
||||
if AgentSelectionPlan_set.issubset(agentboard_set):
|
||||
break # If all items are in agentboard, break the loop
|
||||
|
||||
@@ -1,53 +1,63 @@
|
||||
from AgentCoord.PlanEngine.planOutline_Generator import generate_PlanOutline
|
||||
# from AgentCoord.PlanEngine.AgentSelection_Generator import (
|
||||
# generate_AgentSelection,
|
||||
# )
|
||||
from AgentCoord.PlanEngine.AgentSelection_Generator import (
|
||||
generate_AgentSelection,
|
||||
)
|
||||
from AgentCoord.PlanEngine.taskProcess_Generator import generate_TaskProcess
|
||||
import AgentCoord.util as util
|
||||
|
||||
|
||||
def generate_basePlan(
|
||||
General_Goal, Agent_Board, AgentProfile_Dict, InitialObject_List
|
||||
):
|
||||
"""
|
||||
优化模式:生成大纲 + 智能体选择,但不生成任务流程
|
||||
优化用户体验:
|
||||
1. 快速生成大纲和分配智能体
|
||||
2. 用户可以看到完整的大纲和智能体图标
|
||||
3. TaskProcess由前端通过 fillStepTask API 异步填充
|
||||
|
||||
"""
|
||||
# 参数保留以保持接口兼容性
|
||||
_ = AgentProfile_Dict
|
||||
PlanOutline = generate_PlanOutline(
|
||||
InitialObject_List=InitialObject_List, General_Goal=General_Goal
|
||||
)
|
||||
Agent_Board = [
|
||||
{"Name": (a.get("Name") or "").strip(),"Profile": (a.get("Profile") or "").strip()}
|
||||
for a in Agent_Board
|
||||
if a and a.get("Name") is not None
|
||||
]
|
||||
if not Agent_Board: # 洗完后还是空 → 直接返回空计划
|
||||
return {"Plan_Outline": []}
|
||||
|
||||
basePlan = {
|
||||
"General Goal": General_Goal,
|
||||
"Initial Input Object": InitialObject_List,
|
||||
"Collaboration Process": []
|
||||
"Collaboration Process": [],
|
||||
}
|
||||
|
||||
PlanOutline = generate_PlanOutline(
|
||||
InitialObject_List=[], General_Goal=General_Goal
|
||||
)
|
||||
for stepItem in PlanOutline:
|
||||
# # 为每个步骤分配智能体
|
||||
# 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": {}
|
||||
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,
|
||||
)
|
||||
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)
|
||||
|
||||
return basePlan
|
||||
basePlan["General Goal"] = General_Goal
|
||||
return basePlan
|
||||
|
||||
@@ -9,8 +9,6 @@ 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}
|
||||
|
||||
@@ -87,7 +85,7 @@ def branch_PlanOutline(
|
||||
InitialObject_List=str(InitialObject_List),
|
||||
General_Goal=General_Goal,
|
||||
)
|
||||
#print(prompt)
|
||||
print(prompt)
|
||||
branch_List = []
|
||||
for _ in range(branch_Number):
|
||||
messages = [
|
||||
@@ -97,7 +95,7 @@ def branch_PlanOutline(
|
||||
},
|
||||
{"role": "system", "content": prompt},
|
||||
]
|
||||
Remaining_Steps = read_LLM_Completion(messages, useGroq=False)[
|
||||
Remaining_Steps = read_LLM_Completion(messages)[
|
||||
"Remaining Steps"
|
||||
]
|
||||
branch_List.append(Remaining_Steps)
|
||||
|
||||
@@ -29,8 +29,6 @@ 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}
|
||||
|
||||
@@ -57,39 +55,26 @@ Note: "Modification Requirement" specifies how to modify the "Baseline Completio
|
||||
"ID": "Action4",
|
||||
"ActionType": "Propose",
|
||||
"AgentName": "Mia",
|
||||
"Description": "提议关于人工智能情感发展的心理学理论,重点关注爱与依恋的概念。",
|
||||
"Description": "Propose psychological theories on love and attachment that could be applied to AI's emotional development.",
|
||||
"ImportantInput": [
|
||||
"InputObject:Story Outline"
|
||||
]
|
||||
}},
|
||||
{{
|
||||
"ID": "Action5",
|
||||
"ActionType": "Critique",
|
||||
"ActionType": "Propose",
|
||||
"AgentName": "Noah",
|
||||
"Description": "对Mia提出的心理学理论进行批判性评估,分析其在AI情感发展场景中的适用性和局限性。",
|
||||
"ImportantInput": [
|
||||
"ActionResult:Action4"
|
||||
]
|
||||
"Description": "Propose ethical considerations and philosophical questions regarding AI's capacity for love.",
|
||||
"ImportantInput": []
|
||||
}},
|
||||
{{
|
||||
"ID": "Action6",
|
||||
"ActionType": "Improve",
|
||||
"AgentName": "Liam",
|
||||
"Description": "基于Noah的批判性反馈,改进和完善心理学理论框架,使其更贴合AI情感发展的实际需求。",
|
||||
"ImportantInput": [
|
||||
"ActionResult:Action4",
|
||||
"ActionResult:Action5"
|
||||
]
|
||||
}},
|
||||
{{
|
||||
"ID": "Action7",
|
||||
"ActionType": "Finalize",
|
||||
"AgentName": "Mia",
|
||||
"Description": "综合所有提议、批判和改进意见,整合并提交最终的AI情感发展心理学理论框架。",
|
||||
"AgentName": "Liam",
|
||||
"Description": "Combine the poetic elements and ethical considerations into a cohesive set of core love elements for the story.",
|
||||
"ImportantInput": [
|
||||
"ActionResult:Action4",
|
||||
"ActionResult:Action5",
|
||||
"ActionResult:Action6"
|
||||
"ActionResult:Action1",
|
||||
"ActionResult:Action5"
|
||||
]
|
||||
}}
|
||||
]
|
||||
@@ -99,12 +84,7 @@ 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. **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"
|
||||
ActionType: Specify the type of action, note that only the last action can be of type "Finalize", and the last action must be "Finalize".
|
||||
|
||||
"""
|
||||
|
||||
@@ -155,7 +135,7 @@ def branch_TaskProcess(
|
||||
General_Goal=General_Goal,
|
||||
Act_Set=ACT_SET,
|
||||
)
|
||||
#print(prompt)
|
||||
print(prompt)
|
||||
branch_List = []
|
||||
for i in range(branch_Number):
|
||||
messages = [
|
||||
@@ -165,7 +145,7 @@ def branch_TaskProcess(
|
||||
},
|
||||
{"role": "system", "content": prompt},
|
||||
]
|
||||
Remaining_Steps = read_LLM_Completion(messages, useGroq=False)[
|
||||
Remaining_Steps = read_LLM_Completion(messages)[
|
||||
"Remaining Steps"
|
||||
]
|
||||
|
||||
|
||||
@@ -2,13 +2,11 @@ from AgentCoord.util.converter import read_LLM_Completion
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
|
||||
import asyncio
|
||||
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}
|
||||
|
||||
@@ -71,6 +69,13 @@ class PlanOutline(BaseModel):
|
||||
|
||||
|
||||
def generate_PlanOutline(InitialObject_List, General_Goal):
|
||||
# 新增:校验 General_Goal 必须有有效内容
|
||||
if not isinstance(General_Goal, str) or len(General_Goal.strip()) == 0:
|
||||
raise ValueError("General_Goal 不能为空!必须提供具体的目标描述")
|
||||
|
||||
# 处理 InitialObject_List 为空的情况(可选,但更友好)
|
||||
if not InitialObject_List:
|
||||
InitialObject_List = ["无初始对象"] # 避免空列表导致的歧义
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
@@ -85,16 +90,11 @@ def generate_PlanOutline(InitialObject_List, General_Goal):
|
||||
),
|
||||
},
|
||||
]
|
||||
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"
|
||||
}
|
||||
]
|
||||
|
||||
# 二次校验 messages 内容(防止意外空值)
|
||||
for msg in messages:
|
||||
content = msg.get("content", "").strip()
|
||||
if not content:
|
||||
raise ValueError("生成的 LLM 请求消息内容为空,请检查参数")
|
||||
|
||||
return read_LLM_Completion(messages)["Plan_Outline"]
|
||||
|
||||
@@ -25,8 +25,6 @@ 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}
|
||||
|
||||
@@ -108,6 +106,9 @@ class TaskProcessPlan(BaseModel):
|
||||
|
||||
|
||||
def generate_TaskProcess(General_Goal, Current_Task_Description):
|
||||
# 新增参数验证
|
||||
if not General_Goal or str(General_Goal).strip() == "":
|
||||
raise ValueError("General_Goal cannot be empty")
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
@@ -124,7 +125,7 @@ def generate_TaskProcess(General_Goal, Current_Task_Description):
|
||||
),
|
||||
},
|
||||
]
|
||||
#print(messages[1]["content"])
|
||||
print(messages[1]["content"])
|
||||
|
||||
# write a callback function, if read_LLM_Completion(messages)["Task_Process_Plan"] dont have the right format, call this function again
|
||||
while True:
|
||||
|
||||
@@ -5,12 +5,10 @@ PROMPT_TEMPLATE_TAKE_ACTION_BASE = '''
|
||||
Your name is {agentName}. You will play the role as the Profile indicates.
|
||||
Profile: {agentProfile}
|
||||
|
||||
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".
|
||||
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)
|
||||
@@ -25,8 +23,8 @@ Note: Important Input for your action are marked with *Important Input*
|
||||
### History Action
|
||||
{History_Action}
|
||||
|
||||
## Instruction for Your Current Action
|
||||
{Action_Description}
|
||||
## Instruction for Your Current Action
|
||||
{Action_Description}
|
||||
|
||||
{Action_Custom_Note}
|
||||
|
||||
@@ -82,33 +80,10 @@ 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)
|
||||
|
||||
# Handle missing agent profiles gracefully
|
||||
model_config = None
|
||||
if agentName not in AgentProfile_Dict:
|
||||
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")
|
||||
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)
|
||||
print_colored(text = prompt, text_color="red")
|
||||
messages = [{"role":"system", "content": prompt}]
|
||||
ActionResult = LLM_Completion(messages,True,False,model_config=model_config)
|
||||
ActionResult = LLM_Completion(messages,stream=False)
|
||||
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 = '''
|
||||
注意:由于你在对话中,你的批评必须简洁、清晰且易于阅读,不要让人感到压力过大。如果你要列出一些观点,最多列出2点。
|
||||
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,9 +2,9 @@ from AgentCoord.util.converter import read_outputObject_content
|
||||
from AgentCoord.RehearsalEngine_V2.Action import BaseAction
|
||||
|
||||
ACTION_CUSTOM_NOTE = '''
|
||||
注意:你可以在给出{OutputName}的最终内容之前先说一些话。当你决定给出{OutputName}的最终内容时,应该这样包含:
|
||||
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}的内容)
|
||||
(the content of {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:
|
||||
|
||||
## xxx的改进版本
|
||||
(改进版本的内容)
|
||||
## Improved version of xxx
|
||||
(the improved version of the content)
|
||||
```
|
||||
|
||||
'''
|
||||
|
||||
@@ -83,15 +83,11 @@ 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"]
|
||||
|
||||
if actionType in Action.customAction_Dict:
|
||||
currentAction = Action.customAction_Dict[actionType](
|
||||
info=ActionInfo,
|
||||
@@ -122,4 +118,11 @@ def executePlan(plan, num_StepToRun, RehearsalLog, AgentProfile_Dict):
|
||||
stepLogNode["ActionHistory"] = ActionHistory
|
||||
|
||||
# Return Output
|
||||
print(
|
||||
colored(
|
||||
"$Run " + str(StepRun_count) + "step$",
|
||||
color="black",
|
||||
on_color="on_white",
|
||||
)
|
||||
)
|
||||
return RehearsalLog
|
||||
|
||||
@@ -1,619 +0,0 @@
|
||||
"""
|
||||
优化版执行计划 - 支持动态追加步骤
|
||||
在执行过程中可以接收新的步骤并追加到执行队列
|
||||
"""
|
||||
|
||||
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:
|
||||
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,
|
||||
execution_id: str = None,
|
||||
RehearsalLog: List = None # 用于追加日志到历史记录
|
||||
) -> Generator[Dict, None, None]:
|
||||
"""
|
||||
异步执行单个步骤,支持流式返回
|
||||
|
||||
Args:
|
||||
stepDescrip: 步骤描述
|
||||
General_Goal: 总体目标
|
||||
AgentProfile_Dict: 智能体配置字典
|
||||
KeyObjects: 关键对象字典
|
||||
step_index: 步骤索引
|
||||
total_steps: 总步骤数
|
||||
execution_id: 执行ID
|
||||
|
||||
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(execution_id)
|
||||
if not should_continue:
|
||||
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"🔄 批次 {batch_index + 1}/{len(batches)}: 串行执行",
|
||||
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
|
||||
|
||||
# 收集该步骤使用的 agent(去重)
|
||||
assigned_agents_in_step = list(set(Agent_List)) if Agent_List else []
|
||||
|
||||
# 追加到 RehearsalLog(因为 RehearsalLog 是可变对象,会反映到原列表)
|
||||
if RehearsalLog is not None:
|
||||
RehearsalLog.append(stepLogNode)
|
||||
RehearsalLog.append(objectLogNode)
|
||||
|
||||
yield {
|
||||
"type": "step_complete",
|
||||
"step_index": step_index,
|
||||
"step_name": StepName,
|
||||
"step_log_node": stepLogNode,
|
||||
"object_log_node": objectLogNode,
|
||||
"assigned_agents": {StepName: assigned_agents_in_step}, # 该步骤使用的 agent
|
||||
}
|
||||
|
||||
|
||||
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_id
|
||||
if execution_id is None:
|
||||
import time
|
||||
execution_id = f"{general_goal}_{int(time.time() * 1000)}"
|
||||
|
||||
execution_state_manager.start_execution(execution_id, general_goal)
|
||||
|
||||
# 准备执行
|
||||
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 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)
|
||||
|
||||
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(execution_id)
|
||||
if not should_continue:
|
||||
util.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:
|
||||
break
|
||||
|
||||
# 如果所有步骤都已完成,等待可能的新步骤
|
||||
if completed_steps >= queue_total_steps:
|
||||
if empty_wait_count >= max_empty_wait_cycles:
|
||||
# 等待超时,退出执行
|
||||
break
|
||||
else:
|
||||
# 等待新步骤追加
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
else:
|
||||
# 还有步骤未完成,继续尝试获取
|
||||
await asyncio.sleep(0.5)
|
||||
empty_wait_count = 0 # 重置等待计数
|
||||
continue
|
||||
else:
|
||||
# 执行信息不存在,退出
|
||||
break
|
||||
|
||||
# 重置等待计数
|
||||
empty_wait_count = 0
|
||||
|
||||
# 获取最新的总步骤数(用于显示)
|
||||
execution_info = dynamic_execution_manager.get_execution_info(execution_id)
|
||||
current_total_steps = execution_info.get("total_steps", total_steps) if execution_info else total_steps
|
||||
|
||||
# 执行步骤
|
||||
async for event in execute_step_async_streaming(
|
||||
stepDescrip,
|
||||
plan["General Goal"],
|
||||
AgentProfile_Dict,
|
||||
KeyObjects,
|
||||
step_index,
|
||||
current_total_steps, # 使用动态更新的总步骤数
|
||||
execution_id,
|
||||
RehearsalLog # 传递 RehearsalLog 用于追加日志
|
||||
):
|
||||
if execution_state_manager.is_stopped(execution_id):
|
||||
await queue.put({
|
||||
"type": "error",
|
||||
"message": "执行已被用户停止"
|
||||
})
|
||||
return
|
||||
|
||||
await queue.put(event)
|
||||
|
||||
# 标记步骤完成
|
||||
dynamic_execution_manager.mark_step_completed(execution_id)
|
||||
|
||||
# 更新KeyObjects
|
||||
OutputName = stepDescrip.get("OutputObject", "")
|
||||
if OutputName and OutputName in KeyObjects:
|
||||
# 对象日志节点会在step_complete中发送
|
||||
pass
|
||||
|
||||
step_index += 1
|
||||
|
||||
else:
|
||||
# 非动态模式:按顺序执行所有步骤
|
||||
for step_index, stepDescrip in enumerate(steps_to_run):
|
||||
should_continue = await execution_state_manager.async_check_pause(execution_id)
|
||||
if not should_continue:
|
||||
util.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,
|
||||
execution_id,
|
||||
RehearsalLog # 传递 RehearsalLog 用于追加日志
|
||||
):
|
||||
if execution_state_manager.is_stopped(execution_id):
|
||||
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(execution_id):
|
||||
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)
|
||||
# 清理执行状态
|
||||
execution_state_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
|
||||
@@ -1,226 +0,0 @@
|
||||
"""
|
||||
动态执行管理器
|
||||
用于在任务执行过程中动态追加新步骤
|
||||
"""
|
||||
|
||||
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] 启动执行: {execution_id}, 步骤数={len(initial_steps)}")
|
||||
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:
|
||||
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)
|
||||
|
||||
# 更新总步骤数
|
||||
self._executions[execution_id]["total_steps"] = len(self._step_queues[execution_id])
|
||||
|
||||
print(f"[Execution] 追加步骤: +{len(new_steps)}, 总计={self._executions[execution_id]['total_steps']}")
|
||||
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:
|
||||
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]):
|
||||
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"[Execution] 获取步骤: {step_name} (索引: {step_index})")
|
||||
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"[Execution] 步骤完成: {completed}/{total}")
|
||||
|
||||
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()
|
||||
@@ -1,292 +0,0 @@
|
||||
"""
|
||||
全局执行状态管理器
|
||||
用于支持任务的暂停、恢复和停止功能
|
||||
使用轮询检查机制,确保线程安全
|
||||
支持多用户/多执行ID并行管理
|
||||
"""
|
||||
|
||||
import threading
|
||||
import asyncio
|
||||
from typing import Optional, Dict
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ExecutionStatus(Enum):
|
||||
"""执行状态枚举"""
|
||||
RUNNING = "running" # 正在运行
|
||||
PAUSED = "paused" # 已暂停
|
||||
STOPPED = "stopped" # 已停止
|
||||
IDLE = "idle" # 空闲
|
||||
|
||||
|
||||
class ExecutionStateManager:
|
||||
"""
|
||||
全局执行状态管理器
|
||||
|
||||
功能:
|
||||
- 管理多用户/多执行ID的并行状态(使用字典存储)
|
||||
- 管理任务执行状态(运行/暂停/停止)
|
||||
- 使用轮询检查机制,避免异步事件的线程问题
|
||||
- 提供线程安全的状态查询和修改接口
|
||||
|
||||
设计说明:
|
||||
- 保持单例模式(Manager本身)
|
||||
- 但内部状态按 execution_id 隔离存储
|
||||
- 解决了多用户并发问题
|
||||
"""
|
||||
|
||||
_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
|
||||
|
||||
# 状态存储:execution_id -> 状态字典
|
||||
# 结构:{
|
||||
# 'status': ExecutionStatus,
|
||||
# 'goal': str,
|
||||
# 'should_pause': bool,
|
||||
# 'should_stop': bool
|
||||
# }
|
||||
self._states: Dict[str, Dict] = {}
|
||||
|
||||
# 每个 execution_id 的锁(更细粒度的锁)
|
||||
self._locks: Dict[str, threading.Lock] = {}
|
||||
|
||||
# 全局锁(用于管理 _states 和 _locks 本身的线程安全)
|
||||
self._manager_lock = threading.Lock()
|
||||
|
||||
def _get_lock(self, execution_id: str) -> threading.Lock:
|
||||
"""获取指定 execution_id 的锁,如果不存在则创建"""
|
||||
with self._manager_lock:
|
||||
if execution_id not in self._locks:
|
||||
self._locks[execution_id] = threading.Lock()
|
||||
return self._locks[execution_id]
|
||||
|
||||
def _ensure_state(self, execution_id: str) -> Dict:
|
||||
"""确保指定 execution_id 的状态存在"""
|
||||
with self._manager_lock:
|
||||
if execution_id not in self._states:
|
||||
self._states[execution_id] = {
|
||||
'status': ExecutionStatus.IDLE,
|
||||
'goal': None,
|
||||
'should_pause': False,
|
||||
'should_stop': False
|
||||
}
|
||||
return self._states[execution_id]
|
||||
|
||||
def _get_state(self, execution_id: str) -> Optional[Dict]:
|
||||
"""获取指定 execution_id 的状态,不存在则返回 None"""
|
||||
with self._manager_lock:
|
||||
return self._states.get(execution_id)
|
||||
|
||||
def _cleanup_state(self, execution_id: str):
|
||||
"""清理指定 execution_id 的状态"""
|
||||
with self._manager_lock:
|
||||
self._states.pop(execution_id, None)
|
||||
self._locks.pop(execution_id, None)
|
||||
|
||||
def get_status(self, execution_id: str) -> Optional[ExecutionStatus]:
|
||||
"""获取当前执行状态"""
|
||||
state = self._get_state(execution_id)
|
||||
if state is None:
|
||||
return None
|
||||
with self._get_lock(execution_id):
|
||||
return state['status']
|
||||
|
||||
def set_goal(self, execution_id: str, goal: str):
|
||||
"""设置当前执行的任务目标"""
|
||||
state = self._ensure_state(execution_id)
|
||||
with self._get_lock(execution_id):
|
||||
state['goal'] = goal
|
||||
|
||||
def get_goal(self, execution_id: str) -> Optional[str]:
|
||||
"""获取当前执行的任务目标"""
|
||||
state = self._get_state(execution_id)
|
||||
if state is None:
|
||||
return None
|
||||
with self._get_lock(execution_id):
|
||||
return state['goal']
|
||||
|
||||
def start_execution(self, execution_id: str, goal: str):
|
||||
"""开始执行"""
|
||||
state = self._ensure_state(execution_id)
|
||||
with self._get_lock(execution_id):
|
||||
state['status'] = ExecutionStatus.RUNNING
|
||||
state['goal'] = goal
|
||||
state['should_pause'] = False
|
||||
state['should_stop'] = False
|
||||
|
||||
def pause_execution(self, execution_id: str) -> bool:
|
||||
"""
|
||||
暂停执行
|
||||
|
||||
Args:
|
||||
execution_id: 执行ID
|
||||
|
||||
Returns:
|
||||
bool: 是否成功暂停
|
||||
"""
|
||||
state = self._get_state(execution_id)
|
||||
if state is None:
|
||||
return False
|
||||
|
||||
with self._get_lock(execution_id):
|
||||
if state['status'] != ExecutionStatus.RUNNING:
|
||||
return False
|
||||
state['status'] = ExecutionStatus.PAUSED
|
||||
state['should_pause'] = True
|
||||
return True
|
||||
|
||||
def resume_execution(self, execution_id: str) -> bool:
|
||||
"""
|
||||
恢复执行
|
||||
|
||||
Args:
|
||||
execution_id: 执行ID
|
||||
|
||||
Returns:
|
||||
bool: 是否成功恢复
|
||||
"""
|
||||
state = self._get_state(execution_id)
|
||||
if state is None:
|
||||
return False
|
||||
|
||||
with self._get_lock(execution_id):
|
||||
if state['status'] != ExecutionStatus.PAUSED:
|
||||
return False
|
||||
state['status'] = ExecutionStatus.RUNNING
|
||||
state['should_pause'] = False
|
||||
return True
|
||||
|
||||
def stop_execution(self, execution_id: str) -> bool:
|
||||
"""
|
||||
停止执行
|
||||
|
||||
Args:
|
||||
execution_id: 执行ID
|
||||
|
||||
Returns:
|
||||
bool: 是否成功停止
|
||||
"""
|
||||
state = self._get_state(execution_id)
|
||||
if state is None:
|
||||
return False
|
||||
|
||||
with self._get_lock(execution_id):
|
||||
if state['status'] in [ExecutionStatus.IDLE, ExecutionStatus.STOPPED]:
|
||||
return False
|
||||
state['status'] = ExecutionStatus.STOPPED
|
||||
state['should_stop'] = True
|
||||
state['should_pause'] = False
|
||||
return True
|
||||
|
||||
def reset(self, execution_id: str):
|
||||
"""重置指定 execution_id 的状态为空闲"""
|
||||
state = self._ensure_state(execution_id)
|
||||
with self._get_lock(execution_id):
|
||||
state['status'] = ExecutionStatus.IDLE
|
||||
state['goal'] = None
|
||||
state['should_pause'] = False
|
||||
state['should_stop'] = False
|
||||
|
||||
def cleanup(self, execution_id: str):
|
||||
"""清理指定 execution_id 的所有状态"""
|
||||
self._cleanup_state(execution_id)
|
||||
|
||||
async def async_check_pause(self, execution_id: str):
|
||||
"""
|
||||
异步检查是否需要暂停(轮询方式)
|
||||
|
||||
如果处于暂停状态,会阻塞当前协程直到恢复或停止
|
||||
应该在执行循环的关键点调用此方法
|
||||
|
||||
Args:
|
||||
execution_id: 执行ID
|
||||
|
||||
Returns:
|
||||
bool: 如果返回True表示应该继续执行,False表示应该停止
|
||||
"""
|
||||
state = self._get_state(execution_id)
|
||||
if state is None:
|
||||
# 状态不存在,默认继续执行
|
||||
return True
|
||||
|
||||
# 使用轮询检查,避免异步事件问题
|
||||
while True:
|
||||
with self._get_lock(execution_id):
|
||||
should_stop = state['should_stop']
|
||||
should_pause = state['should_pause']
|
||||
|
||||
# 检查停止标志
|
||||
if should_stop:
|
||||
return False
|
||||
|
||||
# 检查暂停状态
|
||||
if should_pause:
|
||||
# 处于暂停状态,等待恢复
|
||||
await asyncio.sleep(0.1) # 短暂睡眠,避免占用CPU
|
||||
|
||||
# 重新获取状态
|
||||
with self._get_lock(execution_id):
|
||||
should_pause = state['should_pause']
|
||||
should_stop = state['should_stop']
|
||||
|
||||
if not should_pause:
|
||||
continue
|
||||
if should_stop:
|
||||
return False
|
||||
# 继续等待
|
||||
continue
|
||||
|
||||
# 既没有停止也没有暂停,可以继续执行
|
||||
return True
|
||||
|
||||
def is_paused(self, execution_id: str) -> bool:
|
||||
"""检查是否处于暂停状态"""
|
||||
state = self._get_state(execution_id)
|
||||
if state is None:
|
||||
return False
|
||||
with self._get_lock(execution_id):
|
||||
return state['status'] == ExecutionStatus.PAUSED
|
||||
|
||||
def is_running(self, execution_id: str) -> bool:
|
||||
"""检查是否正在运行"""
|
||||
state = self._get_state(execution_id)
|
||||
if state is None:
|
||||
return False
|
||||
with self._get_lock(execution_id):
|
||||
return state['status'] == ExecutionStatus.RUNNING
|
||||
|
||||
def is_stopped(self, execution_id: str) -> bool:
|
||||
"""检查是否已停止"""
|
||||
state = self._get_state(execution_id)
|
||||
if state is None:
|
||||
return True
|
||||
with self._get_lock(execution_id):
|
||||
return state['status'] == ExecutionStatus.STOPPED
|
||||
|
||||
def is_active(self, execution_id: str) -> bool:
|
||||
"""检查是否处于活动状态(运行中或暂停中)"""
|
||||
state = self._get_state(execution_id)
|
||||
if state is None:
|
||||
return False
|
||||
with self._get_lock(execution_id):
|
||||
return state['status'] in [ExecutionStatus.RUNNING, ExecutionStatus.PAUSED]
|
||||
|
||||
|
||||
# 全局单例实例
|
||||
execution_state_manager = ExecutionStateManager()
|
||||
@@ -1,220 +0,0 @@
|
||||
"""
|
||||
生成阶段状态管理器
|
||||
用于支持生成任务的暂停、停止功能
|
||||
使用轮询检查机制,确保线程安全
|
||||
支持多用户/多generation_id并行管理
|
||||
"""
|
||||
|
||||
import threading
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Optional, Dict
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class GenerationStatus(Enum):
|
||||
"""生成状态枚举"""
|
||||
GENERATING = "generating" # 正在生成
|
||||
PAUSED = "paused" # 已暂停
|
||||
STOPPED = "stopped" # 已停止
|
||||
COMPLETED = "completed" # 已完成
|
||||
IDLE = "idle" # 空闲
|
||||
|
||||
|
||||
class GenerationStateManager:
|
||||
"""
|
||||
生成阶段状态管理器
|
||||
|
||||
功能:
|
||||
- 管理多用户/多generation_id的并行状态(使用字典存储)
|
||||
- 管理生成任务状态(生成中/暂停/停止/完成)
|
||||
- 提供线程安全的状态查询和修改接口
|
||||
|
||||
设计说明:
|
||||
- 保持单例模式(Manager本身)
|
||||
- 但内部状态按 generation_id 隔离存储
|
||||
- 解决多用户并发生成时的干扰问题
|
||||
"""
|
||||
|
||||
_instance: Optional['GenerationStateManager'] = None
|
||||
_lock = threading.Lock()
|
||||
|
||||
def __new__(cls):
|
||||
"""单例模式"""
|
||||
if cls._instance is None:
|
||||
with cls._lock:
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance._initialized = False
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""初始化状态管理器"""
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
self._initialized = True
|
||||
|
||||
# 状态存储:generation_id -> 状态字典
|
||||
# 结构:{
|
||||
# 'status': GenerationStatus,
|
||||
# 'goal': str,
|
||||
# 'should_stop': bool
|
||||
# }
|
||||
self._states: Dict[str, Dict] = {}
|
||||
|
||||
# 每个 generation_id 的锁(更细粒度的锁)
|
||||
self._locks: Dict[str, threading.Lock] = {}
|
||||
|
||||
# 全局锁(用于管理 _states 和 _locks 本身的线程安全)
|
||||
self._manager_lock = threading.Lock()
|
||||
|
||||
def _get_lock(self, generation_id: str) -> threading.Lock:
|
||||
"""获取指定 generation_id 的锁,如果不存在则创建"""
|
||||
with self._manager_lock:
|
||||
if generation_id not in self._locks:
|
||||
self._locks[generation_id] = threading.Lock()
|
||||
return self._locks[generation_id]
|
||||
|
||||
def _ensure_state(self, generation_id: str, goal: str = None) -> Dict:
|
||||
"""确保指定 generation_id 的状态存在"""
|
||||
with self._manager_lock:
|
||||
if generation_id not in self._states:
|
||||
self._states[generation_id] = {
|
||||
'status': GenerationStatus.IDLE,
|
||||
'goal': goal,
|
||||
'should_stop': False
|
||||
}
|
||||
return self._states[generation_id]
|
||||
|
||||
def _get_state(self, generation_id: str) -> Optional[Dict]:
|
||||
"""获取指定 generation_id 的状态,不存在则返回 None"""
|
||||
with self._manager_lock:
|
||||
return self._states.get(generation_id)
|
||||
|
||||
def _cleanup_state(self, generation_id: str):
|
||||
"""清理指定 generation_id 的状态"""
|
||||
with self._manager_lock:
|
||||
self._states.pop(generation_id, None)
|
||||
self._locks.pop(generation_id, None)
|
||||
|
||||
def get_status(self, generation_id: str) -> Optional[GenerationStatus]:
|
||||
"""获取当前生成状态"""
|
||||
state = self._get_state(generation_id)
|
||||
if state is None:
|
||||
return None
|
||||
with self._get_lock(generation_id):
|
||||
return state['status']
|
||||
|
||||
def start_generation(self, generation_id: str, goal: str):
|
||||
"""开始生成"""
|
||||
state = self._ensure_state(generation_id, goal)
|
||||
with self._get_lock(generation_id):
|
||||
state['status'] = GenerationStatus.GENERATING
|
||||
state['goal'] = goal
|
||||
state['should_stop'] = False
|
||||
|
||||
def stop_generation(self, generation_id: str) -> bool:
|
||||
"""
|
||||
停止生成
|
||||
|
||||
Args:
|
||||
generation_id: 生成ID
|
||||
|
||||
Returns:
|
||||
bool: 是否成功停止(COMPLETED 状态也返回 True,表示已停止)
|
||||
"""
|
||||
state = self._get_state(generation_id)
|
||||
if state is None:
|
||||
return True # 不存在也算停止成功
|
||||
|
||||
with self._get_lock(generation_id):
|
||||
if state['status'] == GenerationStatus.STOPPED:
|
||||
return True # 已经停止也算成功
|
||||
|
||||
if state['status'] == GenerationStatus.COMPLETED:
|
||||
return True # 已完成也视为停止成功
|
||||
|
||||
if state['status'] == GenerationStatus.IDLE:
|
||||
return True # 空闲状态也视为无需停止
|
||||
|
||||
# 真正需要停止的情况
|
||||
state['status'] = GenerationStatus.STOPPED
|
||||
state['should_stop'] = True
|
||||
return True
|
||||
|
||||
def complete_generation(self, generation_id: str):
|
||||
"""标记生成完成"""
|
||||
state = self._ensure_state(generation_id)
|
||||
with self._get_lock(generation_id):
|
||||
state['status'] = GenerationStatus.COMPLETED
|
||||
|
||||
def cleanup(self, generation_id: str):
|
||||
"""清理指定 generation_id 的所有状态"""
|
||||
self._cleanup_state(generation_id)
|
||||
|
||||
def should_stop(self, generation_id: str) -> bool:
|
||||
"""检查是否应该停止"""
|
||||
state = self._get_state(generation_id)
|
||||
if state is None:
|
||||
return False
|
||||
with self._get_lock(generation_id):
|
||||
return state.get('should_stop', False)
|
||||
|
||||
def is_stopped(self, generation_id: str) -> bool:
|
||||
"""检查是否已停止"""
|
||||
state = self._get_state(generation_id)
|
||||
if state is None:
|
||||
return False
|
||||
with self._get_lock(generation_id):
|
||||
return state['status'] == GenerationStatus.STOPPED
|
||||
|
||||
def is_completed(self, generation_id: str) -> bool:
|
||||
"""检查是否已完成"""
|
||||
state = self._get_state(generation_id)
|
||||
if state is None:
|
||||
return False
|
||||
with self._get_lock(generation_id):
|
||||
return state['status'] == GenerationStatus.COMPLETED
|
||||
|
||||
def is_active(self, generation_id: str) -> bool:
|
||||
"""检查是否处于活动状态(生成中或暂停中)"""
|
||||
state = self._get_state(generation_id)
|
||||
if state is None:
|
||||
return False
|
||||
with self._get_lock(generation_id):
|
||||
return state['status'] == GenerationStatus.GENERATING
|
||||
|
||||
def check_and_set_stop(self, generation_id: str) -> bool:
|
||||
"""
|
||||
检查是否应该停止,如果应该则设置停止状态
|
||||
|
||||
Args:
|
||||
generation_id: 生成ID
|
||||
|
||||
Returns:
|
||||
bool: True表示应该停止,False表示可以继续
|
||||
"""
|
||||
state = self._get_state(generation_id)
|
||||
if state is None:
|
||||
return False
|
||||
with self._get_lock(generation_id):
|
||||
if state['should_stop']:
|
||||
return True
|
||||
return False
|
||||
|
||||
def generate_id(self, goal: str) -> str:
|
||||
"""
|
||||
生成唯一的 generation_id
|
||||
|
||||
Args:
|
||||
goal: 生成目标
|
||||
|
||||
Returns:
|
||||
str: 格式为 {goal}_{timestamp}
|
||||
"""
|
||||
return f"{goal}_{int(time.time() * 1000)}"
|
||||
|
||||
|
||||
# 全局单例实例
|
||||
generation_state_manager = GenerationStateManager()
|
||||
@@ -1,2 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 导出常用模块和函数,根据实际需求调整
|
||||
from .PlanEngine.planOutline_Generator import generate_PlanOutline
|
||||
from .PlanEngine.basePlan_Generator import generate_basePlan
|
||||
from .PlanEngine.AgentSelection_Generator import generate_AgentSelection
|
||||
from .LLMAPI.LLMAPI import LLM_Completion
|
||||
from .util.converter import read_LLM_Completion
|
||||
|
||||
# 定义包元数据
|
||||
__version__ = "0.1.0"
|
||||
__all__ = [
|
||||
"generate_PlanOutline",
|
||||
"generate_basePlan",
|
||||
"generate_AgentSelection",
|
||||
"LLM_Completion",
|
||||
"read_LLM_Completion"
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import re
|
||||
import json
|
||||
from AgentCoord.LLMAPI.LLMAPI import LLM_Completion
|
||||
from AgentCoord.LLMAPI.LLMAPI import LLM_Completion,GROQ_API_KEY
|
||||
|
||||
|
||||
def create_agent_dict(agent_list):
|
||||
@@ -20,8 +20,6 @@ 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 = (
|
||||
[
|
||||
@@ -33,48 +31,29 @@ def generate_template_sentence_for_CollaborationBrief(
|
||||
)
|
||||
output_object = (
|
||||
camel_case_to_normal(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")
|
||||
if is_camel_case(output_object)
|
||||
else output_object
|
||||
)
|
||||
|
||||
# Format the agents into a string with proper grammar
|
||||
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"
|
||||
agent_str = (
|
||||
" and ".join([", ".join(agent_list[:-1]), agent_list[-1]])
|
||||
if len(agent_list) > 1
|
||||
else agent_list[0]
|
||||
)
|
||||
|
||||
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
|
||||
# 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]
|
||||
input_str = (
|
||||
" and ".join(
|
||||
[", ".join(input_object_list[:-1]), input_object_list[-1]]
|
||||
)
|
||||
else:
|
||||
input_str = "unknown inputs"
|
||||
if len(input_object_list) > 1
|
||||
else input_object_list[0]
|
||||
)
|
||||
# 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}."
|
||||
|
||||
@@ -94,25 +73,80 @@ def remove_render_spec(duty_spec):
|
||||
return duty_spec
|
||||
|
||||
|
||||
def read_LLM_Completion(messages, useGroq=None):
|
||||
if useGroq is None:
|
||||
useGroq = bool(GROQ_API_KEY)
|
||||
|
||||
# 添加调试信息和输入验证
|
||||
print(f"[DEBUG] read_LLM_Completion called with {len(messages)} messages", flush=True)
|
||||
if not messages or len(messages) == 0:
|
||||
raise ValueError("No messages provided to read_LLM_Completion")
|
||||
|
||||
# 确保messages中的每个元素都是有效的
|
||||
for i, msg in enumerate(messages):
|
||||
if not isinstance(msg, dict):
|
||||
print(f"[ERROR] Message {i} is not a dictionary: {type(msg)}", flush=True)
|
||||
raise ValueError(f"Message {i} is not a dictionary")
|
||||
if 'content' not in msg or msg['content'] is None:
|
||||
print(f"[ERROR] Message {i} has no content or content is None", flush=True)
|
||||
msg['content'] = "" # 提供默认空字符串
|
||||
|
||||
for attempt in range(3):
|
||||
try:
|
||||
print(f"[DEBUG] Attempt {attempt + 1}/3 to get LLM response", flush=True)
|
||||
text = LLM_Completion(messages, useGroq=useGroq)
|
||||
|
||||
# 确保text是字符串类型
|
||||
if text is None:
|
||||
print(f"[ERROR] Null response from LLM on attempt {attempt + 1}", flush=True)
|
||||
continue
|
||||
|
||||
text = str(text).strip()
|
||||
if not text:
|
||||
print(f"[ERROR] Empty response from LLM on attempt {attempt + 1}", flush=True)
|
||||
continue
|
||||
|
||||
print(f"[DEBUG] LLM response length: {len(text)} characters", flush=True)
|
||||
|
||||
def read_LLM_Completion(messages, useGroq=True):
|
||||
for _ in range(3):
|
||||
text = LLM_Completion(messages, useGroq=useGroq)
|
||||
# 尝试从代码块中提取JSON
|
||||
pattern = r"(?:.*?```json)(.*?)(?:```.*?)"
|
||||
match = re.search(pattern, text, re.DOTALL)
|
||||
|
||||
pattern = r"(?:.*?```json)(.*?)(?:```.*?)"
|
||||
match = re.search(pattern, text, re.DOTALL)
|
||||
if match:
|
||||
json_content = match.group(1).strip()
|
||||
print(f"[DEBUG] Found JSON in code block, length: {len(json_content)}", flush=True)
|
||||
try:
|
||||
result = json.loads(json_content)
|
||||
print(f"[DEBUG] Successfully parsed JSON from code block", flush=True)
|
||||
return result
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"[ERROR] JSON decode error in code block: {e}", flush=True)
|
||||
print(f"[ERROR] JSON content was: {json_content}", flush=True)
|
||||
|
||||
if match:
|
||||
return json.loads(match.group(1).strip())
|
||||
|
||||
pattern = r"\{.*\}"
|
||||
match = re.search(pattern, text, re.DOTALL)
|
||||
if match:
|
||||
try:
|
||||
return json.loads(match.group(0).strip())
|
||||
except Exception:
|
||||
pass
|
||||
return {} # 返回空对象而不是抛出异常
|
||||
# 尝试直接提取JSON对象
|
||||
pattern = r"\{.*\}"
|
||||
match = re.search(pattern, text, re.DOTALL)
|
||||
if match:
|
||||
json_content = match.group(0).strip()
|
||||
print(f"[DEBUG] Found JSON in plain text, length: {len(json_content)}", flush=True)
|
||||
try:
|
||||
result = json.loads(json_content)
|
||||
print(f"[DEBUG] Successfully parsed JSON from plain text", flush=True)
|
||||
return result
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"[ERROR] JSON decode error in plain text: {e}", flush=True)
|
||||
print(f"[ERROR] JSON content was: {json_content}", flush=True)
|
||||
|
||||
print(f"[ERROR] No valid JSON found in response on attempt {attempt + 1}", flush=True)
|
||||
print(f"[ERROR] Full response was: {text[:200]}..." if len(text) > 200 else f"[ERROR] Full response was: {text}", flush=True)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Exception on attempt {attempt + 1}: {e}", flush=True)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
continue
|
||||
|
||||
raise ValueError(f"Failed to get valid JSON response after 3 attempts. Last error: bad format or empty response")
|
||||
|
||||
|
||||
def read_json_content(text):
|
||||
@@ -133,7 +167,7 @@ def read_json_content(text):
|
||||
if match:
|
||||
return json.loads(match.group(0).strip())
|
||||
|
||||
return {} # 返回空对象而不是抛出异常
|
||||
raise ("bad format!")
|
||||
|
||||
|
||||
def read_outputObject_content(text, keyword):
|
||||
@@ -149,4 +183,4 @@ def read_outputObject_content(text, keyword):
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
else:
|
||||
return "" # 返回空字符串而不是抛出异常
|
||||
raise ("bad format!")
|
||||
@@ -1,116 +1,102 @@
|
||||
[
|
||||
{
|
||||
"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": "科学数据空间"
|
||||
}
|
||||
]
|
||||
{
|
||||
"Icon": "Abigail_Chen.png",
|
||||
"Name": "Abigail",
|
||||
"Profile": "AI Engineer"
|
||||
},
|
||||
{
|
||||
"Icon": "Jane_Moreno.png",
|
||||
"Name": "Jane",
|
||||
"Profile": "Cybersecurity Specialist"
|
||||
},
|
||||
{
|
||||
"Icon": "Giorgio_Rossi.png",
|
||||
"Name": "Giorgio",
|
||||
"Profile": "Poet"
|
||||
},
|
||||
{
|
||||
"Icon": "Jennifer_Moore.png",
|
||||
"Name": "Jennifer",
|
||||
"Profile": "Linguist"
|
||||
},
|
||||
{
|
||||
"Icon": "Maria_Lopez.png",
|
||||
"Name": "Maria",
|
||||
"Profile": "Philosopher"
|
||||
},
|
||||
{
|
||||
"Icon": "Sam_Moore.png",
|
||||
"Name": "Sam",
|
||||
"Profile": "Ethicist"
|
||||
},
|
||||
{
|
||||
"Icon": "Yuriko_Yamamoto.png",
|
||||
"Name": "Yuriko",
|
||||
"Profile": "Futurist"
|
||||
},
|
||||
{
|
||||
"Icon": "Carlos_Gomez.png",
|
||||
"Name": "Carlos",
|
||||
"Profile": "Language Expert"
|
||||
},
|
||||
{
|
||||
"Icon": "John_Lin.png",
|
||||
"Name": "John",
|
||||
"Profile": "Software Developer"
|
||||
},
|
||||
{
|
||||
"Icon": "Tamara_Taylor.png",
|
||||
"Name": "Tamara",
|
||||
"Profile": "Music Composer"
|
||||
},
|
||||
{
|
||||
"Icon": "Arthur_Burton.png",
|
||||
"Name": "Arthur",
|
||||
"Profile": "Neuroscientist"
|
||||
},
|
||||
{
|
||||
"Icon": "Eddy_Lin.png",
|
||||
"Name": "Eddy",
|
||||
"Profile": "Cognitive Psychologist"
|
||||
},
|
||||
{
|
||||
"Icon": "Isabella_Rodriguez.png",
|
||||
"Name": "Isabella",
|
||||
"Profile": "Science Fiction Writer"
|
||||
},
|
||||
{
|
||||
"Icon": "Latoya_Williams.png",
|
||||
"Name": "Latoya",
|
||||
"Profile": "Historian of Technology"
|
||||
},
|
||||
{
|
||||
"Icon": "Carmen_Ortiz.png",
|
||||
"Name": "Carmen",
|
||||
"Profile": "Robotics Engineer"
|
||||
},
|
||||
{
|
||||
"Icon": "Rajiv_Patel.png",
|
||||
"Name": "Rajiv",
|
||||
"Profile": "Science Educator"
|
||||
},
|
||||
{
|
||||
"Icon": "Tom_Moreno.png",
|
||||
"Name": "Tom",
|
||||
"Profile": "AI Scientist"
|
||||
},
|
||||
{
|
||||
"Icon": "Ayesha_Khan.png",
|
||||
"Name": "Ayesha",
|
||||
"Profile": "Multimedia Artist"
|
||||
},
|
||||
{
|
||||
"Icon": "Mei_Lin.png",
|
||||
"Name": "Mei",
|
||||
"Profile": "Graphic Designer"
|
||||
},
|
||||
{
|
||||
"Icon": "Hailey_Johnson.png",
|
||||
"Name": "Hailey",
|
||||
"Profile": "Legal Expert on AI Law"
|
||||
}
|
||||
]
|
||||
@@ -1,102 +0,0 @@
|
||||
[
|
||||
{
|
||||
"Icon": "Abigail_Chen.png",
|
||||
"Name": "Abigail",
|
||||
"Profile": "AI Engineer"
|
||||
},
|
||||
{
|
||||
"Icon": "Jane_Moreno.png",
|
||||
"Name": "Jane",
|
||||
"Profile": "Cybersecurity Specialist"
|
||||
},
|
||||
{
|
||||
"Icon": "Giorgio_Rossi.png",
|
||||
"Name": "Giorgio",
|
||||
"Profile": "Poet"
|
||||
},
|
||||
{
|
||||
"Icon": "Jennifer_Moore.png",
|
||||
"Name": "Jennifer",
|
||||
"Profile": "Linguist"
|
||||
},
|
||||
{
|
||||
"Icon": "Maria_Lopez.png",
|
||||
"Name": "Maria",
|
||||
"Profile": "Philosopher"
|
||||
},
|
||||
{
|
||||
"Icon": "Sam_Moore.png",
|
||||
"Name": "Sam",
|
||||
"Profile": "Ethicist"
|
||||
},
|
||||
{
|
||||
"Icon": "Yuriko_Yamamoto.png",
|
||||
"Name": "Yuriko",
|
||||
"Profile": "Futurist"
|
||||
},
|
||||
{
|
||||
"Icon": "Carlos_Gomez.png",
|
||||
"Name": "Carlos",
|
||||
"Profile": "Language Expert"
|
||||
},
|
||||
{
|
||||
"Icon": "John_Lin.png",
|
||||
"Name": "John",
|
||||
"Profile": "Software Developer"
|
||||
},
|
||||
{
|
||||
"Icon": "Tamara_Taylor.png",
|
||||
"Name": "Tamara",
|
||||
"Profile": "Music Composer"
|
||||
},
|
||||
{
|
||||
"Icon": "Arthur_Burton.png",
|
||||
"Name": "Arthur",
|
||||
"Profile": "Neuroscientist"
|
||||
},
|
||||
{
|
||||
"Icon": "Eddy_Lin.png",
|
||||
"Name": "Eddy",
|
||||
"Profile": "Cognitive Psychologist"
|
||||
},
|
||||
{
|
||||
"Icon": "Isabella_Rodriguez.png",
|
||||
"Name": "Isabella",
|
||||
"Profile": "Science Fiction Writer"
|
||||
},
|
||||
{
|
||||
"Icon": "Latoya_Williams.png",
|
||||
"Name": "Latoya",
|
||||
"Profile": "Historian of Technology"
|
||||
},
|
||||
{
|
||||
"Icon": "Carmen_Ortiz.png",
|
||||
"Name": "Carmen",
|
||||
"Profile": "Robotics Engineer"
|
||||
},
|
||||
{
|
||||
"Icon": "Rajiv_Patel.png",
|
||||
"Name": "Rajiv",
|
||||
"Profile": "Science Educator"
|
||||
},
|
||||
{
|
||||
"Icon": "Tom_Moreno.png",
|
||||
"Name": "Tom",
|
||||
"Profile": "AI Scientist"
|
||||
},
|
||||
{
|
||||
"Icon": "Ayesha_Khan.png",
|
||||
"Name": "Ayesha",
|
||||
"Profile": "Multimedia Artist"
|
||||
},
|
||||
{
|
||||
"Icon": "Mei_Lin.png",
|
||||
"Name": "Mei",
|
||||
"Profile": "Graphic Designer"
|
||||
},
|
||||
{
|
||||
"Icon": "Hailey_Johnson.png",
|
||||
"Name": "Hailey",
|
||||
"Profile": "Legal Expert on AI Law"
|
||||
}
|
||||
]
|
||||
@@ -2,28 +2,11 @@
|
||||
OPENAI_API_BASE: "https://ai.gitee.com/v1"
|
||||
OPENAI_API_KEY: "HYCNGM39GGFNSB1F8MBBMI9QYJR3P1CRSYS2PV1A"
|
||||
OPENAI_API_MODEL: "DeepSeek-V3"
|
||||
# OPENAI_API_BASE: "https://qianfan.baidubce.com/v2"
|
||||
# OPENAI_API_KEY: "bce-v3/ALTAK-Rp1HuLgqIdXM1rywQHRxr/96c5a0e99ccddf91193157e7d42d89979981bf05"
|
||||
# OPENAI_API_MODEL: "deepseek-v3"
|
||||
#OPENAI_API_BASE: "https://api.yyds168.net/v1"
|
||||
#OPENAI_API_KEY: "sk-5SJms7yWhGtb0YuwayqaDPjU95s4gapMqkaX5q9opWn3Ati3"
|
||||
#OPENAI_API_MODEL: "deepseek-v3"
|
||||
|
||||
|
||||
## config for fast mode
|
||||
FAST_DESIGN_MODE: False
|
||||
FAST_DESIGN_MODE: True
|
||||
GROQ_API_KEY: ""
|
||||
MISTRAL_API_KEY: ""
|
||||
|
||||
## options under experimentation, leave them as Fasle unless you know what it is for
|
||||
USE_CACHE: False
|
||||
|
||||
# PostgreSQL 数据库配置
|
||||
database:
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
username: "postgres"
|
||||
password: "123456"
|
||||
name: "agentcoord"
|
||||
pool_size: 10
|
||||
max_overflow: 20
|
||||
## options under experimentation, leave them as False unless you know what it is for
|
||||
USE_CACHE: False
|
||||
@@ -1,29 +0,0 @@
|
||||
"""
|
||||
AgentCoord 数据库模块
|
||||
提供 PostgreSQL 数据库连接、模型和 CRUD 操作
|
||||
基于 DATABASE_DESIGN.md 设计
|
||||
"""
|
||||
|
||||
from .database import get_db, get_db_context, test_connection, engine, text
|
||||
from .models import MultiAgentTask, UserAgent, ExportRecord, PlanShare, TaskStatus
|
||||
from .crud import MultiAgentTaskCRUD, UserAgentCRUD, ExportRecordCRUD, PlanShareCRUD
|
||||
|
||||
__all__ = [
|
||||
# 连接管理
|
||||
"get_db",
|
||||
"get_db_context",
|
||||
"test_connection",
|
||||
"engine",
|
||||
"text",
|
||||
# 模型
|
||||
"MultiAgentTask",
|
||||
"UserAgent",
|
||||
"ExportRecord",
|
||||
"PlanShare",
|
||||
"TaskStatus",
|
||||
# CRUD
|
||||
"MultiAgentTaskCRUD",
|
||||
"UserAgentCRUD",
|
||||
"ExportRecordCRUD",
|
||||
"PlanShareCRUD",
|
||||
]
|
||||
@@ -1,577 +0,0 @@
|
||||
"""
|
||||
数据库 CRUD 操作
|
||||
封装所有数据库操作方法 (基于 DATABASE_DESIGN.md)
|
||||
"""
|
||||
|
||||
import copy
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .models import MultiAgentTask, UserAgent, ExportRecord, PlanShare
|
||||
|
||||
|
||||
class MultiAgentTaskCRUD:
|
||||
"""多智能体任务 CRUD 操作"""
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
db: Session,
|
||||
task_id: Optional[str] = None, # 可选,如果为 None 则自动生成
|
||||
user_id: str = "",
|
||||
query: str = "",
|
||||
agents_info: list = [],
|
||||
task_outline: Optional[dict] = None,
|
||||
assigned_agents: Optional[list] = None,
|
||||
agent_scores: Optional[dict] = None,
|
||||
result: Optional[str] = None,
|
||||
) -> MultiAgentTask:
|
||||
"""创建任务记录"""
|
||||
task = MultiAgentTask(
|
||||
task_id=task_id or str(uuid.uuid4()), # 如果没传则生成新的
|
||||
user_id=user_id,
|
||||
query=query,
|
||||
agents_info=agents_info,
|
||||
task_outline=task_outline,
|
||||
assigned_agents=assigned_agents,
|
||||
agent_scores=agent_scores,
|
||||
result=result,
|
||||
)
|
||||
db.add(task)
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(db: Session, task_id: str) -> Optional[MultiAgentTask]:
|
||||
"""根据任务 ID 获取记录"""
|
||||
return db.query(MultiAgentTask).filter(MultiAgentTask.task_id == task_id).first()
|
||||
|
||||
@staticmethod
|
||||
def get_by_user_id(
|
||||
db: Session, user_id: str, limit: int = 50, offset: int = 0
|
||||
) -> List[MultiAgentTask]:
|
||||
"""根据用户 ID 获取任务记录"""
|
||||
return (
|
||||
db.query(MultiAgentTask)
|
||||
.filter(MultiAgentTask.user_id == user_id)
|
||||
.order_by(MultiAgentTask.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_recent(
|
||||
db: Session, limit: int = 20, offset: int = 0, user_id: str = None
|
||||
) -> List[MultiAgentTask]:
|
||||
"""获取最近的任务记录,置顶的排在最前面"""
|
||||
query = db.query(MultiAgentTask)
|
||||
# 按 user_id 过滤
|
||||
if user_id:
|
||||
query = query.filter(MultiAgentTask.user_id == user_id)
|
||||
return (
|
||||
query
|
||||
.order_by(MultiAgentTask.is_pinned.desc(), MultiAgentTask.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def update_result(
|
||||
db: Session, task_id: str, result: list
|
||||
) -> Optional[MultiAgentTask]:
|
||||
"""更新任务结果"""
|
||||
task = db.query(MultiAgentTask).filter(MultiAgentTask.task_id == task_id).first()
|
||||
if task:
|
||||
task.result = result if result else []
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def update_task_outline(
|
||||
db: Session, task_id: str, task_outline: dict
|
||||
) -> Optional[MultiAgentTask]:
|
||||
"""更新任务大纲"""
|
||||
task = db.query(MultiAgentTask).filter(MultiAgentTask.task_id == task_id).first()
|
||||
if task:
|
||||
task.task_outline = task_outline
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def update_assigned_agents(
|
||||
db: Session, task_id: str, assigned_agents: dict
|
||||
) -> Optional[MultiAgentTask]:
|
||||
"""更新分配的智能体(步骤名 -> agent列表)"""
|
||||
task = db.query(MultiAgentTask).filter(MultiAgentTask.task_id == task_id).first()
|
||||
if task:
|
||||
task.assigned_agents = assigned_agents
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def update_agent_scores(
|
||||
db: Session, task_id: str, agent_scores: dict
|
||||
) -> Optional[MultiAgentTask]:
|
||||
"""更新智能体评分(合并模式,追加新步骤的评分)"""
|
||||
task = db.query(MultiAgentTask).filter(MultiAgentTask.task_id == task_id).first()
|
||||
if task:
|
||||
# 合并现有评分数据和新评分数据
|
||||
existing_scores = task.agent_scores or {}
|
||||
merged_scores = {**existing_scores, **agent_scores} # 新数据覆盖/追加旧数据
|
||||
task.agent_scores = merged_scores
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def update_status(
|
||||
db: Session, task_id: str, status: str
|
||||
) -> Optional[MultiAgentTask]:
|
||||
"""更新任务状态"""
|
||||
task = db.query(MultiAgentTask).filter(MultiAgentTask.task_id == task_id).first()
|
||||
if task:
|
||||
task.status = status
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def increment_execution_count(db: Session, task_id: str) -> Optional[MultiAgentTask]:
|
||||
"""增加任务执行次数"""
|
||||
task = db.query(MultiAgentTask).filter(MultiAgentTask.task_id == task_id).first()
|
||||
if task:
|
||||
task.execution_count = (task.execution_count or 0) + 1
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def update_generation_id(
|
||||
db: Session, task_id: str, generation_id: str
|
||||
) -> Optional[MultiAgentTask]:
|
||||
"""更新生成 ID"""
|
||||
task = db.query(MultiAgentTask).filter(MultiAgentTask.task_id == task_id).first()
|
||||
if task:
|
||||
task.generation_id = generation_id
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def update_execution_id(
|
||||
db: Session, task_id: str, execution_id: str
|
||||
) -> Optional[MultiAgentTask]:
|
||||
"""更新执行 ID"""
|
||||
task = db.query(MultiAgentTask).filter(MultiAgentTask.task_id == task_id).first()
|
||||
if task:
|
||||
task.execution_id = execution_id
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def update_rehearsal_log(
|
||||
db: Session, task_id: str, rehearsal_log: list
|
||||
) -> Optional[MultiAgentTask]:
|
||||
"""更新排练日志"""
|
||||
task = db.query(MultiAgentTask).filter(MultiAgentTask.task_id == task_id).first()
|
||||
if task:
|
||||
task.rehearsal_log = rehearsal_log if rehearsal_log else []
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def update_is_pinned(
|
||||
db: Session, task_id: str, is_pinned: bool
|
||||
) -> Optional[MultiAgentTask]:
|
||||
"""更新任务置顶状态"""
|
||||
task = db.query(MultiAgentTask).filter(MultiAgentTask.task_id == task_id).first()
|
||||
if task:
|
||||
task.is_pinned = is_pinned
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def append_rehearsal_log(
|
||||
db: Session, task_id: str, log_entry: dict
|
||||
) -> Optional[MultiAgentTask]:
|
||||
"""追加排练日志条目"""
|
||||
task = db.query(MultiAgentTask).filter(MultiAgentTask.task_id == task_id).first()
|
||||
if task:
|
||||
current_log = task.rehearsal_log or []
|
||||
if isinstance(current_log, list):
|
||||
current_log.append(log_entry)
|
||||
else:
|
||||
current_log = [log_entry]
|
||||
task.rehearsal_log = current_log
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def update_branches(
|
||||
db: Session, task_id: str, branches
|
||||
) -> Optional[MultiAgentTask]:
|
||||
"""更新任务分支数据
|
||||
支持两种格式:
|
||||
- list: 旧格式,直接覆盖
|
||||
- dict: 新格式 { flow_branches: [...], task_process_branches: {...} }
|
||||
两个 key 独立保存,互不干扰。
|
||||
"""
|
||||
import copy
|
||||
task = db.query(MultiAgentTask).filter(MultiAgentTask.task_id == task_id).first()
|
||||
if task:
|
||||
if isinstance(branches, dict):
|
||||
# 新格式:字典,独立保存两个 key,互不干扰
|
||||
# 使用深拷贝避免引用共享问题
|
||||
existing = copy.deepcopy(task.branches) if task.branches else {}
|
||||
if isinstance(existing, dict):
|
||||
# 如果只更新 flow_branches,保留已有的 task_process_branches
|
||||
if 'flow_branches' in branches and 'task_process_branches' not in branches:
|
||||
branches['task_process_branches'] = existing.get('task_process_branches', {})
|
||||
# 如果只更新 task_process_branches,保留已有的 flow_branches
|
||||
if 'task_process_branches' in branches and 'flow_branches' not in branches:
|
||||
branches['flow_branches'] = existing.get('flow_branches', [])
|
||||
task.branches = branches
|
||||
else:
|
||||
# 旧格式:列表
|
||||
task.branches = branches if branches else []
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def get_branches(db: Session, task_id: str) -> Optional[list]:
|
||||
"""获取任务分支数据"""
|
||||
task = db.query(MultiAgentTask).filter(MultiAgentTask.task_id == task_id).first()
|
||||
if task:
|
||||
return task.branches or []
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_by_status(
|
||||
db: Session, status: str, limit: int = 50, offset: int = 0
|
||||
) -> List[MultiAgentTask]:
|
||||
"""根据状态获取任务记录"""
|
||||
return (
|
||||
db.query(MultiAgentTask)
|
||||
.filter(MultiAgentTask.status == status)
|
||||
.order_by(MultiAgentTask.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_by_generation_id(
|
||||
db: Session, generation_id: str
|
||||
) -> List[MultiAgentTask]:
|
||||
"""根据生成 ID 获取任务记录"""
|
||||
return (
|
||||
db.query(MultiAgentTask)
|
||||
.filter(MultiAgentTask.generation_id == generation_id)
|
||||
.all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_by_execution_id(
|
||||
db: Session, execution_id: str
|
||||
) -> List[MultiAgentTask]:
|
||||
"""根据执行 ID 获取任务记录"""
|
||||
return (
|
||||
db.query(MultiAgentTask)
|
||||
.filter(MultiAgentTask.execution_id == execution_id)
|
||||
.all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def delete(db: Session, task_id: str) -> bool:
|
||||
"""删除任务记录"""
|
||||
task = db.query(MultiAgentTask).filter(MultiAgentTask.task_id == task_id).first()
|
||||
if task:
|
||||
db.delete(task)
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class UserAgentCRUD:
|
||||
"""用户智能体配置 CRUD 操作"""
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
db: Session,
|
||||
user_id: str,
|
||||
agent_name: str,
|
||||
agent_config: dict,
|
||||
) -> UserAgent:
|
||||
"""创建用户智能体配置"""
|
||||
agent = UserAgent(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=user_id,
|
||||
agent_name=agent_name,
|
||||
agent_config=agent_config,
|
||||
)
|
||||
db.add(agent)
|
||||
db.commit()
|
||||
db.refresh(agent)
|
||||
return agent
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(db: Session, agent_id: str) -> Optional[UserAgent]:
|
||||
"""根据 ID 获取配置"""
|
||||
return db.query(UserAgent).filter(UserAgent.id == agent_id).first()
|
||||
|
||||
@staticmethod
|
||||
def get_by_user_id(
|
||||
db: Session, user_id: str, limit: int = 50
|
||||
) -> List[UserAgent]:
|
||||
"""根据用户 ID 获取所有智能体配置"""
|
||||
return (
|
||||
db.query(UserAgent)
|
||||
.filter(UserAgent.user_id == user_id)
|
||||
.order_by(UserAgent.created_at.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_by_name(
|
||||
db: Session, user_id: str, agent_name: str
|
||||
) -> List[UserAgent]:
|
||||
"""根据用户 ID 和智能体名称获取配置"""
|
||||
return (
|
||||
db.query(UserAgent)
|
||||
.filter(
|
||||
UserAgent.user_id == user_id,
|
||||
UserAgent.agent_name == agent_name,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def update_config(
|
||||
db: Session, agent_id: str, agent_config: dict
|
||||
) -> Optional[UserAgent]:
|
||||
"""更新智能体配置"""
|
||||
agent = db.query(UserAgent).filter(UserAgent.id == agent_id).first()
|
||||
if agent:
|
||||
agent.agent_config = agent_config
|
||||
db.commit()
|
||||
db.refresh(agent)
|
||||
return agent
|
||||
|
||||
@staticmethod
|
||||
def delete(db: Session, agent_id: str) -> bool:
|
||||
"""删除智能体配置"""
|
||||
agent = db.query(UserAgent).filter(UserAgent.id == agent_id).first()
|
||||
if agent:
|
||||
db.delete(agent)
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def upsert(
|
||||
db: Session,
|
||||
user_id: str,
|
||||
agent_name: str,
|
||||
agent_config: dict,
|
||||
) -> UserAgent:
|
||||
"""更新或插入用户智能体配置(根据 user_id + agent_name 判断唯一性)
|
||||
|
||||
如果已存在相同 user_id 和 agent_name 的记录,则更新配置;
|
||||
否则创建新记录。
|
||||
"""
|
||||
existing = (
|
||||
db.query(UserAgent)
|
||||
.filter(
|
||||
UserAgent.user_id == user_id,
|
||||
UserAgent.agent_name == agent_name,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
# 更新现有记录
|
||||
existing.agent_config = agent_config
|
||||
db.commit()
|
||||
db.refresh(existing)
|
||||
return existing
|
||||
else:
|
||||
# 创建新记录
|
||||
agent = UserAgent(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=user_id,
|
||||
agent_name=agent_name,
|
||||
agent_config=agent_config,
|
||||
)
|
||||
db.add(agent)
|
||||
db.commit()
|
||||
db.refresh(agent)
|
||||
return agent
|
||||
|
||||
|
||||
class ExportRecordCRUD:
|
||||
"""导出记录 CRUD 操作"""
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
db: Session,
|
||||
task_id: str,
|
||||
user_id: str,
|
||||
export_type: str,
|
||||
file_name: str,
|
||||
file_path: str,
|
||||
file_url: str = "",
|
||||
file_size: int = 0,
|
||||
) -> ExportRecord:
|
||||
"""创建导出记录"""
|
||||
record = ExportRecord(
|
||||
task_id=task_id,
|
||||
user_id=user_id,
|
||||
export_type=export_type,
|
||||
file_name=file_name,
|
||||
file_path=file_path,
|
||||
file_url=file_url,
|
||||
file_size=file_size,
|
||||
)
|
||||
db.add(record)
|
||||
db.commit()
|
||||
db.refresh(record)
|
||||
return record
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(db: Session, record_id: int) -> Optional[ExportRecord]:
|
||||
"""根据 ID 获取记录"""
|
||||
return db.query(ExportRecord).filter(ExportRecord.id == record_id).first()
|
||||
|
||||
@staticmethod
|
||||
def get_by_task_id(
|
||||
db: Session, task_id: str, limit: int = 50
|
||||
) -> List[ExportRecord]:
|
||||
"""根据任务 ID 获取导出记录列表"""
|
||||
return (
|
||||
db.query(ExportRecord)
|
||||
.filter(ExportRecord.task_id == task_id)
|
||||
.order_by(ExportRecord.created_at.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_by_user_id(
|
||||
db: Session, user_id: str, limit: int = 50
|
||||
) -> List[ExportRecord]:
|
||||
"""根据用户 ID 获取导出记录列表"""
|
||||
return (
|
||||
db.query(ExportRecord)
|
||||
.filter(ExportRecord.user_id == user_id)
|
||||
.order_by(ExportRecord.created_at.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def delete(db: Session, record_id: int) -> bool:
|
||||
"""删除导出记录"""
|
||||
record = db.query(ExportRecord).filter(ExportRecord.id == record_id).first()
|
||||
if record:
|
||||
db.delete(record)
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def delete_by_task_id(db: Session, task_id: str) -> bool:
|
||||
"""删除任务的所有导出记录"""
|
||||
records = db.query(ExportRecord).filter(ExportRecord.task_id == task_id).all()
|
||||
if records:
|
||||
for record in records:
|
||||
db.delete(record)
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class PlanShareCRUD:
|
||||
"""任务分享 CRUD 操作"""
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
db: Session,
|
||||
share_token: str,
|
||||
task_id: str,
|
||||
task_data: dict,
|
||||
expires_at: Optional[datetime] = None,
|
||||
extraction_code: Optional[str] = None,
|
||||
) -> PlanShare:
|
||||
"""创建分享记录"""
|
||||
share = PlanShare(
|
||||
share_token=share_token,
|
||||
extraction_code=extraction_code,
|
||||
task_id=task_id,
|
||||
task_data=task_data,
|
||||
expires_at=expires_at,
|
||||
)
|
||||
db.add(share)
|
||||
db.commit()
|
||||
db.refresh(share)
|
||||
return share
|
||||
|
||||
@staticmethod
|
||||
def get_by_token(db: Session, share_token: str) -> Optional[PlanShare]:
|
||||
"""根据 token 获取分享记录"""
|
||||
return db.query(PlanShare).filter(PlanShare.share_token == share_token).first()
|
||||
|
||||
@staticmethod
|
||||
def get_by_task_id(
|
||||
db: Session, task_id: str, limit: int = 10
|
||||
) -> List[PlanShare]:
|
||||
"""根据任务 ID 获取分享记录列表"""
|
||||
return (
|
||||
db.query(PlanShare)
|
||||
.filter(PlanShare.task_id == task_id)
|
||||
.order_by(PlanShare.created_at.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def increment_view_count(db: Session, share_token: str) -> Optional[PlanShare]:
|
||||
"""增加查看次数"""
|
||||
share = db.query(PlanShare).filter(PlanShare.share_token == share_token).first()
|
||||
if share:
|
||||
share.view_count = (share.view_count or 0) + 1
|
||||
db.commit()
|
||||
db.refresh(share)
|
||||
return share
|
||||
|
||||
@staticmethod
|
||||
def delete(db: Session, share_token: str) -> bool:
|
||||
"""删除分享记录"""
|
||||
share = db.query(PlanShare).filter(PlanShare.share_token == share_token).first()
|
||||
if share:
|
||||
db.delete(share)
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def delete_by_task_id(db: Session, task_id: str) -> bool:
|
||||
"""删除任务的所有分享记录"""
|
||||
shares = db.query(PlanShare).filter(PlanShare.task_id == task_id).all()
|
||||
if shares:
|
||||
for share in shares:
|
||||
db.delete(share)
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
@@ -1,95 +0,0 @@
|
||||
"""
|
||||
数据库连接管理模块
|
||||
使用 SQLAlchemy ORM,支持同步操作
|
||||
"""
|
||||
|
||||
import os
|
||||
import yaml
|
||||
from typing import Generator
|
||||
from contextlib import contextmanager
|
||||
import json
|
||||
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||
from sqlalchemy.pool import QueuePool
|
||||
from sqlalchemy.dialects.postgresql import dialect as pg_dialect
|
||||
|
||||
# 读取配置
|
||||
yaml_file = os.path.join(os.getcwd(), "config", "config.yaml")
|
||||
try:
|
||||
with open(yaml_file, "r", encoding="utf-8") as file:
|
||||
config = yaml.safe_load(file).get("database", {})
|
||||
except Exception:
|
||||
config = {}
|
||||
|
||||
|
||||
def get_database_url() -> str:
|
||||
"""获取数据库连接 URL"""
|
||||
# 优先使用环境变量
|
||||
host = os.getenv("DB_HOST", config.get("host", "localhost"))
|
||||
port = os.getenv("DB_PORT", config.get("port", "5432"))
|
||||
user = os.getenv("DB_USER", config.get("username", "postgres"))
|
||||
password = os.getenv("DB_PASSWORD", config.get("password", ""))
|
||||
dbname = os.getenv("DB_NAME", config.get("name", "agentcoord"))
|
||||
|
||||
return f"postgresql://{user}:{password}@{host}:{port}/{dbname}"
|
||||
|
||||
|
||||
# 创建引擎
|
||||
DATABASE_URL = get_database_url()
|
||||
engine = create_engine(
|
||||
DATABASE_URL,
|
||||
poolclass=QueuePool,
|
||||
pool_size=config.get("pool_size", 10),
|
||||
max_overflow=config.get("max_overflow", 20),
|
||||
pool_pre_ping=True,
|
||||
echo=False,
|
||||
# JSONB 类型处理器配置
|
||||
json_serializer=lambda obj: json.dumps(obj, ensure_ascii=False),
|
||||
json_deserializer=lambda s: json.loads(s) if isinstance(s, str) else s
|
||||
)
|
||||
|
||||
# 创建会话工厂
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# 基础类
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def get_db() -> Generator:
|
||||
"""
|
||||
获取数据库会话
|
||||
用法: for db in get_db(): ...
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_db_context() -> Generator:
|
||||
"""
|
||||
上下文管理器方式获取数据库会话
|
||||
用法: with get_db_context() as db: ...
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_connection() -> bool:
|
||||
"""测试数据库连接"""
|
||||
try:
|
||||
with engine.connect() as conn:
|
||||
conn.execute(text("SELECT 1"))
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
||||
@@ -1,22 +0,0 @@
|
||||
"""
|
||||
数据库初始化脚本
|
||||
运行此脚本创建所有表结构
|
||||
基于 DATABASE_DESIGN.md 设计
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from db.database import engine, Base
|
||||
from db.models import MultiAgentTask, UserAgent, PlanShare
|
||||
|
||||
|
||||
def init_database():
|
||||
"""初始化数据库表结构"""
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_database()
|
||||
@@ -1,170 +0,0 @@
|
||||
"""
|
||||
SQLAlchemy ORM 数据模型
|
||||
对应数据库表结构 (基于 DATABASE_DESIGN.md)
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum as PyEnum
|
||||
from sqlalchemy import Column, String, Text, DateTime, Integer, Enum, Index, ForeignKey, Boolean
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from .database import Base
|
||||
|
||||
|
||||
class TaskStatus(str, PyEnum):
|
||||
"""任务状态枚举"""
|
||||
GENERATING = "generating" # 生成中 - TaskProcess 生成阶段
|
||||
EXECUTING = "executing" # 执行中 - 任务执行阶段
|
||||
STOPPED = "stopped" # 已停止 - 用户手动停止执行
|
||||
COMPLETED = "completed" # 已完成 - 任务正常完成
|
||||
|
||||
|
||||
def utc_now():
|
||||
"""获取当前 UTC 时间"""
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class MultiAgentTask(Base):
|
||||
"""多智能体任务记录模型"""
|
||||
__tablename__ = "multi_agent_tasks"
|
||||
|
||||
task_id = Column(String(64), primary_key=True)
|
||||
user_id = Column(String(64), nullable=False, index=True)
|
||||
query = Column(Text, nullable=False)
|
||||
agents_info = Column(JSONB, nullable=False)
|
||||
task_outline = Column(JSONB)
|
||||
assigned_agents = Column(JSONB)
|
||||
agent_scores = Column(JSONB)
|
||||
result = Column(JSONB)
|
||||
status = Column(
|
||||
Enum(TaskStatus, name="task_status_enum", create_type=False),
|
||||
default=TaskStatus.GENERATING,
|
||||
nullable=False
|
||||
)
|
||||
execution_count = Column(Integer, default=0, nullable=False)
|
||||
generation_id = Column(String(64))
|
||||
execution_id = Column(String(64))
|
||||
rehearsal_log = Column(JSONB)
|
||||
branches = Column(JSONB) # 任务大纲探索分支数据
|
||||
is_pinned = Column(Boolean, default=False, nullable=False) # 置顶标志
|
||||
created_at = Column(DateTime(timezone=True), default=utc_now)
|
||||
updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_multi_agent_tasks_status", "status"),
|
||||
Index("idx_multi_agent_tasks_generation_id", "generation_id"),
|
||||
Index("idx_multi_agent_tasks_execution_id", "execution_id"),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"task_id": self.task_id,
|
||||
"user_id": self.user_id,
|
||||
"query": self.query,
|
||||
"agents_info": self.agents_info,
|
||||
"task_outline": self.task_outline,
|
||||
"assigned_agents": self.assigned_agents,
|
||||
"agent_scores": self.agent_scores,
|
||||
"result": self.result,
|
||||
"status": self.status.value if self.status else None,
|
||||
"execution_count": self.execution_count,
|
||||
"generation_id": self.generation_id,
|
||||
"execution_id": self.execution_id,
|
||||
"rehearsal_log": self.rehearsal_log,
|
||||
"branches": self.branches,
|
||||
"is_pinned": self.is_pinned,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
class ExportRecord(Base):
|
||||
"""导出记录模型"""
|
||||
__tablename__ = "export_records"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
task_id = Column(String(64), nullable=False, index=True) # 关联任务ID
|
||||
user_id = Column(String(64), nullable=False, index=True) # 用户ID
|
||||
export_type = Column(String(32), nullable=False) # 导出类型: doc/markdown/mindmap/infographic/excel/ppt
|
||||
file_name = Column(String(256), nullable=False) # 文件名
|
||||
file_path = Column(String(512), nullable=False) # 服务器存储路径
|
||||
file_url = Column(String(512)) # 访问URL
|
||||
file_size = Column(Integer, default=0) # 文件大小(字节)
|
||||
created_at = Column(DateTime(timezone=True), default=utc_now)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_export_records_task_user", "task_id", "user_id"),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"task_id": self.task_id,
|
||||
"user_id": self.user_id,
|
||||
"export_type": self.export_type,
|
||||
"file_name": self.file_name,
|
||||
"file_path": self.file_path,
|
||||
"file_url": self.file_url,
|
||||
"file_size": self.file_size,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
class UserAgent(Base):
|
||||
"""用户保存的智能体配置模型 (可选表)"""
|
||||
__tablename__ = "user_agents"
|
||||
|
||||
id = Column(String(64), primary_key=True)
|
||||
user_id = Column(String(64), nullable=False, index=True)
|
||||
agent_name = Column(String(100), nullable=False)
|
||||
agent_config = Column(JSONB, nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), default=utc_now)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_user_agents_user_created", "user_id", "created_at"),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"user_id": self.user_id,
|
||||
"agent_name": self.agent_name,
|
||||
"agent_config": self.agent_config,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
class PlanShare(Base):
|
||||
"""任务分享记录模型"""
|
||||
__tablename__ = "plan_shares"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
share_token = Column(String(64), unique=True, index=True, nullable=False) # 唯一分享码
|
||||
extraction_code = Column(String(8), nullable=True) # 提取码(4位字母数字)
|
||||
task_id = Column(String(64), nullable=False, index=True) # 关联的任务ID
|
||||
task_data = Column(JSONB, nullable=False) # 完整的任务数据(脱敏后)
|
||||
created_at = Column(DateTime(timezone=True), default=utc_now)
|
||||
expires_at = Column(DateTime(timezone=True), nullable=True) # 过期时间
|
||||
view_count = Column(Integer, default=0) # 查看次数
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_plan_shares_token", "share_token"),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"share_token": self.share_token,
|
||||
"extraction_code": self.extraction_code,
|
||||
"task_id": self.task_id,
|
||||
"task_data": self.task_data,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
|
||||
"view_count": self.view_count,
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
-- AgentCoord 数据库表结构
|
||||
-- 基于 DATABASE_DESIGN.md 设计
|
||||
-- 执行方式: psql -U postgres -d agentcoord -f schema.sql
|
||||
|
||||
-- =============================================================================
|
||||
-- 表1: multi_agent_tasks (多智能体任务记录)
|
||||
-- 状态枚举: pending/planning/generating/executing/completed/failed
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS multi_agent_tasks (
|
||||
task_id VARCHAR(64) PRIMARY KEY,
|
||||
user_id VARCHAR(64) NOT NULL,
|
||||
query TEXT NOT NULL,
|
||||
agents_info JSONB NOT NULL,
|
||||
task_outline JSONB,
|
||||
assigned_agents JSONB,
|
||||
agent_scores JSONB,
|
||||
result JSONB,
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
execution_count INTEGER DEFAULT 0,
|
||||
generation_id VARCHAR(64),
|
||||
execution_id VARCHAR(64),
|
||||
rehearsal_log JSONB,
|
||||
branches JSONB, -- 任务大纲探索分支数据
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX IF NOT EXISTS idx_multi_agent_tasks_user_id ON multi_agent_tasks(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_multi_agent_tasks_created_at ON multi_agent_tasks(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_multi_agent_tasks_status ON multi_agent_tasks(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_multi_agent_tasks_generation_id ON multi_agent_tasks(generation_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_multi_agent_tasks_execution_id ON multi_agent_tasks(execution_id);
|
||||
|
||||
-- =============================================================================
|
||||
-- 表2: user_agents (用户保存的智能体配置) - 可选表
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS user_agents (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
user_id VARCHAR(64) NOT NULL,
|
||||
agent_name VARCHAR(100) NOT NULL,
|
||||
agent_config JSONB NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_agents_user_id ON user_agents(user_id);
|
||||
|
||||
-- =============================================================================
|
||||
-- 更新时间触发器函数
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- 为 multi_agent_tasks 表创建触发器
|
||||
CREATE TRIGGER update_multi_agent_tasks_updated_at
|
||||
BEFORE UPDATE ON multi_agent_tasks
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- =============================================================================
|
||||
-- 表3: export_records (导出记录)
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS export_records (
|
||||
id SERIAL PRIMARY KEY,
|
||||
task_id VARCHAR(64) NOT NULL,
|
||||
user_id VARCHAR(64) NOT NULL,
|
||||
export_type VARCHAR(32) NOT NULL,
|
||||
file_name VARCHAR(256) NOT NULL,
|
||||
file_path VARCHAR(512) NOT NULL,
|
||||
file_url VARCHAR(512),
|
||||
file_size INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_export_records_task_user ON export_records(task_id, user_id);
|
||||
|
||||
-- =============================================================================
|
||||
-- 表4: plan_shares (任务分享记录)
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS plan_shares (
|
||||
id SERIAL PRIMARY KEY,
|
||||
share_token VARCHAR(64) NOT NULL UNIQUE,
|
||||
extraction_code VARCHAR(8),
|
||||
task_id VARCHAR(64) NOT NULL,
|
||||
task_data JSONB NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP WITH TIME ZONE,
|
||||
view_count INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_plan_shares_token ON plan_shares(share_token);
|
||||
CREATE INDEX IF NOT EXISTS idx_plan_shares_task_id ON plan_shares(task_id);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '✅ PostgreSQL 数据库表结构创建完成!';
|
||||
RAISE NOTICE '表: multi_agent_tasks (多智能体任务记录)';
|
||||
RAISE NOTICE '表: user_agents (用户智能体配置)';
|
||||
RAISE NOTICE '表: export_records (导出记录)';
|
||||
RAISE NOTICE '表: plan_shares (任务分享记录)';
|
||||
END $$;
|
||||
@@ -1,13 +1,7 @@
|
||||
Flask==3.0.2
|
||||
openai==2.8.1
|
||||
openai==0.28.1
|
||||
PyYAML==6.0.1
|
||||
termcolor==2.4.0
|
||||
groq==0.4.2
|
||||
mistralai==0.1.6
|
||||
flask-socketio==5.3.6
|
||||
python-socketio==5.11.0
|
||||
simple-websocket==1.0.0
|
||||
eventlet==0.40.4
|
||||
python-docx==1.1.2
|
||||
openpyxl==3.1.5
|
||||
python-pptx==1.0.2
|
||||
mistralai==1.5.2
|
||||
socksio==1.0.0
|
||||
|
||||
13
backend/restart.ps1
Normal file
@@ -0,0 +1,13 @@
|
||||
# restart.ps1
|
||||
$port=8000
|
||||
|
||||
$env:PYTHONUNBUFFERED="1"
|
||||
python server.py --port 8000
|
||||
|
||||
|
||||
Write-Host "Killing PID on port $port..." -ForegroundColor Red
|
||||
Get-NetTCPConnection -LocalPort $port -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Stop-Process -Id $_.OwningProcess -Force
|
||||
}
|
||||
Write-Host "Starting Flask..." -ForegroundColor Green
|
||||
python server.py --port $port
|
||||
3892
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';
|
||||
|
||||
9
frontend/.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npm run type-check:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
5
frontend/components.d.ts
vendored
@@ -16,19 +16,18 @@ declare module 'vue' {
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
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']
|
||||
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']
|
||||
TaskContentEditor: typeof import('./src/components/TaskContentEditor/index.vue')['default']
|
||||
}
|
||||
export interface GlobalDirectives {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
|
||||
BIN
frontend/dist.zip
Normal file
11986
frontend/package-lock.json
generated
Normal file
@@ -17,33 +17,19 @@
|
||||
"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",
|
||||
"d3": "^7.9.0",
|
||||
"docx-preview": "^0.3.7",
|
||||
"dompurify": "^3.3.0",
|
||||
"element-plus": "^2.11.5",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^14.1.0",
|
||||
"marked": "^17.0.4",
|
||||
"markmap": "^0.6.1",
|
||||
"markmap-lib": "^0.18.12",
|
||||
"markmap-view": "^0.18.12",
|
||||
"pinia": "^3.0.3",
|
||||
"pptxjs": "^0.0.0",
|
||||
"qs": "^6.14.0",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"uuid": "^13.0.0",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3",
|
||||
"xlsx": "^0.18.5"
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.15",
|
||||
|
||||
9311
frontend/pnpm-lock.yaml
generated
@@ -4,7 +4,7 @@
|
||||
"centerTitle": "多智能体协同平台",
|
||||
"taskPromptWords": [
|
||||
"如何快速筛选慢性肾脏病药物潜在受试者?",
|
||||
"如何补充\"丹芍活血胶囊\"不良反应数据?",
|
||||
"如何补充“丹芍活血胶囊”不良反应数据?",
|
||||
"如何快速研发用于战场失血性休克的药物?",
|
||||
"二维材料的光电性质受哪些关键因素影响?",
|
||||
"如何通过AI模拟的方法分析材料的微观结构?",
|
||||
@@ -15,7 +15,5 @@
|
||||
],
|
||||
"agentRepository": {
|
||||
"storageVersionIdentifier": "1"
|
||||
},
|
||||
"dev": true,
|
||||
"apiBaseUrl": "http://localhost:8000"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { App } from 'vue'
|
||||
|
||||
import { devOnly } from './devOnly'
|
||||
|
||||
|
||||
// 全局注册 directive
|
||||
export function setupDirective(app: App<Element>) {
|
||||
app.directive('dev-only', devOnly)
|
||||
}
|
||||
@@ -1,16 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router'
|
||||
import Layout from './layout/index.vue'
|
||||
import Share from './views/Share.vue'
|
||||
|
||||
const route = useRoute()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 分享页面使用独立布局 -->
|
||||
<Share v-if="route.path.startsWith('/share/')" />
|
||||
<!-- 其他页面使用主布局 -->
|
||||
<Layout v-else />
|
||||
<layout />
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,7 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1 +0,0 @@
|
||||
<?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="1772516996617" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8642" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M354.40128 0c-87.04 0-157.44 70.55872-157.44 157.59872v275.68128H78.72c-21.6576 0-39.36256 17.69984-39.36256 39.36256v236.31872c0 21.6576 17.69984 39.35744 39.36256 39.35744h118.24128v118.08256c0 87.04 70.4 157.59872 157.44 157.59872h472.63744c87.04 0 157.59872-70.55872 157.59872-157.59872V315.0336c0-41.74848-38.9888-81.93024-107.52-149.27872l-29.11744-29.12256L818.87744 107.52C751.5392 38.9888 711.39328 0 669.59872 0H354.4064z m0 78.72h287.20128c28.35456 7.0912 27.99616 42.1376 27.99616 76.8v120.16128c0 21.6576 17.69984 39.35744 39.36256 39.35744h118.07744c39.38816 0 78.87872-0.0256 78.87872 39.36256v512c0 43.32032-35.55328 78.87872-78.87872 78.87872H354.4064c-43.32544 0-78.72-35.5584-78.72-78.87872v-118.08256h393.91744c21.66272 0 39.36256-17.69472 39.36256-39.35744V472.64256c0-21.66272-17.69984-39.36256-39.36256-39.36256H275.68128V157.59872c0-43.32032 35.39456-78.87872 78.72-78.87872zM308.6336 498.07872c23.04 0 41.28256 8.00256 54.72256 24.00256 14.08 15.36 21.12 37.43744 21.12 66.23744 0 29.44-7.04 51.84-21.12 67.2-13.44 15.36-31.68256 23.04-54.72256 23.04-23.68 0-42.24-7.35744-55.68-22.07744-13.44-15.36-20.15744-38.08256-20.15744-68.16256 0-29.44 6.71744-51.84 20.15744-67.2s32-23.04 55.68-23.04z m186.24 0.96256c17.92 0 33.28 3.2 46.08 9.6l-9.6 19.2c-12.16-6.4-24.32-9.6-36.48-9.6-16.64 0-30.39744 6.4-41.27744 19.2-10.24 12.16-15.36 29.44-15.36 51.84 0 23.04 4.79744 40.63744 14.39744 52.79744 9.6 11.52 23.68 17.28 42.24 17.28 10.24 0 23.36256-2.23744 39.36256-6.71744v19.2c-11.52 4.48-25.92256 6.71744-43.20256 6.71744-23.68 0-42.24-7.35744-55.68-22.07744-13.44-15.36-20.15744-38.08256-20.15744-68.16256 0-28.16 7.04-49.92 21.12-65.28 14.72-16 34.23744-23.99744 58.55744-23.99744z m-421.43744 1.92h48.95744c24.96 0 44.48256 7.68 58.56256 23.04 14.72 14.72 22.07744 35.84 22.07744 63.36 0 28.8-7.68 50.87744-23.04 66.23744-14.72 15.36-35.51744 23.04-62.39744 23.04h-44.16V500.96128z m478.08 0h23.99744l39.36256 67.2 40.32-67.2h23.04l-50.88256 83.51744 54.72256 92.16h-24.96l-43.20256-75.83744-43.19744 75.83744h-23.04l54.71744-92.16-50.87744-83.51744z m-242.88256 17.28c-17.28 0-30.39744 6.07744-39.35744 18.23744-8.96 11.52-13.44 28.8-13.44 51.84 0 23.68 4.48 41.6 13.44 53.76 8.96 11.52 22.07744 17.28 39.35744 17.28s30.40256-5.76 39.36256-17.28c8.96-12.16 13.44-30.08 13.44-53.76 0-23.04-4.48-40.32-13.44-51.84-8.96-12.16-22.08256-18.23744-39.36256-18.23744z m-213.12 1.92v137.27744h19.2c21.76 0 37.76-5.76 48-17.28 10.88-11.52 16.32256-28.8 16.32256-51.84s-5.12-39.99744-15.36-50.87744c-9.6-11.52-24.32-17.28-44.16-17.28h-24.00256z" p-id="8643" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -1 +0,0 @@
|
||||
<?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="1772506712715" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5360" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M279.5 354.3l198-162.8V734c0 19.3 15.7 35 35 35s35-15.7 35-35V191.6l197.1 162.6c6.5 5.4 14.4 8 22.3 8 10.1 0 20.1-4.3 27-12.7 12.3-14.9 10.2-37-4.7-49.3L534.7 90.4c-0.2-0.2-0.4-0.3-0.6-0.5-0.2-0.1-0.4-0.3-0.5-0.4-0.6-0.5-1.2-0.9-1.8-1.3-0.6-0.4-1.3-0.8-2-1.2-0.1-0.1-0.2-0.1-0.3-0.2-1.4-0.8-2.9-1.5-4.4-2.1h-0.1c-1.5-0.6-3.1-1.1-4.7-1.4h-0.2c-0.7-0.2-1.5-0.3-2.2-0.4h-0.2c-0.8-0.1-1.5-0.2-2.3-0.3h-0.3c-0.6 0-1.3-0.1-1.9-0.1h-3.1c-0.6 0-1.2 0.1-1.8 0.2-0.2 0-0.4 0-0.6 0.1l-2.1 0.3h-0.1c-0.7 0.1-1.4 0.3-2.1 0.5-0.1 0-0.3 0.1-0.4 0.1-1.5 0.4-2.9 0.9-4.3 1.5-0.1 0-0.1 0.1-0.2 0.1-1.5 0.6-2.9 1.4-4.3 2.2-1.4 0.9-2.8 1.8-4.1 2.9L235 300.3c-14.9 12.3-17.1 34.3-4.8 49.3 12.3 14.9 34.3 17 49.3 4.7z" p-id="5361"></path><path d="M925.8 598.2c-19.3 0-35 15.7-35 35v238.4H133.2V633.2c0-19.3-15.7-35-35-35s-35 15.7-35 35v273.4c0 19.3 15.7 35 35 35h827.5c19.3 0 35-15.7 35-35V633.2c0.1-19.3-15.6-35-34.9-35z" p-id="5362"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1 +0,0 @@
|
||||
<?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="1772248670725" class="icon" viewBox="0 0 1365 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17188" width="341.25" height="256" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M1140.982158 0C1171.511117 0 1196.259731 24.748613 1196.259731 55.277574L1196.259731 481.802069C1196.259731 512.331031 1171.511117 537.079644 1140.982158 537.079644 1110.453196 537.079644 1085.704583 512.331031 1085.704583 481.802069L1085.704583 55.277574 1140.982158 110.555148 707.290659 110.555148C676.761697 110.555148 652.013084 85.806535 652.013084 55.277574 652.013084 24.748613 676.761697 0 707.290659 0L1140.982158 0ZM223.896216 1024.028434C193.367257 1024.028434 168.618642 999.279821 168.618642 968.75086L168.618642 542.226364C168.618642 511.697403 193.367257 486.94879 223.896216 486.94879 254.425178 486.94879 279.17379 511.697403 279.17379 542.226364L279.17379 968.75086 223.896216 913.473286 657.587715 913.473286C688.116677 913.473286 712.865289 938.221898 712.865289 968.75086 712.865289 999.279821 688.116677 1024.028434 657.587715 1024.028434L223.896216 1024.028434Z" p-id="17189"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,12 +0,0 @@
|
||||
<?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="1772011298336" class="icon" viewBox="0 0 1236 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="11698" xmlns:xlink="http://www.w3.org/1999/xlink" width="38.625" height="32">
|
||||
<path d="M741.743 1018.343c-28.287 0-50.917-11.315-73.547-28.288-22.63-22.63-39.602-50.917-39.602-84.862V792.044c-124.464 0-328.133 33.945-435.624
|
||||
181.039-16.973 28.287-56.575 45.26-90.52 50.917H85.478C28.903 1012.685-5.042 961.768 0.616 905.193c28.287-243.27 113.15-418.652 260.243-537.458
|
||||
107.492-84.862 231.956-130.122 367.735-141.437V118.807c0-50.917 22.63-96.177 67.89-113.15C736.086-5.657 781.345 0 815.29 33.945l362.077 367.735c28.288
|
||||
22.63 45.26 56.574 50.918 96.176 5.657 39.603-5.658 79.205-33.945 107.492-5.658 5.658-11.315 16.972-22.63 22.63l-350.762 356.42c-22.63
|
||||
22.63-50.918 33.945-79.205 33.945z m-90.52-339.448h90.52v226.298l356.42-367.734 5.658-5.658c5.657-5.657 5.657-16.972 5.657-22.63
|
||||
0-11.315-5.657-16.972-11.315-22.63l-5.657-5.657-356.42-362.077V333.79l-79.205 5.658c-118.806 0-231.956 39.602-328.132
|
||||
113.149-113.15 90.519-186.696 237.613-209.326 429.967 141.436-175.382 390.364-203.669 531.8-203.669z"
|
||||
p-id="11699"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
||||
<?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="1772076317456" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6281" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M510.976694 146.304134c-201.970968 0-365.695866 163.728992-365.695866 365.695866s163.728992 365.695866 365.695866 365.695866 365.695866-163.728992 365.695866-365.695866S712.947661 146.304134 510.976694 146.304134L510.976694 146.304134zM480.489332 329.151555c0-16.82827 13.631462-30.475082 30.475082-30.475082 16.844643 0 30.472012 13.646811 30.472012 30.475082l0 216.70146c0 16.82827-13.627369 30.475082-30.472012 30.475082-16.84362 0-30.475082-13.646811-30.475082-30.475082L480.489332 329.151555 480.489332 329.151555zM510.976694 694.847421c-23.663956 0-42.846854-19.178805-42.846854-42.842761s19.182898-42.846854 42.846854-42.846854c23.663956 0 42.846854 19.182898 42.846854 42.846854C553.823548 675.664523 534.64065 694.847421 510.976694 694.847421L510.976694 694.847421zM510.976694 694.847421" p-id="6282"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1 +0,0 @@
|
||||
<?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="1772517164462" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15871" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326z m1.8 562H232V136h302v216c0 23.2 18.8 42 42 42h216v494zM429 481.2c-1.9-4.4-6.2-7.2-11-7.2h-35c-6.6 0-12 5.4-12 12v272c0 6.6 5.4 12 12 12h27.1c6.6 0 12-5.4 12-12V582.1l66.8 150.2c1.9 4.3 6.2 7.1 11 7.1H524c4.7 0 9-2.8 11-7.1l66.8-150.6V758c0 6.6 5.4 12 12 12H641c6.6 0 12-5.4 12-12V486c0-6.6-5.4-12-12-12h-34.7c-4.8 0-9.1 2.8-11 7.2l-83.1 191-83.2-191z" p-id="15872"></path></svg>
|
||||
|
Before Width: | Height: | Size: 897 B |
@@ -1 +0,0 @@
|
||||
<?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="1772093306648" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10322" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M512 64C264.576 64 64 264.576 64 512c0 247.422 200.576 448 448 448 247.422 0 448-200.578 448-448C960 264.576 759.422 64 512 64zM593.63 755.834l-107.18 0L486.45 384.388l-68.612 37.82-27.47-89.816 119.986-64.226 83.278 0L593.632 755.834z" p-id="10323"></path></svg>
|
||||
|
Before Width: | Height: | Size: 596 B |
@@ -1 +0,0 @@
|
||||
<?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="1772517680962" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="51236" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M192 384h640a42.666667 42.666667 0 0 1 42.666667 42.666667v362.666666a42.666667 42.666667 0 0 1-42.666667 42.666667H192v106.666667a21.333333 21.333333 0 0 0 21.333333 21.333333h725.333334a21.333333 21.333333 0 0 0 21.333333-21.333333V308.821333L949.909333 298.666667h-126.528A98.048 98.048 0 0 1 725.333333 200.618667V72.661333L716.714667 64H213.333333a21.333333 21.333333 0 0 0-21.333333 21.333333v298.666667zM128 832H42.666667a42.666667 42.666667 0 0 1-42.666667-42.666667V426.666667a42.666667 42.666667 0 0 1 42.666667-42.666667h85.333333V85.333333a85.333333 85.333333 0 0 1 85.333333-85.333333h530.026667L1024 282.453333V938.666667a85.333333 85.333333 0 0 1-85.333333 85.333333H213.333333a85.333333 85.333333 0 0 1-85.333333-85.333333v-106.666667zM94.613333 472.490667V746.666667h44.928v-105.216h67.968c66.816 0 100.224-28.416 100.224-84.864 0-56.064-33.408-84.096-99.456-84.096H94.613333z m44.928 38.4h65.28c19.584 0 34.176 3.456 43.392 10.752 9.216 6.912 14.208 18.432 14.208 34.944 0 16.512-4.608 28.416-13.824 35.712-9.216 6.912-23.808 10.752-43.776 10.752h-65.28v-92.16z m206.592-38.4V746.666667h44.928v-105.216h67.968c66.816 0 100.224-28.416 100.224-84.864 0-56.064-33.408-84.096-99.456-84.096h-113.664z m44.928 38.4h65.28c19.584 0 34.176 3.456 43.392 10.752 9.216 6.912 14.208 18.432 14.208 34.944 0 16.512-4.608 28.416-13.824 35.712-9.216 6.912-23.808 10.752-43.776 10.752h-65.28v-92.16z m185.472-38.4v38.4h89.856V746.666667h44.928V510.890667h89.856v-38.4h-224.64z" p-id="51237"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 1004 B |
@@ -1 +0,0 @@
|
||||
<?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="1772011354532" class="icon" viewBox="0 0 1049 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12849" xmlns:xlink="http://www.w3.org/1999/xlink" width="32.78125" height="32"><path d="M524.816493 830.218181a35.83281 35.83281 0 0 0 35.788334-35.803159v-412.292279a35.803159 35.803159 0 0 0-71.591493 0v412.292279A35.83281 35.83281 0 0 0 524.816493 830.218181zM705.374122 830.218181a35.83281 35.83281 0 0 0 35.788334-35.803159v-412.292279a35.803159 35.803159 0 0 0-71.591493 0v412.292279A35.83281 35.83281 0 0 0 705.374122 830.218181zM344.199563 830.218181a35.83281 35.83281 0 0 0 35.788334-35.803159v-412.292279a35.803159 35.803159 0 0 0-71.591493 0v412.292279c0.756092 19.688031 16.826743 35.803159 35.803159 35.803159z" p-id="12850" ></path><path d="M1013.770526 128.639342H766.721316V86.920879A87.00983 87.00983 0 0 0 679.800437 0H370.633117a87.00983 87.00983 0 0 0-86.906054 86.920879v41.718463H35.788334a35.788334 35.788334 0 0 0 0 71.576668H154.183377V885.071883a139.031895 139.031895 0 0 0 138.868816 138.868816h463.439649A139.031895 139.031895 0 0 0 895.360658 885.071883V199.400617h118.409868A35.83281 35.83281 0 0 0 1049.632986 163.612283c0-19.613905-15.803796-34.972941-35.86246-34.972941zM354.414211 86.920879A15.996525 15.996525 0 0 1 370.633117 70.761275h309.16732a15.996525 15.996525 0 0 1 16.159604 16.159604v41.718463H354.414211zM823.769165 885.071883a67.35145 67.35145 0 0 1-67.277323 67.277323H293.067018A67.366275 67.366275 0 0 1 225.774869 885.071883V199.400617h597.994296z" p-id="12851" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1 +0,0 @@
|
||||
<?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="1772517247243" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19879" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M899.782551 181.281467L758.334453 51.211973a230.1401 230.1401 0 0 0-135.511075-51.195458H147.366201C102.619718 1.337688 67.402197 33.665143 68.335276 72.565434v889.942215h63.581456V72.499375a15.754989 15.754989 0 0 1 16.060511-14.76411h475.44892a89.559023 89.559023 0 0 1 16.060511 1.651467v231.692479h251.964229c1.180798 5.466354 1.767069 33.855061 1.767069 38.404851v621.843159a15.754989 15.754989 0 0 1-16.060511 14.76411H147.960729a16.704583 16.704583 0 0 1-11.345575-4.269041 14.177839 14.177839 0 0 1-4.913112-10.453782v-48.652201H68.343533v48.652201c-0.941336 38.892034 34.276185 71.219489 79.030925 72.548919h729.229762c44.754739-1.321173 79.972261-33.648628 79.030925-72.548919V329.401489a259.437114 259.437114 0 0 0-55.844337-148.120022z m-197.300691 51.195458V83.985324a92.812412 92.812412 0 0 1 11.29603 8.191273l140.870084 130.069494c2.997412 2.733177 5.945279 6.605866 9.528961 10.371209z" p-id="19880"></path><path d="M403.26092 602.950383a14.904484 14.904484 0 1 1 0-29.817225 47.182395 47.182395 0 0 0 47.132851-47.132852V492.302134a76.94182 76.94182 0 0 1 75.884881-76.941819h1.70101a14.904484 14.904484 0 1 1 0.412867 29.808968h-1.70101a47.32277 47.32277 0 0 0-46.48878 47.124594V525.992049a77.03265 77.03265 0 0 1-76.941819 76.958334z" p-id="19881"></path><path d="M528.194353 760.747998H526.286909a76.933562 76.933562 0 0 1-75.884881-76.933562v-33.689915a47.182395 47.182395 0 0 0-47.132851-47.132851 14.904484 14.904484 0 1 1 0-29.817226 77.03265 77.03265 0 0 1 76.94182 76.94182v33.689914a47.314512 47.314512 0 0 0 46.488779 47.124594h1.70101a14.904484 14.904484 0 0 1-0.206433 29.808969z" p-id="19882"></path><path d="M528.186096 602.958641H403.26092a14.904484 14.904484 0 0 1 0-29.817226h124.933433a14.904484 14.904484 0 1 1 0 29.817226z" p-id="19883"></path><path d="M381.032182 650.694277h-102.390917a34.821169 34.821169 0 0 1-34.779882-34.779882v-55.728734a34.821169 34.821169 0 0 1 34.779882-34.779882h102.390917a34.821169 34.821169 0 0 1 34.779882 34.779882v55.720476a34.829426 34.829426 0 0 1-34.779882 34.78814z m-102.390917-95.471273a4.954399 4.954399 0 0 0-4.954399 4.9544v55.720476a4.954399 4.954399 0 0 0 4.954399 4.954399h102.390917a4.954399 4.954399 0 0 0 4.954399-4.954399v-55.712219a4.954399 4.954399 0 0 0-4.954399-4.954399z m343.983937-64.547564h-46.719985a62.640121 62.640121 0 1 1 0-125.280241h46.695213a62.640121 62.640121 0 1 1 0 125.280241z m-46.719985-95.446501a32.831152 32.831152 0 0 0 0 65.662305h46.695213a32.831152 32.831152 0 0 0 0-65.662305z m46.695213 255.465338h-46.695213a62.648378 62.648378 0 0 1 0-125.288498h46.695213a62.648378 62.648378 0 0 1 0 125.288498z m-46.695213-95.47953a32.831152 32.831152 0 0 0 0 65.662304h46.695213a32.831152 32.831152 0 0 0 0-65.662304z" p-id="19884"></path><path d="M622.625202 809.788293h-46.670441a62.648378 62.648378 0 0 1 0-125.288499h46.695213a62.648378 62.648378 0 0 1 0 125.288499zM575.954761 714.341792a32.831152 32.831152 0 0 0 0 65.662304h46.695213a32.831152 32.831152 0 1 0 0-65.662304z" p-id="19885"></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1 +0,0 @@
|
||||
<?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="1772247481697" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15251" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M554.666667 405.333333V85.333333h85.333333v298.666667h298.666667v85.333333h-320.085334A63.914667 63.914667 0 0 1 554.666667 405.333333zM85.333333 554.666667h320.085334c35.584 0 63.914667 28.885333 63.914666 64V938.666667H384v-298.666667H85.333333v-85.333333z" p-id="15252"></path></svg>
|
||||
|
Before Width: | Height: | Size: 621 B |
@@ -1 +0,0 @@
|
||||
<?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="1772093383090" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11068" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M512 64C264.576 64 64 264.576 64 512c0 247.422 200.576 448 448 448 247.422 0 448-200.578 448-448C960 264.576 759.422 64 512 64zM620.256 717.658c-32.132 29.45-77.228 45.014-130.41 45.014-54.27 0-98.822-16.862-120.484-31.298l-12.756-8.504 31.068-92.17 21.712 14.476c13.388 8.926 45.072 22.208 77.488 22.208 33.73 0 69.77-16.142 69.77-61.45 0-31.142-21.452-64.422-81.66-64.422l-50.32 0 0-89.344 49.13 0c28.724 0 69.176-16.298 69.176-52.532 0-27.742-17.812-43.02-50.154-43.02-25.572 0-52.78 12.508-68.308 23.218l-21.586 14.886-30.758-88.038 11.884-8.758c22.908-16.884 69.19-36.6 124.226-36.6 100.06 0 144.848 64.092 144.848 127.61 0 41.306-19.084 77.362-52.648 102.036 37.34 21.14 66.918 60.562 66.918 117.34C667.394 650.968 650.652 689.802 620.256 717.658z" p-id="11069"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1 +0,0 @@
|
||||
<?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="1772093350376" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10815" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M512 64c-247.296 0-448 200.704-448 448s200.704 448 448 448 448-200.704 448-448-200.704-448-448-448zM355.84 759.296v-70.144l52.736-55.296c95.744-97.792 141.312-154.624 141.312-211.968 0-40.96-19.456-61.44-57.344-61.44-29.184 0-56.32 16.896-73.728 30.72l-20.48 16.384-36.864-87.552 11.264-9.728c35.328-29.184 83.968-46.08 133.632-46.08 48.64 0 88.576 15.872 115.712 46.08 23.552 26.624 36.864 63.488 36.864 103.936 0 94.208-69.12 172.032-139.776 243.2l-4.608 4.608h153.6v97.28h-312.32z"p-id="10816"></path></svg>
|
||||
|
Before Width: | Height: | Size: 842 B |
@@ -1 +0,0 @@
|
||||
<?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="1772517560235" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="49165" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M354.40128 0c-87.04 0-157.44 70.55872-157.44 157.59872v275.68128H78.72c-21.6576 0-39.36256 17.69984-39.36256 39.36256v236.31872c0 21.6576 17.69984 39.35744 39.36256 39.35744h118.24128v118.08256c0 87.04 70.4 157.59872 157.44 157.59872h472.63744c87.04 0 157.59872-70.55872 157.59872-157.59872V315.0336c0-41.74848-38.9888-81.93024-107.52-149.27872l-29.11744-29.12256L818.87744 107.52C751.5392 38.9888 711.39328 0 669.59872 0H354.4064z m0 78.72h287.20128c28.35456 7.0912 27.99616 42.1376 27.99616 76.8v120.16128c0 21.6576 17.69984 39.35744 39.36256 39.35744h118.07744c39.38816 0 78.87872-0.0256 78.87872 39.36256v512c0 43.32032-35.55328 78.87872-78.87872 78.87872H354.4064c-43.32544 0-78.72-35.5584-78.72-78.87872v-118.08256h393.91744c21.66272 0 39.36256-17.69472 39.36256-39.35744V472.64256c0-21.66272-17.69984-39.36256-39.36256-39.36256H275.68128V157.59872c0-43.32032 35.39456-78.87872 78.72-78.87872z m79.03744 420.32128c17.28 0 32.64 3.2 46.08 9.6l-7.68 18.23744c-13.44-5.76-26.24-8.63744-38.4-8.63744-10.24 0-17.92 2.23744-23.04 6.71744s-7.68 10.56256-7.68 18.24256c0 8.96 1.92 15.67744 5.76 20.15744 4.48 3.84 15.04256 9.6 31.68256 17.28 17.28 7.04 28.47744 14.40256 33.59744 22.08256 5.76 7.04 8.64256 16 8.64256 26.88 0 14.72-5.12 26.55744-15.36 35.51744s-24.96 13.44-44.16 13.44c-18.56 0-33.28-2.56-44.16-7.68v-21.12c15.36 6.4 30.08 9.6 44.16 9.6 12.8 0 22.07744-2.23744 27.83744-6.71744 6.4-4.48 9.6-11.52 9.6-21.12 0-7.68-2.23744-14.08-6.71744-19.2-3.2-3.2-15.04256-9.28256-35.52256-18.24256-13.44-6.4-23.04-13.44-28.8-21.12s-8.63744-17.59744-8.63744-29.75744c0-13.44 4.48-24.00256 13.44-31.68256 9.6-8.32 22.71744-12.47744 39.35744-12.47744z m-318.72 1.92h24.00256l39.35744 67.2 40.32-67.2h23.04l-50.87744 83.51744 54.71744 92.16h-24.96l-43.19744-75.83744-43.20256 75.83744h-23.04l54.72256-92.16-50.88256-83.51744z m154.56256 0h22.07744v155.52h69.12v20.15744H269.28128V500.96128z m228.48 0h23.99744l39.36256 67.2 40.32-67.2h23.04l-50.88256 83.51744 54.72256 92.16h-24.96l-43.20256-75.83744-43.19744 75.83744h-23.04l54.71744-92.16-50.87744-83.51744z" p-id="49166" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1 +0,0 @@
|
||||
<?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="1772523074969" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="52350" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M897.707 989.867H126.293c-51.2 0-92.16-40.96-92.16-92.16V512c0-13.653 11.947-25.6 25.6-25.6s25.6 11.947 25.6 25.6v385.707c0 22.186 18.774 40.96 40.96 40.96H896c22.187 0 40.96-18.774 40.96-40.96V512c0-13.653 11.947-25.6 25.6-25.6s27.307 11.947 27.307 25.6v385.707c0 51.2-40.96 92.16-92.16 92.16z" p-id="52351"></path><path d="M512 738.987c-6.827 0-13.653-1.707-18.773-6.827l-225.28-226.987c-10.24-10.24-10.24-25.6 0-35.84s25.6-10.24 35.84 0L512 677.547l208.213-208.214c10.24-10.24 25.6-10.24 35.84 0s10.24 25.6 0 35.84l-225.28 225.28c-5.12 5.12-11.946 8.534-18.773 8.534z" p-id="52352"></path><path d="M512 738.987c-13.653 0-25.6-11.947-25.6-25.6V59.733c0-13.653 11.947-25.6 25.6-25.6s25.6 11.947 25.6 25.6v653.654c0 13.653-11.947 25.6-25.6 25.6z" p-id="52353"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,14 +0,0 @@
|
||||
<?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="1772011965983" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="16530" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32">
|
||||
<path d="M853.384158 66.676585 166.360935 66.676585c-43.596937 0-79.272461 35.749202-79.272461
|
||||
79.43619L87.088474 661.150972c0 43.689035 35.675524 66.207911 79.272461 66.207911l317.087798 0c43.596937
|
||||
0 104.495936 32.532951 135.319965 63.433728l46.447868 49.51881c11.95938 11.985986 23.094998 19.252483 32.215726
|
||||
19.252483 14.387685 0 23.831778-19.524682 23.831778-46.2473 0-43.686988 35.675524-85.957721 79.272461-85.957721l52.847625
|
||||
0c43.596937 0 79.272461-22.518876 79.272461-66.207911L932.656619 146.112776C932.656619 102.425787 896.981095
|
||||
66.676585 853.384158 66.676585zM879.80797 661.150972c0 14.350846-12.102642 14.546298-26.423813 14.546298L800.536532
|
||||
675.697269c-58.31822 0-107.942431
|
||||
44.037982-125.411291 96.659457l-18.990516-21.992897c-40.772612-40.869826-115.013477-74.66656-172.685991-74.66656L166.360935 675.697269c-14.322194
|
||||
0-26.423813-0.195451-26.423813-14.546298L139.937123 146.112776c0-14.350846 12.102642-26.479071 26.423813-26.479071l687.024246
|
||||
0c14.322194 0 26.423813 12.128225 26.423813 26.479071L879.808994 661.150972z" p-id="16531"></path><path d="M286.678208 306.010509l264.240173 0c14.591323 0 26.423813-11.856026 26.423813-26.477025 0-14.623046-11.83249-26.479071-36.656875-26.479071L276.445146 253.054413c-4.358261 0-16.190751 11.856026-16.190751 26.479071C260.254396 294.15346 272.086885 306.010509 286.678208 306.010509z" p-id="16532"></path><path d="M734.475977 385.448746 285.268092 385.448746c-14.591323 0-26.423813 11.856026-26.423813 26.479071 0 14.623046 11.83249 26.479071 26.423813 26.479071l449.207885 0c14.591323 0 26.423813-11.856026 26.423813-26.479071C760.89979 397.304771 749.0673 385.448746 734.475977 385.448746z" p-id="16533"></path><path d="M734.475977 491.367077 285.268092 491.367077c-14.591323 0-26.423813 11.856026-26.423813 26.477025 0 14.623046 11.83249 26.479071 26.423813 26.479071l449.207885 0c14.591323 0 26.423813-11.856026 26.423813-26.479071C760.89979 503.223103 749.0673 491.367077 734.475977 491.367077z" p-id="16534"></path></svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1 +0,0 @@
|
||||
<?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="1772517519490" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="47945" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M877.6 342.976l28.448-28.48-45.28-45.248-28.448 28.48 45.28 45.248z m-22.624 22.624l-45.28-45.28-188.448 188.48v45.248h45.248l188.48-188.48zM736 864v32a32 32 0 0 1-32 32H128a32 32 0 0 1-32-32V128a32 32 0 0 1 32-32h496L736 210.24v90.272l-32 31.296v-108.48L610.56 128H128v768h576v-32H256v-32h64v-160h32v160h64v-256h32v256h64V448h32v384h64v-160h32v160h64v-229.792l32-29.12V832h64v-96h32v96h32v32h-128z m124.8-640l90.496 90.496-271.52 271.552h-90.528v-90.528L860.8 224z" p-id="47946"></path></svg>
|
||||
|
Before Width: | Height: | Size: 827 B |
@@ -1 +0,0 @@
|
||||
<?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="1772523106062" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="53481" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M500.655752 944.867748H123.266149a39.466573 39.466573 0 0 1-39.365246-39.365246V118.146909a39.466573 39.466573 0 0 1 39.365246-39.365246h708.523774a39.365246 39.365246 0 0 1 39.365246 39.365246v347.853002h78.730493V118.146909A118.247729 118.247729 0 0 0 831.789923 0.000507H123.012834A118.247729 118.247729 0 0 0 4.866431 118.146909v787.507582a118.247729 118.247729 0 0 0 118.146403 118.146403h377.389602z" p-id="53482"></path><path d="M1006.881675 956.114962l-77.261262-77.261263a229.605273 229.605273 0 1 0-55.729435 55.729436l77.261262 77.261262a39.41591 39.41591 0 1 0 55.729435-55.729435z m-369.840797-102.288845a149.962844 149.962844 0 1 1 212.075833 0 150.216159 150.216159 0 0 1-212.278485 0z" p-id="53483"></path><path d="M599.094199 283.713995H227.42953a39.365246 39.365246 0 0 1 0-78.730493h371.664669a39.365246 39.365246 0 0 1 0 78.730493z" p-id="53484" ></path><path d="M413.312528 505.973115H227.42953a39.365246 39.365246 0 0 1 0-78.730493h185.882998a39.365246 39.365246 0 0 1 0 78.730493z" p-id="53485"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
||||
<?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="1772011258555" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10601" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M916.8 380.8L645 109.4c-7.2-7.2-16.5-10.7-25.9-10.7-9.4 0-18.8 3.5-25.9 10.7L415.5 286.9c-13.5-1.5-27-2.2-40.6-2.2-80.7 0-161.5 26.5-227.8 79.6-17 13.5-18.4 39-3 54.4l200.4 200.1L106.9 856c-2.9 2.9-4.7 6.7-5.1 10.8l-3.7 41c-1 10.4 7.3 19.2 17.5 19.2 0.6 0 1.1 0 1.7-0.1l41-3.7c4.1-0.3 7.9-2.2 10.8-5.1l237.6-237.3 200.4 200.1c7.2 7.2 16.5 10.7 25.9 10.7 10.7 0 21.3-4.6 28.6-13.7 62.1-77.4 87.9-174.4 77.4-268.1l177.7-177.4c14.4-14.2 14.4-37.3 0.1-51.6zM682.9 553.9l-27 27 4.2 37.9c4.1 37.1 1.1 74-9 109.8-6 20.9-14.1 40.9-24.5 59.7L237 399.2c14.2-7.8 29-14.4 44.5-19.7 30-10.4 61.4-15.5 93.4-15.5 10.6 0 21.3 0.5 31.9 1.8l37.9 4.2 174.5-174.3 211.2 210.9-147.5 147.3z m0 0" p-id="10602"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,5 +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="1771990786809" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="5372" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M876.30000001 512l-716.60000001-413.8L159.7 925.8z" p-id="5373" ></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761736278335" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5885" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M226.592 896C167.616 896 128 850.48 128 782.736V241.264C128 173.52 167.616 128 226.592 128c20.176 0 41.136 5.536 62.288 16.464l542.864 280.432C887.648 453.792 896 491.872 896 512s-8.352 58.208-64.272 87.088L288.864 879.536C267.712 890.464 246.768 896 226.592 896z m0-704.304c-31.008 0-34.368 34.656-34.368 49.568v541.472c0 14.896 3.344 49.568 34.368 49.568 9.6 0 20.88-3.2 32.608-9.248l542.864-280.432c21.904-11.328 29.712-23.232 29.712-30.608s-7.808-19.28-29.712-30.592L259.2 200.96c-11.728-6.048-23.008-9.264-32.608-9.264z" p-id="5886"></path></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 413 B After Width: | Height: | Size: 886 B |
@@ -1 +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>
|
||||
<?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="1764640542560" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1335" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M838.611638 631.662714l67.309037-44.87798 87.97131 131.929203a40.41239 40.41239 0 0 1-33.646587 62.843448H555.939064v-80.82478h328.851364l-46.17879-69.069891zM718.135918 979.106157l-67.237652 44.862116-88.050627-131.921271a40.41239 40.41239 0 0 1 33.583133-62.779994h404.37772v80.761326h-328.772047l46.107404 69.149209v-0.071386zM701.510919 407.10625a294.54644 294.54644 0 0 0-84.695487-59.139309c44.34655-35.891279 72.670917-90.69984 72.670917-152.218682 0-108.300447-90.461886-197.31875-199.008219-195.621351A196.089325 196.089325 0 0 0 318.390354 107.673837a7.828661 7.828661 0 0 0 6.202648 11.278983c18.655533 2.014671 36.882751 6.194716 54.126428 12.214932a7.804866 7.804866 0 0 0 9.07395-3.140982 125.203058 125.203058 0 0 1 104.11247-57.632273 124.608175 124.608175 0 0 1 91.262995 37.898018 124.679561 124.679561 0 0 1 35.462963 83.759537 124.846128 124.846128 0 0 1-73.979659 117.953417 7.884184 7.884184 0 0 0-4.386271 8.582179 250.96134 250.96134 0 0 1 2.879234 71.409765l0.13484 0.063454a7.828661 7.828661 0 0 0 5.821923 8.526657 220.661962 220.661962 0 0 1 102.478524 58.369928 221.201323 221.201323 0 0 1 65.278503 150.338851 7.773139 7.773139 0 0 0 7.828661 7.51139h55.189286a7.844525 7.844525 0 0 0 7.574845-8.074546 291.05646 291.05646 0 0 0-85.940775-199.626897z" fill="#17A29E" p-id="1336"></path><path d="M458.386171 627.942712h89.145212a291.072323 291.072323 0 0 0-41.649747-52.38937 293.443924 293.443924 0 0 0-84.568578-59.13931A195.875168 195.875168 0 0 0 493.761885 367.550491c1.808445-108.173539-84.425806-197.310819-192.591413-199.111331l-0.824905-0.007932c-108.768422-0.650405-197.469454 86.987769-198.119859 195.764123a195.296148 195.296148 0 0 0 72.655053 152.21075c-31.441554 14.602397-59.401058 35.288464-84.560647 59.139309C4.371408 657.03646 6.187784 763.131873 4.371408 838.277504a7.836593 7.836593 0 0 0 7.828661 8.011092h54.816492a7.804866 7.804866 0 0 0 7.828662-7.511391c1.840172-56.601142 25.207179-173.483769 65.334025-213.420252 42.157381-42.157381 98.227094-65.334025 157.850241-65.334025a221.827933 221.827933 0 0 1 157.858173 65.334025c0.864563 0.832836 1.657741 1.729127 2.498509 2.585759zM298.037421 489.549113l-0.063454-0.071386a124.663697 124.663697 0 0 1-88.637578-36.636866c-23.668415-23.747732-36.70032-55.141695-36.70032-88.64551 0-33.829018 13.27779-65.643364 37.580747-89.454551a126.05969 126.05969 0 0 1 89.081757-35.764371 125.512397 125.512397 0 0 1 86.686362 36.414776c49.169069 48.820071 49.454613 128.264723 0.642474 177.449656a124.354358 124.354358 0 0 1-88.589988 36.708252z" fill="#17A29E" p-id="1337"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -1 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1 +0,0 @@
|
||||
<?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="1772011015773" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8632" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M512 298.6496a85.3504 85.3504 0 1 0 0-170.6496 85.3504 85.3504 0 0 0 0 170.6496z" p-id="8633"></path><path d="M512 512m-85.3504 0a85.3504 85.3504 0 1 0 170.7008 0 85.3504 85.3504 0 1 0-170.7008 0Z" p-id="8634"></path><path d="M512 896a85.3504 85.3504 0 1 0 0-170.7008 85.3504 85.3504 0 0 0 0 170.7008z" p-id="8635"></path></svg>
|
||||
|
Before Width: | Height: | Size: 661 B |
@@ -1,6 +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="1761204835005" class="icon" viewBox="0 0 1171 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="5692" xmlns:xlink="http://www.w3.org/1999/xlink" width="228.7109375" height="200">
|
||||
<path d="M502.237757 1024 644.426501 829.679301 502.237757 788.716444 502.237757 1024 502.237757
|
||||
1024ZM0 566.713817 403.967637 689.088066 901.485385 266.66003 515.916344 721.68034 947.825442 855.099648 1170.285714 0 0 566.713817 0 566.713817Z" p-id="5693"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761204835005" class="icon" viewBox="0 0 1171 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5692" xmlns:xlink="http://www.w3.org/1999/xlink" width="228.7109375" height="200"><path d="M502.237757 1024 644.426501 829.679301 502.237757 788.716444 502.237757 1024 502.237757 1024ZM0 566.713817 403.967637 689.088066 901.485385 266.66003 515.916344 721.68034 947.825442 855.099648 1170.285714 0 0 566.713817 0 566.713817Z" p-id="5693"></path></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 610 B After Width: | Height: | Size: 603 B |
@@ -1,7 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 709 B |
@@ -1 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 894 B |
@@ -1,163 +0,0 @@
|
||||
<template>
|
||||
<div class="external-input-container">
|
||||
<div class="branch-input-container">
|
||||
<el-input
|
||||
v-model="inputValue"
|
||||
placeholder="输入分支需求..."
|
||||
size="small"
|
||||
class="branch-input"
|
||||
@keydown="handleKeydown"
|
||||
ref="inputRef"
|
||||
/>
|
||||
<span
|
||||
class="submit-icon"
|
||||
:class="{ 'is-disabled': !inputValue.trim() }"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<svg-icon icon-class="paper-plane" size="14px" color="#fff" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from 'vue'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
placeholder: '输入分支需求...'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'submit', value: string): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const inputValue = ref(props.modelValue)
|
||||
const inputRef = ref<InstanceType<typeof HTMLInputElement>>()
|
||||
|
||||
// 聚焦输入框
|
||||
const focus = () => {
|
||||
nextTick(() => {
|
||||
inputRef.value?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
focus
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (inputValue.value.trim()) {
|
||||
emit('submit', inputValue.value.trim())
|
||||
inputValue.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
inputValue.value = ''
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
handleSubmit()
|
||||
} else if (event.key === 'Escape') {
|
||||
handleCancel()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.external-input-container {
|
||||
position: absolute;
|
||||
bottom: -80px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 260px;
|
||||
background: linear-gradient(var(--color-card-bg-task), var(--color-card-bg-task)) padding-box,
|
||||
linear-gradient(to right, var(--color-accent), var(--color-accent-secondary)) border-box;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 30px;
|
||||
padding: 10px 12px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
z-index: 100;
|
||||
animation: slideDown 0.2s ease-out;
|
||||
}
|
||||
|
||||
.branch-input-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.branch-input {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-taskbar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submit-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(to right, var(--color-accent), var(--color-accent-secondary));
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active:not(.is-disabled) {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,161 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:width="width"
|
||||
:modal="false"
|
||||
:close-on-click-modal="false"
|
||||
:center="true"
|
||||
:show-close="false"
|
||||
top="20vh"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<template #header>
|
||||
<div class="dialog-header">
|
||||
<SvgIcon icon-class="JingGao" size="20px" color="#ff6712" />
|
||||
<span>{{ title }}</span>
|
||||
<button class="dialog-close-btn" @click="handleCancel">
|
||||
<SvgIcon icon-class="close" size="18px" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<slot>
|
||||
<span>{{ content }}</span>
|
||||
</slot>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleCancel">{{ cancelText }}</el-button>
|
||||
<el-button class="confirm-btn" type="danger" :loading="loading" @click="handleConfirm">
|
||||
{{ confirmText }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
title?: string
|
||||
content?: string
|
||||
width?: string | number
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '确认删除',
|
||||
content: '删除后,该操作无法恢复!',
|
||||
width: '400px',
|
||||
confirmText: '删除',
|
||||
cancelText: '取消'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'confirm'): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(props.modelValue)
|
||||
|
||||
// 监听 v-model 变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
val => {
|
||||
dialogVisible.value = val
|
||||
}
|
||||
)
|
||||
|
||||
// 监听对话框显示状态变化
|
||||
watch(dialogVisible, val => {
|
||||
emit('update:modelValue', val)
|
||||
// 当对话框关闭时,重置 loading 状态
|
||||
if (!val) {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// 确认删除
|
||||
const handleConfirm = async () => {
|
||||
loading.value = true
|
||||
emit('confirm')
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
// 对话框关闭后重置 loading 状态
|
||||
const handleClosed = () => {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 暴露方法供外部调用
|
||||
defineExpose({
|
||||
setLoading: (val: boolean) => {
|
||||
loading.value = val
|
||||
},
|
||||
close: () => {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
|
||||
.dialog-close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
margin-left: auto;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
|
||||
.el-button:not(.confirm-btn) {
|
||||
background: #ffffff;
|
||||
border-color: #dcdfe6;
|
||||
color: #000000;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #c0c0c0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
--el-button-bg-color: #ff6712;
|
||||
--el-button-border-color: #ff6712;
|
||||
--el-button-hover-bg-color: #e55a0f;
|
||||
--el-button-hover-border-color: #e55a0f;
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,5 @@
|
||||
<template>
|
||||
<el-tooltip
|
||||
:disabled="!isOverflow"
|
||||
effect="light"
|
||||
placement="top"
|
||||
:content="text"
|
||||
popper-class="multi-line-tooltip-popper"
|
||||
>
|
||||
<el-tooltip :disabled="!isOverflow" effect="light" placement="top" :content="text">
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="multi-line-ellipsis"
|
||||
@@ -34,7 +28,7 @@ interface Props {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
text: '',
|
||||
lines: 3,
|
||||
maxWidth: '100%'
|
||||
maxWidth: '100%',
|
||||
})
|
||||
|
||||
const isOverflow = ref(false)
|
||||
@@ -51,8 +45,8 @@ const containerStyle = computed(
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
lineHeight: '1.5',
|
||||
wordBreak: 'break-all'
|
||||
} as HTMLAttributes['style'])
|
||||
wordBreak: 'break-all',
|
||||
}) as HTMLAttributes['style'],
|
||||
)
|
||||
|
||||
// 检查文字是否溢出
|
||||
@@ -97,9 +91,3 @@ onMounted(() => {
|
||||
white-space: normal;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.multi-line-tooltip-popper {
|
||||
max-width: 400px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<div class="notification-container">
|
||||
<transition-group name="notification" tag="div" class="notification-list">
|
||||
<div
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
:class="['notification-item', `notification-${notification.type || 'info'}`]"
|
||||
:style="{ zIndex: notification.zIndex || 1000 }"
|
||||
>
|
||||
<div class="notification-content">
|
||||
<div class="notification-icon">
|
||||
<component :is="getIcon(notification.type)" />
|
||||
</div>
|
||||
<div class="notification-message">
|
||||
<div class="notification-title">{{ notification.title }}</div>
|
||||
<div v-if="notification.detailTitle" class="notification-detail-title">
|
||||
{{ notification.detailTitle }}
|
||||
</div>
|
||||
<div v-if="notification.detailMessage" class="notification-detail-desc">
|
||||
{{ notification.detailMessage }}
|
||||
</div>
|
||||
<div v-else-if="notification.message" class="notification-desc">
|
||||
{{ notification.message }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="notification-close" @click="close(notification.id)">
|
||||
<Close />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="notification.showProgress" class="notification-progress">
|
||||
<div class="progress-bar" :style="{ width: `${notification.progress || 0}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Close,
|
||||
SuccessFilled as IconSuccess,
|
||||
WarningFilled as IconWarning,
|
||||
InfoFilled
|
||||
} from '@element-plus/icons-vue'
|
||||
import type { NotificationItem } from '@/composables/useNotification'
|
||||
|
||||
const props = defineProps<{
|
||||
notifications: NotificationItem[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [id: string]
|
||||
}>()
|
||||
|
||||
const close = (id: string) => {
|
||||
emit('close', id)
|
||||
}
|
||||
|
||||
const getIcon = (type?: string) => {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return IconSuccess
|
||||
case 'warning':
|
||||
return IconWarning
|
||||
case 'error':
|
||||
return IconWarning
|
||||
default:
|
||||
return InfoFilled
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.notification-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.notification-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
pointer-events: auto;
|
||||
min-width: 300px;
|
||||
max-width: 450px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden;
|
||||
border-left: 4px solid #409eff;
|
||||
}
|
||||
|
||||
.notification-success {
|
||||
border-left-color: #67c23a;
|
||||
}
|
||||
|
||||
.notification-warning {
|
||||
border-left-color: #e6a23c;
|
||||
}
|
||||
|
||||
.notification-error {
|
||||
border-left-color: #f56c6c;
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 12px 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.notification-detail-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #409eff;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.notification-detail-desc {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.notification-desc {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.notification-close {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.notification-close:hover {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.notification-progress {
|
||||
height: 2px;
|
||||
background: #f0f2f5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: #409eff;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* 进入动画 */
|
||||
.notification-enter-active {
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* 离开动画 */
|
||||
.notification-leave-active {
|
||||
animation: slideOutRight 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutRight {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* 列表项移动动画 */
|
||||
.notification-move,
|
||||
.notification-enter-active,
|
||||
.notification-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.notification-leave-active {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,515 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:width="width"
|
||||
:close-on-click-modal="true"
|
||||
:center="true"
|
||||
:show-close="false"
|
||||
top="20vh"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<!-- 自定义关闭按钮 -->
|
||||
<template #header>
|
||||
<div class="dialog-header">
|
||||
<span class="dialog-title">分享链接</span>
|
||||
<button class="dialog-close-btn" @click="dialogVisible = false">
|
||||
<SvgIcon icon-class="close" size="18px" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="share-content">
|
||||
<!-- 设置区域 -->
|
||||
<div v-if="!loading && !error" class="settings-section">
|
||||
<!-- 有效期设置 -->
|
||||
<div class="setting-row inline">
|
||||
<div class="setting-label">有效期:</div>
|
||||
<div class="option-box">
|
||||
<div
|
||||
v-for="option in expirationOptions"
|
||||
:key="option.value"
|
||||
:class="['option-item', { active: expirationDays === option.value }]"
|
||||
@click="expirationDays = option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提取码设置 -->
|
||||
<div class="setting-row inline">
|
||||
<div class="setting-label">提取码:</div>
|
||||
<div class="code-option-box">
|
||||
<div
|
||||
:class="['option-item', { active: codeType === 'random' }]"
|
||||
@click="handleCodeTypeChange('random')"
|
||||
>
|
||||
随机生成
|
||||
</div>
|
||||
<div
|
||||
:class="['option-item', { active: codeType === 'custom' }]"
|
||||
@click="handleCodeTypeChange('custom')"
|
||||
>
|
||||
自定义
|
||||
</div>
|
||||
</div>
|
||||
<!-- 自定义输入框 -->
|
||||
<div v-if="codeType === 'custom'" class="custom-code-wrapper">
|
||||
<el-input
|
||||
v-model="customCode"
|
||||
placeholder="仅支持字母/数字"
|
||||
class="custom-code-input"
|
||||
maxlength="4"
|
||||
@input="validateCustomCode"
|
||||
>
|
||||
<template #suffix>
|
||||
<span v-if="customCode" class="code-count">{{ customCode.length }}/4</span>
|
||||
</template>
|
||||
</el-input>
|
||||
<span v-if="!customCode" class="code-hint">请输入提取码</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自动填充提取码选项 -->
|
||||
<div class="setting-row">
|
||||
<el-checkbox v-model="autoFillCode" class="auto-fill-checkbox">
|
||||
分享链接自动填充提取码
|
||||
</el-checkbox>
|
||||
</div>
|
||||
|
||||
<!-- 复制链接按钮 -->
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="generate-btn"
|
||||
:disabled="!canGenerate"
|
||||
:loading="loading"
|
||||
@click="handleCopyLink"
|
||||
>
|
||||
复制链接
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="error" class="error-state">
|
||||
<div class="error-icon">
|
||||
<SvgIcon icon-class="JingGao" size="48px" color="#f56c6c" />
|
||||
</div>
|
||||
<p class="error-text">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import websocket from '@/utils/websocket'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
planId?: string
|
||||
width?: string | number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
planId: '',
|
||||
width: '450px'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
// 有效期选项
|
||||
const expirationOptions = [
|
||||
{ label: '1天', value: 1 },
|
||||
{ label: '7天', value: 7 },
|
||||
{ label: '30天', value: 30 },
|
||||
{ label: '365天', value: 365 },
|
||||
{ label: '永久有效', value: 0 }
|
||||
]
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const dialogVisible = ref(props.modelValue)
|
||||
const expirationDays = ref(7) // 默认7天
|
||||
const codeType = ref<'random' | 'custom'>('random') // 提取码类型
|
||||
const customCode = ref('') // 自定义提取码
|
||||
const generatedCode = ref('') // 生成的提取码
|
||||
const autoFillCode = ref(true) // 分享链接自动填充提取码
|
||||
|
||||
// 是否可以生成链接
|
||||
const canGenerate = computed(() => {
|
||||
if (codeType.value === 'random') {
|
||||
return true // 随机生成总是可以
|
||||
} else {
|
||||
return /^[a-zA-Z0-9]{4}$/.test(customCode.value)
|
||||
}
|
||||
})
|
||||
|
||||
// 生成唯一请求ID
|
||||
let requestIdCounter = 0
|
||||
const generateRequestId = () => `ws_req_${Date.now()}_${++requestIdCounter}`
|
||||
|
||||
// 监听 v-model 变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
val => {
|
||||
dialogVisible.value = val
|
||||
if (val && props.planId) {
|
||||
// 打开弹窗时重置状态
|
||||
loading.value = false
|
||||
error.value = ''
|
||||
// 生成随机提取码
|
||||
generateRandomCode()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 监听对话框显示状态变化
|
||||
watch(dialogVisible, val => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 生成随机提取码(4位字母数字)
|
||||
const generateRandomCode = () => {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||||
let code = ''
|
||||
for (let i = 0; i < 4; i++) {
|
||||
code += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
generatedCode.value = code
|
||||
}
|
||||
|
||||
// 处理提取码类型变化
|
||||
const handleCodeTypeChange = (type: 'random' | 'custom') => {
|
||||
codeType.value = type
|
||||
if (type === 'random') {
|
||||
generateRandomCode()
|
||||
}
|
||||
}
|
||||
|
||||
// 验证自定义提取码
|
||||
const validateCustomCode = (value: string) => {
|
||||
// 只允许字母和数字
|
||||
customCode.value = value.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()
|
||||
}
|
||||
|
||||
// 生成并复制链接
|
||||
const handleCopyLink = async () => {
|
||||
if (!props.planId) {
|
||||
error.value = '任务 ID 为空'
|
||||
return
|
||||
}
|
||||
|
||||
// 获取提取码
|
||||
const extractionCode = codeType.value === 'random' ? generatedCode.value : customCode.value
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
const reqId = generateRequestId()
|
||||
|
||||
try {
|
||||
const result = await websocket.send('share_plan', {
|
||||
id: reqId,
|
||||
data: {
|
||||
plan_id: props.planId,
|
||||
expiration_days: expirationDays.value,
|
||||
extraction_code: extractionCode,
|
||||
auto_fill_code: autoFillCode.value
|
||||
}
|
||||
})
|
||||
|
||||
if (result.data?.share_url) {
|
||||
// 获取当前域名
|
||||
const baseUrl = window.location.origin
|
||||
const fullUrl = baseUrl + result.data.share_url
|
||||
|
||||
// 复制到剪贴板
|
||||
try {
|
||||
await navigator.clipboard.writeText(fullUrl)
|
||||
ElMessage.success('链接已复制到剪贴板')
|
||||
dialogVisible.value = false
|
||||
} catch {
|
||||
ElMessage.error('复制失败,请手动复制')
|
||||
}
|
||||
} else {
|
||||
error.value = '生成分享链接失败'
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '生成分享链接失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 对话框关闭后重置状态
|
||||
const handleClosed = () => {
|
||||
loading.value = false
|
||||
error.value = ''
|
||||
expirationDays.value = 7
|
||||
codeType.value = 'random'
|
||||
customCode.value = ''
|
||||
generatedCode.value = ''
|
||||
autoFillCode.value = true
|
||||
}
|
||||
|
||||
// 暴露方法供外部调用
|
||||
defineExpose({
|
||||
close: () => {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 自定义对话框头部
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.dialog-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.dialog-close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
color: var(--color-text-regular);
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.share-content {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
.setting-row {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&.inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
.setting-label {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.option-box {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
.code-option-box {
|
||||
flex: none;
|
||||
}
|
||||
.custom-code-wrapper {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.option-box {
|
||||
display: flex;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
.option-item {
|
||||
flex: 1;
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
border-right: 1px solid var(--color-border);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-bg-subtle);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-text-result-detail-run);
|
||||
font-weight: 600;
|
||||
box-shadow: inset 0 0 0 2px var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code-option-box {
|
||||
display: flex;
|
||||
width: 160px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
.option-item {
|
||||
flex: 1;
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
border-right: 1px solid var(--color-border);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-bg-subtle);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-text-result-detail-run);
|
||||
font-weight: 600;
|
||||
box-shadow: inset 0 0 0 2px var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-code-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
|
||||
.custom-code-input {
|
||||
width: 100px;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix) {
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
&::placeholder {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.code-count {
|
||||
font-size: 10px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.code-hint {
|
||||
font-size: 12px;
|
||||
color: var(--color-error, #f56c6c);
|
||||
margin-left: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.auto-fill-checkbox {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 0;
|
||||
gap: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
.el-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.success-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
|
||||
.success-icon {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.success-text {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.share-link-box {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.link-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 0;
|
||||
gap: 12px;
|
||||
|
||||
.error-text {
|
||||
color: var(--color-error, #f56c6c);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -18,7 +18,7 @@ const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg aria-hidden="true" class="svg-icon" :style="props.color ? `color:${props.color}` : ''">
|
||||
<svg aria-hidden="true" class="svg-icon" :style="`color:${props.color}`">
|
||||
<use :xlink:href="symbolId" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import type { IRawStepTask } from '@/stores'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
task: IRawStepTask
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'save', taskId: string, content: string): void
|
||||
}>()
|
||||
|
||||
const isEditing = ref(false)
|
||||
const editingContent = ref('')
|
||||
const editorRef = ref<HTMLElement>()
|
||||
|
||||
const startEditing = () => {
|
||||
editingContent.value = props.task.TaskContent || ''
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
const save = () => {
|
||||
const trimmed = editingContent.value.trim()
|
||||
emit('save', props.task.Id!, trimmed)
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
save()
|
||||
} else if (event.key === 'Escape') {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// 点击外部保存
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (isEditing.value && editorRef.value && !editorRef.value.contains(event.target as Node)) {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="editorRef" v-if="isEditing" class="w-full">
|
||||
<el-input
|
||||
v-model="editingContent"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||
placeholder="请输入任务内容"
|
||||
@keydown="handleKeydown"
|
||||
class="task-content-editor"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<div v-else @dblclick="startEditing" class="w-full cursor-pointer task-content-wrapper">
|
||||
<slot name="display">
|
||||
<div class="text-[14px] text-[var(--color-text-secondary)] task-content-display">
|
||||
{{ task.TaskContent }}
|
||||
</div>
|
||||
</slot>
|
||||
<div class="edit-icon-bg" @click.stop="startEditing">
|
||||
<svg-icon icon-class="Edit" class="edit-icon-svg" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.task-content-wrapper {
|
||||
position: relative;
|
||||
padding: 4px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 7px;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-task-edit-hover-border) !important;
|
||||
}
|
||||
|
||||
.edit-icon-bg {
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
width: 26px;
|
||||
height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
cursor: pointer;
|
||||
background: var(--color-edit-icon-bg);
|
||||
border-radius: 8px 0px 7px 0px;
|
||||
|
||||
.edit-icon-svg {
|
||||
color: var(--color-text-detail);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .edit-icon-bg {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.task-content-editor {
|
||||
:deep(.el-textarea__inner) {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-task-edit-hover-border) !important;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
resize: none;
|
||||
}
|
||||
}
|
||||
|
||||
.task-content-display {
|
||||
min-height: 40px;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,163 +0,0 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface NotificationItem {
|
||||
id: string
|
||||
title: string
|
||||
message?: string
|
||||
type?: 'success' | 'warning' | 'info' | 'error'
|
||||
duration?: number
|
||||
showProgress?: boolean
|
||||
progress?: number
|
||||
zIndex?: number
|
||||
onClose?: () => void
|
||||
// 详细进度信息
|
||||
detailTitle?: string
|
||||
detailMessage?: string
|
||||
}
|
||||
|
||||
const notifications = ref<NotificationItem[]>([])
|
||||
let notificationIdCounter = 0
|
||||
let zIndexCounter = 1000
|
||||
|
||||
export function useNotification() {
|
||||
const addNotification = (notification: Omit<NotificationItem, 'id' | 'zIndex'>) => {
|
||||
const id = `notification-${notificationIdCounter++}`
|
||||
const newNotification: NotificationItem = {
|
||||
...notification,
|
||||
id,
|
||||
zIndex: ++zIndexCounter,
|
||||
}
|
||||
|
||||
notifications.value.push(newNotification)
|
||||
|
||||
// 自动关闭
|
||||
if (notification.duration && notification.duration > 0) {
|
||||
setTimeout(() => {
|
||||
removeNotification(id)
|
||||
}, notification.duration)
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
const removeNotification = (id: string) => {
|
||||
const notification = notifications.value.find((n) => n.id === id)
|
||||
if (notification) {
|
||||
const index = notifications.value.indexOf(notification)
|
||||
notifications.value.splice(index, 1)
|
||||
notification.onClose?.()
|
||||
}
|
||||
}
|
||||
|
||||
const success = (title: string, message?: string, options?: Partial<NotificationItem>) => {
|
||||
return addNotification({
|
||||
title,
|
||||
message,
|
||||
type: 'success',
|
||||
duration: 3000,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
const warning = (title: string, message?: string, options?: Partial<NotificationItem>) => {
|
||||
return addNotification({
|
||||
title,
|
||||
message,
|
||||
type: 'warning',
|
||||
duration: 3000,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
const info = (title: string, message?: string, options?: Partial<NotificationItem>) => {
|
||||
return addNotification({
|
||||
title,
|
||||
message,
|
||||
type: 'info',
|
||||
duration: 3000,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
const error = (title: string, message?: string, options?: Partial<NotificationItem>) => {
|
||||
return addNotification({
|
||||
title,
|
||||
message,
|
||||
type: 'error',
|
||||
duration: 5000,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
const progress = (
|
||||
title: string,
|
||||
current: number,
|
||||
total: number,
|
||||
options?: Partial<NotificationItem>,
|
||||
) => {
|
||||
const progressPercent = Math.round((current / total) * 100)
|
||||
return addNotification({
|
||||
title,
|
||||
message: `${current}/${total}`,
|
||||
type: 'info',
|
||||
showProgress: true,
|
||||
progress: progressPercent,
|
||||
duration: 0, // 不自动关闭
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
const updateProgress = (id: string, current: number, total: number) => {
|
||||
const notification = notifications.value.find((n) => n.id === id)
|
||||
if (notification) {
|
||||
notification.progress = Math.round((current / total) * 100)
|
||||
notification.message = `${current}/${total}`
|
||||
}
|
||||
}
|
||||
|
||||
const updateProgressDetail = (
|
||||
id: string,
|
||||
detailTitle: string,
|
||||
detailMessage: string,
|
||||
current?: number,
|
||||
total?: number,
|
||||
) => {
|
||||
const notification = notifications.value.find((n) => n.id === id)
|
||||
if (notification) {
|
||||
notification.detailTitle = detailTitle
|
||||
notification.detailMessage = detailMessage
|
||||
if (current !== undefined && total !== undefined) {
|
||||
notification.progress = Math.round((current / total) * 100)
|
||||
notification.message = `${current}/${total}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新通知的主标题
|
||||
const updateNotificationTitle = (id: string, title: string) => {
|
||||
const notification = notifications.value.find((n) => n.id === id)
|
||||
if (notification) {
|
||||
notification.title = title
|
||||
}
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
notifications.value.forEach((n) => n.onClose?.())
|
||||
notifications.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
notifications,
|
||||
addNotification,
|
||||
removeNotification,
|
||||
success,
|
||||
warning,
|
||||
info,
|
||||
error,
|
||||
progress,
|
||||
updateProgress,
|
||||
updateProgressDetail,
|
||||
updateNotificationTitle,
|
||||
clear,
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed, reactive, nextTick } from 'vue'
|
||||
import { ref, onMounted, computed, reactive } from 'vue'
|
||||
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import { useAgentsStore, useConfigStore, useSelectionStore } from '@/stores'
|
||||
import { useAgentsStore, useConfigStore } from '@/stores'
|
||||
import api from '@/api'
|
||||
import websocket from '@/utils/websocket'
|
||||
import { changeBriefs } from '@/utils/collaboration_Brief_FrontEnd.ts'
|
||||
import { useNotification } from '@/composables/useNotification'
|
||||
import AssignmentButton from './TaskTemplate/TaskSyllabus/components/AssignmentButton.vue'
|
||||
import { withRetry } from '@/utils/retry'
|
||||
import UnifiedSettingsPanel from './TaskTemplate/UnifiedSettingsPanel.vue'
|
||||
import { Setting } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { log } from '@jsplumb/browser-ui'
|
||||
import ProcessCard from './TaskTemplate/TaskProcess/ProcessCard.vue'
|
||||
import AgentAllocation from './TaskTemplate/TaskSyllabus/components/AgentAllocation.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-start'): void
|
||||
(e: 'search', value: string): void
|
||||
(e: 'open-history'): void
|
||||
}>()
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
const configStore = useConfigStore()
|
||||
const selectionStore = useSelectionStore()
|
||||
const { success, warning, error: notifyError } = useNotification()
|
||||
const searchValue = ref('')
|
||||
const triggerOnFocus = ref(true)
|
||||
const isFocus = ref(false)
|
||||
const hasAutoSearched = ref(false)
|
||||
const isExpanded = ref(false)
|
||||
const isFillingSteps = ref(false)
|
||||
const isStopping = ref(false)
|
||||
const isStopPending = ref(false)
|
||||
const currentStepAbortController = ref<{ cancel: () => void } | null>(null)
|
||||
const currentGenerationId = ref('')
|
||||
const currentTaskID = ref('') // 后端数据库主键
|
||||
|
||||
// 监听 currentTaskID 变化,同步到全局变量(供分支保存使用)
|
||||
watch(currentTaskID, newVal => {
|
||||
;(window as any).__CURRENT_TASK_ID__ = newVal || ''
|
||||
})
|
||||
|
||||
const currentPlanOutline = ref<any>(null) // 用于恢复的完整大纲数据
|
||||
const hasAutoSearched = ref(false) // 防止重复自动搜索
|
||||
const agentAllocationVisible = ref(false) // 智能体分配弹窗是否可见
|
||||
const isExpanded = ref(false) // 控制搜索框是否展开
|
||||
|
||||
// 解析URL参数
|
||||
function getUrlParam(param: string): string | null {
|
||||
@@ -46,12 +30,6 @@ function getUrlParam(param: string): string | null {
|
||||
return urlParams.get(param)
|
||||
}
|
||||
|
||||
const planReady = computed(() => {
|
||||
return agentsStore.agentRawPlan.data !== undefined
|
||||
})
|
||||
const openAgentAllocationDialog = () => {
|
||||
agentsStore.openAgentAllocationDialog()
|
||||
}
|
||||
// 自动搜索函数
|
||||
async function autoSearchFromUrl() {
|
||||
const query = getUrlParam('q')
|
||||
@@ -68,304 +46,52 @@ async function autoSearchFromUrl() {
|
||||
}
|
||||
}
|
||||
|
||||
// 智能体分配
|
||||
// 处理智能体分配点击事件
|
||||
function handleAgentAllocation() {
|
||||
agentAllocationVisible.value = true
|
||||
}
|
||||
|
||||
// 关闭智能体分配弹窗
|
||||
function handleAgentAllocationClose() {
|
||||
agentAllocationVisible.value = false
|
||||
}
|
||||
|
||||
// 处理获取焦点事件
|
||||
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, TaskID: string) {
|
||||
const tasks = outlineData['Collaboration Process'] || []
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const task of tasks) {
|
||||
// 检查是否已停止
|
||||
if (agentsStore.isStopping || agentsStore.hasStoppedFilling) {
|
||||
async function handleSearch() {
|
||||
try {
|
||||
triggerOnFocus.value = false
|
||||
if (!searchValue.value) {
|
||||
ElMessage.warning('请输入搜索内容')
|
||||
return
|
||||
}
|
||||
|
||||
// 确保任务有唯一ID
|
||||
if (!task.Id) {
|
||||
task.Id = `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
|
||||
// 调用初始化接口获取评分数据
|
||||
try {
|
||||
const agentScores = await api.agentSelectModifyInit({
|
||||
goal: goal,
|
||||
stepTask: {
|
||||
Id: task.Id, // 小任务步骤ID,用于数据库存储
|
||||
StepName: task.StepName,
|
||||
TaskContent: task.TaskContent,
|
||||
InputObject_List: task.InputObject_List,
|
||||
OutputObject: task.OutputObject
|
||||
},
|
||||
TaskID: TaskID // 后端数据库主键(大任务ID)
|
||||
})
|
||||
|
||||
// 提取维度列表并存储
|
||||
const firstAgent = Object.keys(agentScores)[0]
|
||||
const aspectList = firstAgent ? Object.keys(agentScores[firstAgent] || {}) : []
|
||||
|
||||
agentsStore.setTaskScoreData(task.Id, {
|
||||
aspectList,
|
||||
agentScores
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`❌ 任务 "${task.StepName}" 的评分数据预加载失败:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重置文本区域高度到最小行数
|
||||
function resetTextareaHeight() {
|
||||
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() {
|
||||
// 检查是否有正在进行的生成任务
|
||||
if (!isFillingSteps.value) {
|
||||
warning('提示', '没有正在进行的生成任务')
|
||||
return
|
||||
}
|
||||
|
||||
// 先设置停止状态(立即显示"停止中...")
|
||||
agentsStore.setIsStopping(true)
|
||||
isStopping.value = true
|
||||
isStopPending.value = true
|
||||
success('提示', '正在停止,请稍候...')
|
||||
|
||||
// 发送停止请求(不等待响应,后端设置 should_stop = True)
|
||||
if (websocket.connected && currentGenerationId.value) {
|
||||
websocket
|
||||
.send('stop_generation', {
|
||||
generation_id: currentGenerationId.value
|
||||
})
|
||||
.then((result: any) => {
|
||||
console.log('停止生成响应:', result)
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.log('停止生成请求失败(可能已经停止):', error?.message)
|
||||
})
|
||||
}
|
||||
// 不清空 currentGenerationId,让 fillStepTask 循环检查 isStopping 来停止
|
||||
}
|
||||
|
||||
// 监听后端发送的停止完成事件(备用,如果后端有发送)
|
||||
function onGenerationStopped() {
|
||||
isStopping.value = false
|
||||
isStopPending.value = false
|
||||
currentGenerationId.value = ''
|
||||
success('成功', '已停止生成')
|
||||
}
|
||||
|
||||
// 处理按钮点击事件
|
||||
function handleButtonClick() {
|
||||
if (isFillingSteps.value) {
|
||||
handleStop()
|
||||
} else {
|
||||
handleSearch()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理搜索函数
|
||||
async function handleSearch() {
|
||||
triggerOnFocus.value = false
|
||||
if (!searchValue.value) {
|
||||
warning('提示', '请输入搜索内容')
|
||||
return
|
||||
}
|
||||
emit('search-start')
|
||||
|
||||
// 重置所有状态(处理可能的上一次未完成的停止操作)
|
||||
isStopping.value = false
|
||||
isStopPending.value = false
|
||||
agentsStore.setIsStopping(false)
|
||||
agentsStore.setHasStoppedFilling(false)
|
||||
|
||||
agentsStore.resetAgent()
|
||||
agentsStore.setAgentRawPlan({ loading: true })
|
||||
|
||||
// 重置 generation_id
|
||||
currentGenerationId.value = ''
|
||||
|
||||
// 获取大纲
|
||||
const response = await api.generateBasePlan({
|
||||
goal: searchValue.value,
|
||||
inputs: []
|
||||
})
|
||||
|
||||
// WebSocket 返回格式: { data: { task_id, generation_id, basePlan }, generation_id, execution_id }
|
||||
// REST API 返回格式: {...}
|
||||
// 提取真正的outline数据
|
||||
let outlineData: any
|
||||
if (response?.data?.basePlan) {
|
||||
// WebSocket新格式:数据嵌套在basePlan中
|
||||
outlineData = response.data.basePlan
|
||||
currentGenerationId.value = response.data.generation_id || ''
|
||||
currentTaskID.value = response.data.task_id || ''
|
||||
console.log(
|
||||
'📋 WebSocket格式: 从basePlan提取数据, generation_id:',
|
||||
currentGenerationId.value,
|
||||
'TaskID:',
|
||||
currentTaskID.value
|
||||
)
|
||||
} else if (response?.data) {
|
||||
// 可能是WebSocket旧格式或REST API格式
|
||||
outlineData = response.data
|
||||
currentGenerationId.value = response.generation_id || ''
|
||||
// 从 response.data 中获取 task_id(REST API 格式)
|
||||
currentTaskID.value = response.data?.task_id || response.task_id || ''
|
||||
console.log(
|
||||
'📋 直接格式: generation_id:',
|
||||
currentGenerationId.value,
|
||||
'TaskID:',
|
||||
currentTaskID.value
|
||||
)
|
||||
} else {
|
||||
outlineData = response
|
||||
currentGenerationId.value = ''
|
||||
currentTaskID.value = ''
|
||||
}
|
||||
|
||||
// 处理简报数据格式
|
||||
outlineData['Collaboration Process'] = changeBriefs(outlineData['Collaboration Process'])
|
||||
|
||||
// 立即显示大纲
|
||||
agentsStore.setAgentRawPlan({ data: outlineData, loading: false })
|
||||
emit('search', searchValue.value)
|
||||
|
||||
// 预加载所有任务的智能体评分数据
|
||||
preloadAllTaskAgentScores(outlineData, searchValue.value, currentTaskID.value)
|
||||
|
||||
// 填充步骤详情
|
||||
isFillingSteps.value = true
|
||||
const steps = outlineData['Collaboration Process'] || []
|
||||
|
||||
// 保存 generation_id 和 TaskID 到本地变量,用于 fillStepTask 调用
|
||||
// 这样即使前端停止时清空了 currentGenerationId,当前的 fillStepTask 仍能正确停止
|
||||
const fillTaskGenerationId = currentGenerationId.value
|
||||
const fillTaskTaskID = currentTaskID.value
|
||||
|
||||
console.log('📋 开始填充步骤详情', {
|
||||
generationId: fillTaskGenerationId,
|
||||
TaskID: fillTaskTaskID,
|
||||
stepsCount: steps.length
|
||||
})
|
||||
|
||||
// 串行填充所有步骤的详情
|
||||
try {
|
||||
for (const step of steps) {
|
||||
// 检查是否已停止
|
||||
if (!isFillingSteps.value || agentsStore.isStopping) {
|
||||
break
|
||||
}
|
||||
|
||||
await withRetry(
|
||||
async () => {
|
||||
console.log(`📤 调用 fillStepTask: 步骤=${step.StepName}, TaskID=${fillTaskTaskID}`)
|
||||
const detailedStep = await api.fillStepTask({
|
||||
goal: searchValue.value,
|
||||
stepTask: {
|
||||
StepName: step.StepName,
|
||||
TaskContent: step.TaskContent,
|
||||
InputObject_List: step.InputObject_List,
|
||||
OutputObject: step.OutputObject,
|
||||
Id: step.Id // 传递步骤ID用于后端定位
|
||||
},
|
||||
generation_id: fillTaskGenerationId,
|
||||
TaskID: fillTaskTaskID // 后端数据库主键
|
||||
})
|
||||
console.log(`📥 fillStepTask 返回完成: 步骤=${step.StepName}`)
|
||||
updateStepDetail(step.StepName, detailedStep)
|
||||
},
|
||||
{
|
||||
maxRetries: 2, // 减少重试次数,因为是串行填充
|
||||
initialDelayMs: 1000, // 使用较小的延迟
|
||||
shouldRetry: () => isFillingSteps.value && !agentsStore.isStopping // 可取消的重试
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 填充完成后,重新保存分支数据(同步最新的大纲数据)
|
||||
// 只有在没有停止且成功完成时才执行
|
||||
if (isFillingSteps.value && !agentsStore.isStopping && fillTaskTaskID) {
|
||||
try {
|
||||
// 先从主大纲同步最新数据到分支
|
||||
const mainOutlineTasks = agentsStore.agentRawPlan.data?.['Collaboration Process'] || []
|
||||
selectionStore.syncBranchTasksFromMainOutline(mainOutlineTasks)
|
||||
// 再保存到数据库
|
||||
await selectionStore.saveBranchesToDB(fillTaskTaskID)
|
||||
console.log('✅ 步骤填充完成,分支数据已同步更新')
|
||||
} catch (error) {
|
||||
console.error('同步分支数据失败:', error)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// 重置状态(确保即使出错也会执行)
|
||||
triggerOnFocus.value = true
|
||||
if (isStopPending.value) {
|
||||
isStopping.value = false
|
||||
isStopPending.value = false
|
||||
agentsStore.setIsStopping(false)
|
||||
agentsStore.setHasStoppedFilling(true)
|
||||
}
|
||||
isFillingSteps.value = false
|
||||
currentStepAbortController.value = null
|
||||
// 只有在没有停止请求时才清空 generation_id
|
||||
if (!isStopPending.value) {
|
||||
currentGenerationId.value = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//更新单个步骤的详情
|
||||
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]) {
|
||||
// 保持响应式更新
|
||||
Object.assign(collaborationProcess[index], {
|
||||
AgentSelection: detailedStep.AgentSelection || [],
|
||||
TaskProcess: detailedStep.TaskProcess || [],
|
||||
Collaboration_Brief_frontEnd: detailedStep.Collaboration_Brief_frontEnd || {
|
||||
template: '',
|
||||
data: {}
|
||||
}
|
||||
emit('search-start')
|
||||
agentsStore.resetAgent()
|
||||
agentsStore.setAgentRawPlan({ loading: true })
|
||||
const data = await api.generateBasePlan({
|
||||
goal: searchValue.value,
|
||||
inputs: []
|
||||
})
|
||||
data['Collaboration Process'] = changeBriefs(data['Collaboration Process'])
|
||||
agentsStore.setAgentRawPlan({ data })
|
||||
emit('search', searchValue.value)
|
||||
} finally {
|
||||
triggerOnFocus.value = true
|
||||
agentsStore.setAgentRawPlan({ loading: false })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,7 +99,7 @@ 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 })))
|
||||
}
|
||||
|
||||
@@ -383,167 +109,11 @@ const createFilter = (queryString: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const taskContainerRef = ref<HTMLDivElement | null>(null)
|
||||
|
||||
// 组件挂载时检查URL参数
|
||||
onMounted(() => {
|
||||
autoSearchFromUrl()
|
||||
websocket.on('generation_stopped', onGenerationStopped)
|
||||
})
|
||||
|
||||
// 组件卸载时移除事件监听
|
||||
onUnmounted(() => {
|
||||
websocket.off('generation_stopped', onGenerationStopped)
|
||||
})
|
||||
|
||||
// 打开历史记录弹窗
|
||||
const openHistoryDialog = () => {
|
||||
emit('open-history')
|
||||
}
|
||||
|
||||
// 从历史记录恢复任务
|
||||
const restoreFromHistory = (plan: any) => {
|
||||
// 1. 设置搜索值为历史任务的目标
|
||||
searchValue.value = plan.general_goal
|
||||
|
||||
// 3. 设置当前任务的 task_id(供分支保存使用)
|
||||
currentTaskID.value = plan.id || ''
|
||||
if (plan.id) {
|
||||
;(window as any).__CURRENT_TASK_ID__ = plan.id
|
||||
console.log('♻️ [Task] 已设置 currentTaskID:', currentTaskID.value)
|
||||
}
|
||||
|
||||
// 4. 设置分支初始化标记,告诉 PlanModification.vue 这是从历史记录恢复
|
||||
// 这样 initializeFlow 会走"恢复分支"逻辑,而不是"首次初始化"
|
||||
sessionStorage.setItem('plan-modification-branches-initialized', 'true')
|
||||
|
||||
// 4. 保存完整的任务大纲数据
|
||||
currentPlanOutline.value = plan.task_outline || null
|
||||
|
||||
// 4. 如果有 agents_info,更新到 store
|
||||
if (plan.agents_info && Array.isArray(plan.agents_info)) {
|
||||
agentsStore.setAgents(plan.agents_info)
|
||||
}
|
||||
|
||||
// 5. 如果有智能体评分数据,更新到 store
|
||||
if (plan.agent_scores && currentPlanOutline.value) {
|
||||
const tasks = currentPlanOutline.value['Collaboration Process'] || []
|
||||
console.log(
|
||||
'🔍 [Task] 所有步骤名称:',
|
||||
tasks.map((t: any) => t.StepName)
|
||||
)
|
||||
console.log('🔍 [Task] agent_scores 的 key:', Object.keys(plan.agent_scores))
|
||||
|
||||
for (const task of tasks) {
|
||||
// 使用 task.Id 作为 key
|
||||
const taskId = task.Id
|
||||
console.log(`🔍 [Task] 步骤使用 taskId: ${taskId}`)
|
||||
console.log(`🔍 [Task] plan.agent_scores[taskId] =`, plan.agent_scores[taskId])
|
||||
|
||||
if (taskId && plan.agent_scores[taskId]) {
|
||||
const stepScores = plan.agent_scores[taskId]
|
||||
console.log(`✅ [Task] 找到步骤 ${taskId} 的评分数据`)
|
||||
|
||||
// 后端返回格式: { aspectList: [...], agentScores: { "Agent1": { "Aspect1": {...} } } }
|
||||
// 解构获取 aspectList 和 agentScores
|
||||
const { aspectList, agentScores } = stepScores
|
||||
|
||||
// agentScores 格式: { "Agent1": { "Aspect1": { score, reason } } }
|
||||
// 直接使用,无需转换(已经是前端期望的格式)
|
||||
console.log(`🔍 [Task] 步骤 ${taskId} 的 agentScores:`, agentScores)
|
||||
console.log(`🔍 [Task] 步骤 ${taskId} 的维度列表:`, aspectList)
|
||||
|
||||
// 使用 taskId 作为 key 存储
|
||||
console.log(`🔍 [Task] 调用 setTaskScoreData: taskId=${taskId}`)
|
||||
agentsStore.setTaskScoreData(taskId, {
|
||||
aspectList,
|
||||
agentScores
|
||||
})
|
||||
console.log(`🔍 [Task] setTaskScoreData 完成,检查存储结果:`)
|
||||
const storedData = agentsStore.getTaskScoreData(taskId)
|
||||
console.log(` taskId=${taskId} 的存储结果:`, storedData)
|
||||
} else {
|
||||
console.log(`❌ [Task] 未找到步骤 "${taskId}" 的评分数据,尝试用 StepName 查找`)
|
||||
// 兼容处理:如果找不到,尝试用 StepName
|
||||
const stepName = task.StepName
|
||||
if (stepName && plan.agent_scores[stepName]) {
|
||||
console.log(`✅ [Task] 兼容模式:使用 StepName ${stepName} 找到评分数据`)
|
||||
const stepScores = plan.agent_scores[stepName]
|
||||
// 后端返回格式: { aspectList: [...], agentScores: {...} }
|
||||
const { aspectList, agentScores } = stepScores
|
||||
agentsStore.setTaskScoreData(taskId, {
|
||||
aspectList,
|
||||
agentScores
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 将完整的大纲数据设置到 store(跳过重新生成)
|
||||
if (currentPlanOutline.value) {
|
||||
agentsStore.setAgentRawPlan({
|
||||
data: currentPlanOutline.value,
|
||||
loading: false
|
||||
})
|
||||
}
|
||||
|
||||
// 7. 如果有分支数据,恢复到 selection store
|
||||
if (plan.branches && Array.isArray(plan.branches)) {
|
||||
// 旧格式:数组,恢复任务大纲探索分支
|
||||
console.log('♻️ [Task] 恢复分支数据:', { count: plan.branches.length })
|
||||
selectionStore.restoreBranchesFromDB(plan.branches)
|
||||
} else if (plan.branches && typeof plan.branches === 'object') {
|
||||
// 新格式:对象,可能包含 flow_branches 和 task_process_branches
|
||||
// 恢复任务大纲探索分支
|
||||
if (plan.branches.flow_branches && Array.isArray(plan.branches.flow_branches)) {
|
||||
console.log('♻️ [Task] 恢复任务大纲探索分支:', { count: plan.branches.flow_branches.length })
|
||||
selectionStore.restoreBranchesFromDB(plan.branches.flow_branches)
|
||||
}
|
||||
|
||||
// 恢复任务过程分支
|
||||
if (
|
||||
plan.branches.task_process_branches &&
|
||||
typeof plan.branches.task_process_branches === 'object'
|
||||
) {
|
||||
console.log('♻️ [Task] 恢复任务过程分支:', {
|
||||
stepCount: Object.keys(plan.branches.task_process_branches).length
|
||||
})
|
||||
selectionStore.restoreTaskProcessBranchesFromDB(plan.branches.task_process_branches)
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 如果有 agent 组合数据,恢复到 selection store
|
||||
if (plan.assigned_agents && typeof plan.assigned_agents === 'object') {
|
||||
console.log('♻️ [Task] 恢复 agent 组合数据:', plan.assigned_agents)
|
||||
selectionStore.restoreAgentCombinationsFromDB(plan.assigned_agents, plan.id)
|
||||
}
|
||||
|
||||
// 9. 如果有 rehearsal_log,恢复执行结果
|
||||
if (plan.rehearsal_log && Array.isArray(plan.rehearsal_log)) {
|
||||
console.log('♻️ [Task] 恢复执行结果:', { count: plan.rehearsal_log.length })
|
||||
// rehearsal_log 本身就是 IExecuteRawResponse 格式的数组
|
||||
agentsStore.setExecutePlan(plan.rehearsal_log)
|
||||
}
|
||||
|
||||
// 10. 触发搜索事件
|
||||
emit('search', plan.general_goal)
|
||||
|
||||
success('成功', '已恢复历史任务')
|
||||
}
|
||||
|
||||
// 设置面板引用
|
||||
const unifiedSettingsPanelRef = ref<InstanceType<typeof UnifiedSettingsPanel> | null>(null)
|
||||
|
||||
// 打开设置面板
|
||||
const openSettingsPanel = () => {
|
||||
unifiedSettingsPanelRef.value?.open()
|
||||
}
|
||||
|
||||
// 暴露给父组件
|
||||
defineExpose({
|
||||
currentTaskID,
|
||||
openHistoryDialog,
|
||||
restoreFromHistory,
|
||||
openSettingsPanel
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -563,12 +133,10 @@ defineExpose({
|
||||
>
|
||||
<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 }"
|
||||
:rows="isFocus ? 3 : 1"
|
||||
placeholder="请输入您的任务"
|
||||
type="textarea"
|
||||
:append-to="taskContainerRef"
|
||||
@@ -576,7 +144,6 @@ defineExpose({
|
||||
@change="agentsStore.setSearchValue"
|
||||
:disabled="!(agentsStore.agents.length > 0)"
|
||||
:debounce="0"
|
||||
:clearable="true"
|
||||
:trigger-on-focus="triggerOnFocus"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@@ -587,33 +154,37 @@ defineExpose({
|
||||
class="task-button"
|
||||
color="linear-gradient(to right, #00C7D2, #315AB4)"
|
||||
size="large"
|
||||
:title="isFillingSteps && !isStopping ? '点击停止生成' : '点击搜索任务'"
|
||||
title="点击搜索任务"
|
||||
circle
|
||||
:loading="agentsStore.agentRawPlan.loading || isStopping"
|
||||
:disabled="!searchValue || isStopping"
|
||||
@click.stop="handleButtonClick"
|
||||
:loading="agentsStore.agentRawPlan.loading"
|
||||
:disabled="!searchValue"
|
||||
@click.stop="handleSearch"
|
||||
>
|
||||
<SvgIcon
|
||||
v-if="!agentsStore.agentRawPlan.loading && !isFillingSteps && !isStopping"
|
||||
v-if="!agentsStore.agentRawPlan.loading"
|
||||
icon-class="paper-plane"
|
||||
size="18px"
|
||||
color="#ffffff"
|
||||
/>
|
||||
<SvgIcon
|
||||
v-if="!agentsStore.agentRawPlan.loading && isFillingSteps && !isStopping"
|
||||
icon-class="stoprunning"
|
||||
size="30px"
|
||||
color="#ffffff"
|
||||
/>
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- <AssignmentButton v-if="planReady" @click="openAgentAllocationDialog" /> -->
|
||||
<!-- 设置按钮 -->
|
||||
<el-button class="setting-button" circle title="设置" @click="openSettingsPanel">
|
||||
<el-icon size="18px"><Setting /></el-icon>
|
||||
</el-button>
|
||||
<!-- 统一设置面板 -->
|
||||
<UnifiedSettingsPanel ref="unifiedSettingsPanelRef" />
|
||||
<!-- 智能体分配 -->
|
||||
<!-- <div class="agent-allocation-entry" @click="handleAgentAllocation" title="智能体分配">
|
||||
<SvgIcon icon-class="agent-change" size="20px" color="var(--color-bg)" />
|
||||
</div> -->
|
||||
|
||||
<!-- 智能体分配弹窗 -->
|
||||
<el-dialog
|
||||
v-model="agentAllocationVisible"
|
||||
width="70%"
|
||||
top="10vh"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="true"
|
||||
:show-close="false"
|
||||
custom-class="agent-allocation-dialog"
|
||||
>
|
||||
<AgentAllocation @close="handleAgentAllocationClose" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
@@ -628,6 +199,7 @@ defineExpose({
|
||||
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;
|
||||
@@ -643,19 +215,6 @@ defineExpose({
|
||||
/* 搜索框展开时的样式 */
|
||||
&.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) {
|
||||
@@ -669,7 +228,6 @@ defineExpose({
|
||||
transition: height 0s ease-in-out;
|
||||
border-top: 1px solid var(--color-border);
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
|
||||
li {
|
||||
height: 45px;
|
||||
@@ -707,11 +265,6 @@ defineExpose({
|
||||
resize: none;
|
||||
color: var(--color-text-taskbar);
|
||||
|
||||
/* 聚焦时的样式 */
|
||||
.expanded & {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
line-height: 1.2;
|
||||
font-size: 18px;
|
||||
@@ -755,20 +308,120 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
// 设置按钮
|
||||
.setting-button {
|
||||
.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);
|
||||
}
|
||||
|
||||
.agent-allocation-entry {
|
||||
position: absolute;
|
||||
right: 70px;
|
||||
top: 28px;
|
||||
transform: translateY(-50%);
|
||||
z-index: 999;
|
||||
background: var(--color-bg-taskbar);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-taskbar);
|
||||
right: 0; /* 顶头 */
|
||||
top: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #00aaff; /* 纯蓝色背景 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 6px rgba(0, 170, 255, 0.35);
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-hover);
|
||||
color: var(--color-text-taskbar);
|
||||
transform: scale(1.08);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
|
||||
// 智能体分配弹窗样式
|
||||
:deep(.agent-allocation-dialog) {
|
||||
.el-dialog {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
|
||||
&__header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-right: 0;
|
||||
|
||||
.el-dialog__title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-title);
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 0;
|
||||
max-height: 70vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__headerbtn {
|
||||
top: 16px;
|
||||
right: 20px;
|
||||
|
||||
.el-dialog__close {
|
||||
color: var(--color-text);
|
||||
font-size: 16px;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue'
|
||||
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'
|
||||
@@ -9,20 +9,6 @@ const porps = defineProps<{
|
||||
agentList: Agent[]
|
||||
}>()
|
||||
|
||||
// 每个智能体的标签状态
|
||||
const tabState = reactive<Record<string, 'duty' | 'info'>>({})
|
||||
|
||||
const getTabState = (agentName: string) => {
|
||||
if (!tabState[agentName]) {
|
||||
tabState[agentName] = 'duty' // 默认显示"当前职责"
|
||||
}
|
||||
return tabState[agentName]
|
||||
}
|
||||
|
||||
const setTabState = (agentName: string, tab: 'duty' | 'info') => {
|
||||
tabState[agentName] = tab
|
||||
}
|
||||
|
||||
const taskProcess = computed(() => {
|
||||
const list = agentsStore.currentTask?.TaskProcess ?? []
|
||||
return list.map(item => ({
|
||||
@@ -42,16 +28,14 @@ const agentsStore = useAgentsStore()
|
||||
: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">
|
||||
<div class="flex-1 text-[14px] textClass flex flex-col items-end justify-end truncate ml-1">
|
||||
<div class="flex items-center justify-end gap-2 w-full">
|
||||
<span
|
||||
class="truncate"
|
||||
:style="
|
||||
@@ -84,84 +68,35 @@ const agentsStore = useAgentsStore()
|
||||
<div class="duty-info">
|
||||
<div class="w-full flex justify-center">
|
||||
<div
|
||||
class="rounded-[9px] text-[12px] py-0.5 px-5 text-center my-2 cursor-pointer transition-colors"
|
||||
:class="
|
||||
getTabState(item.Name) === 'info'
|
||||
? 'bg-[var(--color-tab-bg-active)]'
|
||||
: 'bg-[var(--color-tab-bg)]'
|
||||
"
|
||||
:style="{
|
||||
color:
|
||||
getTabState(item.Name) === 'info'
|
||||
? 'var(--color-tab-text)'
|
||||
: 'var(--color-tab-text-inactive)'
|
||||
}"
|
||||
@click="setTabState(item.Name, 'info')"
|
||||
>
|
||||
信息描述
|
||||
</div>
|
||||
<div
|
||||
class="rounded-[9px] text-[12px] py-0.5 px-5 text-center my-2 cursor-pointer transition-colors"
|
||||
:class="
|
||||
getTabState(item.Name) === 'duty'
|
||||
? 'bg-[var(--color-tab-bg-active)]'
|
||||
: 'bg-[var(--color-tab-bg)]'
|
||||
"
|
||||
:style="{
|
||||
color:
|
||||
getTabState(item.Name) === 'duty'
|
||||
? 'var(--color-tab-text)'
|
||||
: 'var(--color-tab-text-inactive)'
|
||||
}"
|
||||
@click="setTabState(item.Name, 'duty')"
|
||||
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">
|
||||
<!-- 信息描述 -->
|
||||
<template v-if="getTabState(item.Name) === 'info'">
|
||||
<!-- 数据空间 -->
|
||||
<div class="text-[12px]">
|
||||
<span class="text-[var(--color-text)] font-bold">数据空间</span>
|
||||
<div class="text-[var(--color-text-secondary)] mt-1">
|
||||
归属于{{ item.Classification }}数据空间
|
||||
<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 class="h-[1px] w-full bg-[var(--color-border-default)] my-[8px]"></div>
|
||||
<!-- 介绍 -->
|
||||
<div class="text-[12px]">
|
||||
<span class="text-[var(--color-text)] font-bold">介绍</span>
|
||||
<div class="text-[var(--color-text-secondary)] mt-1">{{ item.Profile }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 当前职责 -->
|
||||
<template v-else>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,10 +163,4 @@ const agentsStore = useAgentsStore()
|
||||
right: 0 !important;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
// 标签悬浮样式(仅未激活状态)
|
||||
.duty-info .rounded-\[9px\]:not(.bg-\[var\(--color-tab-bg-active\)\]):hover {
|
||||
background: var(--color-tab-bg-hover) !important;
|
||||
color: var(--color-tab-text-hover) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,38 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElNotification } from 'element-plus'
|
||||
import { pick } from 'lodash'
|
||||
|
||||
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 { useNotification } from '@/composables/useNotification.ts'
|
||||
import AgentRepoList from './AgentRepoList.vue'
|
||||
|
||||
const { error, success } = useNotification()
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
|
||||
// 如果agentsStore.agents不存在就读取默认配置的json文件
|
||||
onMounted(async () => {
|
||||
// 先调用 getAgents,获取用户的智能体配置(后端会自动回退到 default_user)
|
||||
const res = await api.getAgents()
|
||||
if (res && res.user_id) {
|
||||
console.log('[AgentRepo] 获取到 user_id:', res.user_id)
|
||||
if (!agentsStore.agents.length) {
|
||||
const res = await readConfig<Agent[]>('agent.json')
|
||||
agentsStore.setAgents(res)
|
||||
}
|
||||
|
||||
// 使用后端返回的智能体配置(数据库有就用数据库的,没有会用 default_user 的)
|
||||
if (res && res.data && res.data.agents && res.data.agents.length > 0) {
|
||||
agentsStore.setAgents(res.data.agents as Agent[])
|
||||
} else if (!agentsStore.agents.length) {
|
||||
// 只有在完全没有数据时才使用默认 json
|
||||
const defaultAgents = await readConfig<Agent[]>('agent.json')
|
||||
agentsStore.setAgents(defaultAgents)
|
||||
}
|
||||
|
||||
await api.setAgents(
|
||||
agentsStore.agents.map(item => pick(item, ['Name', 'Profile', 'Icon', 'Classification', 'apiUrl', 'apiKey', 'apiModel']))
|
||||
)
|
||||
await api.setAgents(agentsStore.agents.map(item => pick(item, ['Name', 'Profile'])))
|
||||
})
|
||||
|
||||
// 上传agent文件
|
||||
@@ -47,77 +34,52 @@ const handleFileSelect = (event: Event) => {
|
||||
if (input.files && input.files[0]) {
|
||||
const file = input.files[0]
|
||||
readFileContent(file)
|
||||
input.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 验证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 readFileContent = async (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,saveToDb=true 表示写入数据库
|
||||
api
|
||||
.setAgents(processedAgents, true)
|
||||
.then(() => {
|
||||
success('智能体上传成功')
|
||||
})
|
||||
.catch(() => {
|
||||
error('智能体上传失败')
|
||||
})
|
||||
} catch {
|
||||
error('JSON解析错误')
|
||||
reader.onload = async e => {
|
||||
if (!e.target?.result) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const json = JSON.parse(e.target.result?.toString?.() ?? '{}')
|
||||
// 处理 JSON 数据
|
||||
if (Array.isArray(json)) {
|
||||
const isValid = json.every(
|
||||
item =>
|
||||
typeof item.Name === 'string' &&
|
||||
typeof item.Icon === 'string' &&
|
||||
typeof item.Profile === 'string'
|
||||
)
|
||||
if (isValid) {
|
||||
// 处理有效的 JSON 数据
|
||||
agentsStore.setAgents(
|
||||
json.map(item => ({
|
||||
Name: item.Name,
|
||||
Icon: item.Icon.replace(/\.png$/, ''),
|
||||
Profile: item.Profile,
|
||||
Classification: item.Classification
|
||||
}))
|
||||
)
|
||||
await api.setAgents(json.map(item => pick(item, ['Name', 'Profile'])))
|
||||
} else {
|
||||
ElNotification.error({
|
||||
title: '错误',
|
||||
message: 'JSON 格式错误'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.error('JSON is not an array')
|
||||
ElNotification.error({
|
||||
title: '错误',
|
||||
message: 'JSON 格式错误'
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
reader.onerror = () => {
|
||||
error('文件读取错误')
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
@@ -130,24 +92,17 @@ const agentList = computed(() => {
|
||||
data: Agent[]
|
||||
}[] = []
|
||||
const obj: Record<string, Agent[]> = {}
|
||||
|
||||
// 获取当前任务中参与流程的智能体名称列表
|
||||
const selectedAgentNames = agentsStore.currentTask?.AgentSelection ?? []
|
||||
|
||||
if (!agentsStore.agents.length) {
|
||||
return {
|
||||
selected,
|
||||
unselected
|
||||
}
|
||||
}
|
||||
|
||||
for (const agent of agentsStore.agents) {
|
||||
// 如果智能体在当前任务的AgentSelection中,则放入selected数组(置顶显示)
|
||||
if (selectedAgentNames.includes(agent.Name)) {
|
||||
selected.push(agent)
|
||||
continue
|
||||
}
|
||||
// 其他智能体按数据空间分类
|
||||
// if (agentsStore.currentTask?.AgentSelection?.includes(agent.Name)) {
|
||||
// selected.push(agent)
|
||||
// continue
|
||||
// }
|
||||
if (obj[agent.Classification]) {
|
||||
obj[agent.Classification]!.push(agent)
|
||||
} else {
|
||||
|
||||
@@ -1,429 +0,0 @@
|
||||
<template>
|
||||
<div class="history-list">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty v-else-if="plans.length === 0" description="暂无历史任务" />
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<div v-else class="plan-list">
|
||||
<div
|
||||
v-for="plan in plans"
|
||||
:key="plan.id"
|
||||
class="plan-item"
|
||||
:class="{ active: selectedPlanId === plan.id }"
|
||||
@click="restorePlan(plan)"
|
||||
>
|
||||
<div class="plan-info">
|
||||
<div class="plan-goal">
|
||||
<SvgIcon icon-class="XiaoXi" size="20px" />
|
||||
{{ plan.general_goal || '未知任务' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="plan-actions">
|
||||
<el-popover
|
||||
:ref="(el: any) => setPopoverRef(plan.id, el)"
|
||||
placement="bottom-end"
|
||||
:show-arrow="false"
|
||||
trigger="click"
|
||||
popper-class="action-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<button class="more-btn" @click.stop>
|
||||
<SvgIcon icon-class="more" class="more-icon" size="16px" />
|
||||
</button>
|
||||
</template>
|
||||
<div class="action-menu">
|
||||
<div class="action-item" @click="handleAction(plan, 'pin')">
|
||||
<SvgIcon icon-class="ZhiDing" size="14px" />
|
||||
<span>{{ plan.is_pinned ? '取消置顶' : '置顶' }}</span>
|
||||
</div>
|
||||
<div class="action-item" @click="handleAction(plan, 'share')">
|
||||
<SvgIcon icon-class="FenXiang" size="14px" />
|
||||
<span>分享</span>
|
||||
</div>
|
||||
<div class="action-item" @click="handleAction(plan, 'delete')">
|
||||
<SvgIcon icon-class="ShanChu" size="14px" />
|
||||
<span>删除</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<DeleteConfirmDialog
|
||||
v-model="dialogVisible"
|
||||
title="确认删除该任务 ?"
|
||||
content="删除后,该任务无法恢复 !"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
|
||||
<!-- 分享对话框 -->
|
||||
<SharePlanDialog
|
||||
v-model="shareDialogVisible"
|
||||
:plan-id="sharePlanId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog/index.vue'
|
||||
import SharePlanDialog from '@/components/SharePlanDialog/index.vue'
|
||||
import websocket from '@/utils/websocket'
|
||||
|
||||
// 事件定义
|
||||
const emit = defineEmits<{
|
||||
(e: 'restore', plan: PlanInfo): void
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
// 数据类型
|
||||
interface PlanInfo {
|
||||
id: string
|
||||
general_goal: string
|
||||
is_pinned?: boolean
|
||||
task_outline?: any
|
||||
assigned_agents?: any
|
||||
agent_scores?: any
|
||||
agents_info?: any[]
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const plans = ref<PlanInfo[]>([])
|
||||
const loading = ref(false)
|
||||
const restoring = ref(false)
|
||||
const selectedPlanId = ref<string | null>(null)
|
||||
|
||||
// 删除对话框相关
|
||||
const dialogVisible = ref(false)
|
||||
const planToDelete = ref<PlanInfo | null>(null)
|
||||
const deleting = ref(false)
|
||||
|
||||
// 分享对话框相关
|
||||
const shareDialogVisible = ref(false)
|
||||
const sharePlanId = ref<string>('')
|
||||
|
||||
// Popover 引用管理
|
||||
const popoverRefs = new Map<string, any>()
|
||||
const setPopoverRef = (planId: string, el: any) => {
|
||||
if (el) {
|
||||
popoverRefs.set(planId, el)
|
||||
}
|
||||
}
|
||||
|
||||
// 统一处理操作点击
|
||||
const handleAction = (plan: PlanInfo, action: 'pin' | 'share' | 'delete') => {
|
||||
// 关闭 popover
|
||||
const popover = popoverRefs.get(plan.id)
|
||||
if (popover) {
|
||||
popover.hide()
|
||||
}
|
||||
|
||||
// 延迟执行操作,让 popover 有时间关闭
|
||||
setTimeout(() => {
|
||||
if (action === 'pin') {
|
||||
pinPlan(plan)
|
||||
} else if (action === 'share') {
|
||||
openShareDialog(plan)
|
||||
} else if (action === 'delete') {
|
||||
deletePlan(plan)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 打开分享弹窗
|
||||
const openShareDialog = (plan: PlanInfo) => {
|
||||
sharePlanId.value = plan.id
|
||||
shareDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 生成唯一请求ID
|
||||
let requestIdCounter = 0
|
||||
const generateRequestId = () => `ws_req_${Date.now()}_${++requestIdCounter}`
|
||||
|
||||
// WebSocket 监听器引用(用于清理)
|
||||
const historyUpdatedHandler = () => {
|
||||
console.log('📡 收到历史列表更新通知,自动刷新')
|
||||
fetchPlans()
|
||||
}
|
||||
|
||||
// 获取任务列表
|
||||
const fetchPlans = async () => {
|
||||
loading.value = true
|
||||
const reqId = generateRequestId()
|
||||
const userId = localStorage.getItem('user_id')
|
||||
|
||||
try {
|
||||
const result = await websocket.send('get_plans', { id: reqId, user_id: userId })
|
||||
plans.value = (result.data || []) as PlanInfo[]
|
||||
} catch (error) {
|
||||
console.error('获取任务列表失败:', error)
|
||||
ElMessage.error('获取任务列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复任务
|
||||
const restorePlan = async (plan: PlanInfo) => {
|
||||
if (restoring.value) return
|
||||
|
||||
console.log('🔍 [HistoryList] 恢复计划:', plan)
|
||||
console.log('🔍 [HistoryList] plan.id:', plan.id)
|
||||
|
||||
if (!plan.id) {
|
||||
ElMessage.error('任务 ID 为空,无法恢复')
|
||||
return
|
||||
}
|
||||
|
||||
restoring.value = true
|
||||
selectedPlanId.value = plan.id
|
||||
|
||||
const reqId = generateRequestId()
|
||||
|
||||
try {
|
||||
const result = await websocket.send('restore_plan', {
|
||||
id: reqId,
|
||||
data: { plan_id: plan.id }
|
||||
})
|
||||
const planData = (result.data || result) as any
|
||||
|
||||
// 发送恢复事件
|
||||
emit('restore', {
|
||||
...plan,
|
||||
...planData
|
||||
})
|
||||
|
||||
ElMessage.success('任务已恢复')
|
||||
} catch (error) {
|
||||
console.error('恢复任务失败:', error)
|
||||
ElMessage.error('恢复任务失败')
|
||||
} finally {
|
||||
restoring.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 置顶/取消置顶任务
|
||||
const pinPlan = async (plan: PlanInfo) => {
|
||||
const newPinnedState = !plan.is_pinned
|
||||
const reqId = generateRequestId()
|
||||
|
||||
try {
|
||||
await websocket.send('pin_plan', {
|
||||
id: reqId,
|
||||
data: { plan_id: plan.id, is_pinned: newPinnedState }
|
||||
})
|
||||
|
||||
ElMessage.success(newPinnedState ? '置顶成功' : '取消置顶成功')
|
||||
// 成功后会自动通过 history_updated 事件刷新列表
|
||||
} catch (error) {
|
||||
console.error('置顶任务失败:', error)
|
||||
ElMessage.error('置顶失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
const deletePlan = (plan: PlanInfo) => {
|
||||
planToDelete.value = plan
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
const confirmDelete = async () => {
|
||||
if (!planToDelete.value) return
|
||||
|
||||
deleting.value = true
|
||||
const reqId = generateRequestId()
|
||||
|
||||
try {
|
||||
await websocket.send('delete_plan', {
|
||||
id: reqId,
|
||||
data: { plan_id: planToDelete.value.id }
|
||||
})
|
||||
|
||||
dialogVisible.value = false
|
||||
ElMessage.success('删除成功')
|
||||
// 删除成功后会自动通过 history_updated 事件刷新列表
|
||||
} catch (error) {
|
||||
console.error('删除任务失败:', error)
|
||||
ElMessage.error('删除任务失败')
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
fetchPlans()
|
||||
// 监听历史列表更新事件(多标签页实时同步)
|
||||
websocket.on('history_updated', historyUpdatedHandler)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除事件监听
|
||||
websocket.off('history_updated', historyUpdatedHandler)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.history-list {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
color: #909399;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.plan-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.plan-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 8px;
|
||||
background: var(--color-bg-three);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-hover);
|
||||
|
||||
.plan-goal {
|
||||
color: var(--color-text-plan-item-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-bg-hover);
|
||||
border-left: 3px solid var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.plan-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.plan-goal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--color-text-plan-item);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.plan-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
// 更多按钮
|
||||
.more-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
.more-icon {
|
||||
color: var(--color-text-plan-item);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-card-border-three);
|
||||
border-radius: 50%;
|
||||
|
||||
.more-icon {
|
||||
color: var(--color-text-plan-item-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 操作菜单
|
||||
.action-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.el-popover__content) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-plan-item);
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
|
||||
.svg-icon {
|
||||
color: var(--color-text-plan-item);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-card-border-three);
|
||||
color: var(--color-text-plan-item-hover);
|
||||
|
||||
.svg-icon {
|
||||
color: var(--color-text-plan-item-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.action-popover {
|
||||
padding: 0 !important;
|
||||
border-radius: 8px !important;
|
||||
width: 120px !important;
|
||||
min-width: 120px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,6 @@
|
||||
<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: {
|
||||
@@ -19,72 +15,34 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
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 {
|
||||
return adjustColor(color, level * 20)
|
||||
}
|
||||
|
||||
// 获取颜色深两号的函数
|
||||
function getDarkColor(color: string, level: number = 2): string {
|
||||
return adjustColor(color, -level * 20)
|
||||
}
|
||||
|
||||
// 通用的颜色调整函数(提取重复逻辑)
|
||||
function adjustColor(color: string, amount: number): string {
|
||||
if (!color || color.length !== 7 || color[0] !== '#') return color
|
||||
|
||||
const r = Math.min(255, Math.max(0, parseInt(color.substr(1, 2), 16) + amount))
|
||||
const g = Math.min(255, Math.max(0, parseInt(color.substr(3, 2), 16) + amount))
|
||||
const b = Math.min(255, Math.max(0, parseInt(color.substr(5, 2), 16) + amount))
|
||||
const r = parseInt(color.substr(1, 2), 16)
|
||||
const g = parseInt(color.substr(3, 2), 16)
|
||||
const b = parseInt(color.substr(5, 2), 16)
|
||||
|
||||
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b
|
||||
// 增加亮度(浅两号)
|
||||
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')}`
|
||||
}
|
||||
|
||||
// 根据主题模式获取调整后的颜色
|
||||
function getAdjustedColor(color: string, level: number = 2): string {
|
||||
if (isDarkMode()) {
|
||||
return getDarkColor(color, level)
|
||||
} else {
|
||||
return getLightColor(color, level)
|
||||
}
|
||||
.padStart(2, '0')}${Math.round(newB).toString(16).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 处理鼠标进入
|
||||
@@ -97,10 +55,11 @@ 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)
|
||||
}
|
||||
|
||||
// 处理保存编辑
|
||||
@@ -120,12 +79,12 @@ function handleCancel() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="process-card" @click="handleCardClick">
|
||||
<div class="process-card">
|
||||
<div class="process-content">
|
||||
<!-- 显示模式 -->
|
||||
<div class="display-content">
|
||||
<span
|
||||
v-for="process in currentTaskProcess"
|
||||
v-for="process in step.TaskProcess"
|
||||
:key="process.ID"
|
||||
class="process-segment"
|
||||
@mouseenter="handleMouseEnter(process.ID)"
|
||||
@@ -147,32 +106,32 @@ function handleCancel() {
|
||||
<!-- 编辑模式 - 修改为卡片样式 -->
|
||||
<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>
|
||||
<el-input
|
||||
v-model="editValue"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||
placeholder="请输入描述内容"
|
||||
autofocus
|
||||
/>
|
||||
<div class="edit-buttons">
|
||||
<el-button
|
||||
type="success"
|
||||
size="small"
|
||||
icon="Check"
|
||||
@click="handleSave(process.ID)"
|
||||
title="保存"
|
||||
>
|
||||
√
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
icon="Close"
|
||||
@click="handleCancel"
|
||||
title="取消"
|
||||
>
|
||||
×
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -186,7 +145,7 @@ function handleCancel() {
|
||||
border: `1px solid ${getActionTypeDisplay(process.ActionType)?.border}`,
|
||||
backgroundColor:
|
||||
hoverProcessId === process.ID
|
||||
? getAdjustedColor(getActionTypeDisplay(process.ActionType)?.color || '#909399')
|
||||
? getLightColor(getActionTypeDisplay(process.ActionType)?.color || '#909399')
|
||||
: 'transparent'
|
||||
}"
|
||||
@dblclick="handleDblClick(process.ID, process.Description)"
|
||||
@@ -194,28 +153,21 @@ function handleCancel() {
|
||||
{{ process.Description }}
|
||||
</span>
|
||||
|
||||
<span class="separator" v-if="process.Description && !process.Description.endsWith('。')"
|
||||
>。</span
|
||||
>
|
||||
<span class="separator" v-if="!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;
|
||||
@@ -243,6 +195,8 @@ function handleCancel() {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.edit-card {
|
||||
position: relative;
|
||||
background: #f0f2f5;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
@@ -259,7 +213,39 @@ function handleCancel() {
|
||||
border-radius: 4px;
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
color: var(--color-text-taskbar);
|
||||
}
|
||||
}
|
||||
|
||||
.edit-buttons {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.el-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
|
||||
&:first-child {
|
||||
background-color: #67c23a;
|
||||
border-color: #67c23a;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
background-color: #f56c6c;
|
||||
border-color: #f56c6c;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,6 +258,10 @@ function handleCancel() {
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&.hovered {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
<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 > 1, '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>
|
||||
@@ -38,12 +38,11 @@ const data = computed<Data | null>(() => {
|
||||
if (result.NodeId === props.nodeId) {
|
||||
// LogNodeType 为 object直接渲染Content
|
||||
if (result.LogNodeType === 'object') {
|
||||
const data = {
|
||||
return {
|
||||
Description: props.nodeId,
|
||||
Content: sanitize(result.content),
|
||||
LogNodeType: result.LogNodeType
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
if (!result.ActionHistory) {
|
||||
|
||||
@@ -59,7 +59,7 @@ function handlePrev() {
|
||||
<!-- <div>{{ `${displayIndex + 1}/${data.length}` }}</div>
|
||||
<el-button type="primary" size="small" @click="handleNext">下一个</el-button> -->
|
||||
<!-- 关闭 -->
|
||||
<!-- <SvgIcon icon-class="close" size="15px" /> -->
|
||||
<SvgIcon icon-class="close" size="15px" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分割线 -->
|
||||
|
||||
@@ -0,0 +1,861 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { throttle } from 'lodash'
|
||||
import { AnchorLocations, BezierConnector } from '@jsplumb/browser-ui'
|
||||
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import { getActionTypeDisplay, getAgentMapIcon } from '@/layout/components/config.ts'
|
||||
import { type ConnectArg, Jsplumb } from '@/layout/components/Main/TaskTemplate/utils.ts'
|
||||
import variables from '@/styles/variables.module.scss'
|
||||
import { type IRawStepTask, useAgentsStore } from '@/stores'
|
||||
import api from '@/api'
|
||||
import ProcessCard from '../TaskProcess/ProcessCard.vue'
|
||||
import ExecutePlan from './ExecutePlan.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'refreshLine'): void
|
||||
(el: 'setCurrentTask', task: IRawStepTask): void
|
||||
}>()
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
const drawerVisible = ref(false)
|
||||
const collaborationProcess = computed(() => {
|
||||
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
|
||||
})
|
||||
|
||||
// 编辑逻辑
|
||||
const editMode = ref(false) //全局编辑开关
|
||||
const editMap = reactive<Record<string, boolean>>({}) //行级编辑状态
|
||||
const editBuffer = reactive<Record<string, string | undefined>>({}) //临时输入
|
||||
|
||||
function getProcessDescription(stepId: string, processId: string) {
|
||||
const step = collaborationProcess.value.find(s => s.Id === stepId)
|
||||
if (step) {
|
||||
const process = step.TaskProcess.find(p => p.ID === processId)
|
||||
return process?.Description || ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function save() {
|
||||
Object.keys(editMap).forEach(key => {
|
||||
if (editMap[key]) {
|
||||
const [stepId, processId] = key.split('-')
|
||||
const value = editBuffer[key]
|
||||
// 确保 value 是字符串类型
|
||||
if (value !== undefined && value !== null) {
|
||||
// @ts-ignore - TypeScript 无法正确推断类型,但运行时是安全的
|
||||
handleSaveEdit(stepId, processId, value)
|
||||
}
|
||||
}
|
||||
})
|
||||
editMode.value = false
|
||||
}
|
||||
|
||||
function handleOpenEdit(stepId: string, processId: string) {
|
||||
if (!editMode.value) return
|
||||
const key = `${stepId}-${processId}`
|
||||
editMap[key] = true
|
||||
editBuffer[key] = getProcessDescription(stepId, processId)
|
||||
}
|
||||
|
||||
function handleSaveEdit(stepId: string, processId: string, value: string) {
|
||||
const key = `${stepId}-${processId}`
|
||||
const step = collaborationProcess.value.find(s => s.Id === stepId)
|
||||
if (step) {
|
||||
const process = step.TaskProcess.find(p => p.ID === processId)
|
||||
if (process) {
|
||||
process.Description = value
|
||||
}
|
||||
}
|
||||
editMap[key] = false
|
||||
ElMessage.success('已保存(前端内存)')
|
||||
}
|
||||
const jsplumb = new Jsplumb('task-results-main', {
|
||||
connector: {
|
||||
type: BezierConnector.type,
|
||||
options: { curviness: 30, stub: 10 }
|
||||
}
|
||||
})
|
||||
|
||||
// 操作折叠面板时要实时的刷新连线
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
function handleCollapse() {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
timer = setInterval(() => {
|
||||
jsplumb.repaintEverything()
|
||||
emit('refreshLine')
|
||||
}, 1) as ReturnType<typeof setInterval>
|
||||
|
||||
// 默认三秒后已经完全打开
|
||||
const timer1 = setTimeout(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 3000)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
if (timer1) {
|
||||
clearInterval(timer1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 创建内部连线
|
||||
function createInternalLine(id?: string) {
|
||||
const arr: ConnectArg[] = []
|
||||
jsplumb.reset()
|
||||
collaborationProcess.value.forEach(item => {
|
||||
// 创建左侧流程与产出的连线
|
||||
arr.push({
|
||||
sourceId: `task-results-${item.Id}-0`,
|
||||
targetId: `task-results-${item.Id}-1`,
|
||||
anchor: [AnchorLocations.Left, AnchorLocations.Left]
|
||||
})
|
||||
collaborationProcess.value.forEach(jitem => {
|
||||
// 创建左侧产出与上一步流程的连线
|
||||
if (item.InputObject_List!.includes(jitem.OutputObject ?? '')) {
|
||||
arr.push({
|
||||
sourceId: `task-results-${jitem.Id}-1`,
|
||||
targetId: `task-results-${item.Id}-0`,
|
||||
anchor: [AnchorLocations.Left, AnchorLocations.Left],
|
||||
config: {
|
||||
type: 'output'
|
||||
}
|
||||
})
|
||||
}
|
||||
// 创建右侧任务程序与InputObject字段的连线
|
||||
jitem.TaskProcess.forEach(i => {
|
||||
if (i.ImportantInput?.includes(`InputObject:${item.OutputObject}`)) {
|
||||
const color = getActionTypeDisplay(i.ActionType)?.color ?? ''
|
||||
const sourceId = `task-results-${item.Id}-1`
|
||||
const targetId = `task-results-${jitem.Id}-0-${i.ID}`
|
||||
arr.push({
|
||||
sourceId,
|
||||
targetId,
|
||||
anchor: [AnchorLocations.Right, AnchorLocations.Right],
|
||||
config: {
|
||||
stops: [
|
||||
[0, color],
|
||||
[1, color]
|
||||
],
|
||||
transparent: targetId !== id
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 创建右侧TaskProcess内部连线
|
||||
item.TaskProcess?.forEach(i => {
|
||||
if (!i.ImportantInput?.length) {
|
||||
return
|
||||
}
|
||||
item.TaskProcess?.forEach(i2 => {
|
||||
if (i.ImportantInput.includes(`ActionResult:${i2.ID}`)) {
|
||||
const color = getActionTypeDisplay(i.ActionType)?.color ?? ''
|
||||
const sourceId = `task-results-${item.Id}-0-${i2.ID}`
|
||||
const targetId = `task-results-${item.Id}-0-${i.ID}`
|
||||
arr.push({
|
||||
sourceId,
|
||||
targetId,
|
||||
anchor: [AnchorLocations.Right, AnchorLocations.Right],
|
||||
config: {
|
||||
stops: [
|
||||
[0, color],
|
||||
[1, color]
|
||||
],
|
||||
transparent: targetId !== id
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
jsplumb.connects(arr)
|
||||
jsplumb.repaintEverything()
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 额外产物编辑状态
|
||||
const editingOutputId = ref<string | null>(null)
|
||||
const editingOutputContent = ref('')
|
||||
|
||||
// 额外产物内容存储
|
||||
const additionalOutputContents = ref<Record<string, string>>({})
|
||||
|
||||
async function handleRun() {
|
||||
try {
|
||||
loading.value = true
|
||||
const d = await api.executePlan(agentsStore.agentRawPlan.data!)
|
||||
agentsStore.setExecutePlan(d)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看任务过程
|
||||
async function handleTaskProcess() {
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
// 开始编辑额外产物内容
|
||||
function startOutputEditing(output: string) {
|
||||
editingOutputId.value = output
|
||||
editingOutputContent.value = getAdditionalOutputContent(output) || ''
|
||||
}
|
||||
|
||||
// 保存额外产物内容
|
||||
function saveOutputEditing() {
|
||||
if (editingOutputId.value && editingOutputContent.value.trim()) {
|
||||
additionalOutputContents.value[editingOutputId.value] = editingOutputContent.value.trim()
|
||||
}
|
||||
editingOutputId.value = null
|
||||
editingOutputContent.value = ''
|
||||
}
|
||||
|
||||
// 取消编辑额外产物内容
|
||||
function cancelOutputEditing() {
|
||||
editingOutputId.value = null
|
||||
editingOutputContent.value = ''
|
||||
}
|
||||
|
||||
// 获取额外产物内容
|
||||
function getAdditionalOutputContent(output: string) {
|
||||
return additionalOutputContents.value[output] || ''
|
||||
}
|
||||
|
||||
// 处理额外产物的键盘事件
|
||||
function handleOutputKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
saveOutputEditing()
|
||||
} else if (event.key === 'Escape') {
|
||||
editingOutputId.value = null
|
||||
editingOutputContent.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 添加滚动状态标识
|
||||
const isScrolling = ref(false)
|
||||
let scrollTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
// 修改滚动处理函数
|
||||
function handleScroll() {
|
||||
isScrolling.value = true
|
||||
emit('refreshLine')
|
||||
// 清除之前的定时器
|
||||
if (scrollTimer) {
|
||||
clearTimeout(scrollTimer)
|
||||
}
|
||||
jsplumb.repaintEverything()
|
||||
|
||||
// 设置滚动结束检测
|
||||
scrollTimer = setTimeout(() => {
|
||||
isScrolling.value = false
|
||||
}, 300) as ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
// 修改鼠标事件处理函数
|
||||
const handleMouseEnter = throttle(id => {
|
||||
if (!isScrolling.value) {
|
||||
createInternalLine(id)
|
||||
}
|
||||
}, 100)
|
||||
|
||||
const handleMouseLeave = throttle(() => {
|
||||
if (!isScrolling.value) {
|
||||
createInternalLine()
|
||||
}
|
||||
}, 100)
|
||||
|
||||
function clear() {
|
||||
jsplumb.reset()
|
||||
}
|
||||
|
||||
// ========== 按钮交互状态管理 ==========
|
||||
const buttonHoverState = ref(null) // null | 'process' | 'execute'
|
||||
|
||||
const handleProcessMouseEnter = () => {
|
||||
buttonHoverState.value = 'process'
|
||||
}
|
||||
|
||||
const handleExecuteMouseEnter = () => {
|
||||
if (agentsStore.agentRawPlan.data) {
|
||||
buttonHoverState.value = 'execute'
|
||||
}
|
||||
}
|
||||
|
||||
const handleButtonMouseLeave = () => {
|
||||
setTimeout(() => {
|
||||
buttonHoverState.value = null
|
||||
}, 150)
|
||||
}
|
||||
|
||||
// 计算按钮类名
|
||||
const processBtnClass = computed(() => {
|
||||
return buttonHoverState.value === 'process' ? 'ellipse' : 'circle'
|
||||
})
|
||||
|
||||
const executeBtnClass = computed(() => {
|
||||
// 鼠标悬停在过程按钮上时,执行按钮变圆形
|
||||
if (buttonHoverState.value === 'process') {
|
||||
return 'circle'
|
||||
}
|
||||
// 其他情况:如果有任务数据就显示椭圆形,否则显示圆形
|
||||
return agentsStore.agentRawPlan.data ? 'ellipse' : 'circle'
|
||||
})
|
||||
|
||||
// 计算按钮是否显示文字
|
||||
const showProcessText = computed(() => {
|
||||
return buttonHoverState.value === 'process'
|
||||
})
|
||||
|
||||
const showExecuteText = computed(() => {
|
||||
// 鼠标悬停在过程按钮上时,执行按钮不显示文字
|
||||
if (buttonHoverState.value === 'process') return false
|
||||
// 其他情况:如果有任务数据就显示文字,否则不显示
|
||||
return agentsStore.agentRawPlan.data
|
||||
})
|
||||
|
||||
// 计算按钮标题
|
||||
const processBtnTitle = computed(() => {
|
||||
return buttonHoverState.value === 'process' ? '任务过程' : '点击查看任务过程'
|
||||
})
|
||||
|
||||
const executeBtnTitle = computed(() => {
|
||||
return showExecuteText.value ? '任务执行' : '点击运行'
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
createInternalLine,
|
||||
clear
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="h-full flex flex-col relative"
|
||||
id="task-results"
|
||||
:class="{ 'is-running': agentsStore.executePlan.length > 0 }"
|
||||
>
|
||||
<!-- 标题与执行按钮 -->
|
||||
<div class="text-[18px] font-bold mb-[18px] flex justify-between items-center px-[20px]">
|
||||
<span class="text-[var(--color-text-title-header)]">执行结果</span>
|
||||
<div
|
||||
class="flex items-center gap-[14px] task-button-group"
|
||||
@mouseleave="handleButtonMouseLeave"
|
||||
>
|
||||
<!-- 任务过程按钮 -->
|
||||
<el-button
|
||||
:class="processBtnClass"
|
||||
:color="variables.tertiary"
|
||||
:title="processBtnTitle"
|
||||
@mouseenter="handleProcessMouseEnter"
|
||||
@click="handleTaskProcess"
|
||||
>
|
||||
<svg-icon icon-class="process" />
|
||||
<span v-if="showProcessText" class="btn-text">任务过程</span>
|
||||
</el-button>
|
||||
|
||||
<!-- 任务执行按钮 -->
|
||||
<el-popover
|
||||
:disabled="Boolean(agentsStore.agentRawPlan.data)"
|
||||
title="请先输入要执行的任务"
|
||||
:visible="showPopover"
|
||||
@hide="showPopover = false"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button
|
||||
:class="executeBtnClass"
|
||||
:color="variables.tertiary"
|
||||
:title="executeBtnTitle"
|
||||
:disabled="!agentsStore.agentRawPlan.data"
|
||||
@mouseenter="handleExecuteMouseEnter"
|
||||
@click="handleRun"
|
||||
>
|
||||
<svg-icon icon-class="action" />
|
||||
<span v-if="showExecuteText" class="btn-text">任务执行</span>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
title="任务过程"
|
||||
direction="rtl"
|
||||
size="30%"
|
||||
:destroy-on-close="false"
|
||||
>
|
||||
<!-- 头部工具栏 -->
|
||||
<template #header>
|
||||
<div class="drawer-header">
|
||||
<span class="title">任务过程</span>
|
||||
<!-- <el-button v-if="!editMode" text icon="Edit" @click="editMode = true" />
|
||||
<el-button v-else text icon="Check" @click="save" /> -->
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar height="calc(100vh - 120px)">
|
||||
<el-empty v-if="!collaborationProcess.length" description="暂无任务过程" />
|
||||
<div v-else class="process-list">
|
||||
<!-- 使用ProcessCard组件显示每个AgentSelection -->
|
||||
<ProcessCard
|
||||
v-for="step in collaborationProcess"
|
||||
:key="step.Id"
|
||||
:step="step"
|
||||
@open-edit="handleOpenEdit"
|
||||
@save-edit="handleSaveEdit"
|
||||
/>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 内容 -->
|
||||
<div
|
||||
v-loading="agentsStore.agentRawPlan.loading"
|
||||
class="flex-1 overflow-auto relative"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div id="task-results-main" class="px-[40px] relative">
|
||||
<!-- 原有的流程和产物 -->
|
||||
<div v-for="item in collaborationProcess" :key="item.Id" class="card-item">
|
||||
<el-card
|
||||
class="card-item w-full relative"
|
||||
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
|
||||
:shadow="true"
|
||||
:id="`task-results-${item.Id}-0`"
|
||||
@click="emit('setCurrentTask', item)"
|
||||
>
|
||||
<div class="text-[18px] mb-[15px]">{{ item.StepName }}</div>
|
||||
<!-- 折叠面板 -->
|
||||
<el-collapse @change="handleCollapse">
|
||||
<el-collapse-item
|
||||
v-for="item1 in item.TaskProcess"
|
||||
:key="`task-results-${item.Id}-${item1.ID}`"
|
||||
:name="`task-results-${item.Id}-${item1.ID}`"
|
||||
:disabled="Boolean(!agentsStore.executePlan.length || loading)"
|
||||
@mouseenter="() => handleMouseEnter(`task-results-${item.Id}-0-${item1.ID}`)"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<template v-if="loading" #icon>
|
||||
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
|
||||
</template>
|
||||
<template v-else-if="!agentsStore.executePlan.length" #icon>
|
||||
<span></span>
|
||||
</template>
|
||||
<template #title>
|
||||
<!-- 运行之前背景颜色是var(--color-bg-detail-list),运行之后背景颜色是var(--color-bg-detail-list-run) -->
|
||||
<div
|
||||
class="flex items-center gap-[15px] rounded-[20px]"
|
||||
:class="{
|
||||
'bg-[var(--color-bg-detail-list)]': !agentsStore.executePlan.length,
|
||||
'bg-[var(--color-bg-detail-list-run)]': agentsStore.executePlan.length
|
||||
}"
|
||||
>
|
||||
<!-- 右侧链接点 -->
|
||||
<div
|
||||
class="absolute right-0 top-1/2 transform -translate-y-1/2"
|
||||
:id="`task-results-${item.Id}-0-${item1.ID}`"
|
||||
></div>
|
||||
<div
|
||||
class="w-[41px] h-[41px] rounded-full flex items-center justify-center"
|
||||
:style="{ background: getAgentMapIcon(item1.AgentName).color }"
|
||||
>
|
||||
<svg-icon
|
||||
:icon-class="getAgentMapIcon(item1.AgentName).icon"
|
||||
color="#fff"
|
||||
size="24px"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-[16px]">
|
||||
<span>{{ item1.AgentName }}: </span>
|
||||
<span :style="{ color: getActionTypeDisplay(item1.ActionType)?.color }">
|
||||
{{ getActionTypeDisplay(item1.ActionType)?.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<ExecutePlan
|
||||
:action-id="item1.ID"
|
||||
:node-id="item.StepName"
|
||||
:execute-plans="agentsStore.executePlan"
|
||||
/>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
class="card-item w-full relative output-object-card"
|
||||
:shadow="true"
|
||||
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
|
||||
:id="`task-results-${item.Id}-1`"
|
||||
@click="emit('setCurrentTask', item)"
|
||||
>
|
||||
<!-- <div class="text-[18px]">{{ item.OutputObject }}</div>-->
|
||||
<el-collapse @change="handleCollapse">
|
||||
<el-collapse-item
|
||||
class="output-object"
|
||||
:disabled="Boolean(!agentsStore.executePlan.length || loading)"
|
||||
>
|
||||
<template v-if="loading" #icon>
|
||||
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
|
||||
</template>
|
||||
<template v-else-if="!agentsStore.executePlan.length" #icon>
|
||||
<span></span>
|
||||
</template>
|
||||
<template #title>
|
||||
<div class="text-[18px]">{{ item.OutputObject }}</div>
|
||||
</template>
|
||||
<ExecutePlan
|
||||
:node-id="item.OutputObject"
|
||||
:execute-plans="agentsStore.executePlan"
|
||||
/>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 额外产物的编辑卡片 -->
|
||||
<div
|
||||
v-for="(output, index) in agentsStore.additionalOutputs"
|
||||
:key="`additional-output-${index}`"
|
||||
class="card-item"
|
||||
>
|
||||
<!-- 空的流程卡片位置 -->
|
||||
<div class="w-full"></div>
|
||||
|
||||
<!-- 额外产物的编辑卡片 -->
|
||||
<el-card
|
||||
class="card-item w-full relative output-object-card additional-output-card"
|
||||
:shadow="false"
|
||||
:id="`additional-output-results-${index}`"
|
||||
>
|
||||
<!-- 产物名称行 -->
|
||||
<div class="text-[18px] mb-3">
|
||||
{{ output }}
|
||||
</div>
|
||||
|
||||
<!-- 编辑区域行 -->
|
||||
<div class="additional-output-editor">
|
||||
<div v-if="editingOutputId === output" class="w-full">
|
||||
<!-- 编辑状态:输入框 + 按钮 -->
|
||||
<div class="flex flex-col gap-3">
|
||||
<el-input
|
||||
v-model="editingOutputContent"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||
placeholder="请输入产物内容"
|
||||
@keydown="handleOutputKeydown"
|
||||
class="output-editor"
|
||||
size="small"
|
||||
/>
|
||||
<div class="flex justify-end gap-2">
|
||||
<el-button @click="saveOutputEditing" type="primary" size="small" class="px-3">
|
||||
√
|
||||
</el-button>
|
||||
<el-button @click="cancelOutputEditing" size="small" class="px-3">
|
||||
×
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="w-full">
|
||||
<!-- 非编辑状态:折叠区域 + 编辑按钮 -->
|
||||
<div
|
||||
class="flex items-center justify-between p-3 bg-[var(--color-bg-quinary)] rounded-[8px]"
|
||||
>
|
||||
<div
|
||||
class="text-[14px] text-[var(--color-text-secondary)] output-content-display"
|
||||
>
|
||||
{{ getAdditionalOutputContent(output) || '暂无内容,点击编辑' }}
|
||||
</div>
|
||||
<el-button
|
||||
@click="startOutputEditing(output)"
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<svg-icon icon-class="action" size="12px" />
|
||||
<span>编辑</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#task-results.is-running {
|
||||
--color-bg-detail-list: var(--color-bg-detail-list-run); // 直接指向 100 % 版本
|
||||
}
|
||||
#task-results {
|
||||
:deep(.el-collapse) {
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
.el-collapse-item + .el-collapse-item {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.el-collapse-item__header {
|
||||
border: none;
|
||||
background: var(--color-bg-detail-list-run);
|
||||
min-height: 41px;
|
||||
line-height: 41px;
|
||||
border-radius: 20px;
|
||||
transition: border-radius 1ms;
|
||||
position: relative;
|
||||
|
||||
.el-collapse-item__title {
|
||||
background: var(--color-bg-detail-list);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
background: var(--color-bg-icon-rotate);
|
||||
border-radius: 50px;
|
||||
color: #d8d8d8;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.output-object {
|
||||
.el-collapse-item__header {
|
||||
background: none;
|
||||
|
||||
.el-collapse-item__title {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-collapse-item__wrap {
|
||||
background: none;
|
||||
|
||||
.card-item {
|
||||
background: var(--color-bg-detail);
|
||||
padding: 25px;
|
||||
padding-top: 10px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-collapse-item__wrap {
|
||||
border: none;
|
||||
background: var(--color-bg-detail-list);
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-card) {
|
||||
.el-card__body {
|
||||
padding-right: 40px;
|
||||
background-color: var(--color-bg-detail);
|
||||
&:hover {
|
||||
background-color: var(--color-card-bg-result-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.output-object-card {
|
||||
:deep(.el-card__body) {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.active-card {
|
||||
background: linear-gradient(var(--color-bg-tertiary), var(--color-bg-tertiary)) padding-box,
|
||||
linear-gradient(to right, #00c8d2, #315ab4) border-box;
|
||||
}
|
||||
|
||||
.card-item + .card-item {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.additional-output-card {
|
||||
border: 1px dashed #dcdfe6;
|
||||
opacity: 0.9;
|
||||
box-shadow: var(--color-agent-list-hover-shadow);
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
// 编辑区域样式调整
|
||||
.el-collapse {
|
||||
border: none;
|
||||
|
||||
.el-collapse-item {
|
||||
.el-collapse-item__header {
|
||||
background: var(--color-bg-detail);
|
||||
min-height: 36px;
|
||||
line-height: 36px;
|
||||
border-radius: 8px;
|
||||
|
||||
.el-collapse-item__title {
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-collapse-item__wrap {
|
||||
background: var(--color-bg-detail);
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 额外产物编辑区域样式
|
||||
.additional-output-editor {
|
||||
.output-editor {
|
||||
:deep(.el-textarea__inner) {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-bg-detail);
|
||||
border: 1px solid #dcdfe6;
|
||||
resize: none;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.output-content-display {
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
transition: all 0.2s ease;
|
||||
line-height: 1.5;
|
||||
min-height: 20px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-color: #409eff;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑按钮样式
|
||||
.el-button {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
|
||||
&.el-button--small {
|
||||
padding: 4px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 新增:按钮交互样式 ==========
|
||||
.task-button-group {
|
||||
.el-button {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
|
||||
overflow: hidden !important;
|
||||
white-space: nowrap !important;
|
||||
border: none !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
position: relative;
|
||||
background-color: var(--color-bg-tertiary);
|
||||
&:hover {
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
filter: brightness(1.1) !important;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed !important;
|
||||
|
||||
&:hover {
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
filter: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 圆形状态
|
||||
.circle {
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
min-width: 40px !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 50% !important;
|
||||
|
||||
.btn-text {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 椭圆形状态
|
||||
.ellipse {
|
||||
height: 40px !important;
|
||||
border-radius: 20px !important;
|
||||
padding: 0 16px !important;
|
||||
gap: 8px;
|
||||
|
||||
.btn-text {
|
||||
display: inline-block !important;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
opacity: 1;
|
||||
animation: fadeIn 0.3s ease forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||