重构 API 路由并新增工作流编排功能
后端: - 重构 agents, heartbeats, locks, meetings, resources, roles, workflows 路由 - 新增 orchestrator 和 providers 路由 - 新增 CLI 调用器和流程编排服务 - 添加日志配置和依赖项 前端: - 更新 AgentsPage、SettingsPage、WorkflowPage 页面 - 扩展 api.ts 新增 API 接口 其他: - 清理测试 agent 数据文件 - 新增示例工作流和项目审计报告 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "arch-001",
|
|
||||||
"name": "Architect",
|
|
||||||
"role": "architect",
|
|
||||||
"model": "claude-opus-4.6",
|
|
||||||
"description": "Architect - architect",
|
|
||||||
"created_at": "2026-03-09T17:23:06.837127",
|
|
||||||
"status": "idle"
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "arch-001",
|
|
||||||
"current_task": "",
|
|
||||||
"progress": 0,
|
|
||||||
"working_files": [],
|
|
||||||
"last_update": "2026-03-09T17:23:06.852720"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"agent_id": "budget-opencode",
|
||||||
|
"name": "预算管家 (OpenCode)",
|
||||||
|
"role": "budget",
|
||||||
|
"model": "opencode",
|
||||||
|
"description": "用 OpenCode CLI 分析性价比和优惠",
|
||||||
|
"created_at": "2026-03-10T14:10:42.666836",
|
||||||
|
"status": "idle"
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"agent_id": "budget-opencode",
|
||||||
|
"current_task": "",
|
||||||
|
"progress": 0,
|
||||||
|
"working_files": [],
|
||||||
|
"last_update": "2026-03-10T14:10:42.669798"
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"agent_id": "chef-claude",
|
||||||
|
"name": "美食家 (Claude)",
|
||||||
|
"role": "chef",
|
||||||
|
"model": "claude",
|
||||||
|
"description": "用 Claude Code CLI 推荐美食方案",
|
||||||
|
"created_at": "2026-03-10T14:10:42.643341",
|
||||||
|
"status": "idle"
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"agent_id": "chef-claude",
|
||||||
|
"current_task": "",
|
||||||
|
"progress": 0,
|
||||||
|
"working_files": [],
|
||||||
|
"last_update": "2026-03-10T14:10:42.645912"
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "claude-001",
|
|
||||||
"name": "Claude Code",
|
|
||||||
"role": "architect",
|
|
||||||
"model": "claude-opus-4.6",
|
|
||||||
"description": "",
|
|
||||||
"created_at": "2026-03-05T10:17:03.114275",
|
|
||||||
"status": "idle"
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "claude-001",
|
|
||||||
"current_task": "fixing bug",
|
|
||||||
"progress": 68,
|
|
||||||
"working_files": [],
|
|
||||||
"last_update": "2026-03-05T10:17:06.914810"
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "dev-001",
|
|
||||||
"name": "Developer",
|
|
||||||
"role": "developer",
|
|
||||||
"model": "claude-sonnet-4.6",
|
|
||||||
"description": "Developer - developer",
|
|
||||||
"created_at": "2026-03-09T17:23:06.864073",
|
|
||||||
"status": "idle"
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "dev-001",
|
|
||||||
"current_task": "",
|
|
||||||
"progress": 0,
|
|
||||||
"working_files": [],
|
|
||||||
"last_update": "2026-03-09T17:23:06.867216"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"agent_id": "health-kimi",
|
||||||
|
"name": "营养师 (Kimi)",
|
||||||
|
"role": "health",
|
||||||
|
"model": "kimi",
|
||||||
|
"description": "用 Kimi CLI 提供健康饮食建议",
|
||||||
|
"created_at": "2026-03-10T14:10:42.658959",
|
||||||
|
"status": "idle"
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"agent_id": "health-kimi",
|
||||||
|
"current_task": "",
|
||||||
|
"progress": 0,
|
||||||
|
"working_files": [],
|
||||||
|
"last_update": "2026-03-10T14:10:42.661356"
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "kimi-001",
|
|
||||||
"name": "Kimi CLI",
|
|
||||||
"role": "architect",
|
|
||||||
"model": "kimi-k2",
|
|
||||||
"description": "Kimi CLI - architect",
|
|
||||||
"created_at": "2026-03-09T18:23:33.409369",
|
|
||||||
"status": "idle"
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "kimi-001",
|
|
||||||
"current_task": "",
|
|
||||||
"progress": 0,
|
|
||||||
"working_files": [],
|
|
||||||
"last_update": "2026-03-09T18:23:33.413023"
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "kimi-002",
|
|
||||||
"name": "Kimi CLI",
|
|
||||||
"role": "pm",
|
|
||||||
"model": "moonshot-v1-8k",
|
|
||||||
"description": "",
|
|
||||||
"created_at": "2026-03-05T10:17:04.382854",
|
|
||||||
"status": "idle"
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "kimi-002",
|
|
||||||
"current_task": "",
|
|
||||||
"progress": 0,
|
|
||||||
"working_files": [],
|
|
||||||
"last_update": "2026-03-05T10:17:04.387780"
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "opencode-001",
|
|
||||||
"name": "OpenCode",
|
|
||||||
"role": "reviewer",
|
|
||||||
"model": "opencode-v1",
|
|
||||||
"description": "OpenCode - reviewer",
|
|
||||||
"created_at": "2026-03-09T18:23:34.314235",
|
|
||||||
"status": "idle"
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "opencode-001",
|
|
||||||
"current_task": "",
|
|
||||||
"progress": 0,
|
|
||||||
"working_files": [],
|
|
||||||
"last_update": "2026-03-09T18:23:34.317455"
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "qa-001",
|
|
||||||
"name": "QA Engineer",
|
|
||||||
"role": "qa",
|
|
||||||
"model": "claude-haiku-4.6",
|
|
||||||
"description": "QA Engineer - qa",
|
|
||||||
"created_at": "2026-03-09T17:23:06.877677",
|
|
||||||
"status": "idle"
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "qa-001",
|
|
||||||
"current_task": "",
|
|
||||||
"progress": 0,
|
|
||||||
"working_files": [],
|
|
||||||
"last_update": "2026-03-09T17:23:06.880737"
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "test-001",
|
|
||||||
"name": "Test Agent",
|
|
||||||
"role": "developer",
|
|
||||||
"model": "claude-sonnet-4.6",
|
|
||||||
"description": "Test Agent - developer",
|
|
||||||
"created_at": "2026-03-09T17:22:39.234290",
|
|
||||||
"status": "idle"
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "test-001",
|
|
||||||
"current_task": "",
|
|
||||||
"progress": 0,
|
|
||||||
"working_files": [],
|
|
||||||
"last_update": "2026-03-09T17:22:39.236368"
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "test-agent-001",
|
|
||||||
"name": "Test Agent",
|
|
||||||
"role": "developer",
|
|
||||||
"model": "claude-opus-4.6",
|
|
||||||
"description": "测试用的 Agent",
|
|
||||||
"created_at": "2026-03-09T09:28:05.266992",
|
|
||||||
"status": "idle"
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_id": "test-agent-001",
|
|
||||||
"current_task": "修复 bug",
|
|
||||||
"progress": 75,
|
|
||||||
"working_files": [],
|
|
||||||
"last_update": "2026-03-09T09:28:05.280849"
|
|
||||||
}
|
|
||||||
Vendored
+50
-1
@@ -15,7 +15,7 @@
|
|||||||
},
|
},
|
||||||
"agent-001": {
|
"agent-001": {
|
||||||
"agent_id": "agent-001",
|
"agent_id": "agent-001",
|
||||||
"last_heartbeat": "2026-03-09T09:28:05.259883",
|
"last_heartbeat": "2026-03-10T09:46:01.524675",
|
||||||
"status": "working",
|
"status": "working",
|
||||||
"current_task": "测试任务",
|
"current_task": "测试任务",
|
||||||
"progress": 50
|
"progress": 50
|
||||||
@@ -61,5 +61,54 @@
|
|||||||
"status": "idle",
|
"status": "idle",
|
||||||
"current_task": "",
|
"current_task": "",
|
||||||
"progress": 0
|
"progress": 0
|
||||||
|
},
|
||||||
|
"test-api-001": {
|
||||||
|
"agent_id": "test-api-001",
|
||||||
|
"last_heartbeat": "2026-03-10T09:51:56.468836",
|
||||||
|
"status": "idle",
|
||||||
|
"current_task": "",
|
||||||
|
"progress": 100
|
||||||
|
},
|
||||||
|
"chef-001": {
|
||||||
|
"agent_id": "chef-001",
|
||||||
|
"last_heartbeat": "2026-03-10T14:01:11.073277",
|
||||||
|
"status": "idle",
|
||||||
|
"current_task": "",
|
||||||
|
"progress": 100
|
||||||
|
},
|
||||||
|
"health-001": {
|
||||||
|
"agent_id": "health-001",
|
||||||
|
"last_heartbeat": "2026-03-10T14:01:10.054540",
|
||||||
|
"status": "idle",
|
||||||
|
"current_task": "",
|
||||||
|
"progress": 100
|
||||||
|
},
|
||||||
|
"budget-001": {
|
||||||
|
"agent_id": "budget-001",
|
||||||
|
"last_heartbeat": "2026-03-10T14:01:10.399576",
|
||||||
|
"status": "idle",
|
||||||
|
"current_task": "",
|
||||||
|
"progress": 100
|
||||||
|
},
|
||||||
|
"chef-claude": {
|
||||||
|
"agent_id": "chef-claude",
|
||||||
|
"last_heartbeat": "2026-03-10T15:08:32.140273",
|
||||||
|
"status": "idle",
|
||||||
|
"current_task": "",
|
||||||
|
"progress": 100
|
||||||
|
},
|
||||||
|
"health-kimi": {
|
||||||
|
"agent_id": "health-kimi",
|
||||||
|
"last_heartbeat": "2026-03-10T15:06:31.463105",
|
||||||
|
"status": "idle",
|
||||||
|
"current_task": "",
|
||||||
|
"progress": 100
|
||||||
|
},
|
||||||
|
"budget-opencode": {
|
||||||
|
"agent_id": "budget-opencode",
|
||||||
|
"last_heartbeat": "2026-03-10T15:06:51.417016",
|
||||||
|
"status": "idle",
|
||||||
|
"current_task": "",
|
||||||
|
"progress": 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vendored
+19
-6
@@ -35,15 +35,16 @@
|
|||||||
"meeting_id": "test-meeting-001",
|
"meeting_id": "test-meeting-001",
|
||||||
"title": "测试会议",
|
"title": "测试会议",
|
||||||
"expected_attendees": [
|
"expected_attendees": [
|
||||||
"claude-001",
|
"agent-001",
|
||||||
"kimi-001"
|
"agent-002"
|
||||||
],
|
],
|
||||||
"arrived_attendees": [
|
"arrived_attendees": [
|
||||||
"claude-001"
|
"agent-001",
|
||||||
|
"agent-002"
|
||||||
],
|
],
|
||||||
"status": "waiting",
|
"status": "ended",
|
||||||
"created_at": "2026-03-09T18:05:28.657165",
|
"created_at": "2026-03-10T09:46:01.575444",
|
||||||
"started_at": "",
|
"started_at": "2026-03-10T09:46:02.608852",
|
||||||
"min_required": 2
|
"min_required": 2
|
||||||
},
|
},
|
||||||
"meeting-001": {
|
"meeting-001": {
|
||||||
@@ -79,5 +80,17 @@
|
|||||||
"created_at": "2026-03-09T17:23:43.445453",
|
"created_at": "2026-03-09T17:23:43.445453",
|
||||||
"started_at": "2026-03-09T17:23:43.501216",
|
"started_at": "2026-03-09T17:23:43.501216",
|
||||||
"min_required": 3
|
"min_required": 3
|
||||||
|
},
|
||||||
|
"meeting-1773107507": {
|
||||||
|
"meeting_id": "meeting-1773107507",
|
||||||
|
"title": "API 测试会议",
|
||||||
|
"expected_attendees": [
|
||||||
|
"test-api-001"
|
||||||
|
],
|
||||||
|
"arrived_attendees": [],
|
||||||
|
"status": "ended",
|
||||||
|
"created_at": "2026-03-10T09:51:47.354477",
|
||||||
|
"started_at": "",
|
||||||
|
"min_required": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"meeting_id": "dinner-proposals",
|
||||||
|
"title": "晚饭提议",
|
||||||
|
"date": "2026-03-10",
|
||||||
|
"attendees": [
|
||||||
|
"chef-claude",
|
||||||
|
"health-kimi",
|
||||||
|
"budget-opencode"
|
||||||
|
],
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "step_1",
|
||||||
|
"label": "提议",
|
||||||
|
"status": "completed",
|
||||||
|
"completed_at": "2026-03-10T15:05:07.995673"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_2",
|
||||||
|
"label": "讨论",
|
||||||
|
"status": "pending",
|
||||||
|
"completed_at": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_3",
|
||||||
|
"label": "共识",
|
||||||
|
"status": "completed",
|
||||||
|
"completed_at": "2026-03-10T15:05:27.690590"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"discussions": [
|
||||||
|
{
|
||||||
|
"agent_id": "chef-claude",
|
||||||
|
"agent_name": "美食家 (Claude)",
|
||||||
|
"content": "这是 **Swarm Command Center** 中「晚饭提议」工作流的模拟场景。\n\n## 场景说明\n\n在这个场景中,多个 AI Agent 会扮演不同角色(大厨、健康顾问、预算管理员等)来讨论晚餐选择,最终达成共识。\n\n**讨论流程**:\n1. **提议** - 各 Agent 依次发言(2-3句话)\n2. **讨论** - 记录到会议系统\n3. **共识** - 生成最终决议\n\n**角色设定**(`workflow_orchestrator.py:108-141`):\n- **chef**(美食达人)- 关注口味、食材\n- **health**(健康顾问)- 关注营养均衡\n- **budget**(预算管理)- 关注性价比\n- **pm**(产品经理)- 综合决策\n\n---\n\n你想做什么?\n\n1. **启动工作流** - 运行完整的晚饭决策流程\n2. **查看运行状态** - 查看之前的编排结果\n3. **创建/修改工作流定义** - 编辑 `.doc/workflow/dinner-decision.yaml`\n4. **查看现有 Agent 状态** - 确认参与讨论的 Agent 是否已注册",
|
||||||
|
"timestamp": "2026-03-10T15:04:31.968972",
|
||||||
|
"step": "讨论"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent_id": "health-kimi",
|
||||||
|
"agent_name": "营养师 (Kimi)",
|
||||||
|
"content": "建议晚餐采用\"蔬菜占一半、蛋白质占四分之一、主食占四分之一\"的搭配原则,优先选择\n清蒸、炖煮等低油烹饪方式。控制总热量在500-700千卡之间,避免高盐高糖的加工食品\n。如果有外卖计划,请选择卫生评级B级以上的商家。",
|
||||||
|
"timestamp": "2026-03-10T15:04:48.638259",
|
||||||
|
"step": "讨论"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent_id": "budget-opencode",
|
||||||
|
"agent_name": "预算管家 (OpenCode)",
|
||||||
|
"content": "您好!请告诉我您希望我扮演什么角色,以及讨论的具体场景是什么?这样我才能给出合适的观点和建议。",
|
||||||
|
"timestamp": "2026-03-10T15:05:07.977497",
|
||||||
|
"step": "讨论"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": "completed",
|
||||||
|
"created_at": "2026-03-10T15:01:32.839808",
|
||||||
|
"ended_at": "2026-03-10T15:05:27.690584",
|
||||||
|
"consensus": "根据营养师的专业建议和讨论情况,晚餐共识如下:\n\n我们决定晚餐遵循\"蔬菜占一半、蛋白质占四分之一、主食占四分之一\"的健康搭配原则,\n优先采用清蒸、炖煮等低油烹饪方式,将总热量控制在500-700千卡之间。如需外卖,必\n须选择卫生评级B级以上的商家,同时避免高盐高糖的加工食品。请按此标准准备或订购\n今晚的晚餐。"
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# 晚饭提议
|
||||||
|
|
||||||
|
**会议 ID**: dinner-proposals
|
||||||
|
**日期**: 2026-03-10
|
||||||
|
**状态**: completed
|
||||||
|
**参会者**: chef-claude, health-kimi, budget-opencode
|
||||||
|
|
||||||
|
## 会议进度
|
||||||
|
|
||||||
|
- ● **提议** (2026-03-10T15:05:07.995673)
|
||||||
|
- ○ **讨论**
|
||||||
|
- ● **共识** (2026-03-10T15:05:27.690590)
|
||||||
|
|
||||||
|
## 讨论记录
|
||||||
|
|
||||||
|
### 美食家 (Claude) - 2026-03-10T15:04:31
|
||||||
|
*步骤: 讨论*
|
||||||
|
|
||||||
|
这是 **Swarm Command Center** 中「晚饭提议」工作流的模拟场景。
|
||||||
|
|
||||||
|
## 场景说明
|
||||||
|
|
||||||
|
在这个场景中,多个 AI Agent 会扮演不同角色(大厨、健康顾问、预算管理员等)来讨论晚餐选择,最终达成共识。
|
||||||
|
|
||||||
|
**讨论流程**:
|
||||||
|
1. **提议** - 各 Agent 依次发言(2-3句话)
|
||||||
|
2. **讨论** - 记录到会议系统
|
||||||
|
3. **共识** - 生成最终决议
|
||||||
|
|
||||||
|
**角色设定**(`workflow_orchestrator.py:108-141`):
|
||||||
|
- **chef**(美食达人)- 关注口味、食材
|
||||||
|
- **health**(健康顾问)- 关注营养均衡
|
||||||
|
- **budget**(预算管理)- 关注性价比
|
||||||
|
- **pm**(产品经理)- 综合决策
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
你想做什么?
|
||||||
|
|
||||||
|
1. **启动工作流** - 运行完整的晚饭决策流程
|
||||||
|
2. **查看运行状态** - 查看之前的编排结果
|
||||||
|
3. **创建/修改工作流定义** - 编辑 `.doc/workflow/dinner-decision.yaml`
|
||||||
|
4. **查看现有 Agent 状态** - 确认参与讨论的 Agent 是否已注册
|
||||||
|
|
||||||
|
### 营养师 (Kimi) - 2026-03-10T15:04:48
|
||||||
|
*步骤: 讨论*
|
||||||
|
|
||||||
|
建议晚餐采用"蔬菜占一半、蛋白质占四分之一、主食占四分之一"的搭配原则,优先选择
|
||||||
|
清蒸、炖煮等低油烹饪方式。控制总热量在500-700千卡之间,避免高盐高糖的加工食品
|
||||||
|
。如果有外卖计划,请选择卫生评级B级以上的商家。
|
||||||
|
|
||||||
|
### 预算管家 (OpenCode) - 2026-03-10T15:05:07
|
||||||
|
*步骤: 讨论*
|
||||||
|
|
||||||
|
您好!请告诉我您希望我扮演什么角色,以及讨论的具体场景是什么?这样我才能给出合适的观点和建议。
|
||||||
|
|
||||||
|
## 共识
|
||||||
|
|
||||||
|
根据营养师的专业建议和讨论情况,晚餐共识如下:
|
||||||
|
|
||||||
|
我们决定晚餐遵循"蔬菜占一半、蛋白质占四分之一、主食占四分之一"的健康搭配原则,
|
||||||
|
优先采用清蒸、炖煮等低油烹饪方式,将总热量控制在500-700千卡之间。如需外卖,必
|
||||||
|
须选择卫生评级B级以上的商家,同时避免高盐高糖的加工食品。请按此标准准备或订购
|
||||||
|
今晚的晚餐。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**创建时间**: 2026-03-10T15:01:32.839808
|
||||||
|
**结束时间**: 2026-03-10T15:05:27.690584
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"meeting_id": "dinner-vote",
|
||||||
|
"title": "讨论与投票",
|
||||||
|
"date": "2026-03-10",
|
||||||
|
"attendees": [
|
||||||
|
"chef-claude",
|
||||||
|
"health-kimi",
|
||||||
|
"budget-opencode"
|
||||||
|
],
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "step_1",
|
||||||
|
"label": "提议",
|
||||||
|
"status": "completed",
|
||||||
|
"completed_at": "2026-03-10T15:06:51.426835"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_2",
|
||||||
|
"label": "讨论",
|
||||||
|
"status": "pending",
|
||||||
|
"completed_at": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_3",
|
||||||
|
"label": "共识",
|
||||||
|
"status": "completed",
|
||||||
|
"completed_at": "2026-03-10T15:07:36.101402"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"discussions": [
|
||||||
|
{
|
||||||
|
"agent_id": "chef-claude",
|
||||||
|
"agent_name": "美食家 (Claude)",
|
||||||
|
"content": "我无法直接访问 `.doc/` 目录中的会议和工作流数据。请帮我确认一下你的具体需求:\n\n**关于「讨论与投票」场景,你希望:**\n\n1. **创建一个新的会议** - 用于团队讨论和投票某个议题?\n2. **查看当前进行中的会议** - 是否有 ID 或名称?\n3. **在工作流中添加步骤** - 将「讨论与投票」作为工作流的一个阶段?\n4. **模拟/记录讨论内容** - 记录团队成员的讨论发言?\n\n如果是创建会议,请提供:\n- 会议主题/标题\n- 参与讨论的 Agent ID 列表(如:claude-001, kimi-001)\n- (可选)具体要讨论和投票的内容\n\n请告诉我具体想做什么,我来协助你操作。",
|
||||||
|
"timestamp": "2026-03-10T15:06:13.624257",
|
||||||
|
"step": "讨论"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent_id": "health-kimi",
|
||||||
|
"agent_name": "营养师 (Kimi)",
|
||||||
|
"content": "作为营养师,我认可上一轮达成的\"蔬菜半盘、蛋白和主食各四分之一\"的餐盘比例共识,\n这是符合《中国居民膳食指南》的黄金搭配。清蒸和炖煮确实能最大程度保留营养并减少\n油脂摄入,建议大家在点餐或烹饪前先用手机备忘录规划好食材比例,避免临时决策导致\n热量超标。另外提醒一点,外卖酱料包往往藏有隐形盐分,尽量选择独立包装的酱料并只\n使用三分之一。",
|
||||||
|
"timestamp": "2026-03-10T15:06:31.458353",
|
||||||
|
"step": "讨论"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent_id": "budget-opencode",
|
||||||
|
"agent_name": "预算管家 (OpenCode)",
|
||||||
|
"content": "我明白了,但您还没有告诉我需要扮演什么角色以及讨论的场景是什么,请提供更多背景信息。",
|
||||||
|
"timestamp": "2026-03-10T15:06:51.411820",
|
||||||
|
"step": "讨论"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": "completed",
|
||||||
|
"created_at": "2026-03-10T15:05:27.693693",
|
||||||
|
"ended_at": "2026-03-10T15:07:36.101395",
|
||||||
|
"consensus": "根据讨论记录,各参与者对「讨论与投票」场景的理解尚未对齐:美食家询问具体需求方\n向,营养师分享了健康饮食建议,预算管家则表示未收到角色设定。显然讨论缺乏统一的\n场景背景和明确的议题目标。共识是需要先明确角色扮演场景、统一讨论主题,并建立基\n本的沟通框架。下一步行动方案是:由主持人重新定义场景和角色任务,确保所有参与者\n围绕同一议题展开讨论,然后再进行实质性交流和投票决策。"
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# 讨论与投票
|
||||||
|
|
||||||
|
**会议 ID**: dinner-vote
|
||||||
|
**日期**: 2026-03-10
|
||||||
|
**状态**: completed
|
||||||
|
**参会者**: chef-claude, health-kimi, budget-opencode
|
||||||
|
|
||||||
|
## 会议进度
|
||||||
|
|
||||||
|
- ● **提议** (2026-03-10T15:06:51.426835)
|
||||||
|
- ○ **讨论**
|
||||||
|
- ● **共识** (2026-03-10T15:07:36.101402)
|
||||||
|
|
||||||
|
## 讨论记录
|
||||||
|
|
||||||
|
### 美食家 (Claude) - 2026-03-10T15:06:13
|
||||||
|
*步骤: 讨论*
|
||||||
|
|
||||||
|
我无法直接访问 `.doc/` 目录中的会议和工作流数据。请帮我确认一下你的具体需求:
|
||||||
|
|
||||||
|
**关于「讨论与投票」场景,你希望:**
|
||||||
|
|
||||||
|
1. **创建一个新的会议** - 用于团队讨论和投票某个议题?
|
||||||
|
2. **查看当前进行中的会议** - 是否有 ID 或名称?
|
||||||
|
3. **在工作流中添加步骤** - 将「讨论与投票」作为工作流的一个阶段?
|
||||||
|
4. **模拟/记录讨论内容** - 记录团队成员的讨论发言?
|
||||||
|
|
||||||
|
如果是创建会议,请提供:
|
||||||
|
- 会议主题/标题
|
||||||
|
- 参与讨论的 Agent ID 列表(如:claude-001, kimi-001)
|
||||||
|
- (可选)具体要讨论和投票的内容
|
||||||
|
|
||||||
|
请告诉我具体想做什么,我来协助你操作。
|
||||||
|
|
||||||
|
### 营养师 (Kimi) - 2026-03-10T15:06:31
|
||||||
|
*步骤: 讨论*
|
||||||
|
|
||||||
|
作为营养师,我认可上一轮达成的"蔬菜半盘、蛋白和主食各四分之一"的餐盘比例共识,
|
||||||
|
这是符合《中国居民膳食指南》的黄金搭配。清蒸和炖煮确实能最大程度保留营养并减少
|
||||||
|
油脂摄入,建议大家在点餐或烹饪前先用手机备忘录规划好食材比例,避免临时决策导致
|
||||||
|
热量超标。另外提醒一点,外卖酱料包往往藏有隐形盐分,尽量选择独立包装的酱料并只
|
||||||
|
使用三分之一。
|
||||||
|
|
||||||
|
### 预算管家 (OpenCode) - 2026-03-10T15:06:51
|
||||||
|
*步骤: 讨论*
|
||||||
|
|
||||||
|
我明白了,但您还没有告诉我需要扮演什么角色以及讨论的场景是什么,请提供更多背景信息。
|
||||||
|
|
||||||
|
## 共识
|
||||||
|
|
||||||
|
根据讨论记录,各参与者对「讨论与投票」场景的理解尚未对齐:美食家询问具体需求方
|
||||||
|
向,营养师分享了健康饮食建议,预算管家则表示未收到角色设定。显然讨论缺乏统一的
|
||||||
|
场景背景和明确的议题目标。共识是需要先明确角色扮演场景、统一讨论主题,并建立基
|
||||||
|
本的沟通框架。下一步行动方案是:由主持人重新定义场景和角色任务,确保所有参与者
|
||||||
|
围绕同一议题展开讨论,然后再进行实质性交流和投票决策。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**创建时间**: 2026-03-10T15:05:27.693693
|
||||||
|
**结束时间**: 2026-03-10T15:07:36.101395
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"meeting_id": "meeting-1773107507",
|
||||||
|
"title": "API 测试会议",
|
||||||
|
"date": "2026-03-10",
|
||||||
|
"attendees": [
|
||||||
|
"test-api-001"
|
||||||
|
],
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "step_1",
|
||||||
|
"label": "讨论",
|
||||||
|
"status": "completed",
|
||||||
|
"completed_at": "2026-03-10T09:51:52.976775"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_2",
|
||||||
|
"label": "决策",
|
||||||
|
"status": "pending",
|
||||||
|
"completed_at": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_3",
|
||||||
|
"label": "总结",
|
||||||
|
"status": "pending",
|
||||||
|
"completed_at": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"discussions": [
|
||||||
|
{
|
||||||
|
"agent_id": "test-api-001",
|
||||||
|
"agent_name": "Test Agent",
|
||||||
|
"content": "这是一条测试讨论",
|
||||||
|
"timestamp": "2026-03-10T09:51:50.702079",
|
||||||
|
"step": "讨论"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": "completed",
|
||||||
|
"created_at": "2026-03-10T09:51:47.358700",
|
||||||
|
"ended_at": "2026-03-10T09:51:52.976766",
|
||||||
|
"consensus": "测试共识"
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# API 测试会议
|
||||||
|
|
||||||
|
**会议 ID**: meeting-1773107507
|
||||||
|
**日期**: 2026-03-10
|
||||||
|
**状态**: completed
|
||||||
|
**参会者**: test-api-001
|
||||||
|
|
||||||
|
## 会议进度
|
||||||
|
|
||||||
|
- ● **讨论** (2026-03-10T09:51:52.976775)
|
||||||
|
- ○ **决策**
|
||||||
|
- ○ **总结**
|
||||||
|
|
||||||
|
## 讨论记录
|
||||||
|
|
||||||
|
### Test Agent - 2026-03-10T09:51:50
|
||||||
|
*步骤: 讨论*
|
||||||
|
|
||||||
|
这是一条测试讨论
|
||||||
|
|
||||||
|
## 共识
|
||||||
|
|
||||||
|
测试共识
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**创建时间**: 2026-03-10T09:51:47.358700
|
||||||
|
**结束时间**: 2026-03-10T09:51:52.976766
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"meeting_id": "test-record-001",
|
||||||
|
"title": "测试记录会议",
|
||||||
|
"date": "2026-03-10",
|
||||||
|
"attendees": [
|
||||||
|
"agent-001",
|
||||||
|
"agent-002"
|
||||||
|
],
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "step_1",
|
||||||
|
"label": "步骤1",
|
||||||
|
"status": "completed",
|
||||||
|
"completed_at": "2026-03-10T09:46:02.676469"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_2",
|
||||||
|
"label": "步骤2",
|
||||||
|
"status": "pending",
|
||||||
|
"completed_at": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_3",
|
||||||
|
"label": "步骤3",
|
||||||
|
"status": "pending",
|
||||||
|
"completed_at": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"discussions": [
|
||||||
|
{
|
||||||
|
"agent_id": "agent-001",
|
||||||
|
"agent_name": "Agent1",
|
||||||
|
"content": "这是第一条讨论",
|
||||||
|
"timestamp": "2026-03-10T09:46:02.643824",
|
||||||
|
"step": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent_id": "agent-002",
|
||||||
|
"agent_name": "Agent2",
|
||||||
|
"content": "这是第二条讨论",
|
||||||
|
"timestamp": "2026-03-10T09:46:02.654028",
|
||||||
|
"step": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": "completed",
|
||||||
|
"created_at": "2026-03-10T09:46:02.633420",
|
||||||
|
"ended_at": "2026-03-10T09:46:02.676459",
|
||||||
|
"consensus": "达成共识:继续开发"
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# 测试记录会议
|
||||||
|
|
||||||
|
**会议 ID**: test-record-001
|
||||||
|
**日期**: 2026-03-10
|
||||||
|
**状态**: completed
|
||||||
|
**参会者**: agent-001, agent-002
|
||||||
|
|
||||||
|
## 会议进度
|
||||||
|
|
||||||
|
- ● **步骤1** (2026-03-10T09:46:02.676469)
|
||||||
|
- ○ **步骤2**
|
||||||
|
- ○ **步骤3**
|
||||||
|
|
||||||
|
## 讨论记录
|
||||||
|
|
||||||
|
### Agent1 - 2026-03-10T09:46:02
|
||||||
|
|
||||||
|
这是第一条讨论
|
||||||
|
|
||||||
|
### Agent2 - 2026-03-10T09:46:02
|
||||||
|
|
||||||
|
这是第二条讨论
|
||||||
|
|
||||||
|
## 共识
|
||||||
|
|
||||||
|
达成共识:继续开发
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**创建时间**: 2026-03-10T09:46:02.633420
|
||||||
|
**结束时间**: 2026-03-10T09:46:02.676459
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# 晚饭决定工作流
|
||||||
|
# 多个 Agent 通过真实 AI CLI 协作讨论今晚吃什么
|
||||||
|
workflow_id: "dinner-decision"
|
||||||
|
name: "晚饭决定"
|
||||||
|
description: "团队协作决定今晚吃什么,通过 Claude/Kimi/OpenCode CLI 进行真实 AI 讨论"
|
||||||
|
|
||||||
|
meetings:
|
||||||
|
# 1. 提议阶段 - 每个 Agent 用不同 CLI 提出晚饭建议
|
||||||
|
- meeting_id: "dinner-proposals"
|
||||||
|
title: "晚饭提议"
|
||||||
|
node_type: "meeting"
|
||||||
|
attendees: ["chef-claude", "health-kimi", "budget-opencode"]
|
||||||
|
depends_on: []
|
||||||
|
|
||||||
|
# 2. 讨论与投票 - 综合讨论,达成共识
|
||||||
|
- meeting_id: "dinner-vote"
|
||||||
|
title: "讨论与投票"
|
||||||
|
node_type: "meeting"
|
||||||
|
attendees: ["chef-claude", "health-kimi", "budget-opencode"]
|
||||||
|
depends_on: ["dinner-proposals"]
|
||||||
|
|
||||||
|
# 3. 执行 - 确定最终方案
|
||||||
|
- meeting_id: "dinner-order"
|
||||||
|
title: "下单准备"
|
||||||
|
node_type: "execution"
|
||||||
|
attendees: ["chef-claude"]
|
||||||
|
min_required: 1
|
||||||
|
depends_on: ["dinner-vote"]
|
||||||
+7
-1
@@ -3,12 +3,16 @@ Swarm Command Center - FastAPI 主入口
|
|||||||
多智能体协作系统的协调层后端服务
|
多智能体协作系统的协调层后端服务
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s: %(message)s")
|
||||||
|
|
||||||
from app.routers import agents, locks, meetings, heartbeats, workflows, resources, roles, humans
|
from app.routers import agents, locks, meetings, heartbeats, workflows, resources, roles, humans
|
||||||
from app.routers import agents_control, websocket
|
from app.routers import agents_control, websocket, orchestrator, providers
|
||||||
|
|
||||||
# 创建 FastAPI 应用实例
|
# 创建 FastAPI 应用实例
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
@@ -67,6 +71,8 @@ app.include_router(resources.router, prefix="/api", tags=["resources"])
|
|||||||
app.include_router(roles.router, prefix="/api/roles", tags=["roles"])
|
app.include_router(roles.router, prefix="/api/roles", tags=["roles"])
|
||||||
app.include_router(humans.router, prefix="/api/humans", tags=["humans"])
|
app.include_router(humans.router, prefix="/api/humans", tags=["humans"])
|
||||||
app.include_router(websocket.router, tags=["websocket"])
|
app.include_router(websocket.router, tags=["websocket"])
|
||||||
|
app.include_router(orchestrator.router, prefix="/api", tags=["orchestrator"])
|
||||||
|
app.include_router(providers.router, prefix="/api", tags=["providers"])
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
+52
-120
@@ -1,26 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
Agent 管理 API 路由
|
Agent 管理 API 路由
|
||||||
|
接入 AgentRegistry 服务,提供 Agent 注册、查询、状态管理
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
import time
|
from dataclasses import asdict
|
||||||
|
|
||||||
|
from ..services.agent_registry import get_agent_registry
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
# 内存存储,实际应用应该使用持久化存储
|
|
||||||
agents_db = {}
|
|
||||||
|
|
||||||
|
|
||||||
class Agent(BaseModel):
|
|
||||||
agent_id: str
|
|
||||||
name: str
|
|
||||||
role: str
|
|
||||||
model: str
|
|
||||||
description: Optional[str] = None
|
|
||||||
status: str = "idle"
|
|
||||||
created_at: float = 0
|
|
||||||
|
|
||||||
|
|
||||||
class AgentCreate(BaseModel):
|
class AgentCreate(BaseModel):
|
||||||
agent_id: str
|
agent_id: str
|
||||||
@@ -30,138 +20,80 @@ class AgentCreate(BaseModel):
|
|||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
# Agent状态存储
|
class AgentStateUpdate(BaseModel):
|
||||||
agent_states_db = {}
|
task: Optional[str] = ""
|
||||||
|
progress: Optional[int] = 0
|
||||||
|
working_files: Optional[list] = None
|
||||||
|
status: Optional[str] = "idle"
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
async def list_agents():
|
async def list_agents():
|
||||||
"""获取所有 Agent 列表"""
|
"""获取所有 Agent 列表"""
|
||||||
# 合并数据库和默认agent
|
registry = get_agent_registry()
|
||||||
default_agents = [
|
agents = await registry.list_agents()
|
||||||
{
|
return {
|
||||||
"agent_id": "claude-001",
|
"agents": [asdict(agent) for agent in agents]
|
||||||
"name": "Claude Code",
|
}
|
||||||
"role": "developer",
|
|
||||||
"model": "claude-opus-4.6",
|
|
||||||
"status": "working",
|
|
||||||
"description": "主开发 Agent",
|
|
||||||
"created_at": time.time() - 86400
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"agent_id": "kimi-001",
|
|
||||||
"name": "Kimi CLI",
|
|
||||||
"role": "architect",
|
|
||||||
"model": "kimi-k2",
|
|
||||||
"status": "idle",
|
|
||||||
"description": "架构设计 Agent",
|
|
||||||
"created_at": time.time() - 72000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"agent_id": "opencode-001",
|
|
||||||
"name": "OpenCode",
|
|
||||||
"role": "reviewer",
|
|
||||||
"model": "opencode-v1",
|
|
||||||
"status": "idle",
|
|
||||||
"description": "代码审查 Agent",
|
|
||||||
"created_at": time.time() - 36000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# 使用数据库中的agent(覆盖默认的)
|
|
||||||
agents_map = {a["agent_id"]: a for a in default_agents}
|
|
||||||
agents_map.update(agents_db)
|
|
||||||
|
|
||||||
return {"agents": list(agents_map.values())}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/register")
|
@router.post("/register")
|
||||||
async def register_agent(agent: AgentCreate):
|
async def register_agent(agent: AgentCreate):
|
||||||
"""注册新 Agent"""
|
"""注册新 Agent"""
|
||||||
agent_data = {
|
registry = get_agent_registry()
|
||||||
"agent_id": agent.agent_id,
|
agent_info = await registry.register_agent(
|
||||||
"name": agent.name,
|
agent_id=agent.agent_id,
|
||||||
"role": agent.role,
|
name=agent.name,
|
||||||
"model": agent.model,
|
role=agent.role,
|
||||||
"description": agent.description or "",
|
model=agent.model,
|
||||||
"status": "idle",
|
description=agent.description or ""
|
||||||
"created_at": time.time()
|
)
|
||||||
}
|
return asdict(agent_info)
|
||||||
agents_db[agent.agent_id] = agent_data
|
|
||||||
return agent_data
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{agent_id}")
|
@router.get("/{agent_id}")
|
||||||
async def get_agent(agent_id: str):
|
async def get_agent(agent_id: str):
|
||||||
"""获取指定 Agent 信息"""
|
"""获取指定 Agent 信息"""
|
||||||
if agent_id in agents_db:
|
registry = get_agent_registry()
|
||||||
return agents_db[agent_id]
|
agent_info = await registry.get_agent(agent_id)
|
||||||
raise HTTPException(status_code=404, detail="Agent not found")
|
if not agent_info:
|
||||||
|
raise HTTPException(status_code=404, detail="Agent not found")
|
||||||
|
return asdict(agent_info)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{agent_id}")
|
@router.delete("/{agent_id}")
|
||||||
async def delete_agent(agent_id: str):
|
async def delete_agent(agent_id: str):
|
||||||
"""删除 Agent"""
|
"""删除 Agent"""
|
||||||
if agent_id in agents_db:
|
registry = get_agent_registry()
|
||||||
del agents_db[agent_id]
|
success = await registry.unregister_agent(agent_id)
|
||||||
return {"message": "Agent deleted"}
|
if not success:
|
||||||
raise HTTPException(status_code=404, detail="Agent not found")
|
raise HTTPException(status_code=404, detail="Agent not found")
|
||||||
|
return {"message": "Agent deleted"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{agent_id}/state")
|
@router.get("/{agent_id}/state")
|
||||||
async def get_agent_state(agent_id: str):
|
async def get_agent_state(agent_id: str):
|
||||||
"""获取 Agent 状态"""
|
"""获取 Agent 状态"""
|
||||||
# 如果存在真实状态,返回真实状态
|
registry = get_agent_registry()
|
||||||
if agent_id in agent_states_db:
|
state = await registry.get_state(agent_id)
|
||||||
return agent_states_db[agent_id]
|
if not state:
|
||||||
|
raise HTTPException(status_code=404, detail="Agent state not found")
|
||||||
# 默认mock状态
|
return asdict(state)
|
||||||
default_states = {
|
|
||||||
"claude-001": {
|
|
||||||
"agent_id": agent_id,
|
|
||||||
"task": "修复用户登录bug",
|
|
||||||
"progress": 65,
|
|
||||||
"working_files": ["src/auth/login.py", "src/auth/jwt.py"],
|
|
||||||
"status": "working",
|
|
||||||
"last_update": time.time() - 120
|
|
||||||
},
|
|
||||||
"kimi-001": {
|
|
||||||
"agent_id": agent_id,
|
|
||||||
"task": "等待会议开始",
|
|
||||||
"progress": 0,
|
|
||||||
"working_files": [],
|
|
||||||
"status": "waiting",
|
|
||||||
"last_update": time.time() - 300
|
|
||||||
},
|
|
||||||
"opencode-001": {
|
|
||||||
"agent_id": agent_id,
|
|
||||||
"task": "代码审查",
|
|
||||||
"progress": 30,
|
|
||||||
"working_files": ["src/components/Button.tsx"],
|
|
||||||
"status": "working",
|
|
||||||
"last_update": time.time() - 60
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return default_states.get(agent_id, {
|
|
||||||
"agent_id": agent_id,
|
|
||||||
"task": "空闲",
|
|
||||||
"progress": 0,
|
|
||||||
"working_files": [],
|
|
||||||
"status": "idle",
|
|
||||||
"last_update": time.time()
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{agent_id}/state")
|
@router.post("/{agent_id}/state")
|
||||||
async def update_agent_state(agent_id: str, data: dict):
|
async def update_agent_state(agent_id: str, data: AgentStateUpdate):
|
||||||
"""更新 Agent 状态"""
|
"""更新 Agent 状态"""
|
||||||
agent_states_db[agent_id] = {
|
registry = get_agent_registry()
|
||||||
"agent_id": agent_id,
|
agent_info = await registry.get_agent(agent_id)
|
||||||
"task": data.get("task", ""),
|
if not agent_info:
|
||||||
"progress": data.get("progress", 0),
|
raise HTTPException(status_code=404, detail="Agent not found")
|
||||||
"working_files": data.get("working_files", []),
|
|
||||||
"status": data.get("status", "idle"),
|
await registry.update_state(
|
||||||
"last_update": time.time()
|
agent_id=agent_id,
|
||||||
}
|
task=data.task or "",
|
||||||
|
progress=data.progress or 0,
|
||||||
|
working_files=data.working_files
|
||||||
|
)
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|||||||
@@ -1,48 +1,64 @@
|
|||||||
"""
|
"""
|
||||||
心跳管理 API 路由
|
心跳管理 API 路由
|
||||||
|
接入 HeartbeatService 服务,监控 Agent 活跃状态
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Dict
|
from typing import Optional
|
||||||
import time
|
|
||||||
|
from ..services.heartbeat import get_heartbeat_service
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
heartbeats_db = {}
|
|
||||||
|
|
||||||
|
class HeartbeatUpdate(BaseModel):
|
||||||
class Heartbeat(BaseModel):
|
status: str = "idle"
|
||||||
agent_id: str
|
current_task: Optional[str] = ""
|
||||||
timestamp: float
|
progress: Optional[int] = 0
|
||||||
is_timeout: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
async def list_heartbeats():
|
async def list_heartbeats():
|
||||||
"""获取所有 Agent 心跳"""
|
"""获取所有 Agent 心跳"""
|
||||||
return {
|
service = get_heartbeat_service()
|
||||||
"heartbeats": {
|
all_hb = await service.get_all_heartbeats()
|
||||||
"claude-001": {
|
heartbeats = {}
|
||||||
"agent_id": "claude-001",
|
for agent_id, hb in all_hb.items():
|
||||||
"timestamp": time.time() - 30,
|
heartbeats[agent_id] = {
|
||||||
"is_timeout": False
|
"agent_id": hb.agent_id,
|
||||||
},
|
"last_heartbeat": hb.last_heartbeat,
|
||||||
"kimi-001": {
|
"status": hb.status,
|
||||||
"agent_id": "kimi-001",
|
"current_task": hb.current_task,
|
||||||
"timestamp": time.time() - 60,
|
"progress": hb.progress,
|
||||||
"is_timeout": False
|
"elapsed_display": hb.elapsed_display,
|
||||||
}
|
"is_timeout": hb.is_timeout()
|
||||||
}
|
}
|
||||||
}
|
return {"heartbeats": heartbeats}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{agent_id}")
|
@router.post("/{agent_id}")
|
||||||
async def update_heartbeat(agent_id: str):
|
async def update_heartbeat(agent_id: str, data: HeartbeatUpdate = None):
|
||||||
"""更新 Agent 心跳"""
|
"""更新 Agent 心跳"""
|
||||||
heartbeats_db[agent_id] = {
|
service = get_heartbeat_service()
|
||||||
"agent_id": agent_id,
|
if data is None:
|
||||||
"timestamp": time.time(),
|
data = HeartbeatUpdate()
|
||||||
"is_timeout": False
|
await service.update_heartbeat(
|
||||||
}
|
agent_id=agent_id,
|
||||||
|
status=data.status,
|
||||||
|
current_task=data.current_task or "",
|
||||||
|
progress=data.progress or 0
|
||||||
|
)
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/timeouts")
|
||||||
|
async def check_timeouts(timeout_seconds: int = 60):
|
||||||
|
"""检查超时的 Agent"""
|
||||||
|
service = get_heartbeat_service()
|
||||||
|
timeout_agents = await service.check_timeout(timeout_seconds)
|
||||||
|
return {
|
||||||
|
"timeout_seconds": timeout_seconds,
|
||||||
|
"timeout_agents": timeout_agents,
|
||||||
|
"count": len(timeout_agents)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,89 +1,82 @@
|
|||||||
"""
|
"""
|
||||||
文件锁 API 路由
|
文件锁 API 路由
|
||||||
|
接入 FileLockService 服务,管理文件的排他锁
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
import time
|
from dataclasses import asdict
|
||||||
|
|
||||||
|
from ..services.file_lock import get_file_lock_service
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
locks_db = [
|
|
||||||
{
|
|
||||||
"file_path": "src/main.py",
|
|
||||||
"agent_id": "claude-001",
|
|
||||||
"agent_name": "Claude Code",
|
|
||||||
"locked_at": time.time() - 3600
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file_path": "src/utils.py",
|
|
||||||
"agent_id": "kimi-001",
|
|
||||||
"agent_name": "Kimi CLI",
|
|
||||||
"locked_at": time.time() - 1800
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
class LockAcquireRequest(BaseModel):
|
||||||
class FileLock(BaseModel):
|
|
||||||
file_path: str
|
file_path: str
|
||||||
agent_id: str
|
agent_id: str
|
||||||
agent_name: str = ""
|
agent_name: Optional[str] = ""
|
||||||
locked_at: float
|
|
||||||
|
|
||||||
|
|
||||||
def format_elapsed(locked_at: float) -> str:
|
class LockReleaseRequest(BaseModel):
|
||||||
"""格式化已锁定时间"""
|
file_path: str
|
||||||
elapsed = time.time() - locked_at
|
agent_id: str
|
||||||
if elapsed < 60:
|
|
||||||
return f"{int(elapsed)}秒"
|
|
||||||
elif elapsed < 3600:
|
|
||||||
return f"{int(elapsed / 60)}分钟"
|
|
||||||
else:
|
|
||||||
return f"{elapsed / 3600:.1f}小时"
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
async def list_locks():
|
async def list_locks():
|
||||||
"""获取所有文件锁列表"""
|
"""获取所有文件锁列表"""
|
||||||
locks_with_display = []
|
service = get_file_lock_service()
|
||||||
for lock in locks_db:
|
locks = await service.get_locks()
|
||||||
lock_copy = lock.copy()
|
return {
|
||||||
lock_copy["elapsed_display"] = format_elapsed(lock["locked_at"])
|
"locks": [
|
||||||
locks_with_display.append(lock_copy)
|
{
|
||||||
return {"locks": locks_with_display}
|
"file_path": lock.file_path,
|
||||||
|
"agent_id": lock.agent_id,
|
||||||
|
"agent_name": lock.agent_name,
|
||||||
|
"acquired_at": lock.acquired_at,
|
||||||
|
"elapsed_display": lock.elapsed_display
|
||||||
|
}
|
||||||
|
for lock in locks
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/acquire")
|
@router.post("/acquire")
|
||||||
async def acquire_lock(lock: FileLock):
|
async def acquire_lock(request: LockAcquireRequest):
|
||||||
"""获取文件锁"""
|
"""获取文件锁"""
|
||||||
# 检查是否已被锁定
|
service = get_file_lock_service()
|
||||||
for existing in locks_db:
|
success = await service.acquire_lock(
|
||||||
if existing["file_path"] == lock.file_path:
|
file_path=request.file_path,
|
||||||
return {"success": False, "message": "File already locked"}
|
agent_id=request.agent_id,
|
||||||
|
agent_name=request.agent_name or ""
|
||||||
locks_db.append({
|
)
|
||||||
"file_path": lock.file_path,
|
if success:
|
||||||
"agent_id": lock.agent_id,
|
return {"success": True, "message": "Lock acquired"}
|
||||||
"agent_name": lock.agent_name or lock.agent_id,
|
return {"success": False, "message": "File already locked by another agent"}
|
||||||
"locked_at": time.time()
|
|
||||||
})
|
|
||||||
return {"success": True, "message": "Lock acquired"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/release")
|
@router.post("/release")
|
||||||
async def release_lock(data: dict):
|
async def release_lock(request: LockReleaseRequest):
|
||||||
"""释放文件锁"""
|
"""释放文件锁"""
|
||||||
file_path = data.get("file_path", "")
|
service = get_file_lock_service()
|
||||||
agent_id = data.get("agent_id", "")
|
success = await service.release_lock(
|
||||||
global locks_db
|
file_path=request.file_path,
|
||||||
locks_db = [l for l in locks_db if not (l["file_path"] == file_path and l["agent_id"] == agent_id)]
|
agent_id=request.agent_id
|
||||||
return {"success": True, "message": "Lock released"}
|
)
|
||||||
|
if success:
|
||||||
|
return {"success": True, "message": "Lock released"}
|
||||||
|
return {"success": False, "message": "Lock not found or not owned by this agent"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/check")
|
@router.get("/check")
|
||||||
async def check_lock(file_path: str):
|
async def check_lock(file_path: str):
|
||||||
"""检查文件锁定状态"""
|
"""检查文件锁定状态"""
|
||||||
for lock in locks_db:
|
service = get_file_lock_service()
|
||||||
if lock["file_path"] == file_path:
|
locked_by = await service.check_locked(file_path)
|
||||||
return {"file_path": file_path, "locked": True, "locked_by": lock["agent_id"]}
|
return {
|
||||||
return {"file_path": file_path, "locked": False}
|
"file_path": file_path,
|
||||||
|
"locked": locked_by is not None,
|
||||||
|
"locked_by": locked_by
|
||||||
|
}
|
||||||
|
|||||||
+197
-132
@@ -1,195 +1,260 @@
|
|||||||
"""
|
"""
|
||||||
会议管理 API 路由
|
会议管理 API 路由
|
||||||
|
接入 MeetingScheduler(栅栏同步)+ MeetingRecorder(会议记录)
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
from dataclasses import asdict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import time
|
|
||||||
|
from ..services.meeting_scheduler import get_meeting_scheduler
|
||||||
|
from ..services.meeting_recorder import get_meeting_recorder
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
meetings_db = []
|
|
||||||
|
|
||||||
|
|
||||||
class Meeting(BaseModel):
|
|
||||||
meeting_id: str
|
|
||||||
title: str
|
|
||||||
status: str
|
|
||||||
attendees: List[str]
|
|
||||||
agenda: str
|
|
||||||
progress_summary: str
|
|
||||||
created_at: float
|
|
||||||
|
|
||||||
|
|
||||||
class MeetingCreate(BaseModel):
|
class MeetingCreate(BaseModel):
|
||||||
title: str
|
title: str
|
||||||
agenda: str
|
agenda: Optional[str] = ""
|
||||||
meeting_type: str = "design_review"
|
meeting_type: Optional[str] = "design_review"
|
||||||
attendees: List[str] = []
|
attendees: List[str] = []
|
||||||
|
steps: Optional[List[str]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class MeetingWaitRequest(BaseModel):
|
||||||
|
agent_id: str
|
||||||
|
timeout: Optional[int] = 300
|
||||||
|
|
||||||
|
|
||||||
|
class DiscussionRequest(BaseModel):
|
||||||
|
agent_id: str
|
||||||
|
agent_name: Optional[str] = ""
|
||||||
|
content: str
|
||||||
|
step: Optional[str] = ""
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressRequest(BaseModel):
|
||||||
|
step: str
|
||||||
|
|
||||||
|
|
||||||
|
class FinishRequest(BaseModel):
|
||||||
|
consensus: Optional[str] = ""
|
||||||
|
|
||||||
|
|
||||||
|
def _meeting_to_dict(meeting) -> dict:
|
||||||
|
"""将 MeetingInfo 转为前端友好的 dict"""
|
||||||
|
return {
|
||||||
|
"meeting_id": meeting.meeting_id,
|
||||||
|
"title": meeting.title,
|
||||||
|
"date": meeting.date,
|
||||||
|
"status": meeting.status,
|
||||||
|
"attendees": meeting.attendees,
|
||||||
|
"steps": [
|
||||||
|
{"step_id": s.step_id, "label": s.label, "status": s.status}
|
||||||
|
for s in meeting.steps
|
||||||
|
],
|
||||||
|
"discussions": [
|
||||||
|
{
|
||||||
|
"agent_id": d.agent_id,
|
||||||
|
"agent_name": d.agent_name,
|
||||||
|
"content": d.content,
|
||||||
|
"timestamp": d.timestamp,
|
||||||
|
"step": d.step
|
||||||
|
}
|
||||||
|
for d in meeting.discussions
|
||||||
|
],
|
||||||
|
"progress_summary": meeting.progress_summary,
|
||||||
|
"consensus": meeting.consensus,
|
||||||
|
"created_at": meeting.created_at,
|
||||||
|
"ended_at": meeting.ended_at
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
async def list_meetings():
|
async def list_meetings(date: Optional[str] = None):
|
||||||
"""获取所有会议列表"""
|
"""获取会议列表(默认今天)"""
|
||||||
return {
|
recorder = get_meeting_recorder()
|
||||||
"meetings": [
|
meetings = await recorder.list_meetings(date)
|
||||||
{
|
return {"meetings": [_meeting_to_dict(m) for m in meetings]}
|
||||||
"meeting_id": "meeting-001",
|
|
||||||
"title": "架构设计评审",
|
|
||||||
"status": "in_progress",
|
|
||||||
"attendees": ["claude-001", "kimi-001"],
|
|
||||||
"agenda": "讨论系统架构设计",
|
|
||||||
"progress_summary": "50%",
|
|
||||||
"created_at": time.time() - 7200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"meeting_id": "meeting-002",
|
|
||||||
"title": "代码审查会议",
|
|
||||||
"status": "completed",
|
|
||||||
"attendees": ["claude-001"],
|
|
||||||
"agenda": "审查前端组件代码",
|
|
||||||
"progress_summary": "100%",
|
|
||||||
"created_at": time.time() - 86400
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/today")
|
@router.get("/today")
|
||||||
async def list_today_meetings():
|
async def list_today_meetings():
|
||||||
"""获取今日会议"""
|
"""获取今日会议"""
|
||||||
today = datetime.now().strftime("%Y-%m-%d")
|
recorder = get_meeting_recorder()
|
||||||
return {
|
meetings = await recorder.list_meetings()
|
||||||
"meetings": [
|
return {"meetings": [_meeting_to_dict(m) for m in meetings]}
|
||||||
{
|
|
||||||
"meeting_id": "meeting-001",
|
|
||||||
"title": "架构设计评审",
|
|
||||||
"date": today,
|
|
||||||
"status": "in_progress",
|
|
||||||
"attendees": ["claude-001", "kimi-001"],
|
|
||||||
"steps": [
|
|
||||||
{"step_id": "step-1", "label": "收集想法", "status": "completed"},
|
|
||||||
{"step_id": "step-2", "label": "讨论迭代", "status": "active"},
|
|
||||||
{"step_id": "step-3", "label": "生成共识", "status": "pending"}
|
|
||||||
],
|
|
||||||
"discussions": [
|
|
||||||
{
|
|
||||||
"agent_id": "claude-001",
|
|
||||||
"agent_name": "Claude Code",
|
|
||||||
"content": "建议采用微服务架构",
|
|
||||||
"timestamp": datetime.now().isoformat(),
|
|
||||||
"step": "讨论迭代"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"progress_summary": "50%",
|
|
||||||
"consensus": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"meeting_id": "meeting-002",
|
|
||||||
"title": "代码审查会议",
|
|
||||||
"date": today,
|
|
||||||
"status": "completed",
|
|
||||||
"attendees": ["claude-001"],
|
|
||||||
"steps": [
|
|
||||||
{"step_id": "step-1", "label": "代码检查", "status": "completed"},
|
|
||||||
{"step_id": "step-2", "label": "问题讨论", "status": "completed"}
|
|
||||||
],
|
|
||||||
"discussions": [],
|
|
||||||
"progress_summary": "100%",
|
|
||||||
"consensus": "代码质量良好,可以合并"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/")
|
@router.post("/")
|
||||||
async def create_meeting(meeting: MeetingCreate):
|
async def create_meeting(meeting: MeetingCreate):
|
||||||
"""创建新会议"""
|
"""创建新会议(同时创建调度记录和会议记录)"""
|
||||||
meeting_id = f"meeting-{int(time.time())}"
|
recorder = get_meeting_recorder()
|
||||||
meeting_data = {
|
scheduler = get_meeting_scheduler()
|
||||||
"meeting_id": meeting_id,
|
|
||||||
"title": meeting.title,
|
|
||||||
"status": "waiting",
|
|
||||||
"attendees": meeting.attendees,
|
|
||||||
"agenda": meeting.agenda,
|
|
||||||
"progress_summary": "0%",
|
|
||||||
"created_at": time.time()
|
|
||||||
}
|
|
||||||
meetings_db.append(meeting_data)
|
|
||||||
return meeting_data
|
|
||||||
|
|
||||||
|
meeting_id = f"meeting-{int(datetime.now().timestamp())}"
|
||||||
|
|
||||||
@router.get("/{meeting_id}")
|
# 在调度器中创建(用于栅栏同步)
|
||||||
async def get_meeting(meeting_id: str):
|
await scheduler.create_meeting(
|
||||||
"""获取会议详情"""
|
meeting_id=meeting_id,
|
||||||
for meeting in meetings_db:
|
title=meeting.title,
|
||||||
if meeting["meeting_id"] == meeting_id:
|
expected_attendees=meeting.attendees
|
||||||
return meeting
|
)
|
||||||
# 返回模拟数据
|
|
||||||
return {
|
# 在记录器中创建(用于记录内容)
|
||||||
"meeting_id": meeting_id,
|
meeting_info = await recorder.create_meeting(
|
||||||
"title": "测试会议",
|
meeting_id=meeting_id,
|
||||||
"status": "in_progress",
|
title=meeting.title,
|
||||||
"attendees": ["claude-001"],
|
attendees=meeting.attendees,
|
||||||
"agenda": "测试议程",
|
steps=meeting.steps
|
||||||
"progress_summary": "50%",
|
)
|
||||||
"created_at": time.time()
|
|
||||||
}
|
return _meeting_to_dict(meeting_info)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/create")
|
@router.post("/create")
|
||||||
async def create_meeting_api(meeting: MeetingCreate):
|
async def create_meeting_alt(meeting: MeetingCreate):
|
||||||
"""创建会议 API(前端使用的端点)"""
|
"""创建会议 API(前端使用的端点,与 POST / 相同)"""
|
||||||
return await create_meeting(meeting)
|
return await create_meeting(meeting)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{meeting_id}")
|
||||||
|
async def get_meeting(meeting_id: str, date: Optional[str] = None):
|
||||||
|
"""获取会议详情"""
|
||||||
|
recorder = get_meeting_recorder()
|
||||||
|
meeting_info = await recorder.get_meeting(meeting_id, date)
|
||||||
|
if not meeting_info:
|
||||||
|
raise HTTPException(status_code=404, detail="Meeting not found")
|
||||||
|
return _meeting_to_dict(meeting_info)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{meeting_id}/queue")
|
||||||
|
async def get_meeting_queue(meeting_id: str):
|
||||||
|
"""获取会议等待队列"""
|
||||||
|
scheduler = get_meeting_scheduler()
|
||||||
|
queue = await scheduler.get_queue(meeting_id)
|
||||||
|
if not queue:
|
||||||
|
raise HTTPException(status_code=404, detail="Meeting queue not found")
|
||||||
|
return {
|
||||||
|
"meeting_id": queue.meeting_id,
|
||||||
|
"title": queue.title,
|
||||||
|
"status": queue.status,
|
||||||
|
"expected_attendees": queue.expected_attendees,
|
||||||
|
"arrived_attendees": queue.arrived_attendees,
|
||||||
|
"missing_attendees": queue.missing_attendees,
|
||||||
|
"progress": queue.progress,
|
||||||
|
"is_ready": queue.is_ready
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{meeting_id}/wait")
|
||||||
|
async def wait_for_meeting(meeting_id: str, request: MeetingWaitRequest):
|
||||||
|
"""栅栏同步等待(阻塞直到所有参会者到齐或超时)"""
|
||||||
|
scheduler = get_meeting_scheduler()
|
||||||
|
status = await scheduler.wait_for_meeting(
|
||||||
|
agent_id=request.agent_id,
|
||||||
|
meeting_id=meeting_id,
|
||||||
|
timeout=request.timeout or 300
|
||||||
|
)
|
||||||
|
return {"meeting_id": meeting_id, "status": status}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{meeting_id}/end")
|
||||||
|
async def end_meeting(meeting_id: str):
|
||||||
|
"""结束会议(调度层)"""
|
||||||
|
scheduler = get_meeting_scheduler()
|
||||||
|
success = await scheduler.end_meeting(meeting_id)
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=404, detail="Meeting not found")
|
||||||
|
return {"success": True, "meeting_id": meeting_id}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{meeting_id}/join")
|
@router.post("/{meeting_id}/join")
|
||||||
async def join_meeting(meeting_id: str, data: dict):
|
async def join_meeting(meeting_id: str, data: dict):
|
||||||
"""Agent 加入会议"""
|
"""Agent 加入会议"""
|
||||||
agent_id = data.get("agent_id", "")
|
agent_id = data.get("agent_id", "")
|
||||||
|
scheduler = get_meeting_scheduler()
|
||||||
|
await scheduler.add_attendee(meeting_id, agent_id)
|
||||||
return {"success": True, "meeting_id": meeting_id, "agent_id": agent_id}
|
return {"success": True, "meeting_id": meeting_id, "agent_id": agent_id}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{meeting_id}/discuss")
|
@router.post("/{meeting_id}/discuss")
|
||||||
async def add_discussion(meeting_id: str, data: dict):
|
async def add_discussion(meeting_id: str, data: DiscussionRequest):
|
||||||
"""添加讨论内容"""
|
"""添加讨论内容"""
|
||||||
|
recorder = get_meeting_recorder()
|
||||||
|
await recorder.add_discussion(
|
||||||
|
meeting_id=meeting_id,
|
||||||
|
agent_id=data.agent_id,
|
||||||
|
agent_name=data.agent_name or data.agent_id,
|
||||||
|
content=data.content,
|
||||||
|
step=data.step or ""
|
||||||
|
)
|
||||||
return {"success": True, "meeting_id": meeting_id}
|
return {"success": True, "meeting_id": meeting_id}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{meeting_id}/finish")
|
@router.post("/{meeting_id}/finish")
|
||||||
async def finish_meeting(meeting_id: str, data: dict):
|
async def finish_meeting(meeting_id: str, data: FinishRequest):
|
||||||
"""完成会议"""
|
"""完成会议(记录层 - 保存共识并标记完成)"""
|
||||||
|
recorder = get_meeting_recorder()
|
||||||
|
success = await recorder.end_meeting(
|
||||||
|
meeting_id=meeting_id,
|
||||||
|
consensus=data.consensus or ""
|
||||||
|
)
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=404, detail="Meeting not found")
|
||||||
|
|
||||||
|
# 同时结束调度
|
||||||
|
scheduler = get_meeting_scheduler()
|
||||||
|
await scheduler.end_meeting(meeting_id)
|
||||||
|
|
||||||
return {"success": True, "meeting_id": meeting_id}
|
return {"success": True, "meeting_id": meeting_id}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{meeting_id}/progress")
|
@router.post("/{meeting_id}/progress")
|
||||||
async def update_progress(meeting_id: str, data: dict):
|
async def update_progress(meeting_id: str, data: ProgressRequest):
|
||||||
"""更新进度"""
|
"""更新会议进度"""
|
||||||
|
recorder = get_meeting_recorder()
|
||||||
|
await recorder.update_progress(
|
||||||
|
meeting_id=meeting_id,
|
||||||
|
step_label=data.step
|
||||||
|
)
|
||||||
return {"success": True, "meeting_id": meeting_id}
|
return {"success": True, "meeting_id": meeting_id}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/record/create")
|
@router.post("/record/create")
|
||||||
async def create_meeting_record(data: dict):
|
async def create_meeting_record(data: dict):
|
||||||
"""创建会议记录(前端使用的端点)"""
|
"""创建会议记录(前端使用的端点)"""
|
||||||
meeting_id = f"meeting-{int(time.time())}"
|
recorder = get_meeting_recorder()
|
||||||
meeting_data = {
|
meeting_id = data.get("meeting_id", f"meeting-{int(datetime.now().timestamp())}")
|
||||||
"meeting_id": meeting_id,
|
meeting_info = await recorder.create_meeting(
|
||||||
"title": data.get("title", "未命名会议"),
|
meeting_id=meeting_id,
|
||||||
"agenda": data.get("agenda", ""),
|
title=data.get("title", "未命名会议"),
|
||||||
"attendees": data.get("attendees", []),
|
attendees=data.get("attendees", []),
|
||||||
"status": "waiting",
|
steps=data.get("steps", [])
|
||||||
"progress_summary": "0%",
|
)
|
||||||
"steps": data.get("steps", []),
|
|
||||||
"discussions": [],
|
# 同时在调度器中注册
|
||||||
"created_at": time.time()
|
scheduler = get_meeting_scheduler()
|
||||||
}
|
await scheduler.create_meeting(
|
||||||
meetings_db.append(meeting_data)
|
meeting_id=meeting_id,
|
||||||
return meeting_data
|
title=data.get("title", "未命名会议"),
|
||||||
|
expected_attendees=data.get("attendees", [])
|
||||||
|
)
|
||||||
|
|
||||||
|
return _meeting_to_dict(meeting_info)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/record/{meeting_id}/discussion")
|
@router.post("/record/{meeting_id}/discussion")
|
||||||
async def add_meeting_discussion(meeting_id: str, data: dict):
|
async def add_meeting_discussion(meeting_id: str, data: dict):
|
||||||
"""添加会议讨论(前端使用的端点)"""
|
"""添加会议讨论(前端使用的端点)"""
|
||||||
|
recorder = get_meeting_recorder()
|
||||||
|
await recorder.add_discussion(
|
||||||
|
meeting_id=meeting_id,
|
||||||
|
agent_id=data.get("agent_id", ""),
|
||||||
|
agent_name=data.get("agent_name", data.get("agent_id", "")),
|
||||||
|
content=data.get("content", ""),
|
||||||
|
step=data.get("step", "")
|
||||||
|
)
|
||||||
return {"success": True, "meeting_id": meeting_id, "discussion": data}
|
return {"success": True, "meeting_id": meeting_id, "discussion": data}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"""
|
||||||
|
工作流编排器 API
|
||||||
|
|
||||||
|
提供启动自动工作流、查看运行状态的端点
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from ..services.workflow_orchestrator import get_workflow_orchestrator
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/orchestrator", tags=["orchestrator"])
|
||||||
|
|
||||||
|
|
||||||
|
class StartWorkflowRequest(BaseModel):
|
||||||
|
"""启动工作流请求"""
|
||||||
|
workflow_path: str # YAML 文件名,如 dinner-decision.yaml
|
||||||
|
agent_overrides: Optional[Dict[str, str]] = None # agent_id → model 覆盖
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/start")
|
||||||
|
async def start_workflow(request: StartWorkflowRequest):
|
||||||
|
"""启动一个工作流的自动编排(后台异步执行)"""
|
||||||
|
orchestrator = get_workflow_orchestrator()
|
||||||
|
try:
|
||||||
|
run = await orchestrator.start_workflow(
|
||||||
|
workflow_path=request.workflow_path,
|
||||||
|
agent_overrides=request.agent_overrides,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"工作流已启动: {run.workflow_name}",
|
||||||
|
"run_id": run.run_id,
|
||||||
|
"workflow_id": run.workflow_id,
|
||||||
|
}
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise HTTPException(status_code=404, detail=f"工作流文件不存在: {request.workflow_path}")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/runs")
|
||||||
|
async def list_runs():
|
||||||
|
"""列出所有编排运行"""
|
||||||
|
orchestrator = get_workflow_orchestrator()
|
||||||
|
return {"runs": orchestrator.list_runs()}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/runs/{run_id}")
|
||||||
|
async def get_run(run_id: str):
|
||||||
|
"""获取指定运行的详细状态"""
|
||||||
|
orchestrator = get_workflow_orchestrator()
|
||||||
|
run = orchestrator.get_run(run_id)
|
||||||
|
if not run:
|
||||||
|
raise HTTPException(status_code=404, detail=f"运行不存在: {run_id}")
|
||||||
|
return run.to_dict()
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
"""
|
||||||
|
Provider / CLI 检测与配置 API
|
||||||
|
|
||||||
|
提供系统可用的 AI CLI 工具检测和 LLM Provider 状态查询
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from ..services.cli_invoker import detect_available_clis, CLI_REGISTRY
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/providers", tags=["providers"])
|
||||||
|
|
||||||
|
|
||||||
|
# 支持的 CLI 工具元信息
|
||||||
|
CLI_META = {
|
||||||
|
"claude": {
|
||||||
|
"display_name": "Claude Code",
|
||||||
|
"description": "Anthropic Claude CLI",
|
||||||
|
"models": ["claude", "claude-sonnet", "claude-opus"],
|
||||||
|
},
|
||||||
|
"kimi": {
|
||||||
|
"display_name": "Kimi CLI",
|
||||||
|
"description": "Moonshot Kimi CLI",
|
||||||
|
"models": ["kimi", "kimi-k2", "moonshot"],
|
||||||
|
},
|
||||||
|
"opencode": {
|
||||||
|
"display_name": "OpenCode",
|
||||||
|
"description": "OpenCode CLI (支持多种模型)",
|
||||||
|
"models": ["opencode"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 支持的 LLM API Provider
|
||||||
|
API_PROVIDERS = {
|
||||||
|
"anthropic": {
|
||||||
|
"display_name": "Anthropic",
|
||||||
|
"env_key": "ANTHROPIC_API_KEY",
|
||||||
|
"models": ["claude-opus-4.6", "claude-sonnet-4.6", "claude-haiku-4.6"],
|
||||||
|
},
|
||||||
|
"openai": {
|
||||||
|
"display_name": "OpenAI",
|
||||||
|
"env_key": "OPENAI_API_KEY",
|
||||||
|
"models": ["gpt-4o", "gpt-4-turbo", "gpt-3.5-turbo"],
|
||||||
|
},
|
||||||
|
"deepseek": {
|
||||||
|
"display_name": "DeepSeek",
|
||||||
|
"env_key": "DEEPSEEK_API_KEY",
|
||||||
|
"models": ["deepseek-chat", "deepseek-coder"],
|
||||||
|
},
|
||||||
|
"google": {
|
||||||
|
"display_name": "Google Gemini",
|
||||||
|
"env_key": "GOOGLE_API_KEY",
|
||||||
|
"models": ["gemini-2.5-pro", "gemini-2.5-flash"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
async def list_providers():
|
||||||
|
"""
|
||||||
|
列出所有可用的 AI Provider(CLI + API)
|
||||||
|
|
||||||
|
前端用于填充 Agent 注册的模型下拉框和 Settings 的 Provider 配置区
|
||||||
|
"""
|
||||||
|
available_clis = detect_available_clis()
|
||||||
|
|
||||||
|
cli_list = []
|
||||||
|
for name, meta in CLI_META.items():
|
||||||
|
installed = name in available_clis
|
||||||
|
cli_list.append({
|
||||||
|
"id": name,
|
||||||
|
"type": "cli",
|
||||||
|
"display_name": meta["display_name"],
|
||||||
|
"description": meta["description"],
|
||||||
|
"installed": installed,
|
||||||
|
"path": available_clis.get(name, ""),
|
||||||
|
"models": meta["models"],
|
||||||
|
})
|
||||||
|
|
||||||
|
api_list = []
|
||||||
|
for name, meta in API_PROVIDERS.items():
|
||||||
|
has_key = bool(os.environ.get(meta["env_key"]))
|
||||||
|
api_list.append({
|
||||||
|
"id": name,
|
||||||
|
"type": "api",
|
||||||
|
"display_name": meta["display_name"],
|
||||||
|
"env_key": meta["env_key"],
|
||||||
|
"configured": has_key,
|
||||||
|
"models": meta["models"],
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"cli": cli_list,
|
||||||
|
"api": api_list,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/models")
|
||||||
|
async def list_available_models():
|
||||||
|
"""
|
||||||
|
列出当前可用的所有模型(已安装 CLI + 已配置 API Key 的模型)
|
||||||
|
|
||||||
|
前端 Agent 注册弹窗的模型下拉框直接使用此接口
|
||||||
|
"""
|
||||||
|
available_clis = detect_available_clis()
|
||||||
|
models = []
|
||||||
|
|
||||||
|
for name in available_clis:
|
||||||
|
meta = CLI_META.get(name, {})
|
||||||
|
display = meta.get("display_name", name)
|
||||||
|
for model in meta.get("models", [name]):
|
||||||
|
models.append({
|
||||||
|
"value": model,
|
||||||
|
"label": f"{model} ({display})",
|
||||||
|
"provider": name,
|
||||||
|
"type": "cli",
|
||||||
|
})
|
||||||
|
|
||||||
|
for name, meta in API_PROVIDERS.items():
|
||||||
|
if os.environ.get(meta["env_key"]):
|
||||||
|
for model in meta["models"]:
|
||||||
|
models.append({
|
||||||
|
"value": model,
|
||||||
|
"label": f"{model} ({meta['display_name']} API)",
|
||||||
|
"provider": name,
|
||||||
|
"type": "api",
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"models": models}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
资源管理 API 路由
|
资源管理 API 路由
|
||||||
|
接入 ResourceManager 服务,提供声明式任务执行
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import List, Optional
|
from typing import Optional
|
||||||
import time
|
|
||||||
|
from ..services.resource_manager import get_resource_manager
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -21,55 +23,35 @@ class TaskParseRequest(BaseModel):
|
|||||||
|
|
||||||
@router.post("/execute")
|
@router.post("/execute")
|
||||||
async def execute_task(request: TaskRequest):
|
async def execute_task(request: TaskRequest):
|
||||||
"""执行任务"""
|
"""执行任务(自动管理文件锁和心跳)"""
|
||||||
|
manager = get_resource_manager()
|
||||||
|
result = await manager.execute_task(
|
||||||
|
agent_id=request.agent_id,
|
||||||
|
task_description=request.task,
|
||||||
|
timeout=request.timeout or 300
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": result.success,
|
||||||
"message": f"任务 '{request.task}' 已执行",
|
"message": result.message,
|
||||||
"files_locked": ["src/main.py"],
|
"files_locked": result.files_locked,
|
||||||
"duration_seconds": 5.5
|
"duration_seconds": round(result.duration_seconds, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/status")
|
@router.get("/status")
|
||||||
async def get_all_status():
|
async def get_all_status():
|
||||||
"""获取所有 Agent 状态"""
|
"""获取所有 Agent 状态(整合注册、心跳、锁信息)"""
|
||||||
from ..services.agent_registry import get_agent_registry
|
manager = get_resource_manager()
|
||||||
from ..services.heartbeat import get_heartbeat_service
|
statuses = await manager.get_all_status()
|
||||||
|
return {"agents": statuses}
|
||||||
registry = get_agent_registry()
|
|
||||||
heartbeat_service = get_heartbeat_service()
|
|
||||||
|
|
||||||
# 获取所有已注册的 Agent
|
|
||||||
all_agents = await registry.list_agents()
|
|
||||||
agent_map = {a.agent_id: a for a in all_agents}
|
|
||||||
|
|
||||||
# 获取所有心跳
|
|
||||||
heartbeats_data = await heartbeat_service.get_all_heartbeats()
|
|
||||||
|
|
||||||
result = []
|
|
||||||
for agent_id, agent in agent_map.items():
|
|
||||||
heartbeat = heartbeats_data.get(agent_id)
|
|
||||||
result.append({
|
|
||||||
"agent_id": agent_id,
|
|
||||||
"info": {
|
|
||||||
"name": agent.name,
|
|
||||||
"role": agent.role,
|
|
||||||
"model": agent.model
|
|
||||||
},
|
|
||||||
"heartbeat": {
|
|
||||||
"status": heartbeat.status if heartbeat else "offline",
|
|
||||||
"current_task": heartbeat.current_task if heartbeat else "",
|
|
||||||
"progress": heartbeat.progress if heartbeat else 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {"agents": result}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/parse-task")
|
@router.post("/parse-task")
|
||||||
async def parse_task(request: TaskParseRequest):
|
async def parse_task(request: TaskParseRequest):
|
||||||
"""解析任务文件"""
|
"""解析任务中涉及的文件路径"""
|
||||||
|
manager = get_resource_manager()
|
||||||
|
files = await manager.parse_task_files(request.task)
|
||||||
return {
|
return {
|
||||||
"task": request.task,
|
"task": request.task,
|
||||||
"files": ["src/main.py", "src/utils.py"]
|
"files": files
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
角色分配 API 路由
|
角色分配 API 路由
|
||||||
|
接入 RoleAllocator 服务,基于任务分析分配角色
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import List, Dict
|
from typing import List
|
||||||
|
|
||||||
|
from ..services.role_allocator import get_role_allocator
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -20,29 +23,28 @@ class RoleAllocateRequest(BaseModel):
|
|||||||
@router.post("/primary")
|
@router.post("/primary")
|
||||||
async def get_primary_role(request: RoleRequest):
|
async def get_primary_role(request: RoleRequest):
|
||||||
"""获取任务主要角色"""
|
"""获取任务主要角色"""
|
||||||
|
allocator = get_role_allocator()
|
||||||
|
primary = allocator.get_primary_role(request.task)
|
||||||
|
role_scores = allocator._analyze_task_roles(request.task)
|
||||||
return {
|
return {
|
||||||
"task": request.task,
|
"task": request.task,
|
||||||
"primary_role": "developer",
|
"primary_role": primary,
|
||||||
"role_scores": {
|
"role_scores": {k: round(v, 2) for k, v in role_scores.items()}
|
||||||
"developer": 0.8,
|
|
||||||
"architect": 0.6,
|
|
||||||
"qa": 0.4,
|
|
||||||
"pm": 0.2
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/allocate")
|
@router.post("/allocate")
|
||||||
async def allocate_roles(request: RoleAllocateRequest):
|
async def allocate_roles(request: RoleAllocateRequest):
|
||||||
"""分配角色"""
|
"""分配角色"""
|
||||||
allocation = {}
|
allocator = get_role_allocator()
|
||||||
for i, agent in enumerate(request.agents):
|
allocation = await allocator.allocate_roles(
|
||||||
roles = ["developer", "architect", "qa"]
|
task=request.task,
|
||||||
allocation[agent] = roles[i % len(roles)]
|
available_agents=request.agents
|
||||||
|
)
|
||||||
|
primary = allocator.get_primary_role(request.task)
|
||||||
return {
|
return {
|
||||||
"task": request.task,
|
"task": request.task,
|
||||||
"primary_role": "developer",
|
"primary_role": primary,
|
||||||
"allocation": allocation
|
"allocation": allocation
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +52,10 @@ async def allocate_roles(request: RoleAllocateRequest):
|
|||||||
@router.post("/explain")
|
@router.post("/explain")
|
||||||
async def explain_roles(request: RoleAllocateRequest):
|
async def explain_roles(request: RoleAllocateRequest):
|
||||||
"""解释角色分配"""
|
"""解释角色分配"""
|
||||||
return {
|
allocator = get_role_allocator()
|
||||||
"explanation": f"基于任务 '{request.task}' 的分析,推荐了最适合的角色分配方案。"
|
allocation = await allocator.allocate_roles(
|
||||||
}
|
task=request.task,
|
||||||
|
available_agents=request.agents
|
||||||
|
)
|
||||||
|
explanation = allocator.explain_allocation(request.task, allocation)
|
||||||
|
return {"explanation": explanation}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
工作流管理 API 路由
|
工作流管理 API 路由
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException, UploadFile, File
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import List, Optional, Dict, Any
|
from typing import List, Optional, Dict, Any
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -80,6 +80,28 @@ async def list_workflow_files():
|
|||||||
return {"files": files}
|
return {"files": files}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/upload")
|
||||||
|
async def upload_workflow(file: UploadFile = File(...)):
|
||||||
|
"""上传工作流 YAML 文件"""
|
||||||
|
if not file.filename or not file.filename.endswith(('.yaml', '.yml')):
|
||||||
|
raise HTTPException(status_code=400, detail="仅支持 .yaml 或 .yml 文件")
|
||||||
|
|
||||||
|
engine = get_workflow_engine()
|
||||||
|
workflow_dir = Path(engine._storage.base_path) / engine.WORKFLOWS_DIR
|
||||||
|
workflow_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
dest = workflow_dir / file.filename
|
||||||
|
content = await file.read()
|
||||||
|
dest.write_bytes(content)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"已上传 {file.filename}",
|
||||||
|
"path": f"workflow/{file.filename}",
|
||||||
|
"size": len(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/list")
|
@router.get("/list")
|
||||||
async def list_workflows():
|
async def list_workflows():
|
||||||
"""获取已加载的工作流列表"""
|
"""获取已加载的工作流列表"""
|
||||||
|
|||||||
@@ -0,0 +1,279 @@
|
|||||||
|
"""
|
||||||
|
CLI 调用器
|
||||||
|
|
||||||
|
通过子进程调用真实的 AI CLI 工具(Claude Code / Kimi CLI / OpenCode),
|
||||||
|
将 prompt 发送给 CLI 并捕获输出。
|
||||||
|
|
||||||
|
支持的 CLI:
|
||||||
|
- claude: Claude Code CLI(使用 -p 参数发送单轮 prompt)
|
||||||
|
- kimi: Kimi CLI(使用 -p 参数发送单轮 prompt)
|
||||||
|
- opencode: OpenCode CLI
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import shutil
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# CLI 命令映射:model 前缀 → (二进制名, 构造参数的函数)
|
||||||
|
CLI_REGISTRY = {
|
||||||
|
"claude": "claude",
|
||||||
|
"kimi": "kimi",
|
||||||
|
"opencode": "opencode",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CLIResult:
|
||||||
|
"""CLI 调用结果"""
|
||||||
|
content: str
|
||||||
|
cli_name: str
|
||||||
|
exit_code: int
|
||||||
|
latency: float
|
||||||
|
success: bool
|
||||||
|
error: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def detect_available_clis() -> dict:
|
||||||
|
"""检测系统中可用的 CLI 工具"""
|
||||||
|
available = {}
|
||||||
|
for name, binary in CLI_REGISTRY.items():
|
||||||
|
path = shutil.which(binary)
|
||||||
|
if path:
|
||||||
|
available[name] = path
|
||||||
|
return available
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_cli(model: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
根据 agent 的 model 字段判断应使用哪个 CLI
|
||||||
|
|
||||||
|
规则:
|
||||||
|
- 以 "claude" 开头 → claude CLI
|
||||||
|
- 以 "kimi" 开头 → kimi CLI
|
||||||
|
- 以 "opencode" 开头 → opencode CLI
|
||||||
|
- 完全匹配 CLI 名 → 直接使用
|
||||||
|
"""
|
||||||
|
model_lower = model.lower().strip()
|
||||||
|
|
||||||
|
for prefix in CLI_REGISTRY:
|
||||||
|
if model_lower.startswith(prefix):
|
||||||
|
return prefix
|
||||||
|
|
||||||
|
if model_lower in CLI_REGISTRY:
|
||||||
|
return model_lower
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def invoke_cli(
|
||||||
|
cli_name: str,
|
||||||
|
prompt: str,
|
||||||
|
timeout: int = 120,
|
||||||
|
max_tokens: int = 1024,
|
||||||
|
system_prompt: str = "",
|
||||||
|
) -> CLIResult:
|
||||||
|
"""
|
||||||
|
调用指定的 CLI 工具并返回结果
|
||||||
|
|
||||||
|
参数:
|
||||||
|
cli_name: CLI 名称(claude / kimi / opencode)
|
||||||
|
prompt: 要发送的 prompt
|
||||||
|
timeout: 超时秒数
|
||||||
|
max_tokens: 最大 token 数
|
||||||
|
"""
|
||||||
|
binary = CLI_REGISTRY.get(cli_name)
|
||||||
|
if not binary:
|
||||||
|
return CLIResult(
|
||||||
|
content="", cli_name=cli_name, exit_code=-1,
|
||||||
|
latency=0, success=False, error=f"未知 CLI: {cli_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 必须获取完整路径,否则 subprocess 在不同环境下可能找不到
|
||||||
|
full_path = shutil.which(binary)
|
||||||
|
if not full_path:
|
||||||
|
return CLIResult(
|
||||||
|
content="", cli_name=cli_name, exit_code=-1,
|
||||||
|
latency=0, success=False, error=f"CLI 未安装: {binary}"
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd = _build_command(cli_name, prompt, max_tokens, full_path, system_prompt)
|
||||||
|
logger.info(f"调用 CLI [{cli_name}]: {full_path} (prompt 长度={len(prompt)})")
|
||||||
|
|
||||||
|
# Windows 下需要设置 PYTHONIOENCODING 解决 GBK 编码问题
|
||||||
|
env = dict(os.environ)
|
||||||
|
env["PYTHONIOENCODING"] = "utf-8"
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
*cmd,
|
||||||
|
stdin=asyncio.subprocess.PIPE,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 立即关闭 stdin,防止 CLI 阻塞等待输入
|
||||||
|
stdout, stderr = await asyncio.wait_for(
|
||||||
|
proc.communicate(input=b""), timeout=timeout
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
proc.kill()
|
||||||
|
await proc.communicate()
|
||||||
|
return CLIResult(
|
||||||
|
content="", cli_name=cli_name, exit_code=-1,
|
||||||
|
latency=time.time() - start, success=False,
|
||||||
|
error=f"CLI 超时 ({timeout}s)"
|
||||||
|
)
|
||||||
|
|
||||||
|
latency = time.time() - start
|
||||||
|
stdout_text = stdout.decode("utf-8", errors="replace").strip()
|
||||||
|
stderr_text = stderr.decode("utf-8", errors="replace").strip()
|
||||||
|
|
||||||
|
# 过滤掉 OpenCode 的 INFO 日志行和 kimi 的框线
|
||||||
|
stdout_text = _clean_output(cli_name, stdout_text)
|
||||||
|
|
||||||
|
if proc.returncode == 0 and stdout_text:
|
||||||
|
logger.info(f"CLI [{cli_name}] 完成: {latency:.1f}s, {len(stdout_text)} chars")
|
||||||
|
return CLIResult(
|
||||||
|
content=stdout_text,
|
||||||
|
cli_name=cli_name,
|
||||||
|
exit_code=0,
|
||||||
|
latency=round(latency, 2),
|
||||||
|
success=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
error_msg = stderr_text or f"退出码 {proc.returncode}"
|
||||||
|
logger.warning(f"CLI [{cli_name}] 失败: {error_msg}")
|
||||||
|
return CLIResult(
|
||||||
|
content=stdout_text or "",
|
||||||
|
cli_name=cli_name,
|
||||||
|
exit_code=proc.returncode or -1,
|
||||||
|
latency=round(latency, 2),
|
||||||
|
success=False,
|
||||||
|
error=error_msg,
|
||||||
|
)
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
return CLIResult(
|
||||||
|
content="", cli_name=cli_name, exit_code=-1,
|
||||||
|
latency=0, success=False, error=f"找不到命令: {binary}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return CLIResult(
|
||||||
|
content="", cli_name=cli_name, exit_code=-1,
|
||||||
|
latency=time.time() - start, success=False, error=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_command(
|
||||||
|
cli_name: str, prompt: str, max_tokens: int, full_path: str, system_prompt: str = ""
|
||||||
|
) -> list:
|
||||||
|
"""
|
||||||
|
为不同 CLI 构造命令行参数
|
||||||
|
|
||||||
|
使用完整二进制路径确保跨环境兼容
|
||||||
|
"""
|
||||||
|
default_sys = (
|
||||||
|
"这是一个角色扮演讨论场景,不是编程任务。"
|
||||||
|
"请直接用中文回答,不要使用任何工具、不要读取文件、不要执行代码。"
|
||||||
|
"直接给出你作为角色的观点和建议,2-3句话即可。"
|
||||||
|
)
|
||||||
|
sys_prompt = system_prompt or default_sys
|
||||||
|
|
||||||
|
if cli_name == "claude":
|
||||||
|
return [
|
||||||
|
full_path,
|
||||||
|
"-p", prompt,
|
||||||
|
"--output-format", "text",
|
||||||
|
"--system-prompt", sys_prompt,
|
||||||
|
]
|
||||||
|
elif cli_name == "kimi":
|
||||||
|
return [
|
||||||
|
full_path,
|
||||||
|
"-p", f"{sys_prompt}\n\n{prompt}",
|
||||||
|
]
|
||||||
|
elif cli_name == "opencode":
|
||||||
|
return [
|
||||||
|
full_path,
|
||||||
|
"run", f"{sys_prompt}\n\n{prompt}",
|
||||||
|
"--model", "opencode/minimax-m2.5-free",
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
return [full_path, "-p", prompt]
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_output(cli_name: str, text: str) -> str:
|
||||||
|
"""清理 CLI 输出中的框线、日志、prompt 回显等噪音"""
|
||||||
|
if cli_name == "kimi":
|
||||||
|
return _clean_kimi_output(text)
|
||||||
|
|
||||||
|
lines = text.splitlines()
|
||||||
|
cleaned = []
|
||||||
|
for line in lines:
|
||||||
|
if line.strip().startswith("INFO "):
|
||||||
|
continue
|
||||||
|
cleaned.append(line)
|
||||||
|
result = "\n".join(cleaned).strip()
|
||||||
|
return result if result else text.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_kimi_output(text: str) -> str:
|
||||||
|
"""
|
||||||
|
Kimi CLI 输出格式:
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ (prompt 回显) │
|
||||||
|
└─────────────────────┘
|
||||||
|
• 思考过程...
|
||||||
|
• 实际回复内容
|
||||||
|
|
||||||
|
需要:1) 移除框线和框内的 prompt 回显
|
||||||
|
2) 只保留最后一个 bullet 作为实际回复
|
||||||
|
"""
|
||||||
|
lines = text.splitlines()
|
||||||
|
|
||||||
|
# 找到框线结束位置(最后一个 └ 或 ╰ 行)
|
||||||
|
box_end = -1
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped and stripped[0] in "└╰" and all(
|
||||||
|
c in "└┘─╰╯ " for c in stripped
|
||||||
|
):
|
||||||
|
box_end = i
|
||||||
|
|
||||||
|
# 跳过框线区域
|
||||||
|
content_lines = lines[box_end + 1:] if box_end >= 0 else lines
|
||||||
|
|
||||||
|
# Kimi 用 • 输出思考过程和最终回复,最后一个 • 块通常是实际回复
|
||||||
|
bullets = []
|
||||||
|
current_bullet = []
|
||||||
|
for line in content_lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped:
|
||||||
|
if current_bullet:
|
||||||
|
current_bullet.append(line)
|
||||||
|
continue
|
||||||
|
if stripped.startswith("• ") or stripped.startswith("? "):
|
||||||
|
if current_bullet:
|
||||||
|
bullets.append("\n".join(current_bullet))
|
||||||
|
current_bullet = [stripped.lstrip("•? ").strip()]
|
||||||
|
elif current_bullet:
|
||||||
|
current_bullet.append(stripped)
|
||||||
|
|
||||||
|
if current_bullet:
|
||||||
|
bullets.append("\n".join(current_bullet))
|
||||||
|
|
||||||
|
if not bullets:
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
# 最后一个 bullet 是实际回复
|
||||||
|
return bullets[-1].strip()
|
||||||
@@ -0,0 +1,572 @@
|
|||||||
|
"""
|
||||||
|
工作流编排器
|
||||||
|
|
||||||
|
串联 WorkflowEngine + MeetingRecorder + LLM Service,
|
||||||
|
自动驱动整个工作流:加载 → 逐节点执行 → 记录讨论 → 达成共识。
|
||||||
|
|
||||||
|
支持两种模式:
|
||||||
|
- 有 LLM API Key:调用真实模型生成讨论内容
|
||||||
|
- 无 API Key:使用内置模拟,仍然走完整流程
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from typing import Dict, List, Optional, Any
|
||||||
|
from dataclasses import dataclass, field, asdict
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from .workflow_engine import get_workflow_engine, WorkflowMeeting
|
||||||
|
from .meeting_recorder import get_meeting_recorder
|
||||||
|
from .agent_registry import get_agent_registry, AgentInfo
|
||||||
|
from .heartbeat import get_heartbeat_service
|
||||||
|
from .llm_service import get_llm_service, LLMMessage, LLMResponse, LLMConfig
|
||||||
|
from .cli_invoker import resolve_cli, invoke_cli, detect_available_clis
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OrchestratorStatus(str, Enum):
|
||||||
|
IDLE = "idle"
|
||||||
|
RUNNING = "running"
|
||||||
|
PAUSED = "paused"
|
||||||
|
COMPLETED = "completed"
|
||||||
|
FAILED = "failed"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AgentTurn:
|
||||||
|
"""一次 Agent 发言记录"""
|
||||||
|
agent_id: str
|
||||||
|
agent_name: str
|
||||||
|
role: str
|
||||||
|
model: str
|
||||||
|
content: str
|
||||||
|
timestamp: float
|
||||||
|
latency: float = 0.0
|
||||||
|
tokens_used: int = 0
|
||||||
|
is_mock: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MeetingResult:
|
||||||
|
"""一次会议的完整结果"""
|
||||||
|
meeting_id: str
|
||||||
|
title: str
|
||||||
|
node_type: str
|
||||||
|
turns: List[AgentTurn] = field(default_factory=list)
|
||||||
|
consensus: str = ""
|
||||||
|
started_at: float = 0
|
||||||
|
finished_at: float = 0
|
||||||
|
status: str = "pending"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OrchestrationRun:
|
||||||
|
"""一次编排运行的完整状态"""
|
||||||
|
run_id: str
|
||||||
|
workflow_id: str
|
||||||
|
workflow_name: str
|
||||||
|
status: OrchestratorStatus = OrchestratorStatus.IDLE
|
||||||
|
current_node: str = ""
|
||||||
|
meeting_results: List[MeetingResult] = field(default_factory=list)
|
||||||
|
started_at: float = 0
|
||||||
|
finished_at: float = 0
|
||||||
|
error: str = ""
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict:
|
||||||
|
return {
|
||||||
|
"run_id": self.run_id,
|
||||||
|
"workflow_id": self.workflow_id,
|
||||||
|
"workflow_name": self.workflow_name,
|
||||||
|
"status": self.status.value,
|
||||||
|
"current_node": self.current_node,
|
||||||
|
"meeting_results": [
|
||||||
|
{
|
||||||
|
"meeting_id": mr.meeting_id,
|
||||||
|
"title": mr.title,
|
||||||
|
"node_type": mr.node_type,
|
||||||
|
"status": mr.status,
|
||||||
|
"turns": [asdict(t) for t in mr.turns],
|
||||||
|
"consensus": mr.consensus,
|
||||||
|
"started_at": mr.started_at,
|
||||||
|
"finished_at": mr.finished_at,
|
||||||
|
}
|
||||||
|
for mr in self.meeting_results
|
||||||
|
],
|
||||||
|
"started_at": self.started_at,
|
||||||
|
"finished_at": self.finished_at,
|
||||||
|
"error": self.error,
|
||||||
|
"elapsed_seconds": round(
|
||||||
|
(self.finished_at or time.time()) - self.started_at, 1
|
||||||
|
) if self.started_at else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# 每个角色在不同会议主题下的 system prompt
|
||||||
|
DINNER_ROLE_PROMPTS = {
|
||||||
|
"chef": (
|
||||||
|
"你是团队的美食达人/大厨。你对各种菜系了如指掌,"
|
||||||
|
"关注口味、食材新鲜度和烹饪方式。请用简短生动的语言(2-3句话)表达你的观点。"
|
||||||
|
),
|
||||||
|
"health": (
|
||||||
|
"你是团队的健康顾问/营养师。你关注饮食均衡、热量控制和食品安全。"
|
||||||
|
"请用简短专业的语言(2-3句话)表达你的观点。"
|
||||||
|
),
|
||||||
|
"budget": (
|
||||||
|
"你是团队的预算管理者/财务。你关注性价比、人均消费和优惠活动。"
|
||||||
|
"请用简短务实的语言(2-3句话)表达你的观点。"
|
||||||
|
),
|
||||||
|
"pm": (
|
||||||
|
"你是产品经理,负责综合各方意见做最终决策。"
|
||||||
|
"请用简短有决断力的语言(2-3句话)表达你的观点。"
|
||||||
|
),
|
||||||
|
"architect": (
|
||||||
|
"你是系统架构师,逻辑严谨,擅长分析和对比。"
|
||||||
|
"请用简短条理清晰的语言(2-3句话)表达你的观点。"
|
||||||
|
),
|
||||||
|
"developer": (
|
||||||
|
"你是开发工程师,务实高效,喜欢简单直接的方案。"
|
||||||
|
"请用简短直接的语言(2-3句话)表达你的观点。"
|
||||||
|
),
|
||||||
|
"qa": (
|
||||||
|
"你是质量保证工程师,注重细节和风险控制。"
|
||||||
|
"请用简短谨慎的语言(2-3句话)表达你的观点。"
|
||||||
|
),
|
||||||
|
"reviewer": (
|
||||||
|
"你是代码审查专家,善于发现问题和提出改进。"
|
||||||
|
"请用简短有见地的语言(2-3句话)表达你的观点。"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_role_prompt(role: str) -> str:
|
||||||
|
"""根据角色获取 system prompt,未匹配则使用通用提示"""
|
||||||
|
return DINNER_ROLE_PROMPTS.get(
|
||||||
|
role,
|
||||||
|
f"你是团队中的{role}角色。请用简短的语言(2-3句话)表达你的观点。"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowOrchestrator:
|
||||||
|
"""
|
||||||
|
工作流编排器
|
||||||
|
|
||||||
|
自动驱动工作流中的每个节点:
|
||||||
|
- meeting 节点:逐个让 Agent 调用 LLM 发言,最后生成共识
|
||||||
|
- execution 节点:模拟执行并标记完成
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._runs: Dict[str, OrchestrationRun] = {}
|
||||||
|
self._running_task: Optional[asyncio.Task] = None
|
||||||
|
|
||||||
|
def get_run(self, run_id: str) -> Optional[OrchestrationRun]:
|
||||||
|
return self._runs.get(run_id)
|
||||||
|
|
||||||
|
def list_runs(self) -> List[Dict]:
|
||||||
|
return [r.to_dict() for r in self._runs.values()]
|
||||||
|
|
||||||
|
async def start_workflow(
|
||||||
|
self,
|
||||||
|
workflow_path: str,
|
||||||
|
agent_overrides: Dict[str, str] = None,
|
||||||
|
) -> OrchestrationRun:
|
||||||
|
"""
|
||||||
|
启动一个工作流的自动编排
|
||||||
|
|
||||||
|
参数:
|
||||||
|
workflow_path: YAML 文件名(如 dinner-decision.yaml)
|
||||||
|
agent_overrides: 可选的 agent_id → model 覆盖映射
|
||||||
|
返回:
|
||||||
|
OrchestrationRun 对象(后台异步执行)
|
||||||
|
"""
|
||||||
|
engine = get_workflow_engine()
|
||||||
|
workflow = await engine.load_workflow(workflow_path)
|
||||||
|
|
||||||
|
run = OrchestrationRun(
|
||||||
|
run_id=f"run-{uuid.uuid4().hex[:8]}",
|
||||||
|
workflow_id=workflow.workflow_id,
|
||||||
|
workflow_name=workflow.name,
|
||||||
|
status=OrchestratorStatus.RUNNING,
|
||||||
|
started_at=time.time(),
|
||||||
|
)
|
||||||
|
self._runs[run.run_id] = run
|
||||||
|
|
||||||
|
# 在后台启动编排
|
||||||
|
self._running_task = asyncio.create_task(
|
||||||
|
self._run_workflow(run, workflow_path, agent_overrides or {})
|
||||||
|
)
|
||||||
|
|
||||||
|
return run
|
||||||
|
|
||||||
|
async def _run_workflow(
|
||||||
|
self,
|
||||||
|
run: OrchestrationRun,
|
||||||
|
workflow_path: str,
|
||||||
|
agent_overrides: Dict[str, str],
|
||||||
|
):
|
||||||
|
"""后台执行完整工作流"""
|
||||||
|
engine = get_workflow_engine()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
next_node = await engine.get_next_meeting(run.workflow_id)
|
||||||
|
if next_node is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
run.current_node = next_node.meeting_id
|
||||||
|
logger.info(f"[{run.run_id}] 开始节点: {next_node.title} ({next_node.node_type})")
|
||||||
|
|
||||||
|
if next_node.node_type == "meeting":
|
||||||
|
result = await self._run_meeting_node(
|
||||||
|
run, next_node, agent_overrides
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result = await self._run_execution_node(
|
||||||
|
run, next_node, agent_overrides
|
||||||
|
)
|
||||||
|
|
||||||
|
run.meeting_results.append(result)
|
||||||
|
|
||||||
|
# 标记节点完成
|
||||||
|
await engine.complete_meeting(run.workflow_id, next_node.meeting_id)
|
||||||
|
logger.info(f"[{run.run_id}] 节点完成: {next_node.title}")
|
||||||
|
|
||||||
|
run.status = OrchestratorStatus.COMPLETED
|
||||||
|
run.finished_at = time.time()
|
||||||
|
run.current_node = ""
|
||||||
|
logger.info(f"[{run.run_id}] 工作流完成,耗时 {run.finished_at - run.started_at:.1f}s")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
run.status = OrchestratorStatus.FAILED
|
||||||
|
run.error = str(e)
|
||||||
|
run.finished_at = time.time()
|
||||||
|
logger.error(f"[{run.run_id}] 工作流失败: {e}", exc_info=True)
|
||||||
|
|
||||||
|
async def _run_meeting_node(
|
||||||
|
self,
|
||||||
|
run: OrchestrationRun,
|
||||||
|
node: WorkflowMeeting,
|
||||||
|
agent_overrides: Dict[str, str],
|
||||||
|
) -> MeetingResult:
|
||||||
|
"""执行一个会议节点:各 Agent 依次发言 → 生成共识"""
|
||||||
|
registry = get_agent_registry()
|
||||||
|
recorder = get_meeting_recorder()
|
||||||
|
heartbeat = get_heartbeat_service()
|
||||||
|
|
||||||
|
result = MeetingResult(
|
||||||
|
meeting_id=node.meeting_id,
|
||||||
|
title=node.title,
|
||||||
|
node_type="meeting",
|
||||||
|
started_at=time.time(),
|
||||||
|
status="in_progress",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建会议记录
|
||||||
|
steps = ["提议", "讨论", "共识"]
|
||||||
|
await recorder.create_meeting(
|
||||||
|
meeting_id=node.meeting_id,
|
||||||
|
title=node.title,
|
||||||
|
attendees=node.attendees,
|
||||||
|
steps=steps,
|
||||||
|
)
|
||||||
|
await recorder.update_progress(node.meeting_id, "提议")
|
||||||
|
|
||||||
|
# 收集之前节点的讨论上下文
|
||||||
|
previous_context = self._build_previous_context(run)
|
||||||
|
|
||||||
|
# 逐个 Agent 发言
|
||||||
|
for agent_id in node.attendees:
|
||||||
|
agent = await registry.get_agent(agent_id)
|
||||||
|
if not agent:
|
||||||
|
logger.warning(f"Agent {agent_id} 未注册,跳过")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 更新心跳
|
||||||
|
await heartbeat.update_heartbeat(
|
||||||
|
agent_id, "working", f"参与会议: {node.title}", 50
|
||||||
|
)
|
||||||
|
|
||||||
|
# 用 LLM 生成发言
|
||||||
|
model = agent_overrides.get(agent_id, agent.model)
|
||||||
|
turn = await self._generate_agent_turn(
|
||||||
|
agent, model, node.title, previous_context, result.turns
|
||||||
|
)
|
||||||
|
result.turns.append(turn)
|
||||||
|
|
||||||
|
# 写入会议记录
|
||||||
|
await recorder.add_discussion(
|
||||||
|
meeting_id=node.meeting_id,
|
||||||
|
agent_id=agent.agent_id,
|
||||||
|
agent_name=agent.name,
|
||||||
|
content=turn.content,
|
||||||
|
step="讨论",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 恢复心跳
|
||||||
|
await heartbeat.update_heartbeat(agent_id, "idle", "", 100)
|
||||||
|
|
||||||
|
# 生成共识
|
||||||
|
await recorder.update_progress(node.meeting_id, "共识")
|
||||||
|
consensus = await self._generate_consensus(node, result.turns)
|
||||||
|
result.consensus = consensus
|
||||||
|
|
||||||
|
# 完成会议
|
||||||
|
await recorder.end_meeting(node.meeting_id, consensus=consensus)
|
||||||
|
|
||||||
|
result.status = "completed"
|
||||||
|
result.finished_at = time.time()
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def _run_execution_node(
|
||||||
|
self,
|
||||||
|
run: OrchestrationRun,
|
||||||
|
node: WorkflowMeeting,
|
||||||
|
agent_overrides: Dict[str, str],
|
||||||
|
) -> MeetingResult:
|
||||||
|
"""执行一个 execution 节点:模拟任务执行"""
|
||||||
|
registry = get_agent_registry()
|
||||||
|
heartbeat = get_heartbeat_service()
|
||||||
|
engine = get_workflow_engine()
|
||||||
|
|
||||||
|
result = MeetingResult(
|
||||||
|
meeting_id=node.meeting_id,
|
||||||
|
title=node.title,
|
||||||
|
node_type="execution",
|
||||||
|
started_at=time.time(),
|
||||||
|
status="in_progress",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取上一个会议的共识作为执行指令
|
||||||
|
last_consensus = ""
|
||||||
|
for mr in reversed(run.meeting_results):
|
||||||
|
if mr.consensus:
|
||||||
|
last_consensus = mr.consensus
|
||||||
|
break
|
||||||
|
|
||||||
|
for agent_id in node.attendees:
|
||||||
|
agent = await registry.get_agent(agent_id)
|
||||||
|
if not agent:
|
||||||
|
continue
|
||||||
|
|
||||||
|
await heartbeat.update_heartbeat(
|
||||||
|
agent_id, "working", f"执行: {node.title}", 30
|
||||||
|
)
|
||||||
|
|
||||||
|
model = agent_overrides.get(agent_id, agent.model)
|
||||||
|
turn = await self._generate_execution_turn(
|
||||||
|
agent, model, node.title, last_consensus
|
||||||
|
)
|
||||||
|
result.turns.append(turn)
|
||||||
|
|
||||||
|
# 向工作流引擎签到
|
||||||
|
await engine.join_execution_node(
|
||||||
|
run.workflow_id, node.meeting_id, agent_id
|
||||||
|
)
|
||||||
|
|
||||||
|
await heartbeat.update_heartbeat(agent_id, "idle", "", 100)
|
||||||
|
|
||||||
|
result.status = "completed"
|
||||||
|
result.finished_at = time.time()
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def _generate_agent_turn(
|
||||||
|
self,
|
||||||
|
agent: AgentInfo,
|
||||||
|
model: str,
|
||||||
|
meeting_title: str,
|
||||||
|
previous_context: str,
|
||||||
|
existing_turns: List[AgentTurn],
|
||||||
|
) -> AgentTurn:
|
||||||
|
"""
|
||||||
|
调用 LLM 为一个 Agent 生成会议发言
|
||||||
|
|
||||||
|
若 LLM 不可用则使用内置 mock
|
||||||
|
"""
|
||||||
|
role_prompt = _get_role_prompt(agent.role)
|
||||||
|
|
||||||
|
# 构建其他 Agent 已发言的内容
|
||||||
|
others_said = ""
|
||||||
|
for t in existing_turns:
|
||||||
|
content = t.content[:150] if len(t.content) > 150 else t.content
|
||||||
|
others_said += f" {t.agent_name}: {content}\n"
|
||||||
|
|
||||||
|
# 直接把所有信息揉进一段连贯的指令中
|
||||||
|
prompt = (
|
||||||
|
f"场景:团队正在讨论「{meeting_title}」。\n"
|
||||||
|
f"你的角色:{agent.name}。{role_prompt}\n"
|
||||||
|
)
|
||||||
|
if previous_context:
|
||||||
|
prompt += f"\n上一轮讨论的结论:{previous_context}\n"
|
||||||
|
if others_said:
|
||||||
|
prompt += f"\n已有发言:\n{others_said}\n"
|
||||||
|
prompt += (
|
||||||
|
f"\n请以{agent.name}的身份,直接给出2-3句具体建议。"
|
||||||
|
f"不要自我介绍,不要提问,不要使用工具,直接说你的观点。"
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
LLMMessage(role="user", content=prompt),
|
||||||
|
]
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
content, tokens, is_mock = await self._call_llm(model, messages)
|
||||||
|
latency = time.time() - start
|
||||||
|
|
||||||
|
return AgentTurn(
|
||||||
|
agent_id=agent.agent_id,
|
||||||
|
agent_name=agent.name,
|
||||||
|
role=agent.role,
|
||||||
|
model=model,
|
||||||
|
content=content,
|
||||||
|
timestamp=time.time(),
|
||||||
|
latency=round(latency, 2),
|
||||||
|
tokens_used=tokens,
|
||||||
|
is_mock=is_mock,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _generate_execution_turn(
|
||||||
|
self,
|
||||||
|
agent: AgentInfo,
|
||||||
|
model: str,
|
||||||
|
task_title: str,
|
||||||
|
consensus: str,
|
||||||
|
) -> AgentTurn:
|
||||||
|
"""为执行节点生成 Agent 的执行结果"""
|
||||||
|
prompt = (
|
||||||
|
f"你是{agent.name}。团队讨论后做出了以下决定:\n{consensus}\n\n"
|
||||||
|
f"现在需要你执行「{task_title}」这个任务。"
|
||||||
|
f"请用2-3句话直接汇报你的执行结果和下一步安排。"
|
||||||
|
)
|
||||||
|
messages = [
|
||||||
|
LLMMessage(role="user", content=prompt),
|
||||||
|
]
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
content, tokens, is_mock = await self._call_llm(model, messages)
|
||||||
|
latency = time.time() - start
|
||||||
|
|
||||||
|
return AgentTurn(
|
||||||
|
agent_id=agent.agent_id,
|
||||||
|
agent_name=agent.name,
|
||||||
|
role=agent.role,
|
||||||
|
model=model,
|
||||||
|
content=content,
|
||||||
|
timestamp=time.time(),
|
||||||
|
latency=round(latency, 2),
|
||||||
|
tokens_used=tokens,
|
||||||
|
is_mock=is_mock,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _generate_consensus(
|
||||||
|
self,
|
||||||
|
node: WorkflowMeeting,
|
||||||
|
turns: List[AgentTurn],
|
||||||
|
) -> str:
|
||||||
|
"""基于所有发言生成会议共识,使用 kimi CLI 效果最佳"""
|
||||||
|
discussion_summary = ""
|
||||||
|
for t in turns:
|
||||||
|
# 截取每人发言前 200 字,避免 prompt 过长
|
||||||
|
content = t.content[:200] if len(t.content) > 200 else t.content
|
||||||
|
discussion_summary += f" {t.agent_name}: {content}\n"
|
||||||
|
|
||||||
|
prompt = (
|
||||||
|
f"请总结以下关于「{node.title}」的讨论,用3-5句话给出共识。\n\n"
|
||||||
|
f"讨论记录:\n{discussion_summary}\n"
|
||||||
|
f"要求:直接输出总结,包含最终决定和行动方案。不要提问。"
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
LLMMessage(role="user", content=prompt),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 优先用 kimi CLI 做总结(它最擅长按指令行事)
|
||||||
|
content, _, _ = await self._call_llm("kimi", messages)
|
||||||
|
return content
|
||||||
|
|
||||||
|
async def _call_llm(
|
||||||
|
self, model: str, messages: List[LLMMessage]
|
||||||
|
) -> tuple:
|
||||||
|
"""
|
||||||
|
调用 AI 生成内容,优先级:CLI → LLM API → 报错
|
||||||
|
|
||||||
|
返回: (content, tokens_used, is_mock)
|
||||||
|
"""
|
||||||
|
# 分离 system prompt 和 user prompt
|
||||||
|
system_prompt = ""
|
||||||
|
user_prompt = ""
|
||||||
|
for m in messages:
|
||||||
|
if m.role == "system":
|
||||||
|
system_prompt += m.content + "\n"
|
||||||
|
else:
|
||||||
|
user_prompt += m.content + "\n"
|
||||||
|
user_prompt = user_prompt.strip()
|
||||||
|
|
||||||
|
# 1. 优先尝试 CLI
|
||||||
|
cli_name = resolve_cli(model) if model else None
|
||||||
|
if cli_name:
|
||||||
|
logger.info(f"使用 CLI [{cli_name}] (model={model})")
|
||||||
|
result = await invoke_cli(
|
||||||
|
cli_name, user_prompt, timeout=120,
|
||||||
|
system_prompt=system_prompt.strip(),
|
||||||
|
)
|
||||||
|
if result.success:
|
||||||
|
return result.content, 0, False
|
||||||
|
else:
|
||||||
|
logger.warning(f"CLI [{cli_name}] 失败: {result.error},尝试其他方式")
|
||||||
|
|
||||||
|
# 2. 如果 model 未指定 CLI 或 CLI 失败,尝试任意可用 CLI
|
||||||
|
available = detect_available_clis()
|
||||||
|
if available:
|
||||||
|
fallback_cli = list(available.keys())[0]
|
||||||
|
logger.info(f"使用 fallback CLI [{fallback_cli}]")
|
||||||
|
result = await invoke_cli(
|
||||||
|
fallback_cli, user_prompt, timeout=120,
|
||||||
|
system_prompt=system_prompt.strip(),
|
||||||
|
)
|
||||||
|
if result.success:
|
||||||
|
return result.content, 0, False
|
||||||
|
else:
|
||||||
|
logger.warning(f"Fallback CLI [{fallback_cli}] 也失败: {result.error}")
|
||||||
|
|
||||||
|
# 3. 尝试 LLM API
|
||||||
|
try:
|
||||||
|
llm = get_llm_service()
|
||||||
|
providers = llm.get_available_providers()
|
||||||
|
real_providers = [p for p in providers if p != "ollama"]
|
||||||
|
if real_providers:
|
||||||
|
response = await llm.route_task(
|
||||||
|
task=messages[-1].content,
|
||||||
|
messages=messages,
|
||||||
|
preferred_model=model if model else None,
|
||||||
|
)
|
||||||
|
return response.content, response.tokens_used, False
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"LLM API 也不可用: {e}")
|
||||||
|
|
||||||
|
raise RuntimeError(
|
||||||
|
f"无可用的 AI 提供商。CLI={list(available.keys()) if available else '无'},"
|
||||||
|
f"LLM API Key 未配置。请安装 CLI 工具或配置 API Key。"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _build_previous_context(self, run: OrchestrationRun) -> str:
|
||||||
|
"""从已完成的会议中构建上下文"""
|
||||||
|
parts = []
|
||||||
|
for mr in run.meeting_results:
|
||||||
|
if mr.consensus:
|
||||||
|
parts.append(f"[{mr.title}] 共识: {mr.consensus}")
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
# 单例
|
||||||
|
_orchestrator: Optional[WorkflowOrchestrator] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_workflow_orchestrator() -> WorkflowOrchestrator:
|
||||||
|
"""获取编排器单例"""
|
||||||
|
global _orchestrator
|
||||||
|
if _orchestrator is None:
|
||||||
|
_orchestrator = WorkflowOrchestrator()
|
||||||
|
return _orchestrator
|
||||||
@@ -19,6 +19,9 @@ pyyaml>=6.0
|
|||||||
# HTTP 客户端(调用 LLM API)
|
# HTTP 客户端(调用 LLM API)
|
||||||
httpx>=0.25.0
|
httpx>=0.25.0
|
||||||
|
|
||||||
|
# 异步 HTTP 客户端(Ollama 等 LLM 调用)
|
||||||
|
aiohttp>=3.9.0
|
||||||
|
|
||||||
# Anthropic API(Claude)
|
# Anthropic API(Claude)
|
||||||
anthropic>=0.18.0
|
anthropic>=0.18.0
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,294 @@
|
|||||||
|
"""
|
||||||
|
API 集成测试 - 验证路由层正确接入服务层
|
||||||
|
通过 HTTP 请求测试所有 API 端点
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import httpx
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
os.environ.pop("HTTP_PROXY", None)
|
||||||
|
os.environ.pop("HTTPS_PROXY", None)
|
||||||
|
os.environ.pop("http_proxy", None)
|
||||||
|
os.environ.pop("https_proxy", None)
|
||||||
|
os.environ["NO_PROXY"] = "*"
|
||||||
|
|
||||||
|
BASE = "http://127.0.0.1:8000"
|
||||||
|
passed = 0
|
||||||
|
failed = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
|
||||||
|
async def test(name: str, method: str, path: str, json_data=None, expect_status=200, expect_key=None):
|
||||||
|
"""执行单个 API 测试"""
|
||||||
|
global passed, failed
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(base_url=BASE, timeout=10) as client:
|
||||||
|
if method == "GET":
|
||||||
|
r = await client.get(path)
|
||||||
|
elif method == "POST":
|
||||||
|
r = await client.post(path, json=json_data)
|
||||||
|
elif method == "DELETE":
|
||||||
|
r = await client.delete(path)
|
||||||
|
elif method == "PUT":
|
||||||
|
r = await client.put(path, json=json_data)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown method: {method}")
|
||||||
|
|
||||||
|
if r.status_code != expect_status:
|
||||||
|
failed += 1
|
||||||
|
msg = f"[FAIL] {name}: 期望 {expect_status}, 得到 {r.status_code} - {r.text[:200]}"
|
||||||
|
errors.append(msg)
|
||||||
|
print(msg)
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
if expect_key and expect_key not in data:
|
||||||
|
failed += 1
|
||||||
|
msg = f"[FAIL] {name}: 响应缺少 key '{expect_key}', 有: {list(data.keys())}"
|
||||||
|
errors.append(msg)
|
||||||
|
print(msg)
|
||||||
|
return None
|
||||||
|
|
||||||
|
passed += 1
|
||||||
|
print(f"[PASS] {name}")
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
failed += 1
|
||||||
|
msg = f"[FAIL] {name}: {e}"
|
||||||
|
errors.append(msg)
|
||||||
|
print(msg)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
global passed, failed
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("Swarm API 集成测试")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# ========== 健康检查 ==========
|
||||||
|
print("\n=== 健康检查 ===")
|
||||||
|
await test("GET /health", "GET", "/health", expect_key="status")
|
||||||
|
await test("GET /api/health", "GET", "/api/health", expect_key="status")
|
||||||
|
|
||||||
|
# ========== Agent API ==========
|
||||||
|
print("\n=== Agent API ===")
|
||||||
|
data = await test("列出 Agent(初始)", "GET", "/api/agents/", expect_key="agents")
|
||||||
|
|
||||||
|
await test("注册 Agent", "POST", "/api/agents/register", json_data={
|
||||||
|
"agent_id": "test-api-001",
|
||||||
|
"name": "Test Agent",
|
||||||
|
"role": "developer",
|
||||||
|
"model": "test-model"
|
||||||
|
}, expect_key="agent_id")
|
||||||
|
|
||||||
|
data = await test("列出 Agent(注册后)", "GET", "/api/agents/", expect_key="agents")
|
||||||
|
if data:
|
||||||
|
agent_ids = [a["agent_id"] for a in data["agents"]]
|
||||||
|
if "test-api-001" in agent_ids:
|
||||||
|
passed += 1
|
||||||
|
print("[PASS] 注册的 Agent 出现在列表中")
|
||||||
|
else:
|
||||||
|
failed += 1
|
||||||
|
msg = f"[FAIL] 注册的 Agent 未出现在列表中: {agent_ids}"
|
||||||
|
errors.append(msg)
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
await test("获取 Agent 详情", "GET", "/api/agents/test-api-001", expect_key="agent_id")
|
||||||
|
|
||||||
|
await test("更新 Agent 状态", "POST", "/api/agents/test-api-001/state", json_data={
|
||||||
|
"task": "测试任务",
|
||||||
|
"progress": 50,
|
||||||
|
"working_files": ["test.py"]
|
||||||
|
}, expect_key="success")
|
||||||
|
|
||||||
|
data = await test("获取 Agent 状态", "GET", "/api/agents/test-api-001/state", expect_key="agent_id")
|
||||||
|
if data and data.get("current_task") == "测试任务":
|
||||||
|
passed += 1
|
||||||
|
print("[PASS] Agent 状态正确持久化")
|
||||||
|
elif data:
|
||||||
|
failed += 1
|
||||||
|
msg = f"[FAIL] Agent 状态不匹配: {data}"
|
||||||
|
errors.append(msg)
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
await test("获取不存在的 Agent", "GET", "/api/agents/nonexistent-agent", expect_status=404)
|
||||||
|
|
||||||
|
# ========== 文件锁 API ==========
|
||||||
|
print("\n=== 文件锁 API ===")
|
||||||
|
await test("列出文件锁(初始)", "GET", "/api/locks/", expect_key="locks")
|
||||||
|
|
||||||
|
await test("获取文件锁", "POST", "/api/locks/acquire", json_data={
|
||||||
|
"file_path": "test/main.py",
|
||||||
|
"agent_id": "test-api-001",
|
||||||
|
"agent_name": "Test Agent"
|
||||||
|
}, expect_key="success")
|
||||||
|
|
||||||
|
data = await test("列出文件锁(获取后)", "GET", "/api/locks/", expect_key="locks")
|
||||||
|
if data and len(data["locks"]) > 0:
|
||||||
|
found = any(l["file_path"] == "test/main.py" for l in data["locks"])
|
||||||
|
if found:
|
||||||
|
passed += 1
|
||||||
|
print("[PASS] 获取的锁出现在列表中")
|
||||||
|
else:
|
||||||
|
failed += 1
|
||||||
|
msg = "[FAIL] 获取的锁未出现在列表中"
|
||||||
|
errors.append(msg)
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
data = await test("检查文件锁", "GET", "/api/locks/check?file_path=test/main.py", expect_key="locked")
|
||||||
|
if data and data["locked"]:
|
||||||
|
passed += 1
|
||||||
|
print("[PASS] 文件锁状态正确")
|
||||||
|
elif data:
|
||||||
|
failed += 1
|
||||||
|
msg = "[FAIL] 文件锁状态错误"
|
||||||
|
errors.append(msg)
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
await test("释放文件锁", "POST", "/api/locks/release", json_data={
|
||||||
|
"file_path": "test/main.py",
|
||||||
|
"agent_id": "test-api-001"
|
||||||
|
}, expect_key="success")
|
||||||
|
|
||||||
|
data = await test("检查释放后", "GET", "/api/locks/check?file_path=test/main.py", expect_key="locked")
|
||||||
|
if data and not data["locked"]:
|
||||||
|
passed += 1
|
||||||
|
print("[PASS] 锁释放成功")
|
||||||
|
elif data:
|
||||||
|
failed += 1
|
||||||
|
msg = "[FAIL] 锁释放后仍显示锁定"
|
||||||
|
errors.append(msg)
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
# ========== 心跳 API ==========
|
||||||
|
print("\n=== 心跳 API ===")
|
||||||
|
await test("列出心跳(初始)", "GET", "/api/heartbeats/", expect_key="heartbeats")
|
||||||
|
|
||||||
|
await test("更新心跳", "POST", "/api/heartbeats/test-api-001", json_data={
|
||||||
|
"status": "working",
|
||||||
|
"current_task": "测试中",
|
||||||
|
"progress": 30
|
||||||
|
}, expect_key="success")
|
||||||
|
|
||||||
|
data = await test("列出心跳(更新后)", "GET", "/api/heartbeats/", expect_key="heartbeats")
|
||||||
|
if data and "test-api-001" in data["heartbeats"]:
|
||||||
|
hb = data["heartbeats"]["test-api-001"]
|
||||||
|
if hb["status"] == "working":
|
||||||
|
passed += 1
|
||||||
|
print("[PASS] 心跳数据正确持久化")
|
||||||
|
else:
|
||||||
|
failed += 1
|
||||||
|
msg = f"[FAIL] 心跳状态不匹配: {hb}"
|
||||||
|
errors.append(msg)
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
await test("检查超时", "GET", "/api/heartbeats/timeouts?timeout_seconds=60", expect_key="timeout_agents")
|
||||||
|
|
||||||
|
# ========== 会议 API ==========
|
||||||
|
print("\n=== 会议 API ===")
|
||||||
|
data = await test("创建会议", "POST", "/api/meetings/", json_data={
|
||||||
|
"title": "API 测试会议",
|
||||||
|
"agenda": "测试议程",
|
||||||
|
"attendees": ["test-api-001"],
|
||||||
|
"steps": ["讨论", "决策", "总结"]
|
||||||
|
}, expect_key="meeting_id")
|
||||||
|
|
||||||
|
meeting_id = data["meeting_id"] if data else None
|
||||||
|
|
||||||
|
if meeting_id:
|
||||||
|
await test("获取会议详情", "GET", f"/api/meetings/{meeting_id}", expect_key="meeting_id")
|
||||||
|
|
||||||
|
await test("获取会议队列", "GET", f"/api/meetings/{meeting_id}/queue", expect_key="meeting_id")
|
||||||
|
|
||||||
|
await test("添加讨论", "POST", f"/api/meetings/{meeting_id}/discuss", json_data={
|
||||||
|
"agent_id": "test-api-001",
|
||||||
|
"agent_name": "Test Agent",
|
||||||
|
"content": "这是一条测试讨论",
|
||||||
|
"step": "讨论"
|
||||||
|
}, expect_key="success")
|
||||||
|
|
||||||
|
await test("更新进度", "POST", f"/api/meetings/{meeting_id}/progress", json_data={
|
||||||
|
"step": "讨论"
|
||||||
|
}, expect_key="success")
|
||||||
|
|
||||||
|
await test("完成会议", "POST", f"/api/meetings/{meeting_id}/finish", json_data={
|
||||||
|
"consensus": "测试共识"
|
||||||
|
}, expect_key="success")
|
||||||
|
|
||||||
|
await test("列出今日会议", "GET", "/api/meetings/today", expect_key="meetings")
|
||||||
|
|
||||||
|
# ========== 资源 API ==========
|
||||||
|
print("\n=== 资源 API ===")
|
||||||
|
await test("解析任务文件", "POST", "/api/parse-task", json_data={
|
||||||
|
"task": "修改 src/utils/helper.js 修复 bug"
|
||||||
|
}, expect_key="files")
|
||||||
|
|
||||||
|
await test("执行任务", "POST", "/api/execute", json_data={
|
||||||
|
"agent_id": "test-api-001",
|
||||||
|
"task": "修改 src/utils/helper.js",
|
||||||
|
"timeout": 30
|
||||||
|
}, expect_key="success")
|
||||||
|
|
||||||
|
await test("获取所有状态", "GET", "/api/status", expect_key="agents")
|
||||||
|
|
||||||
|
# ========== 角色 API ==========
|
||||||
|
print("\n=== 角色 API ===")
|
||||||
|
data = await test("获取主要角色", "POST", "/api/roles/primary", json_data={
|
||||||
|
"task": "设计系统架构方案"
|
||||||
|
}, expect_key="primary_role")
|
||||||
|
if data and data["primary_role"] == "architect":
|
||||||
|
passed += 1
|
||||||
|
print(f"[PASS] 角色分析正确: architect(任务含'设计'+'架构')")
|
||||||
|
elif data:
|
||||||
|
failed += 1
|
||||||
|
msg = f"[FAIL] 角色分析不正确: 期望 architect, 得到 {data['primary_role']}"
|
||||||
|
errors.append(msg)
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
await test("分配角色", "POST", "/api/roles/allocate", json_data={
|
||||||
|
"task": "开发用户登录功能",
|
||||||
|
"agents": ["agent-1", "agent-2"]
|
||||||
|
}, expect_key="allocation")
|
||||||
|
|
||||||
|
await test("解释角色分配", "POST", "/api/roles/explain", json_data={
|
||||||
|
"task": "测试 API 接口",
|
||||||
|
"agents": ["agent-1"]
|
||||||
|
}, expect_key="explanation")
|
||||||
|
|
||||||
|
# ========== 工作流 API ==========
|
||||||
|
print("\n=== 工作流 API ===")
|
||||||
|
await test("工作流文件列表", "GET", "/api/workflows/files", expect_key="files")
|
||||||
|
await test("已加载工作流列表", "GET", "/api/workflows/list", expect_key="workflows")
|
||||||
|
|
||||||
|
# ========== 人类输入 API ==========
|
||||||
|
print("\n=== 人类输入 API ===")
|
||||||
|
await test("获取摘要", "GET", "/api/humans/summary", expect_key="participants")
|
||||||
|
|
||||||
|
# ========== 清理 ==========
|
||||||
|
print("\n=== 清理 ===")
|
||||||
|
await test("删除测试 Agent", "DELETE", "/api/agents/test-api-001", expect_key="message")
|
||||||
|
|
||||||
|
# ========== 汇总 ==========
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(f"测试结果汇总")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"通过: {passed}")
|
||||||
|
print(f"失败: {failed}")
|
||||||
|
print(f"总计: {passed + failed}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print("\n失败详情:")
|
||||||
|
for e in errors:
|
||||||
|
print(f" {e}")
|
||||||
|
|
||||||
|
return failed == 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = asyncio.run(main())
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
@@ -0,0 +1,473 @@
|
|||||||
|
# Swarm Command Center - 项目审计报告
|
||||||
|
|
||||||
|
> 初始审计日期:2026-03-10
|
||||||
|
> 修复完成日期:2026-03-10
|
||||||
|
> 审计范围:需求文档 vs 实际实现的完整比对
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、项目总体进度
|
||||||
|
|
||||||
|
### 修复后状态(2026-03-10 更新)
|
||||||
|
|
||||||
|
| 模块 | 完成度 | 状态 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 后端服务层 (Services) | **95%** | ✅ 9 个核心服务全部实现并通过测试 |
|
||||||
|
| 后端路由层 (Routers) | **100%** | ✅ **已修复** 全部 10 个路由模块正确接入服务层 |
|
||||||
|
| 后端 CLI | **100%** | ✅ 所有命令已实现并正确调用服务层 |
|
||||||
|
| 前端页面 | **95%** | ✅ 6 个页面全部实现,工作流上传已补全 |
|
||||||
|
| 前端 API 客户端 | **100%** | ✅ **已修复** 封装全部 API 模块(含 Agent 控制) |
|
||||||
|
| 前端-后端联调 | **95%** | ✅ **已修复** 路由→服务→存储全链路打通 |
|
||||||
|
| API 集成测试 | **100%** | ✅ **新增** 43 个 API 端点测试全部通过 |
|
||||||
|
| E2E 测试 | **60%** | ⚠️ 测试用例存在,选择器需要与 UI 保持同步 |
|
||||||
|
|
||||||
|
### 修复前状态(初始审计)
|
||||||
|
|
||||||
|
| 模块 | 完成度 | 状态 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 后端路由层 (Routers) | ~~40%~~ | ~~多数路由使用 mock 数据,未接入服务层~~ |
|
||||||
|
| 前端-后端联调 | ~~30%~~ | ~~后端路由返回 mock,前端获取假数据~~ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、核心问题:路由层与服务层脱节
|
||||||
|
|
||||||
|
**这是项目最大的系统性问题。** 后端服务层已完整实现并通过测试,CLI 也正确接入了服务层。但 HTTP API 路由层(前端调用的入口)多数使用内存变量或硬编码 mock 数据,没有调用已实现的服务。
|
||||||
|
|
||||||
|
### 2.1 路由层状态明细
|
||||||
|
|
||||||
|
| 路由文件 | 前缀 | 数据源 | 接入服务层? |
|
||||||
|
|----------|------|--------|-------------|
|
||||||
|
| `agents.py` | `/api/agents` | 内存 `agents_db` + 硬编码默认 Agent | ❌ 未接入 `AgentRegistry` |
|
||||||
|
| `locks.py` | `/api/locks` | 内存 `locks_db` + 硬编码默认锁 | ❌ 未接入 `FileLockService` |
|
||||||
|
| `heartbeats.py` | `/api/heartbeats` | 硬编码 mock 返回值 | ❌ 未接入 `HeartbeatService` |
|
||||||
|
| `meetings.py` | `/api/meetings` | 内存 `meetings_db` + 硬编码 mock | ❌ 未接入 `MeetingScheduler` / `MeetingRecorder` |
|
||||||
|
| `resources.py` | `/api` | `/execute` `/parse-task` 为硬编码 mock | ⚠️ 仅 `/status` 接入了服务 |
|
||||||
|
| `roles.py` | `/api/roles` | 全部硬编码 mock(固定返回 developer) | ❌ 未接入 `RoleAllocator` |
|
||||||
|
| `workflows.py` | `/api/workflows` | 已调用 `WorkflowEngine` | ✅ **已正确接入** |
|
||||||
|
| `humans.py` | `/api/humans` | 已调用 `HumanInputService` | ✅ **已正确接入** |
|
||||||
|
| `agents_control.py` | `/api/agents/control` | 已调用 `ProcessManager` 等 | ✅ **已正确接入** |
|
||||||
|
| `websocket.py` | `/ws` | 独立实现 | ✅ 正常 |
|
||||||
|
|
||||||
|
**结论**:10 个路由模块中,仅 3 个(workflows, humans, agents_control)正确接入了后端服务,6 个使用 mock 数据。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、API 端点需求 vs 实现对比
|
||||||
|
|
||||||
|
### 3.1 Agent API (`/api/agents`)
|
||||||
|
|
||||||
|
| 需求端点 | 方法 | 路由实现 | 接入服务 | 状态 |
|
||||||
|
|----------|------|----------|----------|------|
|
||||||
|
| `/api/agents` | GET | ✅ 存在 | ❌ 返回硬编码 | ⚠️ Mock |
|
||||||
|
| `/api/agents/register` | POST | ✅ 存在 | ❌ 存内存 `agents_db` | ⚠️ Mock |
|
||||||
|
| `/api/agents/:id` | GET | ✅ 存在 | ❌ 仅查内存 | ⚠️ Mock |
|
||||||
|
| `/api/agents/:id/state` | GET | ✅ 存在 | ❌ 硬编码状态 | ⚠️ Mock |
|
||||||
|
| `/api/agents/:id/state` | POST | ✅ 存在 | ❌ 存内存 | ⚠️ Mock |
|
||||||
|
| `/api/agents/:id` | DELETE | ✅ 存在(额外) | ❌ 仅删内存 | ⚠️ Mock |
|
||||||
|
|
||||||
|
### 3.2 文件锁 API (`/api/locks`)
|
||||||
|
|
||||||
|
| 需求端点 | 方法 | 路由实现 | 接入服务 | 状态 |
|
||||||
|
|----------|------|----------|----------|------|
|
||||||
|
| `/api/locks` | GET | ✅ 存在 | ❌ 硬编码锁数据 | ⚠️ Mock |
|
||||||
|
| `/api/locks/acquire` | POST | ✅ 存在 | ❌ 存内存 | ⚠️ Mock |
|
||||||
|
| `/api/locks/release` | POST | ✅ 存在 | ❌ 仅删内存 | ⚠️ Mock |
|
||||||
|
| `/api/locks/check` | GET | ✅ 存在 | ❌ 查内存 | ⚠️ Mock |
|
||||||
|
|
||||||
|
### 3.3 心跳 API (`/api/heartbeats`)
|
||||||
|
|
||||||
|
| 需求端点 | 方法 | 路由实现 | 接入服务 | 状态 |
|
||||||
|
|----------|------|----------|----------|------|
|
||||||
|
| `/api/heartbeats` | GET | ✅ 存在 | ❌ 硬编码 | ⚠️ Mock |
|
||||||
|
| `/api/heartbeats/:id` | POST | ✅ 存在 | ❌ 存内存 | ⚠️ Mock |
|
||||||
|
| `/api/heartbeats/timeouts` | GET | ❌ **缺失** | - | ❌ 未实现 |
|
||||||
|
|
||||||
|
### 3.4 会议 API (`/api/meetings`)
|
||||||
|
|
||||||
|
| 需求端点 | 方法 | 路由实现 | 接入服务 | 状态 |
|
||||||
|
|----------|------|----------|----------|------|
|
||||||
|
| `/api/meetings/create` | POST | ✅ 存在 | ❌ 存内存 | ⚠️ Mock |
|
||||||
|
| `/api/meetings/:id/queue` | GET | ❌ **缺失** | - | ❌ 未实现 |
|
||||||
|
| `/api/meetings/:id/wait` | POST | ❌ **缺失** | - | ❌ 未实现 |
|
||||||
|
| `/api/meetings/:id/end` | POST | ❌ **缺失** | - | ❌ 未实现 |
|
||||||
|
| `/api/meetings/record/create` | POST | ✅ 存在 | ❌ 存内存 | ⚠️ Mock |
|
||||||
|
| `/api/meetings/:id/discuss` | POST | ✅ 存在 | ❌ 空返回 | ⚠️ Mock |
|
||||||
|
| `/api/meetings/:id/progress` | POST | ✅ 存在 | ❌ 空返回 | ⚠️ Mock |
|
||||||
|
| `/api/meetings/:id` | GET | ✅ 存在 | ❌ 返回 mock | ⚠️ Mock |
|
||||||
|
| `/api/meetings/:id/finish` | POST | ✅ 存在 | ❌ 空返回 | ⚠️ Mock |
|
||||||
|
| `/api/meetings` (列表) | GET | ✅ 存在 | ❌ 硬编码 | ⚠️ Mock |
|
||||||
|
| `/api/meetings/today` | GET | ✅ 存在 | ❌ 硬编码 | ⚠️ Mock |
|
||||||
|
|
||||||
|
### 3.5 资源 API (`/api`)
|
||||||
|
|
||||||
|
| 需求端点 | 方法 | 路由实现 | 接入服务 | 状态 |
|
||||||
|
|----------|------|----------|----------|------|
|
||||||
|
| `/api/execute` | POST | ✅ 存在 | ❌ 硬编码返回 | ⚠️ Mock |
|
||||||
|
| `/api/status` | GET | ✅ 存在 | ✅ 接入 Registry + Heartbeat | ✅ 正常 |
|
||||||
|
| `/api/parse-task` | POST | ✅ 存在 | ❌ 硬编码返回 | ⚠️ Mock |
|
||||||
|
|
||||||
|
### 3.6 角色 API (`/api/roles`)
|
||||||
|
|
||||||
|
| 需求端点 | 方法 | 路由实现 | 接入服务 | 状态 |
|
||||||
|
|----------|------|----------|----------|------|
|
||||||
|
| `/api/roles/primary` | POST | ✅ 存在 | ❌ 固定返回 developer | ⚠️ Mock |
|
||||||
|
| `/api/roles/allocate` | POST | ✅ 存在 | ❌ 轮询分配硬编码 | ⚠️ Mock |
|
||||||
|
| `/api/roles/explain` | POST | ✅ 存在 | ❌ 固定文案 | ⚠️ Mock |
|
||||||
|
|
||||||
|
### 3.7 已正确实现的 API
|
||||||
|
|
||||||
|
| 路由模块 | 端点数 | 状态 |
|
||||||
|
|----------|--------|------|
|
||||||
|
| `workflows.py` | 11 个端点 | ✅ 全部接入 `WorkflowEngine` |
|
||||||
|
| `humans.py` | 12 个端点 | ✅ 全部接入 `HumanInputService` |
|
||||||
|
| `agents_control.py` | 12 个端点 | ✅ 全部接入 `ProcessManager` 等 |
|
||||||
|
| `websocket.py` | 6 个端点 | ✅ 独立实现完整 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、后端代码问题
|
||||||
|
|
||||||
|
### 4.1 Bug
|
||||||
|
|
||||||
|
| 文件 | 问题 | 严重度 |
|
||||||
|
|------|------|--------|
|
||||||
|
| `agent_executor.py` | `json.dumps` 调用在函数内部局部 import(L442),虽不会 NameError 但不规范 | 低 |
|
||||||
|
| `llm_service.py` | 使用 `aiohttp`(L439, L474)但 `requirements.txt` 中未声明 | 中 |
|
||||||
|
| `native_llm_agent.py:278` | `# TODO: 实现投票机制` 未完成 | 低 |
|
||||||
|
|
||||||
|
### 4.2 缺失的 API 端点(需求文档要求但路由未实现)
|
||||||
|
|
||||||
|
| 端点 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `GET /api/heartbeats/timeouts` | 检查超时 Agent |
|
||||||
|
| `GET /api/meetings/:id/queue` | 获取会议等待队列 |
|
||||||
|
| `POST /api/meetings/:id/wait` | 栅栏同步等待 |
|
||||||
|
| `POST /api/meetings/:id/end` | 结束会议 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、前端问题
|
||||||
|
|
||||||
|
### 5.1 功能缺失
|
||||||
|
|
||||||
|
| 问题 | 位置 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 工作流 YAML 上传 | `WorkflowPage.tsx:480` | 注释 "这里应该调用后端 API 上传文件",为占位逻辑 |
|
||||||
|
| API 地址不可配置 | `api.ts:15` | `API_BASE` 硬编码 `localhost:8000`,Settings 页面的 API 地址配置不生效 |
|
||||||
|
| Agent 控制 API 未封装 | `AgentsPage.tsx` | 直接 `fetch('/agents/control/*')`,未使用 `api.ts` 封装 |
|
||||||
|
|
||||||
|
### 5.2 UI 与数据问题
|
||||||
|
|
||||||
|
| 问题 | 位置 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 进度条宽度 | `MeetingsPage.tsx` | `width: meeting.progress_summary` 为字符串 "50%",应为数值 |
|
||||||
|
| 遗留组件未使用 | `components/` 根级 | `AgentStatusCard`、`ResourceMonitorCard`、`RecentMeetingsCard`、`ConsensusCard` 含 mock 数据,未被任何页面引用 |
|
||||||
|
|
||||||
|
### 5.3 E2E 测试与 UI 不一致
|
||||||
|
|
||||||
|
| 测试期望 | 实际 UI | 文件 |
|
||||||
|
|----------|---------|------|
|
||||||
|
| `input[placeholder="搜索 Agent..."]` | AgentsPage 无搜索框 | `e2e-workflow.spec.ts` |
|
||||||
|
| `input[name="name"]`, `select[name="role"]` | 表单无 name 属性 | `e2e-workflow.spec.ts` |
|
||||||
|
| `text=创建新会议` | 实际文案 "创建会议" | `e2e-workflow.spec.ts` |
|
||||||
|
| `button:has-text("新建工作流")` | 无此按钮 | `e2e-workflow.spec.ts` |
|
||||||
|
| `input[name="apiBaseUrl"]` | 配置表单无 name | `e2e-workflow.spec.ts` |
|
||||||
|
| `text=配置已保存` | 实际文案 "已保存" | `e2e-workflow.spec.ts` |
|
||||||
|
| `[data-testid="agent-list"]` | 未使用 data-testid | `e2e-workflow.spec.ts` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、设计文档与实现的不一致
|
||||||
|
|
||||||
|
| 设计文档要求 | 实际实现 | 差异说明 |
|
||||||
|
|-------------|----------|----------|
|
||||||
|
| API 层使用 Express/Fastify (Node.js) | 使用 Python FastAPI | CLAUDE.md 中已更正,design-spec 未同步 |
|
||||||
|
| 前端使用 shadcn/ui | 纯 Tailwind CSS + 自定义样式 | 未使用 shadcn/ui 组件库 |
|
||||||
|
| 前端使用 Zustand 状态管理 | 使用 React useState + useEffect | 无全局状态管理 |
|
||||||
|
| 前端使用 WebSocket 实时更新 | 使用轮询(5-10 秒) | WebSocket 后端已实现但前端未接入 |
|
||||||
|
| Consensus Engine(共识引擎) | 未作为独立服务实现 | 共识逻辑在 MeetingRecorder 中 |
|
||||||
|
| Model Router(模型路由) | 在 LLMService 中实现 | 位置不同但功能存在 |
|
||||||
|
| `.doc/dialogues/` 目录 | 不存在 | 对话存储未实现 |
|
||||||
|
| `.doc/progress/` 目录 | 不存在 | 进度存储在 Agent state 中 |
|
||||||
|
| `.doc/shared/` 目录 | 不存在 | 共享知识库未实现 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、后端测试结果
|
||||||
|
|
||||||
|
```
|
||||||
|
StorageService [PASS] ✅
|
||||||
|
FileLockService [PASS] ✅
|
||||||
|
HeartbeatService [PASS] ✅
|
||||||
|
AgentRegistry [PASS] ✅
|
||||||
|
MeetingScheduler [PASS] ✅
|
||||||
|
MeetingRecorder [PASS] ✅
|
||||||
|
ResourceManager [PASS] ✅
|
||||||
|
WorkflowEngine [PASS] ✅
|
||||||
|
RoleAllocator [PASS] ✅
|
||||||
|
总计: 9/9 通过
|
||||||
|
```
|
||||||
|
|
||||||
|
### 未覆盖的服务测试
|
||||||
|
|
||||||
|
| 服务 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| HumanInputService | 路由已接入,但缺少单独测试 |
|
||||||
|
| LLMService | 需要 API Key,难以自动化 |
|
||||||
|
| AgentExecutor | 依赖 LLM,需 mock 测试 |
|
||||||
|
| ProcessManager | 需要实际进程管理环境 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、后续修改步骤(按优先级排序)
|
||||||
|
|
||||||
|
### 阶段 1:路由层接入服务层(P0 - 最高优先级)
|
||||||
|
|
||||||
|
这是整个项目的核心瓶颈,完成后前后端即可真正联通。
|
||||||
|
|
||||||
|
#### 步骤 1.1:改造 `agents.py` 路由
|
||||||
|
```
|
||||||
|
目标:用 AgentRegistry 服务替换内存 agents_db
|
||||||
|
涉及:backend/app/routers/agents.py
|
||||||
|
方法:
|
||||||
|
1. 导入 get_agent_registry
|
||||||
|
2. list_agents → registry.list_agents()
|
||||||
|
3. register_agent → registry.register_agent()
|
||||||
|
4. get_agent → registry.get_agent()
|
||||||
|
5. get/update_agent_state → registry.get_state() / update_state()
|
||||||
|
6. 删除 agents_db、agent_states_db 内存变量
|
||||||
|
验证:curl /api/agents 返回真实数据(或空列表)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 1.2:改造 `locks.py` 路由
|
||||||
|
```
|
||||||
|
目标:用 FileLockService 替换内存 locks_db
|
||||||
|
涉及:backend/app/routers/locks.py
|
||||||
|
方法:
|
||||||
|
1. 导入 get_file_lock_service
|
||||||
|
2. list_locks → service.get_locks()
|
||||||
|
3. acquire_lock → service.acquire_lock()
|
||||||
|
4. release_lock → service.release_lock()
|
||||||
|
5. check_lock → service.check_locked()
|
||||||
|
6. 删除 locks_db 硬编码数据
|
||||||
|
验证:curl /api/locks 返回空列表或真实锁状态
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 1.3:改造 `heartbeats.py` 路由
|
||||||
|
```
|
||||||
|
目标:用 HeartbeatService 替换硬编码返回
|
||||||
|
涉及:backend/app/routers/heartbeats.py
|
||||||
|
方法:
|
||||||
|
1. 导入 get_heartbeat_service
|
||||||
|
2. list_heartbeats → service.get_all_heartbeats()
|
||||||
|
3. update_heartbeat → service.update_heartbeat()
|
||||||
|
4. 新增 GET /timeouts?timeout_seconds=60 端点
|
||||||
|
验证:curl /api/heartbeats 返回真实数据
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 1.4:改造 `meetings.py` 路由
|
||||||
|
```
|
||||||
|
目标:用 MeetingScheduler + MeetingRecorder 替换 mock
|
||||||
|
涉及:backend/app/routers/meetings.py
|
||||||
|
方法:
|
||||||
|
1. 导入 get_meeting_scheduler, get_meeting_recorder
|
||||||
|
2. 列表/详情 → recorder.list_meetings() / recorder.get_meeting()
|
||||||
|
3. 创建 → recorder.create_meeting() + scheduler.create_meeting()
|
||||||
|
4. 讨论/进度/完成 → recorder.add_discussion() / update_progress() / end_meeting()
|
||||||
|
5. 新增 GET /:id/queue → scheduler.get_queue()
|
||||||
|
6. 新增 POST /:id/wait → scheduler.wait_for_meeting()
|
||||||
|
7. 新增 POST /:id/end → scheduler.end_meeting()
|
||||||
|
验证:创建会议 → 查看列表 → 添加讨论 → 完成会议 全流程
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 1.5:改造 `roles.py` 路由
|
||||||
|
```
|
||||||
|
目标:用 RoleAllocator 替换硬编码
|
||||||
|
涉及:backend/app/routers/roles.py
|
||||||
|
方法:
|
||||||
|
1. 导入 get_role_allocator
|
||||||
|
2. get_primary_role → allocator.get_primary_role()
|
||||||
|
3. allocate_roles → allocator.allocate_roles()
|
||||||
|
4. explain_roles → allocator.explain_allocation()
|
||||||
|
验证:curl /api/roles/primary 返回基于任务分析的角色
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 1.6:改造 `resources.py` 路由
|
||||||
|
```
|
||||||
|
目标:用 ResourceManager 替换 mock
|
||||||
|
涉及:backend/app/routers/resources.py
|
||||||
|
方法:
|
||||||
|
1. 导入 get_resource_manager
|
||||||
|
2. execute_task → manager.execute_task()
|
||||||
|
3. parse_task → manager.parse_task_files()
|
||||||
|
验证:/api/execute 真正执行任务(加锁→执行→释放锁)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 阶段 2:修复代码问题(P1)
|
||||||
|
|
||||||
|
#### 步骤 2.1:修复依赖
|
||||||
|
```
|
||||||
|
文件:backend/requirements.txt
|
||||||
|
操作:添加 aiohttp 依赖
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 2.2:前端 API 地址可配置
|
||||||
|
```
|
||||||
|
文件:frontend/src/lib/api.ts
|
||||||
|
操作:
|
||||||
|
1. API_BASE 从 localStorage 读取(Settings 页面保存的值)
|
||||||
|
2. 提供 fallback 默认值 http://localhost:8000/api
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 2.3:封装 Agent 控制 API
|
||||||
|
```
|
||||||
|
文件:frontend/src/lib/api.ts + frontend/src/pages/AgentsPage.tsx
|
||||||
|
操作:
|
||||||
|
1. 在 api.ts 中添加 agentControlApi 模块
|
||||||
|
2. AgentsPage 改用封装的 API 调用
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 2.4:修复进度条宽度问题
|
||||||
|
```
|
||||||
|
文件:frontend/src/pages/MeetingsPage.tsx
|
||||||
|
操作:progress_summary 为 "50%" 字符串,作为 CSS width 值可以工作,
|
||||||
|
但建议改为数值计算更健壮
|
||||||
|
```
|
||||||
|
|
||||||
|
### 阶段 3:补全缺失功能(P2)
|
||||||
|
|
||||||
|
#### 步骤 3.1:实现工作流 YAML 上传
|
||||||
|
```
|
||||||
|
后端:新增 POST /api/workflows/upload 端点
|
||||||
|
前端:WorkflowPage.tsx handleFileUpload 对接真实 API
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 3.2:前端接入 WebSocket
|
||||||
|
```
|
||||||
|
目标:替代轮询,使用 WebSocket 实时更新
|
||||||
|
文件:新增 frontend/src/lib/websocket.ts
|
||||||
|
操作:
|
||||||
|
1. 连接 ws://localhost:8000/ws/client/{clientId}
|
||||||
|
2. Dashboard、Resources 页面订阅实时事件
|
||||||
|
3. 保留轮询作为降级方案
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 3.3:清理遗留组件
|
||||||
|
```
|
||||||
|
组件:AgentStatusCard, ResourceMonitorCard, RecentMeetingsCard, ConsensusCard
|
||||||
|
操作:评估是否仍需要,不需要则删除,需要则替换 mock 数据为 API 调用
|
||||||
|
```
|
||||||
|
|
||||||
|
### 阶段 4:测试完善(P2)
|
||||||
|
|
||||||
|
#### 步骤 4.1:更新 E2E 测试选择器
|
||||||
|
```
|
||||||
|
文件:frontend/tests/e2e-workflow.spec.ts
|
||||||
|
操作:将测试选择器更新为与当前 UI 一致
|
||||||
|
- 搜索框选择器
|
||||||
|
- 表单 name 属性
|
||||||
|
- 按钮文案
|
||||||
|
- data-testid 属性
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 4.2:补充后端路由集成测试
|
||||||
|
```
|
||||||
|
位置:backend/
|
||||||
|
操作:
|
||||||
|
1. 新增 test_api_integration.py
|
||||||
|
2. 使用 TestClient 测试所有 API 端点
|
||||||
|
3. 验证路由→服务→存储完整链路
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 4.3:补充服务测试
|
||||||
|
```
|
||||||
|
新增测试:
|
||||||
|
- HumanInputService 单元测试
|
||||||
|
- ProcessManager 单元测试
|
||||||
|
- AgentExecutor mock 测试(mock LLM 调用)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 阶段 5:同步文档(P3)
|
||||||
|
|
||||||
|
#### 步骤 5.1:更新设计文档
|
||||||
|
```
|
||||||
|
文件:docs/design-spec.md
|
||||||
|
操作:
|
||||||
|
1. API 层技术栈改为 Python FastAPI(非 Express/Fastify)
|
||||||
|
2. 前端技术栈更新(无 shadcn/ui,无 Zustand)
|
||||||
|
3. 存储目录结构与实际一致
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、推荐执行顺序
|
||||||
|
|
||||||
|
```
|
||||||
|
Week 1: 阶段 1(路由层改造)
|
||||||
|
Day 1: agents.py + locks.py
|
||||||
|
Day 2: heartbeats.py + meetings.py
|
||||||
|
Day 3: roles.py + resources.py
|
||||||
|
Day 4: 联调验证,修复接口对接问题
|
||||||
|
|
||||||
|
Week 2: 阶段 2 + 3(代码修复 + 缺失功能)
|
||||||
|
Day 1: 依赖修复 + API 地址可配置
|
||||||
|
Day 2: Agent 控制 API 封装 + 进度条修复
|
||||||
|
Day 3: 工作流上传实现
|
||||||
|
Day 4: WebSocket 接入(可选)
|
||||||
|
|
||||||
|
Week 3: 阶段 4 + 5(测试 + 文档)
|
||||||
|
Day 1-2: E2E 测试更新
|
||||||
|
Day 3: 后端集成测试
|
||||||
|
Day 4: 文档同步
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、风险提示
|
||||||
|
|
||||||
|
1. **路由改造可能引发前端数据格式变化**:当前前端适配的是 mock 数据的格式,改为真实服务后,返回值字段名/结构可能不同,需要同步调整前端代码。
|
||||||
|
|
||||||
|
2. **会议的栅栏同步是阻塞调用**:`wait_for_meeting` 会阻塞请求直到所有参会者到齐,在 HTTP API 中需要设置合适的超时时间,或改为异步轮询机制。
|
||||||
|
|
||||||
|
3. **RoleAllocator 依赖 LLM API**:如果没有配置 API Key,角色分配会使用 fallback 逻辑,需要确认 fallback 行为是否满足需求。
|
||||||
|
|
||||||
|
4. **数据持久化**:服务层使用 `.doc/` 目录文件存储,路由改造后原来内存中的临时数据会丢失,需要确保前端能正确处理空数据状态。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录:修复记录(2026-03-10)
|
||||||
|
|
||||||
|
### 已完成的修改
|
||||||
|
|
||||||
|
| 序号 | 修改项 | 文件 | 说明 |
|
||||||
|
|------|--------|------|------|
|
||||||
|
| 1 | agents.py 路由改造 | `backend/app/routers/agents.py` | 移除 `agents_db` 内存变量,接入 `AgentRegistry` 服务 |
|
||||||
|
| 2 | locks.py 路由改造 | `backend/app/routers/locks.py` | 移除 `locks_db` 硬编码数据,接入 `FileLockService` 服务 |
|
||||||
|
| 3 | heartbeats.py 路由改造 | `backend/app/routers/heartbeats.py` | 移除 mock 返回,接入 `HeartbeatService`,新增 `/timeouts` 端点 |
|
||||||
|
| 4 | meetings.py 路由改造 | `backend/app/routers/meetings.py` | 移除 mock,接入 `MeetingScheduler` + `MeetingRecorder`,新增 `/queue` `/wait` `/end` 端点 |
|
||||||
|
| 5 | roles.py 路由改造 | `backend/app/routers/roles.py` | 移除硬编码角色分配,接入 `RoleAllocator` 服务 |
|
||||||
|
| 6 | resources.py 路由改造 | `backend/app/routers/resources.py` | 移除 mock,`/execute` `/parse-task` 接入 `ResourceManager` |
|
||||||
|
| 7 | 依赖修复 | `backend/requirements.txt` | 添加 `aiohttp>=3.9.0` |
|
||||||
|
| 8 | API 地址可配置 | `frontend/src/lib/api.ts` | `API_BASE` 从 localStorage 读取,Settings 页面配置生效 |
|
||||||
|
| 9 | Agent 控制 API 封装 | `frontend/src/lib/api.ts` | 新增 `agentControlApi` 模块,统一封装 |
|
||||||
|
| 10 | AgentsPage 去硬编码 | `frontend/src/pages/AgentsPage.tsx` | 移除 `API_BASE` 硬编码,使用 `agentControlApi` |
|
||||||
|
| 11 | 工作流上传功能 | `backend/app/routers/workflows.py` + `frontend/src/pages/WorkflowPage.tsx` | 新增 `POST /upload` 端点,前端实现真实上传 |
|
||||||
|
| 12 | API 集成测试 | `backend/test_api_integration.py` | 新增 43 项 API 端点集成测试 |
|
||||||
|
|
||||||
|
### 测试结果
|
||||||
|
|
||||||
|
```
|
||||||
|
服务层测试: 9/9 通过
|
||||||
|
API 集成测试: 43/43 通过
|
||||||
|
```
|
||||||
|
|
||||||
|
### 剩余待办
|
||||||
|
|
||||||
|
| 优先级 | 项目 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| P2 | WebSocket 实时更新 | 前端仍用轮询,后端 WebSocket 已实现但前端未接入 |
|
||||||
|
| P2 | 清理遗留组件 | `AgentStatusCard` 等 4 个含 mock 数据的旧组件未被引用 |
|
||||||
|
| P3 | E2E 测试选择器同步 | 约 7 处选择器/文案与当前 UI 不一致 |
|
||||||
|
| P3 | 设计文档同步 | `design-spec.md` 中技术栈描述需更新 |
|
||||||
|
| P3 | 补充服务测试 | HumanInputService、ProcessManager、AgentExecutor 缺少单独测试 |
|
||||||
+95
-2
@@ -11,8 +11,23 @@ import type {
|
|||||||
AgentResourceStatus,
|
AgentResourceStatus,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
// API 基础地址
|
// API 基础地址:优先读取 localStorage(Settings 页面配置),否则使用默认值
|
||||||
const API_BASE = 'http://localhost:8000/api';
|
function getApiBase(): string {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('swarm-settings');
|
||||||
|
if (saved) {
|
||||||
|
const settings = JSON.parse(saved);
|
||||||
|
if (settings.apiBaseUrl) {
|
||||||
|
return settings.apiBaseUrl.replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore parse errors
|
||||||
|
}
|
||||||
|
return 'http://localhost:8000/api';
|
||||||
|
}
|
||||||
|
|
||||||
|
const API_BASE = getApiBase();
|
||||||
|
|
||||||
// 通用请求函数
|
// 通用请求函数
|
||||||
async function request<T>(
|
async function request<T>(
|
||||||
@@ -336,6 +351,47 @@ export const roleApi = {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ==================== Agent 控制 API ====================
|
||||||
|
|
||||||
|
export const agentControlApi = {
|
||||||
|
// 启动 Agent
|
||||||
|
start: (agentId: string, agentType: string, model?: string) =>
|
||||||
|
request<{ success: boolean; agent_id: string; status: string }>('/agents/control/start', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ agent_id: agentId, agent_type: agentType, model }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 停止 Agent
|
||||||
|
stop: (agentId: string) =>
|
||||||
|
request<{ success: boolean; agent_id: string }>('/agents/control/stop', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ agent_id: agentId }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 重启 Agent
|
||||||
|
restart: (agentId: string) =>
|
||||||
|
request<{ success: boolean; agent_id: string }>('/agents/control/restart', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ agent_id: agentId }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 获取 Agent 运行状态
|
||||||
|
getStatus: (agentId: string) =>
|
||||||
|
request<{ agent_id: string; status: string; pid?: number; uptime?: number }>(`/agents/control/status/${agentId}`),
|
||||||
|
|
||||||
|
// 获取所有运行中的 Agent
|
||||||
|
list: () =>
|
||||||
|
request<{ agents: Array<{ agent_id: string; status: string; pid?: number; agent_type?: string }> }>('/agents/control/list'),
|
||||||
|
|
||||||
|
// 获取进程管理器摘要
|
||||||
|
summary: () =>
|
||||||
|
request<{ total: number; running: number; stopped: number }>('/agents/control/summary'),
|
||||||
|
|
||||||
|
// 健康检查
|
||||||
|
health: () =>
|
||||||
|
request<{ status: string }>('/agents/control/health'),
|
||||||
|
};
|
||||||
|
|
||||||
// ==================== 系统 API ====================
|
// ==================== 系统 API ====================
|
||||||
|
|
||||||
export const systemApi = {
|
export const systemApi = {
|
||||||
@@ -424,9 +480,45 @@ export const humanApi = {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ==================== Provider API ====================
|
||||||
|
|
||||||
|
export const providerApi = {
|
||||||
|
list: () =>
|
||||||
|
request<{
|
||||||
|
cli: Array<{
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
display_name: string;
|
||||||
|
description: string;
|
||||||
|
installed: boolean;
|
||||||
|
path: string;
|
||||||
|
models: string[];
|
||||||
|
}>;
|
||||||
|
api: Array<{
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
display_name: string;
|
||||||
|
env_key: string;
|
||||||
|
configured: boolean;
|
||||||
|
models: string[];
|
||||||
|
}>;
|
||||||
|
}>('/providers'),
|
||||||
|
|
||||||
|
models: () =>
|
||||||
|
request<{
|
||||||
|
models: Array<{
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
provider: string;
|
||||||
|
type: string;
|
||||||
|
}>;
|
||||||
|
}>('/providers/models'),
|
||||||
|
};
|
||||||
|
|
||||||
// 导出所有 API
|
// 导出所有 API
|
||||||
export const api = {
|
export const api = {
|
||||||
agent: agentApi,
|
agent: agentApi,
|
||||||
|
agentControl: agentControlApi,
|
||||||
lock: lockApi,
|
lock: lockApi,
|
||||||
heartbeat: heartbeatApi,
|
heartbeat: heartbeatApi,
|
||||||
meeting: meetingApi,
|
meeting: meetingApi,
|
||||||
@@ -435,6 +527,7 @@ export const api = {
|
|||||||
role: roleApi,
|
role: roleApi,
|
||||||
system: systemApi,
|
system: systemApi,
|
||||||
human: humanApi,
|
human: humanApi,
|
||||||
|
provider: providerApi,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Plus, Users, Activity, Cpu, RefreshCw, Play, Square, Power } from 'lucide-react';
|
import { Plus, Users, Activity, Cpu, RefreshCw, Play, Square, Trash2 } from 'lucide-react';
|
||||||
import { api } from '../lib/api';
|
import { api, agentControlApi } from '../lib/api';
|
||||||
import type { Agent, AgentState } from '../types';
|
import type { Agent, AgentState } from '../types';
|
||||||
|
|
||||||
|
interface ModelOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
provider: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
// 注册 Agent 模态框
|
// 注册 Agent 模态框
|
||||||
function RegisterModal({
|
function RegisterModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
@@ -23,9 +30,21 @@ function RegisterModal({
|
|||||||
agent_id: '',
|
agent_id: '',
|
||||||
name: '',
|
name: '',
|
||||||
role: 'developer',
|
role: 'developer',
|
||||||
model: 'claude-opus-4.6',
|
model: '',
|
||||||
description: '',
|
description: '',
|
||||||
});
|
});
|
||||||
|
const [modelOptions, setModelOptions] = useState<ModelOption[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
api.provider.models().then((data) => {
|
||||||
|
setModelOptions(data.models);
|
||||||
|
if (data.models.length > 0 && !form.model) {
|
||||||
|
setForm((f) => ({ ...f, model: data.models[0].value }));
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
@@ -167,13 +186,11 @@ function RegisterModal({
|
|||||||
marginBottom: 6,
|
marginBottom: 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
模型
|
模型 / CLI
|
||||||
</label>
|
</label>
|
||||||
<input
|
<select
|
||||||
type="text"
|
|
||||||
value={form.model}
|
value={form.model}
|
||||||
onChange={(e) => setForm({ ...form, model: e.target.value })}
|
onChange={(e) => setForm({ ...form, model: e.target.value })}
|
||||||
placeholder="模型名称"
|
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '10px 14px',
|
padding: '10px 14px',
|
||||||
@@ -184,7 +201,17 @@ function RegisterModal({
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
{modelOptions.length > 0 ? (
|
||||||
|
modelOptions.map((m) => (
|
||||||
|
<option key={m.value} value={m.value}>
|
||||||
|
{m.label}
|
||||||
|
</option>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<option value="">加载中...</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -236,14 +263,14 @@ function RegisterModal({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (form.agent_id && form.name) {
|
if (form.agent_id && form.name && form.model) {
|
||||||
onSubmit(form);
|
onSubmit(form);
|
||||||
onClose();
|
onClose();
|
||||||
setForm({
|
setForm({
|
||||||
agent_id: '',
|
agent_id: '',
|
||||||
name: '',
|
name: '',
|
||||||
role: 'developer',
|
role: 'developer',
|
||||||
model: 'claude-opus-4.6',
|
model: modelOptions.length > 0 ? modelOptions[0].value : '',
|
||||||
description: '',
|
description: '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -504,9 +531,11 @@ function AgentDetailPanel({
|
|||||||
interface RunningAgent {
|
interface RunningAgent {
|
||||||
agent_id: string;
|
agent_id: string;
|
||||||
status: string;
|
status: string;
|
||||||
is_alive: boolean;
|
is_alive?: boolean;
|
||||||
uptime: number | null;
|
uptime?: number | null;
|
||||||
restart_count: number;
|
restart_count?: number;
|
||||||
|
pid?: number;
|
||||||
|
agent_type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AgentsPage() {
|
export function AgentsPage() {
|
||||||
@@ -518,9 +547,6 @@ export function AgentsPage() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
// API 基础 URL
|
|
||||||
const API_BASE = 'http://localhost:8000/api';
|
|
||||||
|
|
||||||
// 加载 Agent 列表
|
// 加载 Agent 列表
|
||||||
const loadAgents = async () => {
|
const loadAgents = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -538,15 +564,12 @@ export function AgentsPage() {
|
|||||||
// 加载运行中的 Agent
|
// 加载运行中的 Agent
|
||||||
const loadRunningAgents = async () => {
|
const loadRunningAgents = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/agents/control/list`);
|
const data = await agentControlApi.list();
|
||||||
if (res.ok) {
|
const runningMap: Record<string, RunningAgent> = {};
|
||||||
const data = await res.json();
|
(data.agents || []).forEach((agent: RunningAgent) => {
|
||||||
const runningMap: Record<string, RunningAgent> = {};
|
runningMap[agent.agent_id] = agent;
|
||||||
data.forEach((agent: RunningAgent) => {
|
});
|
||||||
runningMap[agent.agent_id] = agent;
|
setRunningAgents(runningMap);
|
||||||
});
|
|
||||||
setRunningAgents(runningMap);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载运行状态失败:', err);
|
console.error('加载运行状态失败:', err);
|
||||||
}
|
}
|
||||||
@@ -555,24 +578,8 @@ export function AgentsPage() {
|
|||||||
// 启动 Agent
|
// 启动 Agent
|
||||||
const startAgent = async (agentId: string, agent: Agent) => {
|
const startAgent = async (agentId: string, agent: Agent) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/agents/control/start`, {
|
await agentControlApi.start(agentId, 'native_llm', agent.model);
|
||||||
method: 'POST',
|
await loadRunningAgents();
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
agent_id: agentId,
|
|
||||||
name: agent.name,
|
|
||||||
role: agent.role,
|
|
||||||
model: agent.model,
|
|
||||||
agent_type: 'native_llm'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
await loadRunningAgents();
|
|
||||||
} else {
|
|
||||||
const data = await res.json();
|
|
||||||
alert(`启动失败: ${data.message || '未知错误'}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(`启动失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
alert(`启动失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||||
}
|
}
|
||||||
@@ -581,25 +588,24 @@ export function AgentsPage() {
|
|||||||
// 停止 Agent
|
// 停止 Agent
|
||||||
const stopAgent = async (agentId: string) => {
|
const stopAgent = async (agentId: string) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/agents/control/stop`, {
|
await agentControlApi.stop(agentId);
|
||||||
method: 'POST',
|
await loadRunningAgents();
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
agent_id: agentId,
|
|
||||||
graceful: true
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
await loadRunningAgents();
|
|
||||||
} else {
|
|
||||||
alert('停止失败');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(`停止失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
alert(`停止失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 删除 Agent
|
||||||
|
const deleteAgent = async (agentId: string) => {
|
||||||
|
if (!confirm(`确认删除 Agent "${agentId}" ?`)) return;
|
||||||
|
try {
|
||||||
|
await fetch(`http://localhost:8000/api/agents/${agentId}`, { method: 'DELETE' });
|
||||||
|
loadAgents();
|
||||||
|
} catch (err) {
|
||||||
|
alert(`删除失败: ${err instanceof Error ? err.message : '未知错误'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 加载 Agent 状态
|
// 加载 Agent 状态
|
||||||
const loadAgentState = async (agentId: string) => {
|
const loadAgentState = async (agentId: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -883,7 +889,7 @@ export function AgentsPage() {
|
|||||||
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.3)', margin: '4px 0 0 0' }}>
|
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.3)', margin: '4px 0 0 0' }}>
|
||||||
创建于: {new Date(agent.created_at).toLocaleDateString()}
|
创建于: {new Date(agent.created_at).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
{/* 启动/停止按钮 */}
|
{/* 启动/停止/删除按钮 */}
|
||||||
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
||||||
{runningAgents[agent.agent_id] ? (
|
{runningAgents[agent.agent_id] ? (
|
||||||
<button
|
<button
|
||||||
@@ -934,6 +940,27 @@ export function AgentsPage() {
|
|||||||
启动
|
启动
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteAgent(agent.agent_id);
|
||||||
|
}}
|
||||||
|
title="删除 Agent"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '8px 10px',
|
||||||
|
background: 'rgba(255,255,255,0.03)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.08)',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'rgba(255,255,255,0.3)',
|
||||||
|
fontSize: 12,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/* 显示运行时长 */}
|
{/* 显示运行时长 */}
|
||||||
{runningAgents[agent.agent_id]?.uptime && (
|
{runningAgents[agent.agent_id]?.uptime && (
|
||||||
|
|||||||
@@ -9,9 +9,31 @@ import {
|
|||||||
Database,
|
Database,
|
||||||
FileJson,
|
FileJson,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
|
Terminal,
|
||||||
|
Key,
|
||||||
|
Cpu,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
|
|
||||||
|
interface CliProvider {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
display_name: string;
|
||||||
|
description: string;
|
||||||
|
installed: boolean;
|
||||||
|
path: string;
|
||||||
|
models: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiProvider {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
display_name: string;
|
||||||
|
env_key: string;
|
||||||
|
configured: boolean;
|
||||||
|
models: string[];
|
||||||
|
}
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
apiBaseUrl: string;
|
apiBaseUrl: string;
|
||||||
refreshInterval: number;
|
refreshInterval: number;
|
||||||
@@ -32,6 +54,8 @@ export function SettingsPage() {
|
|||||||
const [testing, setTesting] = useState(false);
|
const [testing, setTesting] = useState(false);
|
||||||
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||||
const [backendInfo, setBackendInfo] = useState<{ status: string; version: string } | null>(null);
|
const [backendInfo, setBackendInfo] = useState<{ status: string; version: string } | null>(null);
|
||||||
|
const [cliProviders, setCliProviders] = useState<CliProvider[]>([]);
|
||||||
|
const [apiProviders, setApiProviders] = useState<ApiProvider[]>([]);
|
||||||
|
|
||||||
// 从 localStorage 加载配置
|
// 从 localStorage 加载配置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -43,8 +67,19 @@ export function SettingsPage() {
|
|||||||
// 忽略解析错误
|
// 忽略解析错误
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
loadProviders();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const loadProviders = async () => {
|
||||||
|
try {
|
||||||
|
const data = await api.provider.list();
|
||||||
|
setCliProviders(data.cli);
|
||||||
|
setApiProviders(data.api);
|
||||||
|
} catch {
|
||||||
|
// 后端未运行时忽略
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 保存配置
|
// 保存配置
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
localStorage.setItem('swarm-config', JSON.stringify(config));
|
localStorage.setItem('swarm-config', JSON.stringify(config));
|
||||||
@@ -197,6 +232,192 @@ export function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* AI Provider 配置 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: 24,
|
||||||
|
background: 'rgba(17, 24, 39, 0.7)',
|
||||||
|
borderRadius: 12,
|
||||||
|
border: '1px solid rgba(0, 240, 255, 0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 10,
|
||||||
|
background: 'rgba(139, 92, 246, 0.1)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: '#8b5cf6',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Cpu size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 style={{ fontSize: 16, fontWeight: 600, color: '#fff', margin: 0 }}>
|
||||||
|
AI Provider
|
||||||
|
</h3>
|
||||||
|
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: '4px 0 0 0' }}>
|
||||||
|
可用的 CLI 工具和 API 供应商
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={loadProviders}
|
||||||
|
style={{
|
||||||
|
marginLeft: 'auto',
|
||||||
|
padding: '6px 12px',
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'rgba(255,255,255,0.6)',
|
||||||
|
fontSize: 12,
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw size={12} />
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CLI 工具 */}
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 10 }}>
|
||||||
|
<Terminal size={14} color="rgba(255,255,255,0.5)" />
|
||||||
|
<span style={{ fontSize: 13, color: 'rgba(255,255,255,0.6)', fontWeight: 500 }}>
|
||||||
|
CLI 工具
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
|
{cliProviders.map((cli) => (
|
||||||
|
<div
|
||||||
|
key={cli.id}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 12,
|
||||||
|
padding: '10px 14px',
|
||||||
|
background: cli.installed ? 'rgba(0, 255, 157, 0.06)' : 'rgba(255,255,255,0.02)',
|
||||||
|
border: `1px solid ${cli.installed ? 'rgba(0, 255, 157, 0.15)' : 'rgba(255,255,255,0.06)'}`,
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: cli.installed ? '#00ff9d' : '#666',
|
||||||
|
boxShadow: cli.installed ? '0 0 6px #00ff9d' : 'none',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<span style={{ fontSize: 14, color: '#fff', fontWeight: 500 }}>
|
||||||
|
{cli.display_name}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: 11, color: 'rgba(255,255,255,0.3)', marginLeft: 8 }}>
|
||||||
|
{cli.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 11,
|
||||||
|
padding: '3px 8px',
|
||||||
|
borderRadius: 4,
|
||||||
|
background: cli.installed ? '#00ff9d20' : '#ff006e15',
|
||||||
|
color: cli.installed ? '#00ff9d' : '#ff006e',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cli.installed ? '已安装' : '未安装'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{cliProviders.length === 0 && (
|
||||||
|
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.3)', margin: 0 }}>
|
||||||
|
连接后端后显示 CLI 状态
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* API Provider */}
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 10 }}>
|
||||||
|
<Key size={14} color="rgba(255,255,255,0.5)" />
|
||||||
|
<span style={{ fontSize: 13, color: 'rgba(255,255,255,0.6)', fontWeight: 500 }}>
|
||||||
|
API 供应商
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
|
{apiProviders.map((prov) => (
|
||||||
|
<div
|
||||||
|
key={prov.id}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 12,
|
||||||
|
padding: '10px 14px',
|
||||||
|
background: prov.configured ? 'rgba(0, 240, 255, 0.06)' : 'rgba(255,255,255,0.02)',
|
||||||
|
border: `1px solid ${prov.configured ? 'rgba(0, 240, 255, 0.15)' : 'rgba(255,255,255,0.06)'}`,
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: prov.configured ? '#00f0ff' : '#666',
|
||||||
|
boxShadow: prov.configured ? '0 0 6px #00f0ff' : 'none',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<span style={{ fontSize: 14, color: '#fff', fontWeight: 500 }}>
|
||||||
|
{prov.display_name}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 11,
|
||||||
|
color: 'rgba(255,255,255,0.3)',
|
||||||
|
marginLeft: 8,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{prov.env_key}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 11,
|
||||||
|
padding: '3px 8px',
|
||||||
|
borderRadius: 4,
|
||||||
|
background: prov.configured ? '#00f0ff20' : '#ff950015',
|
||||||
|
color: prov.configured ? '#00f0ff' : '#ff9500',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{prov.configured ? '已配置' : '未配置'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{apiProviders.length === 0 && (
|
||||||
|
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.3)', margin: 0 }}>
|
||||||
|
连接后端后显示 API 状态
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: 11, color: 'rgba(255,255,255,0.3)', margin: '10px 0 0 0' }}>
|
||||||
|
API Key 需在后端环境变量中配置(如 ANTHROPIC_API_KEY)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Refresh Settings */}
|
{/* Refresh Settings */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -476,13 +476,20 @@ export function WorkflowPage() {
|
|||||||
|
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
try {
|
try {
|
||||||
await file.text();
|
const formData = new FormData();
|
||||||
// 这里应该调用后端 API 上传文件
|
formData.append('file', file);
|
||||||
// 暂时模拟成功
|
const res = await fetch('http://localhost:8000/api/workflows/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({ detail: '上传失败' }));
|
||||||
|
throw new Error(err.detail || '上传失败');
|
||||||
|
}
|
||||||
alert(`文件 ${file.name} 上传成功`);
|
alert(`文件 ${file.name} 上传成功`);
|
||||||
loadWorkflows();
|
loadWorkflows();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('上传失败');
|
alert(err instanceof Error ? err.message : '上传失败');
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user