完整实现 Swarm 多智能体协作系统

- 新增 CLIPluginAdapter 统一接口 (backend/app/core/agent_adapter.py)
- 新增 LLM 服务层,支持 Anthropic/OpenAI/DeepSeek/Ollama (backend/app/services/llm_service.py)
- 新增 Agent 执行引擎,支持文件锁自动管理 (backend/app/services/agent_executor.py)
- 新增 NativeLLMAgent 原生 LLM 适配器 (backend/app/adapters/native_llm_agent.py)
- 新增进程管理器 (backend/app/services/process_manager.py)
- 新增 Agent 控制 API (backend/app/routers/agents_control.py)
- 新增 WebSocket 实时通信 (backend/app/routers/websocket.py)
- 更新前端 AgentsPage,支持启动/停止 Agent
- 测试通过:Agent 启动、批量操作、栅栏同步

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Code
2026-03-09 17:32:11 +08:00
commit dc398d7c7b
118 changed files with 23120 additions and 0 deletions

12
.claude/settings.json Normal file
View File

@@ -0,0 +1,12 @@
{
"permissions": {
"allow": [
"Bash(powershell -Command \"Get-Process | Where-Object {$_ProcessName -eq ''python''} | Select-Object Id,Path | Format-Table\")",
"Bash(wmic:*)",
"Bash(fuser:*)",
"Read(//d/ccProg/swarm/backend/**)",
"Bash(xargs:*)",
"Bash(powershell -Command \"Stop-Process -Id 31392 -Force -ErrorAction SilentlyContinue; Start-Sleep -Seconds 2\")"
]
}
}

View File

@@ -0,0 +1,9 @@
{
"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"
}

View File

@@ -0,0 +1,7 @@
{
"agent_id": "arch-001",
"current_task": "",
"progress": 0,
"working_files": [],
"last_update": "2026-03-09T17:23:06.852720"
}

View File

@@ -0,0 +1,9 @@
{
"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"
}

View File

@@ -0,0 +1,7 @@
{
"agent_id": "claude-001",
"current_task": "fixing bug",
"progress": 68,
"working_files": [],
"last_update": "2026-03-05T10:17:06.914810"
}

View File

@@ -0,0 +1,9 @@
{
"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"
}

View File

@@ -0,0 +1,7 @@
{
"agent_id": "dev-001",
"current_task": "",
"progress": 0,
"working_files": [],
"last_update": "2026-03-09T17:23:06.867216"
}

View File

@@ -0,0 +1,9 @@
{
"agent_id": "kimi-002",
"name": "Kimi CLI",
"role": "pm",
"model": "moonshot-v1-8k",
"description": "",
"created_at": "2026-03-05T10:17:04.382854",
"status": "idle"
}

View File

@@ -0,0 +1,7 @@
{
"agent_id": "kimi-002",
"current_task": "",
"progress": 0,
"working_files": [],
"last_update": "2026-03-05T10:17:04.387780"
}

View File

@@ -0,0 +1,9 @@
{
"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"
}

View File

@@ -0,0 +1,7 @@
{
"agent_id": "qa-001",
"current_task": "",
"progress": 0,
"working_files": [],
"last_update": "2026-03-09T17:23:06.880737"
}

View File

@@ -0,0 +1,9 @@
{
"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"
}

View File

@@ -0,0 +1,7 @@
{
"agent_id": "test-001",
"current_task": "",
"progress": 0,
"working_files": [],
"last_update": "2026-03-09T17:22:39.236368"
}

View File

@@ -0,0 +1,9 @@
{
"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"
}

View File

@@ -0,0 +1,7 @@
{
"agent_id": "test-agent-001",
"current_task": "修复 bug",
"progress": 75,
"working_files": [],
"last_update": "2026-03-09T09:28:05.280849"
}

1
.doc/cache/file_locks.json vendored Normal file
View File

@@ -0,0 +1 @@
{}

51
.doc/cache/heartbeats.json vendored Normal file
View File

@@ -0,0 +1,51 @@
{
"claude-001": {
"agent_id": "claude-001",
"last_heartbeat": "2026-03-05T10:31:09.197892",
"status": "idle",
"current_task": "",
"progress": 100
},
"kimi-002": {
"agent_id": "kimi-002",
"last_heartbeat": "2026-03-05T10:16:03.435209",
"status": "waiting",
"current_task": "waiting for meeting",
"progress": 0
},
"agent-001": {
"agent_id": "agent-001",
"last_heartbeat": "2026-03-09T09:28:05.259883",
"status": "working",
"current_task": "测试任务",
"progress": 50
},
"test-001": {
"agent_id": "test-001",
"last_heartbeat": "2026-03-09T17:23:02.120192",
"status": "offline",
"current_task": "",
"progress": 0
},
"arch-001": {
"agent_id": "arch-001",
"last_heartbeat": "2026-03-09T17:23:43.464396",
"status": "waiting",
"current_task": "等待会议: meeting-002",
"progress": 0
},
"dev-001": {
"agent_id": "dev-001",
"last_heartbeat": "2026-03-09T17:23:43.476403",
"status": "waiting",
"current_task": "等待会议: meeting-002",
"progress": 0
},
"qa-001": {
"agent_id": "qa-001",
"last_heartbeat": "2026-03-09T17:23:43.485633",
"status": "waiting",
"current_task": "等待会议: meeting-002",
"progress": 0
}
}

84
.doc/cache/meeting_queue.json vendored Normal file
View File

@@ -0,0 +1,84 @@
{
"design-review": {
"meeting_id": "design-review",
"title": "Design Review",
"expected_attendees": [
"claude-001",
"kimi-002",
"opencode-003"
],
"arrived_attendees": [
"claude-001",
"kimi-002",
"opencode-003"
],
"status": "waiting",
"created_at": "2026-03-05T10:18:10.717613",
"started_at": "",
"min_required": 3
},
"quick-test": {
"meeting_id": "quick-test",
"title": "Quick Test",
"expected_attendees": [
"agent-1"
],
"arrived_attendees": [
"agent-1"
],
"status": "ready",
"created_at": "2026-03-05T10:23:53.403438",
"started_at": "2026-03-05T10:23:53.430888",
"min_required": 1
},
"test-meeting-001": {
"meeting_id": "test-meeting-001",
"title": "测试会议",
"expected_attendees": [
"agent-001",
"agent-002"
],
"arrived_attendees": [
"agent-001",
"agent-002"
],
"status": "ended",
"created_at": "2026-03-09T09:28:05.309297",
"started_at": "2026-03-09T09:28:05.357846",
"min_required": 2
},
"meeting-001": {
"meeting_id": "meeting-001",
"title": "项目设计讨论",
"expected_attendees": [
"arch-001",
"dev-001",
"qa-001"
],
"arrived_attendees": [
"arch-001"
],
"status": "waiting",
"created_at": "2026-03-09T17:23:17.393649",
"started_at": "",
"min_required": 3
},
"meeting-002": {
"meeting_id": "meeting-002",
"title": "测试会议",
"expected_attendees": [
"arch-001",
"dev-001",
"qa-001"
],
"arrived_attendees": [
"arch-001",
"dev-001",
"qa-001"
],
"status": "ready",
"created_at": "2026-03-09T17:23:43.445453",
"started_at": "2026-03-09T17:23:43.501216",
"min_required": 3
}
}

16
.doc/humans.json Normal file
View File

@@ -0,0 +1,16 @@
{
"version": "1.0",
"last_updated": "2026-03-09T16:12:21.544408",
"participants": {
"user001": {
"id": "user001",
"name": "开发者",
"role": "tech_lead",
"status": "offline",
"avatar": "👨‍💻"
}
},
"task_requests": [],
"meeting_comments": [],
"human_states": {}
}

View File

@@ -0,0 +1,49 @@
{
"meeting_id": "auth-review",
"title": "认证方案设计评审",
"date": "2026-03-05",
"attendees": [
"claude-001",
"kimi-002",
"opencode-003"
],
"steps": [
{
"step_id": "step_1",
"label": "收集初步想法",
"status": "pending",
"completed_at": ""
},
{
"step_id": "step_2",
"label": "讨论与迭代",
"status": "active",
"completed_at": ""
},
{
"step_id": "step_3",
"label": "生成共识版本",
"status": "pending",
"completed_at": ""
},
{
"step_id": "step_4",
"label": "记录会议文件",
"status": "pending",
"completed_at": ""
}
],
"discussions": [
{
"agent_id": "claude-001",
"agent_name": "CLA",
"content": "建议使用 JWT",
"timestamp": "2026-03-05T10:29:55.080827",
"step": "收集初步想法"
}
],
"status": "in_progress",
"created_at": "2026-03-05T10:29:22.772247",
"ended_at": "",
"consensus": ""
}

View File

@@ -0,0 +1,24 @@
# 认证方案设计评审
**会议 ID**: auth-review
**日期**: 2026-03-05
**状态**: in_progress
**参会者**: claude-001, kimi-002, opencode-003
## 会议进度
-**收集初步想法**
-**讨论与迭代**
-**生成共识版本**
-**记录会议文件**
## 讨论记录
### CLA - 2026-03-05T10:29:55
*步骤: 收集初步想法*
建议使用 JWT
---
**创建时间**: 2026-03-05T10:29:22.772247

View File

@@ -0,0 +1,49 @@
{
"meeting_id": "test-record-001",
"title": "测试记录会议",
"date": "2026-03-09",
"attendees": [
"agent-001",
"agent-002"
],
"steps": [
{
"step_id": "step_1",
"label": "步骤1",
"status": "completed",
"completed_at": "2026-03-09T09:28:05.413770"
},
{
"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-09T09:28:05.386072",
"step": ""
},
{
"agent_id": "agent-002",
"agent_name": "Agent2",
"content": "这是第二条讨论",
"timestamp": "2026-03-09T09:28:05.393792",
"step": ""
}
],
"status": "completed",
"created_at": "2026-03-09T09:28:05.378802",
"ended_at": "2026-03-09T09:28:05.413764",
"consensus": "达成共识:继续开发"
}

View File

@@ -0,0 +1,31 @@
# 测试记录会议
**会议 ID**: test-record-001
**日期**: 2026-03-09
**状态**: completed
**参会者**: agent-001, agent-002
## 会议进度
-**步骤1** (2026-03-09T09:28:05.413770)
-**步骤2**
-**步骤3**
## 讨论记录
### Agent1 - 2026-03-09T09:28:05
这是第一条讨论
### Agent2 - 2026-03-09T09:28:05
这是第二条讨论
## 共识
达成共识:继续开发
---
**创建时间**: 2026-03-09T09:28:05.378802
**结束时间**: 2026-03-09T09:28:05.413764

View File

@@ -0,0 +1,67 @@
# 默认开发工作流
# 标准软件开发流程:需求 → 分配 → 执行 → 对齐 → 测试
workflow_id: "default-dev-flow"
name: "默认开发工作流"
description: "标准软件开发全流程,包含需求分析、任务分配、执行、对齐、测试等阶段"
meetings:
# 1. 需求会议 - 讨论和确认项目需求
- meeting_id: "requirements-meeting"
title: "需求会议"
node_type: "meeting"
attendees: ["pm-001", "architect-001", "developer-001"]
depends_on: []
# 2. 任务分配会议 - 将需求分解为具体任务并分配
- meeting_id: "task-allocation-meeting"
title: "任务分配会议"
node_type: "meeting"
attendees: ["pm-001", "architect-001", "qa-001", "developer-001"]
depends_on: ["requirements-meeting"]
# 3. 任务执行 - Agent 并行执行分配的任务
- meeting_id: "task-execution"
title: "任务执行"
node_type: "execution"
attendees: ["developer-001", "developer-002"]
min_required: 2
depends_on: ["task-allocation-meeting"]
# 4. 中段任务对齐会议 - 检查进度,同步状态,解决问题
- meeting_id: "mid-alignment-meeting"
title: "中段任务对齐会议"
node_type: "meeting"
attendees: ["developer-001", "developer-002", "qa-001"]
depends_on: ["task-execution"]
# 5. 任务继续 - 根据对齐结果继续完成剩余任务
- meeting_id: "task-continue"
title: "任务继续"
node_type: "execution"
attendees: ["developer-001", "developer-002"]
min_required: 2
depends_on: ["mid-alignment-meeting"]
# 6. 测试任务会议 - 制定测试计划和测试用例
- meeting_id: "test-planning-meeting"
title: "测试任务会议"
node_type: "meeting"
attendees: ["qa-001", "developer-001", "pm-001"]
depends_on: ["task-continue"]
# 7. 测试任务执行 - QA 执行测试
- meeting_id: "test-execution"
title: "测试任务执行"
node_type: "execution"
attendees: ["qa-001"]
min_required: 1
depends_on: ["test-planning-meeting"]
# 8. 测试结果研读会议 - 分析测试结果,决定下一步
# 如果测试不通过,跳转回任务分配会议进行修复
- meeting_id: "test-review-meeting"
title: "测试结果研读会议"
node_type: "meeting"
attendees: ["pm-001", "qa-001", "developer-001", "architect-001"]
depends_on: ["test-execution"]
on_failure: "task-allocation-meeting" # 测试不通过时回到任务分配

View File

@@ -0,0 +1,25 @@
# 示例工作流定义
workflow_id: "example_project"
name: "示例项目工作流"
description: "展示多智能体协作的典型工作流"
meetings:
- meeting_id: "requirements-review"
title: "需求评审"
attendees: ["claude-001", "kimi-002"]
depends_on: []
- meeting_id: "design-review"
title: "设计评审"
attendees: ["claude-001", "opencode-003"]
depends_on: ["requirements-review"]
- meeting_id: "implementation-planning"
title: "实现计划"
attendees: ["claude-001", "kimi-002", "opencode-003"]
depends_on: ["design-review"]
- meeting_id: "code-review"
title: "代码评审"
attendees: ["claude-001", "opencode-003"]
depends_on: ["implementation-planning"]

13
.doc/workflow/test.yaml Normal file
View File

@@ -0,0 +1,13 @@
workflow_id: "test-workflow"
name: "测试工作流"
description: "用于测试的工作流"
meetings:
- meeting_id: "step1"
title: "第一步"
attendees: ["agent-001"]
depends_on: []
- meeting_id: "step2"
title: "第二步"
attendees: ["agent-001", "agent-002"]
depends_on: ["step1"]

43
.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
ENV/
*.egg-info/
dist/
build/
.pytest_cache/
.pytest_cache/
# Node
frontend/node_modules/
frontend/dist/
*.log
.DS_Store
# IDE
.vscode/
.idea/
*.swp
*.swo
# Temporary files
*.tmp
*.temp
NUL
# Claude
.claude/projects/
.claude/cache/
# Playwright
.playwright-mcp/
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini

323
CLAUDE.md Normal file
View File

@@ -0,0 +1,323 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
**Swarm Command Center** 是一个多智能体协作系统,支持 Claude Code、Kimi CLI、OpenCode 等多种 AI CLI 工具的协同工作。
**当前状态**:前端 UI 已完成,后端协调引擎已完整实现并通过测试。
## 开发命令
### 前端React 18 + TypeScript + Vite + Tailwind CSS v4
```bash
cd frontend
npm install # 安装依赖
npm run dev # 启动开发服务器(端口 3000
npm run build # 构建生产版本tsc && vite build
npm run preview # 预览生产构建
npm run lint # ESLint 代码检查
npm run test # 运行 Playwright E2E 测试
npm run test:ui # 运行 Playwright 测试UI 模式)
```
**前端页面**
- [DashboardPage.tsx](frontend/src/pages/DashboardPage.tsx) - 仪表盘概览、Agent 状态、最近会议)
- [AgentsPage.tsx](frontend/src/pages/AgentsPage.tsx) - Agent 管理
- [MeetingsPage.tsx](frontend/src/pages/MeetingsPage.tsx) - 会议管理
- [ResourcesPage.tsx](frontend/src/pages/ResourcesPage.tsx) - 资源监控
- [WorkflowPage.tsx](frontend/src/pages/WorkflowPage.tsx) - 工作流
- [SettingsPage.tsx](frontend/src/pages/SettingsPage.tsx) - 配置
### 后端Python + FastAPI
```bash
cd backend
python -m venv venv
# Windows:
venv\Scripts\activate
# Linux/Mac:
source venv/bin/activate
pip install -r requirements.txt
# 启动服务
python -m uvicorn main:app --reload --port 8000
# 或直接运行
python -m app.main
# 运行所有服务测试
python test_all_services.py
```
**后端 CLI 命令**(在 `backend/` 目录下执行):
```bash
# Agent 管理
python cli.py agent list # 列出所有 Agent
python cli.py agent register <id> --name <名称> --role <角色> --model <模型>
python cli.py agent info <id> # 查看 Agent 详情
python cli.py agent state <id> get # 获取 Agent 状态
python cli.py agent state <id> set --task <任务> --progress <进度>
# 文件锁
python cli.py lock status # 查看所有锁
python cli.py lock acquire <file_path> <agent_id> # 获取锁
python cli.py lock release <file_path> <agent_id> # 释放锁
python cli.py lock check <file_path> # 检查文件是否被锁定
# 心跳
python cli.py heartbeat list # 查看所有心跳
python cli.py heartbeat ping <agent_id> --status <状态> --task <任务> --progress <进度>
python cli.py heartbeat check-timeout <秒数> # 检查超时的 Agent
# 会议调度(栅栏同步)
python cli.py meeting create <id> --title <标题> --attendees <id1,id2,...>
python cli.py meeting wait <meeting_id> --agent <agent_id> # 栅栏同步等待(阻塞)
python cli.py meeting queue <meeting_id> # 查看等待队列
python cli.py meeting end <meeting_id> # 结束会议
# 会议记录
python cli.py meeting record-create <id> --title <标题> --attendees <ids> --steps <步骤>
python cli.py meeting discuss <id> --agent <agent_id> --content <内容> --step <步骤>
python cli.py meeting progress <id> <步骤> # 更新会议进度
python cli.py meeting show <id> [--date <日期>] # 显示会议详情
python cli.py meeting list [--date <日期>] # 列出会议
python cli.py meeting finish <id> --consensus <共识> # 完成会议
# 工作流
python cli.py workflow show <path> # 显示工作流详情
python cli.py workflow load <path> # 加载工作流
python cli.py workflow next <workflow_id> # 获取下一个会议
python cli.py workflow status <workflow_id> # 获取工作流状态
python cli.py workflow complete <workflow_id> <meeting_id> # 标记会议完成
python cli.py workflow list-files # 列出可用工作流文件
python cli.py workflow detail <workflow_id> # 获取工作流详细信息
python cli.py workflow execution-status <workflow_id> <meeting_id> # 获取执行节点状态
# 角色分配
python cli.py role allocate <任务描述> <agent1,agent2,...> # AI 分配角色
python cli.py role primary <任务描述> # 获取主要角色
python cli.py role explain <任务描述> <agents> # 解释角色分配原因
# 人类输入
python cli.py human register <user_id> --name <名称> --role <角色>
python cli.py human add-task <内容> --from <用户> --priority <优先级>
python cli.py human pending-tasks # 查看待处理任务
python cli.py human urgent-tasks # 查看紧急任务
```
## 系统架构
### 四层架构
```
┌─────────────────────────────────────────┐
│ 用户界面层 (React SPA) │
│ - DashboardPage 仪表盘 │
│ - AgentsPage Agent 管理 │
│ - MeetingsPage 会议管理 │
│ - ResourcesPage 资源监控 │
│ - WorkflowPage 工作流 │
│ - SettingsPage 配置 │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 协调层 (Python FastAPI) │
│ - WorkflowEngine 工作流编排 │
│ - MeetingScheduler 栅栏同步 │
│ - MeetingRecorder 会议记录 │
│ - ResourceManager 资源管理 │
│ - FileLockService 文件锁 │
│ - HeartbeatService 心跳检测 │
│ - AgentRegistry Agent 注册 │
│ - RoleAllocator AI 角色分配 │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Agent 适配层 │
│ - CLIPluginAdapter 统一接口 │
│ - ClaudeCode / KimiCLI / OpenCode │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 模型层 (Anthropic / OpenAI / Ollama) │
└─────────────────────────────────────────┘
```
### 共享存储结构 (`.doc/`)
```
.doc/
├── agents/ # Agent 注册与状态
│ └── {agent_id}/
│ ├── info.json
│ └── state.json
├── meetings/ # 会议记录与共识
│ └── {YYYY-MM-DD}/
│ ├── {meeting_id}.json
│ └── {meeting_id}.md
├── resources/ # 资源分配
├── cache/ # 实时缓存
│ ├── file_locks.json
│ ├── heartbeats.json
│ └── meeting_queue.json
├── workflow/ # 工作流定义 (YAML)
└── humans.json # 人类参与者输入
```
## 核心设计模式
### 1. 声明式资源管理
Agent 只需声明任务意图,系统通过 `TaskExecutor` 自动管理文件锁:
```python
# Agent 代码 - 无需手动处理锁
result = executor.execute("修改 src/main.py 修复登录 bug")
# 内部自动: 1)解析文件 2)获取锁 3)执行 4)释放锁
```
### 2. 栅栏同步(会议驱动)
```python
# Agent 调用 wait_for_meeting 进入等待
status = scheduler.wait_for_meeting(agent_id, meeting_id)
# 最后一个到达的 Agent 触发会议开始,所有等待者返回
```
### 3. 角色权重系统
| 角色 | 权重 | 职责 |
|------|------|------|
| pm | 1.5 | 需求分析、优先级排序 |
| architect | 1.5 | 系统设计、技术选型 |
| reviewer | 1.3 | 代码审查、安全检查 |
| qa | 1.2 | 测试用例、自动化测试 |
| developer | 1.0 | 编码实现 |
## 前端样式系统
### 颜色体系
- 主色Cyan `#00f0ff`
- 辅助Purple `#8b5cf6`
- 成功Green `#00ff9d`
- 警告Amber `#ff9500`
- 错误Pink `#ff006e`
- 背景Dark `#0a0a0a` / `#111111`
### 字体
- 标题Orbitron未来感
- 中文Noto Sans SC
- 代码JetBrains Mono
### 动画效果
- Agent 卡片呼吸光晕动画3s infinite
- 状态指示器:脉动效果
- 边框:渐变发光
## 后端服务架构
### 服务层(`backend/app/services/`
| 服务 | 文件 | 职责 |
|------|------|------|
| StorageService | [storage.py](backend/app/services/storage.py) | 统一文件存储JSON读写、存在检查 |
| FileLockService | [file_lock.py](backend/app/services/file_lock.py) | 文件锁管理(获取、释放、超时检测) |
| HeartbeatService | [heartbeat.py](backend/app/services/heartbeat.py) | Agent 心跳检测与活跃状态 |
| AgentRegistry | [agent_registry.py](backend/app/services/agent_registry.py) | Agent 注册、状态管理 |
| MeetingScheduler | [meeting_scheduler.py](backend/app/services/meeting_scheduler.py) | 栅栏同步、会议等待队列 |
| MeetingRecorder | [meeting_recorder.py](backend/app/services/meeting_recorder.py) | 会议记录、讨论、共识 |
| ResourceManager | [resource_manager.py](backend/app/services/resource_manager.py) | 资源协调、任务执行 |
| WorkflowEngine | [workflow_engine.py](backend/app/services/workflow_engine.py) | YAML 工作流加载与执行 |
| RoleAllocator | [role_allocator.py](backend/app/services/role_allocator.py) | AI 驱动的角色分配 |
| HumanInputService | [human_input.py](backend/app/services/human_input.py) | 人类参与者输入管理 |
### 路由层(`backend/app/routers/`
- [agents.py](backend/app/routers/agents.py) - Agent 管理 API
- [locks.py](backend/app/routers/locks.py) - 文件锁 API
- [meetings.py](backend/app/routers/meetings.py) - 会议 API
- [heartbeats.py](backend/app/routers/heartbeats.py) - 心跳 API
- [workflows.py](backend/app/routers/workflows.py) - 工作流 API
- [resources.py](backend/app/routers/resources.py) - 资源 API
- [roles.py](backend/app/routers/roles.py) - 角色 API
- [humans.py](backend/app/routers/humans.py) - 人类输入 API
## API 接口
- **基础地址**: `http://localhost:8000/api`
- **健康检查**: `GET /health``GET /api/health`
**主要端点**:
- `GET /api/agents` - Agent 列表
- `GET /api/locks` - 文件锁状态
- `GET /api/heartbeats` - 心跳状态
- `POST /api/meetings/create` - 创建会议
- `GET /api/meetings/:id` - 会议详情
- `POST /api/workflows/load` - 加载工作流
详见 `docs/api-reference.md`
## 文档索引
- `docs/design-spec.md` - 完整系统设计文档
- `docs/api-reference.md` - API 接口文档
- `docs/backend-steps.md` - 后端开发步骤
- `docs/frontend-steps.md` - 前端开发步骤
- `docs/reference-projects.md` - 参考项目
## 测试
### 后端测试
运行完整的服务测试套件:
```bash
cd backend
python test_all_services.py
```
这会测试所有 9 个核心服务:
- StorageService - 文件读写
- FileLockService - 文件锁
- HeartbeatService - 心跳
- AgentRegistry - Agent 注册
- MeetingScheduler - 会议调度(栅栏同步)
- MeetingRecorder - 会议记录
- ResourceManager - 资源管理
- WorkflowEngine - 工作流引擎
- RoleAllocator - 角色分配
### 前端测试
Playwright E2E 测试配置在 [playwright.config.ts](frontend/playwright.config.ts)
```bash
cd frontend
npm run test # 运行测试headless
npm run test:ui # 运行测试UI 模式)
```
测试文件位于 `frontend/tests/` 目录。
## 开发提示
### Windows 环境注意事项
- 使用 `venv\Scripts\activate` 激活 Python 虚拟环境
- 路径使用正斜杠 `/` 或双反斜杠 `\\`
- 使用 PowerShell 或 Git Bash 执行 shell 命令
### 单例服务
所有后端服务通过单例模式获取:
```python
from app.services.storage import get_storage
from app.services.file_lock import get_file_lock_service
# ...
storage = get_storage() # 自动初始化并缓存
```

1
backend/app/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Swarm Command Center Backend App"""

View File

@@ -0,0 +1,8 @@
"""Agent 适配器模块"""
from .native_llm_agent import NativeLLMAgent, NativeLLMAgentFactory
__all__ = [
"NativeLLMAgent",
"NativeLLMAgentFactory"
]

View File

@@ -0,0 +1,497 @@
"""
原生 LLM Agent 适配器
直接调用 LLM API 实现 Agent不需要外部进程。
这是主要使用的适配器类型。
"""
import asyncio
import logging
from typing import Dict, Optional, Any
from datetime import datetime
from ..core.agent_adapter import (
CLIPluginAdapter,
Task,
Result,
AgentCapabilities
)
from ..services.llm_service import ModelRouter, get_llm_service
from ..services.agent_executor import AgentExecutor, get_agent_executor
from ..services.meeting_scheduler import get_meeting_scheduler
from ..services.agent_registry import get_agent_registry
from ..services.heartbeat import get_heartbeat_service
logger = logging.getLogger(__name__)
class NativeLLMAgent(CLIPluginAdapter):
"""
原生 LLM Agent
直接通过 LLM API 实现的 Agent特点
1. 无需外部进程,完全异步
2. 支持所有主流 LLM 提供商
3. 自动管理资源(文件锁、心跳)
4. 支持会议协作
"""
def __init__(
self,
agent_id: str,
name: str,
role: str,
model: str,
config: Dict[str, Any] = None,
llm_service: ModelRouter = None,
executor: AgentExecutor = None
):
self._id = agent_id
self._name = name
self._role = role
self._model = model
self.config = config or {}
self._version = "1.0.0"
# 获取服务
self.llm_service = llm_service or get_llm_service()
self.executor = executor or get_agent_executor(self.llm_service)
self.scheduler = get_meeting_scheduler()
self.registry = get_agent_registry()
self.heartbeat_service = get_heartbeat_service()
# 状态
self._is_running = False
self._current_task: Optional[Task] = None
logger.info(f"NativeLLMAgent 初始化: {self._id} ({self._name})")
# ========== 属性 ==========
@property
def id(self) -> str:
return self._id
@property
def name(self) -> str:
return self._name
@property
def version(self) -> str:
return self._version
@property
def role(self) -> str:
return self._role
@property
def model(self) -> str:
return self._model
@property
def capabilities(self) -> AgentCapabilities:
"""返回 Agent 能力声明"""
return AgentCapabilities(
can_execute_code=True,
can_read_files=True,
can_write_files=True,
can_analyze_code=True,
can_generate_tests=self._role in ["developer", "qa"],
can_review_code=self._role == "reviewer",
supported_languages=["Python", "JavaScript", "TypeScript", "Java", "Go"],
max_context_length=200000
)
@property
def is_running(self) -> bool:
return self._is_running
# ========== 核心能力 ==========
async def execute(self, task: Task) -> Result:
"""
执行任务
通过 AgentExecutor 协调 LLM 调用和资源管理
"""
self._current_task = task
try:
# 获取或注册 Agent 信息
agent_info = await self._ensure_registered()
# 使用执行引擎执行任务
result = await self.executor.execute_task(
agent_info,
task,
context=self.config.get("context", {})
)
logger.info(f"任务执行完成: {task.task_id} -> {result.success}")
return result
except Exception as e:
logger.error(f"任务执行失败: {task.task_id}: {e}", exc_info=True)
return Result(
success=False,
output="",
error=str(e)
)
finally:
self._current_task = None
async def join_meeting(self, meeting_id: str, timeout: int = 300) -> str:
"""
加入会议等待队列(栅栏同步)
当最后一个参与者到达时,会议自动开始
"""
logger.info(f"Agent {self._id} 等待会议: {meeting_id}")
# 更新心跳为等待状态
await self.update_heartbeat("waiting", f"等待会议: {meeting_id}", 0)
# 调用会议调度器
result = await self.scheduler.wait_for_meeting(
self._id,
meeting_id,
timeout=timeout
)
logger.info(f"会议 {meeting_id} 结果: {result}")
return result
async def write_state(self, state: Dict) -> None:
"""
写入状态到注册表
状态包含:当前任务、进度、临时数据等
"""
task = state.get("task", "")
progress = state.get("progress", 0)
await self.registry.update_state(self._id, task, progress)
logger.debug(f"状态已更新: {self._id} -> {progress}%")
async def read_others(self, agent_id: str) -> Dict:
"""
读取其他 Agent 的状态
用于 Agent 之间互相了解工作状态
"""
agent = await self.registry.get_agent(agent_id)
if not agent:
return {"error": f"Agent {agent_id} 不存在"}
state = await self.registry.get_state(agent_id)
heartbeat = await self.heartbeat_service.get_heartbeat(agent_id)
return {
"agent": {
"agent_id": agent.agent_id,
"name": agent.name,
"role": agent.role,
"model": agent.model,
"status": agent.status
},
"state": {
"current_task": state.current_task if state else None,
"progress": state.progress if state else 0
} if state else None,
"heartbeat": {
"status": heartbeat.status if heartbeat else None,
"last_seen": heartbeat.last_seen.isoformat() if heartbeat else None,
"is_alive": heartbeat.is_alive() if heartbeat else False
} if heartbeat else None
}
async def update_heartbeat(self, status: str, task: str = "", progress: int = 0) -> None:
"""
更新心跳
参数:
status: working, waiting, idle, error
task: 当前任务描述
progress: 进度 0-100
"""
await self.heartbeat_service.update_heartbeat(
self._id,
status,
task,
progress
)
# ========== 生命周期 ==========
async def initialize(self) -> None:
"""Agent 初始化"""
logger.info(f"Agent 初始化: {self._id}")
# 确保已注册
await self._ensure_registered()
# 发送初始心跳
await self.update_heartbeat("idle", "", 0)
self._is_running = True
async def shutdown(self) -> None:
"""Agent 关闭"""
logger.info(f"Agent 关闭: {self._id}")
# 更新状态为离线
await self.update_heartbeat("offline", "", 0)
self._is_running = False
async def health_check(self) -> bool:
"""健康检查"""
try:
heartbeat = await self.heartbeat_service.get_heartbeat(self._id)
return heartbeat is not None and heartbeat.is_alive()
except Exception:
return False
# ========== 会议相关 ==========
async def propose(self, meeting_id: str, content: str, step: str = "") -> None:
"""在会议中提出提案"""
from ..services.meeting_recorder import get_meeting_recorder
recorder = get_meeting_recorder()
await recorder.add_discussion(
meeting_id,
self._id,
self._role.upper(),
content,
step
)
logger.debug(f"提案已添加: {self._id} -> {meeting_id}")
async def discuss(self, meeting_id: str, content: str, step: str = "") -> None:
"""在会议中参与讨论"""
await self.propose(meeting_id, content, step)
async def vote(self, meeting_id: str, proposal_id: str, agree: bool) -> None:
"""对提案进行投票"""
# TODO: 实现投票机制
logger.debug(f"投票: {self._id} -> {proposal_id}: {agree}")
# ========== 私有方法 ==========
async def _ensure_registered(self):
"""确保 Agent 已注册"""
agent = await self.registry.get_agent(self._id)
if agent is None:
# 注册 Agent
agent = await self.registry.register_agent(
self._id,
self._name,
self._role,
self._model,
self.config.get("description", f"{self._name} - {self._role}")
)
logger.info(f"Agent 已注册: {self._id}")
return agent
# ========== 高级功能 ==========
async def collaborate_with(
self,
other_agent_ids: list,
task: str,
meeting_id: str = None
) -> Dict:
"""
与其他 Agent 协作完成任务
流程:
1. 创建或加入会议
2. 等待所有 Agent 到达
3. 讨论和分工
4. 执行分配的任务
5. 汇总结果
"""
if not meeting_id:
# 生成会议 ID
import uuid
meeting_id = f"meeting_{uuid.uuid4().hex[:12]}"
# 创建会议
await self.scheduler.create_meeting(
meeting_id,
f"协作任务: {task[:50]}",
[self._id] + other_agent_ids
)
# 更新心跳
await self.update_heartbeat("waiting", f"等待协作会议: {meeting_id}", 0)
# 提出初始提案
await self.propose(meeting_id, f"任务: {task}")
return {
"meeting_id": meeting_id,
"status": "waiting",
"participants": [self._id] + other_agent_ids
}
async def start_collaboration_loop(
self,
meeting_id: str,
max_iterations: int = 5
) -> Dict:
"""
启动协作循环
持续参与会议讨论,直到达成共识或达到最大迭代次数
"""
from ..services.meeting_recorder import get_meeting_recorder
recorder = get_meeting_recorder()
for iteration in range(max_iterations):
# 等待会议开始
result = await self.join_meeting(meeting_id)
if result == "started":
# 获取会议信息
meeting = await recorder.get_meeting(meeting_id)
if meeting and meeting.status == "completed":
return {
"status": "consensus_reached",
"consensus": meeting.consensus,
"iterations": iteration + 1
}
# 分析当前讨论,提出自己的观点
await self._analyze_and_respond(meeting_id)
elif result == "timeout":
return {
"status": "timeout",
"iterations": iteration + 1
}
return {
"status": "max_iterations_reached",
"iterations": max_iterations
}
async def _analyze_and_respond(self, meeting_id: str) -> None:
"""分析会议讨论并响应"""
from ..services.meeting_recorder import get_meeting_recorder
recorder = get_meeting_recorder()
meeting = await recorder.get_meeting(meeting_id)
if not meeting:
return
# 获取最近的讨论
recent_discussions = meeting.discussions[-5:] if meeting.discussions else []
# 构建分析提示
discussion_summary = "\n".join([
f"{d.agent} ({d.timestamp}): {d.content}"
for d in recent_discussions
])
response_prompt = f"""
你是 {self._name} ({self._role})。
以下是会议讨论的摘要:
{discussion_summary}
请基于你的角色 ({self._role}),给出你的回应:
- 如果你不同意之前的提案,说明理由
- 如果你有更好的建议,提出新方案
- 如果你同意,可以表示支持或补充细节
保持简洁,直接回应。
"""
# 调用 LLM 生成响应
if self.llm_service:
try:
llm_response = await self.llm_service.route_task(
task=response_prompt,
messages=[
{"role": "system", "content": f"你是 {self._name},一个 {self._role}"},
{"role": "user", "content": response_prompt}
]
)
# 发送响应到会议
await self.discuss(meeting_id, llm_response.content)
except Exception as e:
logger.error(f"生成会议响应失败: {e}")
class NativeLLMAgentFactory:
"""原生 LLM Agent 工厂类"""
@staticmethod
async def create(
agent_id: str,
name: str = None,
role: str = "developer",
model: str = "claude-sonnet-4.6",
config: Dict = None
) -> NativeLLMAgent:
"""
创建并初始化一个 Agent
参数:
agent_id: Agent 唯一标识
name: 显示名称(默认从 agent_id 生成)
role: 角色 (architect, pm, developer, qa, reviewer)
model: 使用的模型
config: 额外配置
"""
if name is None:
name = agent_id.replace("-", " ").title()
agent = NativeLLMAgent(
agent_id=agent_id,
name=name,
role=role,
model=model,
config=config
)
# 初始化 Agent
await agent.initialize()
return agent
@staticmethod
async def create_team(team_config: Dict) -> Dict[str, NativeLLMAgent]:
"""
创建一个 Agent 团队
配置格式:
{
"team_id": "dev-team-1",
"agents": [
{"id": "arch-001", "role": "architect", "model": "claude-opus-4.6"},
{"id": "dev-001", "role": "developer", "model": "claude-sonnet-4.6"},
{"id": "qa-001", "role": "qa", "model": "claude-haiku-4.6"}
]
}
"""
agents = {}
for agent_config in team_config.get("agents", []):
agent = await NativeLLMAgentFactory.create(
agent_id=agent_config["id"],
role=agent_config.get("role", "developer"),
model=agent_config.get("model", "claude-sonnet-4.6"),
config=agent_config.get("config", {})
)
agents[agent.id] = agent
return agents

View File

@@ -0,0 +1,23 @@
"""Swarm 核心模块"""
from .agent_adapter import (
CLIPluginAdapter,
Task,
Result,
AgentCapabilities,
AdapterError,
AdapterConnectionError,
AdapterExecutionError,
AdapterTimeoutError
)
__all__ = [
"CLIPluginAdapter",
"Task",
"Result",
"AgentCapabilities",
"AdapterError",
"AdapterConnectionError",
"AdapterExecutionError",
"AdapterTimeoutError"
]

View File

@@ -0,0 +1,224 @@
"""
Swarm Agent 适配器核心接口
定义所有 Agent 适配器必须实现的统一接口,确保不同类型的 Agent
(原生 LLM Agent、外部 CLI 工具包装 Agent 等)能够无缝协作。
"""
from abc import ABC, abstractmethod
from typing import Dict, Optional, Any
from dataclasses import dataclass, field
from datetime import datetime
import asyncio
@dataclass
class Task:
"""Agent 任务描述"""
description: str
task_id: str
context: Dict[str, Any] = field(default_factory=dict)
priority: str = "medium" # high, medium, low
deadline: Optional[datetime] = None
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict:
return {
"task_id": self.task_id,
"description": self.description,
"context": self.context,
"priority": self.priority,
"deadline": self.deadline.isoformat() if self.deadline else None,
"metadata": self.metadata
}
@dataclass
class Result:
"""Agent 任务执行结果"""
success: bool
output: str
error: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)
execution_time: float = 0.0
tokens_used: int = 0
def to_dict(self) -> Dict:
return {
"success": self.success,
"output": self.output,
"error": self.error,
"metadata": self.metadata,
"execution_time": self.execution_time,
"tokens_used": self.tokens_used
}
@dataclass
class AgentCapabilities:
"""Agent 能力声明"""
can_execute_code: bool = False
can_read_files: bool = False
can_write_files: bool = False
can_analyze_code: bool = False
can_generate_tests: bool = False
can_review_code: bool = False
supported_languages: list = field(default_factory=list)
max_context_length: int = 200000
class CLIPluginAdapter(ABC):
"""
CLI 工具适配器统一接口
所有 Agent 适配器必须实现此接口,确保:
1. 统一的任务执行方式
2. 一致的会议参与机制
3. 标准化的状态管理
4. 可靠的心跳报告
"""
@property
@abstractmethod
def id(self) -> str:
"""Agent 唯一标识符"""
pass
@property
@abstractmethod
def name(self) -> str:
"""Agent 显示名称"""
pass
@property
@abstractmethod
def version(self) -> str:
"""适配器版本号"""
pass
@property
def capabilities(self) -> AgentCapabilities:
"""Agent 能力声明"""
return AgentCapabilities()
# ========== 核心能力 ==========
@abstractmethod
async def execute(self, task: Task) -> Result:
"""
执行任务
这是 Agent 的核心方法,负责:
1. 解析任务描述
2. 调用适当的模型/API
3. 处理执行结果
4. 返回标准化结果
"""
pass
@abstractmethod
async def join_meeting(self, meeting_id: str, timeout: int = 300) -> str:
"""
加入会议等待队列(栅栏同步)
当 Agent 需要参与会议时调用此方法:
1. 向协调服务报告"我准备好了"
2. 等待其他参与者到达
3. 当所有人都到达时,会议自动开始
返回值:
- "started": 会议已开始
- "timeout": 等待超时
- "cancelled": 会议被取消
"""
pass
@abstractmethod
async def write_state(self, state: Dict) -> None:
"""
写入自己的状态文件
状态文件存储在 .doc/agents/{agent_id}/state.json
包含:当前任务、进度、临时数据等
"""
pass
@abstractmethod
async def read_others(self, agent_id: str) -> Dict:
"""
读取其他 Agent 的状态
用于 Agent 之间互相了解对方的工作状态
"""
pass
@abstractmethod
async def update_heartbeat(self, status: str, task: str = "", progress: int = 0) -> None:
"""
更新心跳
参数:
- status: working, waiting, idle, error
- task: 当前任务描述
- progress: 进度百分比 0-100
"""
pass
# ========== 生命周期钩子 ==========
async def initialize(self) -> None:
"""Agent 初始化时调用"""
pass
async def shutdown(self) -> None:
"""Agent 关闭前调用"""
pass
async def health_check(self) -> bool:
"""健康检查"""
return True
# ========== 会议相关 ==========
async def propose(self, meeting_id: str, content: str, step: str = "") -> None:
"""在会议中提出提案"""
pass
async def discuss(self, meeting_id: str, content: str, step: str = "") -> None:
"""在会议中参与讨论"""
pass
async def vote(self, meeting_id: str, proposal_id: str, agree: bool) -> None:
"""对提案进行投票"""
pass
# ========== 工具方法 ==========
def _generate_task_id(self) -> str:
"""生成唯一任务 ID"""
import uuid
return f"task_{uuid.uuid4().hex[:12]}"
async def _delay(self, seconds: float):
"""异步延迟"""
await asyncio.sleep(seconds)
class AdapterError(Exception):
"""适配器错误基类"""
pass
class AdapterConnectionError(AdapterError):
"""连接错误"""
pass
class AdapterExecutionError(AdapterError):
"""执行错误"""
pass
class AdapterTimeoutError(AdapterError):
"""超时错误"""
pass

75
backend/app/main.py Normal file
View File

@@ -0,0 +1,75 @@
"""
Swarm Command Center - FastAPI 主入口
多智能体协作系统的协调层后端服务
"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from app.routers import agents, locks, meetings, heartbeats, workflows, resources, roles, humans
from app.routers import agents_control, websocket
# 创建 FastAPI 应用实例
app = FastAPI(
title="Swarm Command Center API",
description="多智能体协作系统的协调层后端服务",
version="0.1.0",
)
# 配置 CORS - 允许前端访问
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 基础健康检查端点
@app.get("/")
async def root():
"""健康检查端点"""
return {"status": "ok", "service": "Swarm Command Center"}
@app.get("/health")
@app.get("/api/health")
async def health():
"""详细健康检查"""
return {
"status": "healthy",
"version": "0.1.0",
"services": {
"api": "ok",
"storage": "ok",
}
}
# 注册 API 路由
app.include_router(agents.router, prefix="/api/agents", tags=["agents"])
app.include_router(agents_control.router, tags=["agents-control"])
app.include_router(locks.router, prefix="/api/locks", tags=["locks"])
app.include_router(meetings.router, prefix="/api/meetings", tags=["meetings"])
app.include_router(heartbeats.router, prefix="/api/heartbeats", tags=["heartbeats"])
app.include_router(workflows.router, prefix="/api/workflows", tags=["workflows"])
app.include_router(resources.router, prefix="/api", tags=["resources"])
app.include_router(roles.router, prefix="/api/roles", tags=["roles"])
app.include_router(humans.router, prefix="/api/humans", tags=["humans"])
app.include_router(websocket.router, tags=["websocket"])
def main():
"""启动开发服务器"""
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=8000,
reload=True,
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,17 @@
"""API 路由模块"""
from . import agents, locks, meetings, heartbeats, workflows, resources, roles, humans
from . import agents_control, websocket
__all__ = [
"agents",
"locks",
"meetings",
"heartbeats",
"workflows",
"resources",
"roles",
"humans",
"agents_control",
"websocket"
]

View File

@@ -0,0 +1,166 @@
"""
Agent 管理 API 路由
"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional
import time
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):
agent_id: str
name: str
role: str = "developer"
model: str
description: Optional[str] = None
# Agent状态存储
agent_states_db = {}
@router.get("/")
async def list_agents():
"""获取所有 Agent 列表"""
# 合并数据库和默认agent
default_agents = [
{
"agent_id": "claude-001",
"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")
async def register_agent(agent: AgentCreate):
"""注册新 Agent"""
agent_data = {
"agent_id": agent.agent_id,
"name": agent.name,
"role": agent.role,
"model": agent.model,
"description": agent.description or "",
"status": "idle",
"created_at": time.time()
}
agents_db[agent.agent_id] = agent_data
return agent_data
@router.get("/{agent_id}")
async def get_agent(agent_id: str):
"""获取指定 Agent 信息"""
if agent_id in agents_db:
return agents_db[agent_id]
raise HTTPException(status_code=404, detail="Agent not found")
@router.delete("/{agent_id}")
async def delete_agent(agent_id: str):
"""删除 Agent"""
if agent_id in agents_db:
del agents_db[agent_id]
return {"message": "Agent deleted"}
raise HTTPException(status_code=404, detail="Agent not found")
@router.get("/{agent_id}/state")
async def get_agent_state(agent_id: str):
"""获取 Agent 状态"""
# 如果存在真实状态,返回真实状态
if agent_id in agent_states_db:
return agent_states_db[agent_id]
# 默认mock状态
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")
async def update_agent_state(agent_id: str, data: dict):
"""更新 Agent 状态"""
agent_states_db[agent_id] = {
"agent_id": agent_id,
"task": data.get("task", ""),
"progress": data.get("progress", 0),
"working_files": data.get("working_files", []),
"status": data.get("status", "idle"),
"last_update": time.time()
}
return {"success": True}

View File

@@ -0,0 +1,391 @@
"""
Agent 控制 API 路由
提供 Agent 启动、停止、状态查询等控制接口
"""
import logging
import uuid
from typing import Dict, List, Optional, Any
from datetime import datetime
from fastapi import APIRouter, HTTPException, BackgroundTasks
from pydantic import BaseModel, Field
from ..services.process_manager import get_process_manager, AgentStatus
from ..services.agent_registry import get_agent_registry
from ..services.heartbeat import get_heartbeat_service
from ..adapters.native_llm_agent import NativeLLMAgent, NativeLLMAgentFactory
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/agents/control", tags=["agents-control"])
# ========== 请求模型 ==========
class StartAgentRequest(BaseModel):
"""启动 Agent 请求"""
agent_id: str = Field(..., description="Agent 唯一标识")
name: Optional[str] = Field(None, description="Agent 显示名称")
role: str = Field("developer", description="Agent 角色")
model: str = Field("claude-sonnet-4.6", description="使用的模型")
agent_type: str = Field("native_llm", description="Agent 类型")
config: Dict[str, Any] = Field(default_factory=dict, description="额外配置")
class StopAgentRequest(BaseModel):
"""停止 Agent 请求"""
agent_id: str = Field(..., description="Agent ID")
graceful: bool = Field(True, description="是否优雅关闭")
class ExecuteTaskRequest(BaseModel):
"""执行任务请求"""
agent_id: str = Field(..., description="Agent ID")
task_description: str = Field(..., description="任务描述")
context: Dict[str, Any] = Field(default_factory=dict, description="任务上下文")
class CreateMeetingRequest(BaseModel):
"""创建会议请求"""
meeting_id: str = Field(..., description="会议 ID")
title: str = Field(..., description="会议标题")
attendees: List[str] = Field(..., description="参会 Agent ID 列表")
class JoinMeetingRequest(BaseModel):
"""加入会议请求"""
agent_id: str = Field(..., description="Agent ID")
meeting_id: str = Field(..., description="会议 ID")
timeout: int = Field(300, description="等待超时时间(秒)")
# ========== 响应模型 ==========
class AgentControlResponse(BaseModel):
"""Agent 控制响应"""
success: bool
agent_id: str
status: str
message: str
class AgentStatusResponse(BaseModel):
"""Agent 状态响应"""
agent_id: str
status: str
is_alive: bool
uptime: Optional[float] = None
restart_count: int = 0
class ProcessManagerSummary(BaseModel):
"""进程管理器摘要"""
total_agents: int
running_agents: int
running_agent_ids: List[str]
status_counts: Dict[str, int]
monitor_running: bool
# ========== API 端点 ==========
@router.post("/start", response_model=AgentControlResponse)
async def start_agent(request: StartAgentRequest, background_tasks: BackgroundTasks):
"""
启动 Agent
启动一个新的 Agent 实例,支持两种类型:
- native_llm: 原生 LLM Agent异步任务
- process_wrapper: 进程包装 Agent外部 CLI 工具)
"""
process_manager = get_process_manager()
# 检查是否已在运行
if request.agent_id in process_manager.get_all_agents():
existing = process_manager.get_agent_status(request.agent_id)
if existing != AgentStatus.STOPPED:
return AgentControlResponse(
success=False,
agent_id=request.agent_id,
status=existing.value,
message="Agent 已在运行"
)
# 准备配置
config = request.config.copy()
config["name"] = request.name or request.agent_id.replace("-", " ").title()
config["role"] = request.role
config["model"] = request.model
# 启动 Agent
success = await process_manager.start_agent(
agent_id=request.agent_id,
agent_type=request.agent_type,
config=config
)
if success:
return AgentControlResponse(
success=True,
agent_id=request.agent_id,
status=AgentStatus.RUNNING.value,
message=f"Agent {request.agent_id} 启动成功"
)
else:
raise HTTPException(status_code=500, detail="启动 Agent 失败")
@router.post("/stop", response_model=AgentControlResponse)
async def stop_agent(request: StopAgentRequest):
"""停止 Agent"""
process_manager = get_process_manager()
success = await process_manager.stop_agent(
agent_id=request.agent_id,
graceful=request.graceful
)
if success:
return AgentControlResponse(
success=True,
agent_id=request.agent_id,
status=AgentStatus.STOPPED.value,
message=f"Agent {request.agent_id} 已停止"
)
else:
return AgentControlResponse(
success=False,
agent_id=request.agent_id,
status=AgentStatus.UNKNOWN.value,
message=f"停止 Agent 失败或 Agent 未运行"
)
@router.post("/restart", response_model=AgentControlResponse)
async def restart_agent(agent_id: str):
"""重启 Agent"""
process_manager = get_process_manager()
success = await process_manager.restart_agent(agent_id)
if success:
return AgentControlResponse(
success=True,
agent_id=agent_id,
status=AgentStatus.RUNNING.value,
message=f"Agent {agent_id} 重启成功"
)
else:
raise HTTPException(status_code=500, detail="重启 Agent 失败")
@router.get("/status/{agent_id}", response_model=AgentStatusResponse)
async def get_agent_status(agent_id: str):
"""获取 Agent 状态"""
process_manager = get_process_manager()
status = process_manager.get_agent_status(agent_id)
all_agents = process_manager.get_all_agents()
if agent_id in all_agents:
process_info = all_agents[agent_id]
return AgentStatusResponse(
agent_id=agent_id,
status=status.value,
is_alive=process_info.is_alive,
uptime=process_info.uptime,
restart_count=process_info.restart_count
)
else:
raise HTTPException(status_code=404, detail="Agent 不存在")
@router.get("/list", response_model=List[AgentStatusResponse])
async def list_agents():
"""列出所有 Agent 状态"""
process_manager = get_process_manager()
heartbeat_service = get_heartbeat_service()
agents = []
for agent_id, process_info in process_manager.get_all_agents().items():
# 获取心跳信息
heartbeat = await heartbeat_service.get_heartbeat(agent_id)
agents.append(AgentStatusResponse(
agent_id=agent_id,
status=process_info.status.value,
is_alive=process_info.is_alive,
uptime=process_info.uptime,
restart_count=process_info.restart_count
))
return agents
@router.get("/summary", response_model=ProcessManagerSummary)
async def get_summary():
"""获取进程管理器摘要"""
process_manager = get_process_manager()
summary = process_manager.get_summary()
return ProcessManagerSummary(**summary)
@router.post("/execute")
async def execute_task(request: ExecuteTaskRequest):
"""
让 Agent 执行任务
Agent 会自动:
1. 分析任务,识别需要的文件
2. 获取文件锁
3. 调用 LLM 执行任务
4. 释放文件锁
"""
process_manager = get_process_manager()
# 检查 Agent 是否运行
all_agents = process_manager.get_all_agents()
if request.agent_id not in all_agents:
raise HTTPException(status_code=404, detail="Agent 未运行")
process_info = all_agents[request.agent_id]
if not process_info.is_alive:
raise HTTPException(status_code=400, detail="Agent 未运行")
# 获取 Agent 实例
agent = process_info.agent
if not agent:
raise HTTPException(status_code=500, detail="Agent 实例不可用")
# 创建任务
from ..core.agent_adapter import Task
task = Task(
task_id=f"task_{uuid.uuid4().hex[:12]}",
description=request.task_description,
context=request.context
)
# 执行任务
result = await agent.execute(task)
return {
"success": result.success,
"output": result.output,
"error": result.error,
"metadata": result.metadata,
"execution_time": result.execution_time
}
@router.post("/meeting/create")
async def create_meeting(request: CreateMeetingRequest):
"""创建协作会议"""
from ..services.meeting_scheduler import get_meeting_scheduler
scheduler = get_meeting_scheduler()
queue = await scheduler.create_meeting(
request.meeting_id,
request.title,
request.attendees
)
return {
"success": True,
"meeting_id": request.meeting_id,
"title": queue.title,
"expected_attendees": queue.expected_attendees,
"min_required": queue.min_required,
"status": queue.status
}
@router.post("/meeting/join")
async def join_meeting(request: JoinMeetingRequest):
"""让 Agent 加入会议(栅栏同步)"""
process_manager = get_process_manager()
# 检查 Agent 是否运行
all_agents = process_manager.get_all_agents()
if request.agent_id not in all_agents:
raise HTTPException(status_code=404, detail="Agent 未运行")
process_info = all_agents[request.agent_id]
agent = process_info.agent
if not agent:
raise HTTPException(status_code=500, detail="Agent 实例不可用")
# 加入会议
result = await agent.join_meeting(request.meeting_id, request.timeout)
return {
"agent_id": request.agent_id,
"meeting_id": request.meeting_id,
"result": result
}
@router.post("/shutdown-all")
async def shutdown_all_agents():
"""关闭所有 Agent"""
process_manager = get_process_manager()
await process_manager.shutdown_all()
return {
"success": True,
"message": "所有 Agent 已关闭"
}
@router.post("/batch-start")
async def batch_start_agents(agents: List[StartAgentRequest]):
"""
批量启动 Agent
用于快速创建团队
"""
results = []
process_manager = get_process_manager()
for agent_request in agents:
config = agent_request.config.copy()
config["name"] = agent_request.name or agent_request.agent_id.replace("-", " ").title()
config["role"] = agent_request.role
config["model"] = agent_request.model
success = await process_manager.start_agent(
agent_id=agent_request.agent_id,
agent_type=agent_request.agent_type,
config=config
)
results.append({
"agent_id": agent_request.agent_id,
"success": success,
"status": AgentStatus.RUNNING.value if success else AgentStatus.CRASHED.value
})
return {
"results": results,
"total": len(results),
"successful": sum(1 for r in results if r["success"])
}
# 健康检查端点
@router.get("/health")
async def health_check():
"""健康检查"""
process_manager = get_process_manager()
summary = process_manager.get_summary()
return {
"status": "healthy",
"running_agents": summary["running_agents"],
"monitor_running": summary["monitor_running"]
}

View File

@@ -0,0 +1,47 @@
"""
心跳管理 API 路由
"""
from fastapi import APIRouter
from pydantic import BaseModel
from typing import Dict
import time
router = APIRouter()
heartbeats_db = {}
class Heartbeat(BaseModel):
agent_id: str
timestamp: float
is_timeout: bool = False
@router.get("/")
async def list_heartbeats():
"""获取所有 Agent 心跳"""
return {
"heartbeats": {
"claude-001": {
"agent_id": "claude-001",
"timestamp": time.time() - 30,
"is_timeout": False
},
"kimi-001": {
"agent_id": "kimi-001",
"timestamp": time.time() - 60,
"is_timeout": False
}
}
}
@router.post("/{agent_id}")
async def update_heartbeat(agent_id: str):
"""更新 Agent 心跳"""
heartbeats_db[agent_id] = {
"agent_id": agent_id,
"timestamp": time.time(),
"is_timeout": False
}
return {"success": True}

View File

@@ -0,0 +1,236 @@
"""
人类输入 API 路由
"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from app.services.human_input import get_human_input_service
router = APIRouter()
# ========== 请求/响应模型 ==========
class TaskRequest(BaseModel):
"""任务请求"""
content: str
from_user: str = "user001"
priority: str = "medium"
title: str = ""
target_files: List[str] = []
suggested_agent: str = ""
urgent: bool = False
class CommentRequest(BaseModel):
"""评论请求"""
meeting_id: str
content: str
from_user: str = "user001"
comment_type: str = "proposal"
priority: str = "normal"
class ParticipantRegister(BaseModel):
"""参与者注册"""
user_id: str
name: str
role: str = ""
avatar: str = "👤"
class UserStatusUpdate(BaseModel):
"""用户状态更新"""
status: str
current_focus: str = ""
# ========== API 端点 ==========
@router.get("/summary")
async def get_summary():
"""获取人类输入服务摘要"""
service = get_human_input_service()
summary = await service.get_summary()
return summary
@router.post("/register")
async def register_participant(request: ParticipantRegister):
"""注册人类参与者"""
service = get_human_input_service()
await service.register_participant(
request.user_id,
request.name,
request.role,
request.avatar
)
return {"success": True, "user_id": request.user_id}
@router.get("/participants")
async def get_participants():
"""获取所有参与者"""
service = get_human_input_service()
participants = await service.get_participants()
return {
"participants": [
{
"id": p.id,
"name": p.name,
"role": p.role,
"status": p.status,
"avatar": p.avatar
}
for p in participants
]
}
@router.post("/tasks")
async def add_task_request(request: TaskRequest):
"""提交任务请求"""
service = get_human_input_service()
task_id = await service.add_task_request(
from_user=request.from_user,
content=request.content,
priority=request.priority,
title=request.title,
target_files=request.target_files,
suggested_agent=request.suggested_agent,
urgent=request.urgent
)
return {"success": True, "task_id": task_id}
@router.get("/tasks")
async def get_pending_tasks(
priority: Optional[str] = None,
agent: Optional[str] = None
):
"""获取待处理任务"""
service = get_human_input_service()
tasks = await service.get_pending_tasks(
priority_filter=priority,
agent_filter=agent
)
return {
"tasks": [
{
"id": t.id,
"from_user": t.from_user,
"timestamp": t.timestamp,
"priority": t.priority,
"type": t.type,
"title": t.title,
"content": t.content,
"target_files": t.target_files,
"suggested_agent": t.suggested_agent,
"urgent": t.urgent,
"is_urgent": t.is_urgent
}
for t in tasks
],
"count": len(tasks)
}
@router.get("/tasks/urgent")
async def get_urgent_tasks():
"""获取紧急任务"""
service = get_human_input_service()
tasks = await service.get_urgent_tasks()
return {
"tasks": [
{
"id": t.id,
"from_user": t.from_user,
"content": t.content,
"title": t.title,
"suggested_agent": t.suggested_agent
}
for t in tasks
],
"count": len(tasks)
}
@router.put("/tasks/{task_id}/processing")
async def mark_task_processing(task_id: str):
"""标记任务为处理中"""
service = get_human_input_service()
success = await service.mark_task_processing(task_id)
if not success:
raise HTTPException(status_code=404, detail="Task not found")
return {"success": True}
@router.put("/tasks/{task_id}/complete")
async def mark_task_completed(task_id: str):
"""标记任务为已完成"""
service = get_human_input_service()
success = await service.mark_task_completed(task_id)
if not success:
raise HTTPException(status_code=404, detail="Task not found")
return {"success": True}
@router.post("/comments")
async def add_meeting_comment(request: CommentRequest):
"""提交会议评论"""
service = get_human_input_service()
comment_id = await service.add_meeting_comment(
from_user=request.from_user,
meeting_id=request.meeting_id,
content=request.content,
comment_type=request.comment_type,
priority=request.priority
)
return {"success": True, "comment_id": comment_id}
@router.get("/comments")
async def get_pending_comments(meeting_id: Optional[str] = None):
"""获取待处理评论"""
service = get_human_input_service()
comments = await service.get_pending_comments(meeting_id)
return {
"comments": [
{
"id": c.id,
"from_user": c.from_user,
"meeting_id": c.meeting_id,
"timestamp": c.timestamp,
"type": c.type,
"priority": c.priority,
"content": c.content
}
for c in comments
],
"count": len(comments)
}
@router.put("/comments/{comment_id}/addressed")
async def mark_comment_addressed(comment_id: str):
"""标记评论为已处理"""
service = get_human_input_service()
success = await service.mark_comment_addressed(comment_id)
if not success:
raise HTTPException(status_code=404, detail="Comment not found")
return {"success": True}
@router.put("/users/{user_id}/status")
async def update_user_status(user_id: str, request: UserStatusUpdate):
"""更新用户状态"""
service = get_human_input_service()
success = await service.update_user_status(
user_id,
request.status,
request.current_focus
)
if not success:
raise HTTPException(status_code=404, detail="User not found")
return {"success": True}

View File

@@ -0,0 +1,88 @@
"""
文件锁 API 路由
"""
from fastapi import APIRouter
from pydantic import BaseModel
from typing import List, Optional
import time
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 FileLock(BaseModel):
file_path: str
agent_id: str
agent_name: str = ""
locked_at: float
def format_elapsed(locked_at: float) -> str:
"""格式化已锁定时间"""
elapsed = time.time() - locked_at
if elapsed < 60:
return f"{int(elapsed)}"
elif elapsed < 3600:
return f"{int(elapsed / 60)}分钟"
else:
return f"{elapsed / 3600:.1f}小时"
@router.get("/")
async def list_locks():
"""获取所有文件锁列表"""
locks_with_display = []
for lock in locks_db:
lock_copy = lock.copy()
lock_copy["elapsed_display"] = format_elapsed(lock["locked_at"])
locks_with_display.append(lock_copy)
return {"locks": locks_with_display}
@router.post("/acquire")
async def acquire_lock(lock: FileLock):
"""获取文件锁"""
# 检查是否已被锁定
for existing in locks_db:
if existing["file_path"] == lock.file_path:
return {"success": False, "message": "File already locked"}
locks_db.append({
"file_path": lock.file_path,
"agent_id": lock.agent_id,
"agent_name": lock.agent_name or lock.agent_id,
"locked_at": time.time()
})
return {"success": True, "message": "Lock acquired"}
@router.post("/release")
async def release_lock(data: dict):
"""释放文件锁"""
file_path = data.get("file_path", "")
agent_id = data.get("agent_id", "")
global locks_db
locks_db = [l for l in locks_db if not (l["file_path"] == file_path and l["agent_id"] == agent_id)]
return {"success": True, "message": "Lock released"}
@router.get("/check")
async def check_lock(file_path: str):
"""检查文件锁定状态"""
for lock in locks_db:
if lock["file_path"] == file_path:
return {"file_path": file_path, "locked": True, "locked_by": lock["agent_id"]}
return {"file_path": file_path, "locked": False}

View File

@@ -0,0 +1,194 @@
"""
会议管理 API 路由
"""
from fastapi import APIRouter
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime
import time
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):
title: str
agenda: str
meeting_type: str = "design_review"
attendees: List[str] = []
@router.get("/")
async def list_meetings():
"""获取所有会议列表"""
return {
"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")
async def list_today_meetings():
"""获取今日会议"""
today = datetime.now().strftime("%Y-%m-%d")
return {
"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("/")
async def create_meeting(meeting: MeetingCreate):
"""创建新会议"""
meeting_id = f"meeting-{int(time.time())}"
meeting_data = {
"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
@router.get("/{meeting_id}")
async def get_meeting(meeting_id: str):
"""获取会议详情"""
for meeting in meetings_db:
if meeting["meeting_id"] == meeting_id:
return meeting
# 返回模拟数据
return {
"meeting_id": meeting_id,
"title": "测试会议",
"status": "in_progress",
"attendees": ["claude-001"],
"agenda": "测试议程",
"progress_summary": "50%",
"created_at": time.time()
}
@router.post("/create")
async def create_meeting_api(meeting: MeetingCreate):
"""创建会议 API前端使用的端点"""
return await create_meeting(meeting)
@router.post("/{meeting_id}/join")
async def join_meeting(meeting_id: str, data: dict):
"""Agent 加入会议"""
agent_id = data.get("agent_id", "")
return {"success": True, "meeting_id": meeting_id, "agent_id": agent_id}
@router.post("/{meeting_id}/discuss")
async def add_discussion(meeting_id: str, data: dict):
"""添加讨论内容"""
return {"success": True, "meeting_id": meeting_id}
@router.post("/{meeting_id}/finish")
async def finish_meeting(meeting_id: str, data: dict):
"""完成会议"""
return {"success": True, "meeting_id": meeting_id}
@router.post("/{meeting_id}/progress")
async def update_progress(meeting_id: str, data: dict):
"""更新进度"""
return {"success": True, "meeting_id": meeting_id}
@router.post("/record/create")
async def create_meeting_record(data: dict):
"""创建会议记录(前端使用的端点)"""
meeting_id = f"meeting-{int(time.time())}"
meeting_data = {
"meeting_id": meeting_id,
"title": data.get("title", "未命名会议"),
"agenda": data.get("agenda", ""),
"attendees": data.get("attendees", []),
"status": "waiting",
"progress_summary": "0%",
"steps": data.get("steps", []),
"discussions": [],
"created_at": time.time()
}
meetings_db.append(meeting_data)
return meeting_data
@router.post("/record/{meeting_id}/discussion")
async def add_meeting_discussion(meeting_id: str, data: dict):
"""添加会议讨论(前端使用的端点)"""
return {"success": True, "meeting_id": meeting_id, "discussion": data}

View File

@@ -0,0 +1,60 @@
"""
资源管理 API 路由
"""
from fastapi import APIRouter
from pydantic import BaseModel
from typing import List, Optional
import time
router = APIRouter()
class TaskRequest(BaseModel):
agent_id: str
task: str
timeout: Optional[int] = 300
class TaskParseRequest(BaseModel):
task: str
@router.post("/execute")
async def execute_task(request: TaskRequest):
"""执行任务"""
return {
"success": True,
"message": f"任务 '{request.task}' 已执行",
"files_locked": ["src/main.py"],
"duration_seconds": 5.5
}
@router.get("/status")
async def get_all_status():
"""获取所有 Agent 状态"""
return {
"agents": [
{
"agent_id": "claude-001",
"status": "working",
"current_task": "开发功能",
"progress": 75
},
{
"agent_id": "kimi-001",
"status": "idle",
"current_task": "",
"progress": 0
}
]
}
@router.post("/parse-task")
async def parse_task(request: TaskParseRequest):
"""解析任务文件"""
return {
"task": request.task,
"files": ["src/main.py", "src/utils.py"]
}

View File

@@ -0,0 +1,55 @@
"""
角色分配 API 路由
"""
from fastapi import APIRouter
from pydantic import BaseModel
from typing import List, Dict
router = APIRouter()
class RoleRequest(BaseModel):
task: str
class RoleAllocateRequest(BaseModel):
task: str
agents: List[str]
@router.post("/primary")
async def get_primary_role(request: RoleRequest):
"""获取任务主要角色"""
return {
"task": request.task,
"primary_role": "developer",
"role_scores": {
"developer": 0.8,
"architect": 0.6,
"qa": 0.4,
"pm": 0.2
}
}
@router.post("/allocate")
async def allocate_roles(request: RoleAllocateRequest):
"""分配角色"""
allocation = {}
for i, agent in enumerate(request.agents):
roles = ["developer", "architect", "qa"]
allocation[agent] = roles[i % len(roles)]
return {
"task": request.task,
"primary_role": "developer",
"allocation": allocation
}
@router.post("/explain")
async def explain_roles(request: RoleAllocateRequest):
"""解释角色分配"""
return {
"explanation": f"基于任务 '{request.task}' 的分析,推荐了最适合的角色分配方案。"
}

View File

@@ -0,0 +1,392 @@
"""
WebSocket 实时通信
提供 Agent 与服务器之间的实时双向通信
"""
import json
import logging
import asyncio
from typing import Dict, Set, Optional, Any
from datetime import datetime
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from pydantic import BaseModel
logger = logging.getLogger(__name__)
router = APIRouter()
class ConnectionManager:
"""
WebSocket 连接管理器
管理 WebSocket 连接,支持:
1. Agent 连接管理
2. 消息广播
3. 私信发送
4. 心跳检测
"""
def __init__(self):
# Agent 连接: {agent_id: WebSocket}
self.agent_connections: Dict[str, WebSocket] = {}
# 客户端连接: {client_id: WebSocket}
self.client_connections: Dict[str, WebSocket] = {}
# 连接元数据: {connection_id: {"type": "agent"|"client", "connected_at": datetime}}
self.connection_metadata: Dict[str, Dict[str, Any]] = {}
async def connect_agent(self, websocket: WebSocket, agent_id: str):
"""Agent 连接"""
await websocket.accept()
self.agent_connections[agent_id] = websocket
self.connection_metadata[agent_id] = {
"type": "agent",
"connected_at": datetime.now()
}
logger.info(f"Agent 连接: {agent_id}")
# 发送欢迎消息
await self.send_to_agent(agent_id, {
"type": "connected",
"agent_id": agent_id,
"message": "连接成功"
})
async def connect_client(self, websocket: WebSocket, client_id: str):
"""客户端连接"""
await websocket.accept()
self.client_connections[client_id] = websocket
self.connection_metadata[client_id] = {
"type": "client",
"connected_at": datetime.now()
}
logger.info(f"客户端连接: {client_id}")
# 发送欢迎消息
await self.send_to_client(client_id, {
"type": "connected",
"client_id": client_id,
"message": "连接成功"
})
def disconnect_agent(self, agent_id: str):
"""断开 Agent 连接"""
if agent_id in self.agent_connections:
del self.agent_connections[agent_id]
if agent_id in self.connection_metadata:
del self.connection_metadata[agent_id]
logger.info(f"Agent 断开: {agent_id}")
def disconnect_client(self, client_id: str):
"""断开客户端连接"""
if client_id in self.client_connections:
del self.client_connections[client_id]
if client_id in self.connection_metadata:
del self.connection_metadata[client_id]
logger.info(f"客户端断开: {client_id}")
async def send_to_agent(self, agent_id: str, message: Dict) -> bool:
"""发送消息给 Agent"""
if agent_id in self.agent_connections:
try:
await self.agent_connections[agent_id].send_json(message)
return True
except Exception as e:
logger.error(f"发送消息给 Agent 失败: {agent_id}: {e}")
self.disconnect_agent(agent_id)
return False
return False
async def send_to_client(self, client_id: str, message: Dict) -> bool:
"""发送消息给客户端"""
if client_id in self.client_connections:
try:
await self.client_connections[client_id].send_json(message)
return True
except Exception as e:
logger.error(f"发送消息给客户端失败: {client_id}: {e}")
self.disconnect_client(client_id)
return False
return False
async def broadcast_to_agents(self, message: Dict):
"""广播消息给所有 Agent"""
failed_agents = []
for agent_id, websocket in self.agent_connections.items():
try:
await websocket.send_json(message)
except Exception as e:
logger.error(f"广播消息失败: {agent_id}: {e}")
failed_agents.append(agent_id)
# 清理失败的连接
for agent_id in failed_agents:
self.disconnect_agent(agent_id)
async def broadcast_to_clients(self, message: Dict):
"""广播消息给所有客户端"""
failed_clients = []
for client_id, websocket in self.client_connections.items():
try:
await websocket.send_json(message)
except Exception as e:
logger.error(f"广播消息失败: {client_id}: {e}")
failed_clients.append(client_id)
# 清理失败的连接
for client_id in failed_clients:
self.disconnect_client(client_id)
async def broadcast_to_all(self, message: Dict):
"""广播消息给所有连接"""
await self.broadcast_to_agents(message)
await self.broadcast_to_clients(message)
def get_connected_agents(self) -> Set[str]:
"""获取已连接的 Agent"""
return set(self.agent_connections.keys())
def get_connected_clients(self) -> Set[str]:
"""获取已连接的客户端"""
return set(self.client_connections.keys())
def get_connection_count(self) -> Dict[str, int]:
"""获取连接数量"""
return {
"agents": len(self.agent_connections),
"clients": len(self.client_connections),
"total": len(self.agent_connections) + len(self.client_connections)
}
# 全局连接管理器
manager = ConnectionManager()
# ========== WebSocket 端点 ==========
@router.websocket("/ws/agent/{agent_id}")
async def agent_websocket_endpoint(websocket: WebSocket, agent_id: str):
"""
Agent WebSocket 端点
Agent 连接后可以:
1. 接收任务分配
2. 发送状态更新
3. 参与实时协作
"""
await manager.connect_agent(websocket, agent_id)
try:
while True:
# 接收来自 Agent 的消息
data = await websocket.receive_json()
await handle_agent_message(agent_id, data)
except WebSocketDisconnect:
manager.disconnect_agent(agent_id)
except Exception as e:
logger.error(f"Agent WebSocket 错误: {agent_id}: {e}")
manager.disconnect_agent(agent_id)
@router.websocket("/ws/client/{client_id}")
async def client_websocket_endpoint(websocket: WebSocket, client_id: str):
"""
客户端 WebSocket 端点
客户端连接后可以:
1. 实时监控 Agent 状态
2. 接收事件通知
3. 发送控制指令
"""
await manager.connect_client(websocket, client_id)
# 发送初始状态
await manager.send_to_client(client_id, {
"type": "initial_state",
"connected_agents": list(manager.get_connected_agents()),
"timestamp": datetime.now().isoformat()
})
try:
while True:
# 接收来自客户端的消息
data = await websocket.receive_json()
await handle_client_message(client_id, data)
except WebSocketDisconnect:
manager.disconnect_client(client_id)
except Exception as e:
logger.error(f"客户端 WebSocket 错误: {client_id}: {e}")
manager.disconnect_client(client_id)
@router.websocket("/ws")
async def public_websocket_endpoint(websocket: WebSocket):
"""
公共 WebSocket 端点
自动生成 client_id 的客户端连接
"""
import uuid
client_id = f"client_{uuid.uuid4().hex[:12]}"
await manager.connect_client(websocket, client_id)
try:
while True:
data = await websocket.receive_json()
await handle_client_message(client_id, data)
except WebSocketDisconnect:
manager.disconnect_client(client_id)
except Exception as e:
logger.error(f"公共 WebSocket 错误: {client_id}: {e}")
manager.disconnect_client(client_id)
# ========== 消息处理 ==========
async def handle_agent_message(agent_id: str, data: Dict):
"""处理来自 Agent 的消息"""
message_type = data.get("type")
if message_type == "heartbeat":
# Agent 心跳更新
await broadcast_agent_status(agent_id, data)
elif message_type == "status_update":
# Agent 状态更新
await broadcast_agent_status(agent_id, data)
elif message_type == "task_progress":
# 任务进度更新
await broadcast_task_progress(agent_id, data)
elif message_type == "meeting_joined":
# Agent 加入会议
await broadcast_event({
"type": "meeting_event",
"event": "agent_joined",
"agent_id": agent_id,
"meeting_id": data.get("meeting_id"),
"timestamp": datetime.now().isoformat()
})
elif message_type == "meeting_proposal":
# 会议提案
await broadcast_event({
"type": "meeting_event",
"event": "proposal",
"agent_id": agent_id,
"meeting_id": data.get("meeting_id"),
"content": data.get("content"),
"timestamp": datetime.now().isoformat()
})
else:
# 其他消息类型
await broadcast_event({
"type": "agent_message",
"agent_id": agent_id,
"message_type": message_type,
"data": data,
"timestamp": datetime.now().isoformat()
})
async def handle_client_message(client_id: str, data: Dict):
"""处理来自客户端的消息"""
message_type = data.get("type")
if message_type == "subscribe_agents":
# 客户端订阅 Agent 状态
await manager.send_to_client(client_id, {
"type": "subscription_confirmed",
"subscription": "agents"
})
elif message_type == "send_to_agent":
# 发送消息给特定 Agent
agent_id = data.get("agent_id")
message = data.get("message")
if agent_id:
await manager.send_to_agent(agent_id, {
"type": "client_message",
"from_client": client_id,
"message": message
})
elif message_type == "broadcast":
# 广播消息给所有 Agent
message = data.get("message")
await manager.broadcast_to_agents({
"type": "broadcast",
"from_client": client_id,
"message": message
})
async def broadcast_agent_status(agent_id: str, data: Dict):
"""广播 Agent 状态更新"""
await manager.broadcast_to_clients({
"type": "agent_status",
"agent_id": agent_id,
"data": data,
"timestamp": datetime.now().isoformat()
})
async def broadcast_task_progress(agent_id: str, data: Dict):
"""广播任务进度更新"""
await manager.broadcast_to_clients({
"type": "task_progress",
"agent_id": agent_id,
"task_id": data.get("task_id"),
"progress": data.get("progress"),
"message": data.get("message"),
"timestamp": datetime.now().isoformat()
})
async def broadcast_event(event: Dict):
"""广播事件"""
await manager.broadcast_to_all(event)
# ========== HTTP API ==========
@router.get("/ws/connections")
async def get_connections():
"""获取当前连接信息"""
return {
"agents": list(manager.get_connected_agents()),
"clients": list(manager.get_connected_clients()),
"count": manager.get_connection_count()
}
@router.post("/ws/broadcast")
async def broadcast_message(message: Dict):
"""通过 HTTP 广播消息到所有连接"""
await manager.broadcast_to_all({
"type": "broadcast",
"message": message,
"timestamp": datetime.now().isoformat()
})
return {"success": True, "message": "消息已广播"}
@router.post("/ws/send/{agent_id}")
async def send_to_agent(agent_id: str, message: Dict):
"""通过 HTTP 发送消息给特定 Agent"""
success = await manager.send_to_agent(agent_id, message)
if success:
return {"success": True, "agent_id": agent_id}
else:
return {"success": False, "error": "发送失败或 Agent 未连接"}

View File

@@ -0,0 +1,218 @@
"""
工作流管理 API 路由
"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from pathlib import Path
from app.services.workflow_engine import get_workflow_engine
router = APIRouter()
# ========== 请求/响应模型 ==========
class MeetingNode(BaseModel):
"""工作流节点"""
meeting_id: str
title: str
node_type: str = "meeting"
attendees: List[str]
depends_on: List[str] = []
completed: bool = False
on_failure: Optional[str] = None
progress: Optional[str] = None
class WorkflowDetail(BaseModel):
"""工作流详情"""
workflow_id: str
name: str
description: str
status: str
progress: str
current_node: Optional[str] = None
meetings: List[MeetingNode]
class WorkflowSummary(BaseModel):
"""工作流摘要"""
workflow_id: str
name: str
status: str
progress: str
class JoinExecutionRequest(BaseModel):
"""加入执行节点请求"""
agent_id: str
class JumpRequest(BaseModel):
"""跳转请求"""
target_meeting_id: str
# ========== API 端点 ==========
@router.get("/files")
async def list_workflow_files():
"""获取工作流文件列表"""
engine = get_workflow_engine()
workflow_dir = Path(engine._storage.base_path) / engine.WORKFLOWS_DIR
if not workflow_dir.exists():
return {"files": []}
yaml_files = list(workflow_dir.glob("*.yaml")) + list(workflow_dir.glob("*.yml"))
files = []
for f in yaml_files:
stat = f.stat()
files.append({
"name": f.name,
"path": f"workflow/{f.name}",
"size": stat.st_size,
"modified": stat.st_mtime
})
return {"files": files}
@router.get("/list")
async def list_workflows():
"""获取已加载的工作流列表"""
engine = get_workflow_engine()
workflows = await engine.list_workflows()
return {"workflows": workflows}
@router.post("/start/{workflow_path:path}")
async def start_workflow(workflow_path: str):
"""
启动工作流
加载 YAML 工作流文件并准备执行
"""
engine = get_workflow_engine()
try:
workflow = await engine.load_workflow(workflow_path)
detail = await engine.get_workflow_detail(workflow.workflow_id)
return detail
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{workflow_id}")
async def get_workflow(workflow_id: str):
"""获取工作流详情"""
engine = get_workflow_engine()
detail = await engine.get_workflow_detail(workflow_id)
if not detail:
raise HTTPException(status_code=404, detail="Workflow not found")
return detail
@router.get("/{workflow_id}/status")
async def get_workflow_status(workflow_id: str):
"""获取工作流状态"""
engine = get_workflow_engine()
status = await engine.get_workflow_status(workflow_id)
if not status:
raise HTTPException(status_code=404, detail="Workflow not found")
return status
@router.get("/{workflow_id}/next")
async def get_next_node(workflow_id: str):
"""获取下一个待执行节点"""
engine = get_workflow_engine()
meeting = await engine.get_next_meeting(workflow_id)
if not meeting:
return {"meeting": None, "message": "Workflow completed"}
return {
"meeting": {
"meeting_id": meeting.meeting_id,
"title": meeting.title,
"node_type": meeting.node_type,
"attendees": meeting.attendees,
"depends_on": meeting.depends_on
}
}
@router.post("/{workflow_id}/complete/{meeting_id}")
async def complete_node(workflow_id: str, meeting_id: str):
"""标记节点完成"""
engine = get_workflow_engine()
success = await engine.complete_meeting(workflow_id, meeting_id)
if not success:
raise HTTPException(status_code=404, detail="Workflow or meeting not found")
return {"success": True, "message": "Node completed"}
@router.post("/{workflow_id}/join/{meeting_id}")
async def join_execution_node(workflow_id: str, meeting_id: str, request: JoinExecutionRequest):
"""
Agent 加入执行节点
标记 Agent 已完成执行,当所有 Agent 都完成时返回 ready
"""
engine = get_workflow_engine()
result = await engine.join_execution_node(workflow_id, meeting_id, request.agent_id)
if result.get("status") == "error":
raise HTTPException(status_code=400, detail=result.get("message"))
return result
@router.get("/{workflow_id}/execution/{meeting_id}")
async def get_execution_node_status(workflow_id: str, meeting_id: str):
"""获取执行节点状态"""
engine = get_workflow_engine()
status = await engine.get_execution_status(workflow_id, meeting_id)
if not status:
raise HTTPException(status_code=404, detail="Execution node not found")
return status
@router.post("/{workflow_id}/jump")
async def jump_to_node(workflow_id: str, request: JumpRequest):
"""
强制跳转到指定节点
重置目标节点及所有后续节点的完成状态
"""
engine = get_workflow_engine()
success = await engine.jump_to_node(workflow_id, request.target_meeting_id)
if not success:
raise HTTPException(status_code=404, detail="Target node not found")
return {
"success": True,
"message": f"Jumped to {request.target_meeting_id}",
"detail": await engine.get_workflow_detail(workflow_id)
}
@router.post("/{workflow_id}/fail/{meeting_id}")
async def handle_node_failure(workflow_id: str, meeting_id: str):
"""
处理节点失败
根据 on_failure 配置跳转到指定节点
"""
engine = get_workflow_engine()
target = await engine.handle_failure(workflow_id, meeting_id)
if target:
return {
"success": True,
"message": f"Jumped to {target} due to failure",
"target": target,
"detail": await engine.get_workflow_detail(workflow_id)
}
return {
"success": True,
"message": "No failure handler configured"
}

View File

@@ -0,0 +1,4 @@
"""Services Package"""
from .storage import StorageService, get_storage
__all__ = ["StorageService", "get_storage"]

View File

@@ -0,0 +1,486 @@
"""
Agent 执行引擎
负责协调 LLM 调用和资源管理,提供声明式的任务执行接口。
自动管理文件锁、心跳更新等生命周期。
"""
import asyncio
import logging
import re
import time
from typing import Dict, List, Optional, Any, Callable
from dataclasses import dataclass, field
from pathlib import Path
from .llm_service import ModelRouter, LLMMessage, TaskType
from .storage import get_storage
from .file_lock import get_file_lock_service
from .heartbeat import get_heartbeat_service
from .agent_registry import get_agent_registry, AgentInfo
from ..core.agent_adapter import Task, Result
logger = logging.getLogger(__name__)
@dataclass
class ExecutionPlan:
"""任务执行计划"""
steps: List[str] = field(default_factory=list)
required_files: List[str] = field(default_factory=list)
estimated_duration: str = ""
complexity: str = "medium"
requires_code_execution: bool = False
subtasks: List[str] = field(default_factory=list)
@dataclass
class ExecutionContext:
"""执行上下文"""
agent_id: str
agent_role: str
agent_model: str
task_id: str
acquired_locks: List[str] = field(default_factory=list)
start_time: float = 0
messages: List[LLMMessage] = field(default_factory=list)
metadata: Dict[str, Any] = field(default_factory=dict)
class AgentExecutor:
"""
Agent 任务执行引擎
功能:
1. 任务分析 - 解析任务描述,识别需要的文件
2. 计划生成 - 调用 LLM 生成执行计划
3. 资源管理 - 自动获取和释放文件锁
4. 任务执行 - 调用 LLM 执行任务
5. 结果处理 - 格式化输出,更新状态
"""
def __init__(
self,
llm_service: ModelRouter = None,
storage=None,
lock_service=None,
heartbeat_service=None,
registry=None
):
self.llm = llm_service
self.storage = storage or get_storage()
self.locks = lock_service or get_file_lock_service()
self.heartbeat = heartbeat_service or get_heartbeat_service()
self.registry = registry or get_agent_registry()
# 工作目录
self.work_dir = Path.cwd()
async def execute_task(
self,
agent: AgentInfo,
task: Task,
context: Dict[str, Any] = None
) -> Result:
"""
执行任务的主入口
自动管理:
1. 文件锁获取和释放
2. 心跳更新
3. 任务进度跟踪
4. 错误处理和恢复
"""
execution_context = ExecutionContext(
agent_id=agent.agent_id,
agent_role=agent.role,
agent_model=agent.model,
task_id=task.task_id,
start_time=time.time(),
metadata=context or {}
)
try:
# 1. 更新心跳 - 开始执行
await self.heartbeat.update_heartbeat(
agent.agent_id,
"working",
task.description[:100], # 截断过长描述
0
)
# 2. 分析任务,识别需要的文件
execution_context.required_files = await self._analyze_required_files(
task.description
)
# 3. 获取文件锁
await self._acquire_locks(execution_context)
# 4. 构建执行上下文消息
execution_context.messages = await self._build_messages(
agent, task, execution_context
)
# 5. 调用 LLM 执行任务
llm_response = await self._call_llm(execution_context)
# 6. 处理结果
result = await self._process_result(
llm_response, execution_context
)
# 7. 更新心跳 - 完成
await self.heartbeat.update_heartbeat(
agent.agent_id,
"idle",
"",
100
)
return result
except Exception as e:
logger.error(f"任务执行失败: {e}", exc_info=True)
# 更新心跳为错误状态
await self.heartbeat.update_heartbeat(
agent.agent_id,
"error",
str(e),
0
)
return Result(
success=False,
output="",
error=str(e),
execution_time=time.time() - execution_context.start_time
)
finally:
# 8. 释放所有锁
await self._release_locks(execution_context)
async def _analyze_required_files(self, task_description: str) -> List[str]:
"""
分析任务描述,识别需要的文件
使用 LLM 分析任务中提到的文件路径
"""
# 使用正则表达式快速匹配文件路径
file_patterns = [
r'[a-zA-Z_/\\][a-zA-Z0-9_/\\]*\.(?:py|js|ts|tsx|jsx|java|go|rs|c|h|cpp|hpp|css|html|md|json|yaml|yml)',
r'[a-zA-Z_/\\][a-zA-Z0-9_/\\]*\.(?:py|js|ts|tsx|jsx)',
r'src/[a-zA-Z0-9_/\\]*',
r'app/[a-zA-Z0-9_/\\]*',
r'components/[a-zA-Z0-9_/\\]*',
r'pages/[a-zA-Z0-9_/\\]*',
r'services/[a-zA-Z0-9_/\\]*',
r'utils/[a-zA-Z0-9_/\\]*',
]
files = set()
for pattern in file_patterns:
matches = re.findall(pattern, task_description)
files.update(matches)
# 规范化路径
normalized_files = []
for f in files:
# 转换反斜杠
f = f.replace('\\', '/')
# 移除重复的斜杠
f = re.sub(r'/+', '/', f)
if f not in normalized_files:
normalized_files.append(f)
logger.debug(f"识别到文件: {normalized_files}")
return normalized_files
async def _acquire_locks(self, context: ExecutionContext) -> None:
"""获取所有需要的文件锁"""
for file_path in context.required_files:
success = await self.locks.acquire_lock(
file_path,
context.agent_id,
context.agent_role.upper()
)
if success:
context.acquired_locks.append(file_path)
logger.debug(f"获取锁成功: {file_path}")
else:
logger.warning(f"获取锁失败: {file_path} (可能被其他 Agent 占用)")
async def _release_locks(self, context: ExecutionContext) -> None:
"""释放所有获取的文件锁"""
for file_path in context.acquired_locks:
try:
await self.locks.release_lock(file_path, context.agent_id)
logger.debug(f"释放锁: {file_path}")
except Exception as e:
logger.warning(f"释放锁失败: {file_path}: {e}")
async def _build_messages(
self,
agent: AgentInfo,
task: Task,
context: ExecutionContext
) -> List[LLMMessage]:
"""构建 LLM 消息列表"""
messages = []
# 系统提示词
system_prompt = self._build_system_prompt(agent, context)
messages.append(LLMMessage(role="system", content=system_prompt))
# 添加上下文信息
if context.required_files:
context_info = f"\n相关文件: {', '.join(context.required_files)}\n"
# 尝试读取文件内容
file_contents = await self._read_file_contents(context.required_files)
if file_contents:
context_info += f"\n文件内容:\n{file_contents}\n"
messages.append(LLMMessage(role="user", content=context_info))
# 任务描述
messages.append(LLMMessage(role="user", content=task.description))
# 添加额外上下文
if task.context:
context_str = f"\n额外上下文:\n{json.dumps(task.context, ensure_ascii=False, indent=2)}\n"
messages.append(LLMMessage(role="user", content=context_str))
return messages
def _build_system_prompt(self, agent: AgentInfo, context: ExecutionContext) -> str:
"""构建系统提示词"""
role_prompts = {
"architect": """
你是一个系统架构师。你擅长:
- 系统设计和模块划分
- 技术选型和架构决策
- 接口设计和数据流规划
- 性能优化和扩展性考虑
请给出清晰、完整的架构方案。
""",
"pm": """
你是一个产品经理。你擅长:
- 需求分析和用户故事
- 功能优先级排序
- 产品规划
- 用户体验考虑
请从用户角度分析需求。
""",
"developer": """
你是一个高级开发工程师。你擅长:
- 编写高质量、可维护的代码
- 遵循最佳实践和编码规范
- 考虑边界情况和错误处理
- 编写清晰的注释和文档
请给出可以直接使用的代码实现。
""",
"reviewer": """
你是一个代码审查专家。你擅长:
- 发现代码中的潜在问题
- 安全漏洞检测
- 性能问题识别
- 代码风格和可读性改进
请给出详细的审查意见。
""",
"qa": """
你是一个测试工程师。你擅长:
- 编写全面的测试用例
- 边界条件测试
- 自动化测试
- 测试策略制定
请给出完整的测试方案。
"""
}
base_prompt = f"""你是 {agent.name},一个 AI 编程助手。
当前任务 ID: {context.task_id}
你的角色: {agent.role}
使用的模型: {agent.model}
工作原则:
1. 仔细理解任务需求
2. 给出清晰、具体的回答
3. 如果涉及代码,确保代码正确且可运行
4. 考虑边界情况和错误处理
5. 必要时给出解释和说明
"""
role_prompt = role_prompts.get(agent.role, "")
return base_prompt + role_prompt
async def _read_file_contents(self, file_paths: List[str]) -> str:
"""读取文件内容(如果存在)"""
contents = []
for file_path in file_paths[:3]: # 限制读取文件数量
full_path = self.work_dir / file_path
if full_path.exists():
try:
with open(full_path, 'r', encoding='utf-8') as f:
content = f.read()
# 限制每个文件的内容长度
if len(content) > 2000:
content = content[:2000] + "\n... (文件过长,已截断)"
contents.append(f"### {file_path}\n```\n{content}\n```")
except Exception as e:
logger.warning(f"读取文件失败: {file_path}: {e}")
return "\n\n".join(contents)
async def _call_llm(self, context: ExecutionContext) -> str:
"""调用 LLM 执行任务"""
if not self.llm:
# 如果没有配置 LLM 服务,使用模拟响应
return await self._mock_llm_response(context)
response = await self.llm.route_task(
task=context.messages[-1].content,
messages=context.messages,
preferred_model=context.agent_model
)
logger.info(f"LLM 调用完成: {response.provider}/{response.model}, "
f"tokens: {response.tokens_used}, latency: {response.latency:.2f}s")
return response.content
async def _mock_llm_response(self, context: ExecutionContext) -> str:
"""模拟 LLM 响应(用于测试)"""
await asyncio.sleep(0.5) # 模拟延迟
return f"""[模拟响应]
作为 {context.agent_role},我对任务的分析如下:
任务需要处理的文件: {', '.join(context.required_files) or ''}
## 分析
这是一个模拟响应,表示系统正在正常工作。
## 建议
1. 配置 LLM API 密钥以启用真实 AI 能力
2. 在环境变量中设置 ANTHROPIC_API_KEY 或 DEEPSEEK_API_KEY
3. 重启服务后即可使用完整功能
---
*Agent ID: {context.agent_id}*
*任务 ID: {context.task_id}*
"""
async def _process_result(
self,
llm_output: str,
context: ExecutionContext
) -> Result:
"""处理 LLM 输出,返回格式化结果"""
execution_time = time.time() - context.start_time
return Result(
success=True,
output=llm_output,
metadata={
"agent_id": context.agent_id,
"agent_role": context.agent_role,
"agent_model": context.agent_model,
"task_id": context.task_id,
"required_files": context.required_files,
"acquired_locks": context.acquired_locks,
"execution_time": execution_time
},
execution_time=execution_time
)
async def create_execution_plan(
self,
agent: AgentInfo,
task: str
) -> ExecutionPlan:
"""
创建任务执行计划
使用 LLM 分析任务,生成详细的执行步骤
"""
if not self.llm:
return self._create_mock_plan(task)
plan_prompt = f"""
请分析以下任务,生成执行计划。
任务: {task}
请返回 JSON 格式的执行计划,包含:
{{
"steps": ["步骤1", "步骤2", ...],
"required_files": ["file1.py", "file2.js", ...],
"estimated_duration": "预计时间",
"complexity": "simple|medium|complex",
"requires_code_execution": true/false,
"subtasks": ["子任务1", "子任务2", ...]
}}
"""
try:
response = await self.llm.route_task(
task=plan_prompt,
messages=[LLMMessage(role="user", content=plan_prompt)]
)
# 尝试解析 JSON
import json
plan_data = json.loads(response)
return ExecutionPlan(
steps=plan_data.get("steps", []),
required_files=plan_data.get("required_files", []),
estimated_duration=plan_data.get("estimated_duration", ""),
complexity=plan_data.get("complexity", "medium"),
requires_code_execution=plan_data.get("requires_code_execution", False),
subtasks=plan_data.get("subtasks", [])
)
except Exception as e:
logger.warning(f"解析执行计划失败: {e}")
return self._create_mock_plan(task)
def _create_mock_plan(self, task: str) -> ExecutionPlan:
"""创建模拟执行计划"""
return ExecutionPlan(
steps=[
"1. 分析任务需求",
"2. 查看相关文件",
"3. 制定实现方案",
"4. 执行实现"
],
estimated_duration="5-10 分钟",
complexity="medium"
)
# 单例获取函数
_executor: Optional[AgentExecutor] = None
def get_agent_executor(llm_service: ModelRouter = None) -> AgentExecutor:
"""获取 Agent 执行引擎单例"""
global _executor
if _executor is None:
_executor = AgentExecutor(llm_service=llm_service)
return _executor
def reset_agent_executor():
"""重置执行引擎(主要用于测试)"""
global _executor
_executor = None

View File

@@ -0,0 +1,261 @@
"""
Agent 注册服务 - 管理 Agent 的注册信息和状态
每个 Agent 有独立的目录存储其配置和状态
"""
import asyncio
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
from dataclasses import dataclass, asdict
from enum import Enum
from .storage import get_storage
class AgentRole(str, Enum):
"""Agent 角色枚举"""
ARCHITECT = "architect"
PRODUCT_MANAGER = "pm"
DEVELOPER = "developer"
QA = "qa"
REVIEWER = "reviewer"
HUMAN = "human"
@dataclass
class AgentInfo:
"""Agent 基本信息"""
agent_id: str # 唯一标识符,如 claude-001
name: str # 显示名称,如 Claude Code
role: str # 角色architect, pm, developer, qa, reviewer, human
model: str # 模型claude-opus-4.6, gpt-4o, human 等
description: str = "" # 描述
created_at: str = "" # 注册时间
status: str = "idle" # 当前状态
def __post_init__(self):
if not self.created_at:
self.created_at = datetime.now().isoformat()
@dataclass
class AgentState:
"""Agent 运行时状态"""
agent_id: str
current_task: str = ""
progress: int = 0
working_files: List[str] = None
last_update: str = ""
def __post_init__(self):
if self.working_files is None:
self.working_files = []
if not self.last_update:
self.last_update = datetime.now().isoformat()
class AgentRegistry:
"""
Agent 注册服务
管理所有 Agent 的注册信息和运行时状态
"""
def __init__(self):
self._storage = get_storage()
self._lock = asyncio.Lock()
def _get_agent_dir(self, agent_id: str) -> str:
"""获取 Agent 目录路径"""
return f"agents/{agent_id}"
def _get_agent_info_file(self, agent_id: str) -> str:
"""获取 Agent 信息文件路径"""
return f"{self._get_agent_dir(agent_id)}/info.json"
def _get_agent_state_file(self, agent_id: str) -> str:
"""获取 Agent 状态文件路径"""
return f"{self._get_agent_dir(agent_id)}/state.json"
async def register_agent(
self,
agent_id: str,
name: str,
role: str,
model: str,
description: str = ""
) -> AgentInfo:
"""
注册新 Agent
Args:
agent_id: Agent ID
name: 显示名称
role: 角色
model: 模型
description: 描述
Returns:
注册的 Agent 信息
"""
async with self._lock:
agent_info = AgentInfo(
agent_id=agent_id,
name=name,
role=role,
model=model,
description=description
)
# 确保 Agent 目录存在
await self._storage.ensure_dir(self._get_agent_dir(agent_id))
# 保存 Agent 信息
await self._storage.write_json(
self._get_agent_info_file(agent_id),
asdict(agent_info)
)
# 初始化状态
await self._storage.write_json(
self._get_agent_state_file(agent_id),
asdict(AgentState(agent_id=agent_id))
)
return agent_info
async def get_agent(self, agent_id: str) -> Optional[AgentInfo]:
"""
获取 Agent 信息
Args:
agent_id: Agent ID
Returns:
Agent 信息,不存在返回 None
"""
data = await self._storage.read_json(self._get_agent_info_file(agent_id))
if data:
return AgentInfo(**data)
return None
async def list_agents(self) -> List[AgentInfo]:
"""
列出所有已注册的 Agent
Returns:
Agent 信息列表
"""
agents = []
agents_dir = Path(self._storage.base_path) / "agents"
if not agents_dir.exists():
return []
for agent_dir in agents_dir.iterdir():
if agent_dir.is_dir():
info_file = agent_dir / "info.json"
if info_file.exists():
data = await self._storage.read_json(f"agents/{agent_dir.name}/info.json")
if data:
agents.append(AgentInfo(**data))
return agents
async def update_state(
self,
agent_id: str,
task: str = "",
progress: int = 0,
working_files: List[str] = None
) -> None:
"""
更新 Agent 状态
Args:
agent_id: Agent ID
task: 当前任务
progress: 进度 0-100
working_files: 正在处理的文件列表
"""
async with self._lock:
state_file = self._get_agent_state_file(agent_id)
# 读取现有状态
existing = await self._storage.read_json(state_file)
# 更新状态
state = AgentState(
agent_id=agent_id,
current_task=task or existing.get("current_task", ""),
progress=progress or existing.get("progress", 0),
working_files=working_files or existing.get("working_files", []),
last_update=datetime.now().isoformat()
)
await self._storage.write_json(state_file, asdict(state))
async def get_state(self, agent_id: str) -> Optional[AgentState]:
"""
获取 Agent 状态
Args:
agent_id: Agent ID
Returns:
Agent 状态,不存在返回 None
"""
data = await self._storage.read_json(self._get_agent_state_file(agent_id))
if data:
return AgentState(**data)
return None
async def unregister_agent(self, agent_id: str) -> bool:
"""
注销 Agent
Args:
agent_id: Agent ID
Returns:
是否成功注销
"""
async with self._lock:
agent_info = await self.get_agent(agent_id)
if not agent_info:
return False
# 删除 Agent 目录
agent_dir = self._get_agent_dir(agent_id)
# 实际实现中可能需要递归删除
# 这里简化处理,只删除 info.json 和 state.json
await self._storage.delete(f"{agent_dir}/info.json")
await self._storage.delete(f"{agent_dir}/state.json")
return True
async def get_agents_by_role(self, role: str) -> List[AgentInfo]:
"""
获取指定角色的所有 Agent
Args:
role: 角色
Returns:
符合条件的 Agent 列表
"""
all_agents = await self.list_agents()
return [agent for agent in all_agents if agent.role == role]
# 全局单例
_registry_instance: Optional[AgentRegistry] = None
def get_agent_registry() -> AgentRegistry:
"""获取 Agent 注册服务单例"""
global _registry_instance
if _registry_instance is None:
_registry_instance = AgentRegistry()
return _registry_instance

View File

@@ -0,0 +1,115 @@
"""
文件锁服务 - 管理 Agent 对文件的访问锁
"""
import asyncio
from datetime import datetime
from typing import Dict, List, Optional
from dataclasses import dataclass, asdict
from .storage import get_storage
@dataclass
class LockInfo:
"""文件锁信息"""
file_path: str
agent_id: str
acquired_at: str
agent_name: str = ""
@property
def elapsed_seconds(self) -> int:
acquired_time = datetime.fromisoformat(self.acquired_at)
return int((datetime.now() - acquired_time).total_seconds())
@property
def elapsed_display(self) -> str:
seconds = self.elapsed_seconds
if seconds < 60:
return f"{seconds}s ago"
minutes = seconds // 60
secs = seconds % 60
return f"{minutes}m {secs:02d}s ago"
class FileLockService:
"""文件锁服务"""
LOCKS_FILE = "cache/file_locks.json"
LOCK_TIMEOUT = 300
def __init__(self):
self._storage = get_storage()
self._lock = asyncio.Lock()
async def _load_locks(self) -> Dict[str, Dict]:
return await self._storage.read_json(self.LOCKS_FILE)
async def _save_locks(self, locks: Dict[str, Dict]) -> None:
await self._storage.write_json(self.LOCKS_FILE, locks)
def _is_expired(self, lock_data: Dict) -> bool:
acquired_at = datetime.fromisoformat(lock_data["acquired_at"])
return (datetime.now() - acquired_at).total_seconds() >= self.LOCK_TIMEOUT
async def _cleanup_expired(self, locks: Dict[str, Dict]) -> Dict[str, Dict]:
return {k: v for k, v in locks.items() if not self._is_expired(v)}
async def acquire_lock(self, file_path: str, agent_id: str, agent_name: str = "") -> bool:
async with self._lock:
locks = await self._cleanup_expired(await self._load_locks())
if file_path in locks and locks[file_path]["agent_id"] != agent_id:
return False
locks[file_path] = asdict(LockInfo(
file_path=file_path,
agent_id=agent_id,
acquired_at=datetime.now().isoformat(),
agent_name=agent_name
))
await self._save_locks(locks)
return True
async def release_lock(self, file_path: str, agent_id: str) -> bool:
async with self._lock:
locks = await self._load_locks()
if file_path not in locks or locks[file_path]["agent_id"] != agent_id:
return False
del locks[file_path]
await self._save_locks(locks)
return True
async def get_locks(self) -> List[LockInfo]:
locks = await self._cleanup_expired(await self._load_locks())
return [LockInfo(**data) for data in locks.values()]
async def check_locked(self, file_path: str) -> Optional[str]:
locks = await self._cleanup_expired(await self._load_locks())
return locks.get(file_path, {}).get("agent_id")
async def get_agent_locks(self, agent_id: str) -> List[LockInfo]:
return [lock for lock in await self.get_locks() if lock.agent_id == agent_id]
async def release_all_agent_locks(self, agent_id: str) -> int:
async with self._lock:
locks = await self._load_locks()
to_remove = [k for k, v in locks.items() if v["agent_id"] == agent_id]
for k in to_remove:
del locks[k]
await self._save_locks(locks)
return len(to_remove)
# 简化单例实现
_file_lock_service: Optional[FileLockService] = None
def get_file_lock_service() -> FileLockService:
global _file_lock_service
if _file_lock_service is None:
_file_lock_service = FileLockService()
return _file_lock_service

View File

@@ -0,0 +1,212 @@
"""
心跳服务 - 管理 Agent 心跳和超时检测
用于监控 Agent 活跃状态和检测掉线 Agent
"""
import asyncio
from datetime import datetime, timedelta
from typing import Dict, List, Optional
from dataclasses import dataclass, asdict
from .storage import get_storage
@dataclass
class HeartbeatInfo:
"""心跳信息"""
agent_id: str
last_heartbeat: str # 最后心跳时间 (ISO format)
status: str # Agent 状态working, waiting, idle, error
current_task: str = "" # 当前任务描述
progress: int = 0 # 任务进度 0-100
@property
def elapsed_seconds(self) -> int:
"""距最后心跳的秒数"""
last_time = datetime.fromisoformat(self.last_heartbeat)
return int((datetime.now() - last_time).total_seconds())
def is_timeout(self, timeout_seconds: int = 60) -> bool:
"""是否超时"""
return self.elapsed_seconds > timeout_seconds
@property
def elapsed_display(self) -> str:
"""格式化的时间差"""
seconds = self.elapsed_seconds
if seconds < 10:
return f"{seconds}s ago"
elif seconds < 60:
return f"{seconds}s"
minutes = seconds // 60
secs = seconds % 60
return f"{minutes}m {secs:02d}s"
class HeartbeatService:
"""
心跳服务
管理所有 Agent 的心跳记录,检测超时 Agent
"""
HEARTBEATS_FILE = "cache/heartbeats.json"
DEFAULT_TIMEOUT = 60 # 默认超时时间(秒)
def __init__(self):
self._storage = get_storage()
self._lock = asyncio.Lock()
async def _load_heartbeats(self) -> Dict[str, Dict]:
"""加载心跳记录"""
return await self._storage.read_json(self.HEARTBEATS_FILE)
async def _save_heartbeats(self, heartbeats: Dict[str, Dict]) -> None:
"""保存心跳记录"""
await self._storage.write_json(self.HEARTBEATS_FILE, heartbeats)
async def update_heartbeat(
self,
agent_id: str,
status: str,
current_task: str = "",
progress: int = 0
) -> None:
"""
更新 Agent 心跳
Args:
agent_id: Agent ID
status: 状态 (working, waiting, idle, error)
current_task: 当前任务
progress: 进度 0-100
"""
async with self._lock:
heartbeats = await self._load_heartbeats()
heartbeat_info = HeartbeatInfo(
agent_id=agent_id,
last_heartbeat=datetime.now().isoformat(),
status=status,
current_task=current_task,
progress=progress
)
heartbeats[agent_id] = asdict(heartbeat_info)
await self._save_heartbeats(heartbeats)
async def get_heartbeat(self, agent_id: str) -> Optional[HeartbeatInfo]:
"""
获取指定 Agent 的心跳信息
Args:
agent_id: Agent ID
Returns:
心跳信息,如果不存在返回 None
"""
heartbeats = await self._load_heartbeats()
data = heartbeats.get(agent_id)
if data:
return HeartbeatInfo(**data)
return None
async def get_all_heartbeats(self) -> Dict[str, HeartbeatInfo]:
"""
获取所有 Agent 的心跳信息
Returns:
Agent ID -> 心跳信息 的字典
"""
heartbeats = await self._load_heartbeats()
result = {}
for agent_id, data in heartbeats.items():
result[agent_id] = HeartbeatInfo(**data)
return result
async def check_timeout(self, timeout_seconds: int = None) -> List[str]:
"""
检查超时的 Agent
Args:
timeout_seconds: 超时秒数,默认使用 DEFAULT_TIMEOUT
Returns:
超时的 Agent ID 列表
"""
if timeout_seconds is None:
timeout_seconds = self.DEFAULT_TIMEOUT
all_heartbeats = await self.get_all_heartbeats()
timeout_agents = []
for agent_id, heartbeat in all_heartbeats.items():
if heartbeat.is_timeout(timeout_seconds):
timeout_agents.append(agent_id)
return timeout_agents
async def remove_heartbeat(self, agent_id: str) -> bool:
"""
移除 Agent 的心跳记录
Args:
agent_id: Agent ID
Returns:
是否成功移除
"""
async with self._lock:
heartbeats = await self._load_heartbeats()
if agent_id in heartbeats:
del heartbeats[agent_id]
await self._save_heartbeats(heartbeats)
return True
return False
async def get_active_agents(self, within_seconds: int = 120) -> List[str]:
"""
获取活跃的 Agent 列表
Args:
within_seconds: 活跃判定时间窗口(秒)
Returns:
活跃 Agent ID 列表
"""
all_heartbeats = await self.get_all_heartbeats()
active_agents = []
for agent_id, heartbeat in all_heartbeats.items():
if heartbeat.elapsed_seconds <= within_seconds:
active_agents.append(agent_id)
return active_agents
async def get_agents_by_status(self, status: str) -> List[HeartbeatInfo]:
"""
获取指定状态的所有 Agent
Args:
status: 状态 (working, waiting, idle, error)
Returns:
符合条件的 Agent 心跳信息列表
"""
all_heartbeats = await self.get_all_heartbeats()
return [
hb for hb in all_heartbeats.values()
if hb.status == status
]
# 全局单例
_heartbeat_service_instance: Optional[HeartbeatService] = None
def get_heartbeat_service() -> HeartbeatService:
"""获取心跳服务单例"""
global _heartbeat_service_instance
if _heartbeat_service_instance is None:
_heartbeat_service_instance = HeartbeatService()
return _heartbeat_service_instance

View File

@@ -0,0 +1,378 @@
"""
人类输入服务 - 管理人类参与者的任务请求和会议评论
"""
import asyncio
import uuid
from datetime import datetime
from typing import Dict, List, Optional
from dataclasses import dataclass, field, asdict
from .storage import get_storage
@dataclass
class TaskRequest:
"""人类任务请求"""
id: str
from_user: str # 提交者 ID
timestamp: str # 提交时间
priority: str # high | medium | low
type: str # 任务类型
title: str = "" # 任务标题
content: str = "" # 任务内容
target_files: List[str] = field(default_factory=list)
suggested_agent: str = "" # 建议的 Agent
urgent: bool = False
status: str = "pending" # pending | processing | completed | rejected
@property
def is_urgent(self) -> bool:
"""是否为紧急任务(高优先级 + urgent 标记)"""
return self.priority == "high" and self.urgent
@dataclass
class MeetingComment:
"""会议评论"""
id: str
from_user: str # 提交者 ID
meeting_id: str
timestamp: str
type: str # proposal | question | correction
priority: str = "normal"
content: str = ""
status: str = "pending" # pending | addressed | ignored
@dataclass
class HumanParticipant:
"""人类参与者信息"""
id: str
name: str
role: str = "" # tech_lead | product_owner | developer
status: str = "offline" # online | offline | busy
avatar: str = "👤"
class HumanInputService:
"""
人类输入服务
管理 humans.json 文件,处理人类任务请求和会议评论
"""
HUMANS_FILE = "humans.json"
# 优先级权重
PRIORITY_WEIGHT = {
"high": 3,
"medium": 2,
"low": 1,
"normal": 0
}
def __init__(self):
self._storage = get_storage()
self._lock = asyncio.Lock()
async def _load_humans(self) -> Dict:
"""加载 humans.json"""
return await self._storage.read_json(self.HUMANS_FILE)
async def _save_humans(self, data: Dict) -> None:
"""保存 humans.json"""
await self._storage.write_json(self.HUMANS_FILE, data)
async def _ensure_structure(self) -> None:
"""确保 humans.json 结构完整"""
async with self._lock:
data = await self._load_humans()
if not data:
data = {
"version": "1.0",
"last_updated": datetime.now().isoformat(),
"participants": {},
"task_requests": [],
"meeting_comments": [],
"human_states": {}
}
await self._save_humans(data)
async def register_participant(
self,
user_id: str,
name: str,
role: str = "",
avatar: str = "👤"
) -> None:
"""注册人类参与者"""
await self._ensure_structure()
async with self._lock:
data = await self._load_humans()
data["participants"][user_id] = asdict(HumanParticipant(
id=user_id,
name=name,
role=role,
avatar=avatar
))
data["last_updated"] = datetime.now().isoformat()
await self._save_humans(data)
async def add_task_request(
self,
from_user: str,
content: str,
priority: str = "medium",
task_type: str = "user_task",
title: str = "",
target_files: List[str] = None,
suggested_agent: str = "",
urgent: bool = False
) -> str:
"""
添加任务请求
Returns:
任务 ID
"""
await self._ensure_structure()
async with self._lock:
data = await self._load_humans()
task_id = f"req_{uuid.uuid4().hex[:8]}"
task = TaskRequest(
id=task_id,
from_user=from_user,
timestamp=datetime.now().isoformat(),
priority=priority,
type=task_type,
title=title,
content=content,
target_files=target_files or [],
suggested_agent=suggested_agent,
urgent=urgent
)
# 保存时转换为 JSON 兼容格式from_user -> from
task_dict = asdict(task)
task_dict["from"] = task_dict.pop("from_user")
data["task_requests"].append(task_dict)
data["last_updated"] = datetime.now().isoformat()
await self._save_humans(data)
return task_id
async def get_pending_tasks(
self,
priority_filter: str = None,
agent_filter: str = None
) -> List[TaskRequest]:
"""
获取待处理的任务请求
Args:
priority_filter: 过滤优先级
agent_filter: 过滤建议的 Agent
Returns:
按优先级排序的任务列表
"""
await self._ensure_structure()
data = await self._load_humans()
tasks = []
for t in data.get("task_requests", []):
if t["status"] != "pending":
continue
if priority_filter and t["priority"] != priority_filter:
continue
if agent_filter and t.get("suggested_agent") != agent_filter:
continue
# 转换 JSON 格式from -> from_user
t_dict = t.copy()
t_dict["from_user"] = t_dict.pop("from", "")
tasks.append(TaskRequest(**t_dict))
# 按优先级排序
tasks.sort(
key=lambda t: (
-self.PRIORITY_WEIGHT.get(t.priority, 0),
-t.urgent,
t.timestamp
)
)
return tasks
async def get_urgent_tasks(self) -> List[TaskRequest]:
"""获取紧急任务(高优先级 + urgent"""
return [t for t in await self.get_pending_tasks() if t.is_urgent]
async def mark_task_processing(self, task_id: str) -> bool:
"""标记任务为处理中"""
async with self._lock:
data = await self._load_humans()
for task in data.get("task_requests", []):
if task["id"] == task_id:
task["status"] = "processing"
data["last_updated"] = datetime.now().isoformat()
await self._save_humans(data)
return True
return False
async def mark_task_completed(self, task_id: str) -> bool:
"""标记任务为已完成"""
async with self._lock:
data = await self._load_humans()
for task in data.get("task_requests", []):
if task["id"] == task_id:
task["status"] = "completed"
data["last_updated"] = datetime.now().isoformat()
await self._save_humans(data)
return True
return False
async def add_meeting_comment(
self,
from_user: str,
meeting_id: str,
content: str,
comment_type: str = "proposal",
priority: str = "normal"
) -> str:
"""
添加会议评论
Returns:
评论 ID
"""
await self._ensure_structure()
async with self._lock:
data = await self._load_humans()
comment_id = f"comment_{uuid.uuid4().hex[:8]}"
comment = MeetingComment(
id=comment_id,
from_user=from_user,
meeting_id=meeting_id,
timestamp=datetime.now().isoformat(),
type=comment_type,
priority=priority,
content=content
)
# 保存时转换为 JSON 兼容格式from_user -> from
comment_dict = asdict(comment)
comment_dict["from"] = comment_dict.pop("from_user")
data["meeting_comments"].append(comment_dict)
data["last_updated"] = datetime.now().isoformat()
await self._save_humans(data)
return comment_id
async def get_pending_comments(self, meeting_id: str = None) -> List[MeetingComment]:
"""
获取待处理的会议评论
Args:
meeting_id: 过滤指定会议的评论
Returns:
评论列表
"""
await self._ensure_structure()
data = await self._load_humans()
comments = []
for c in data.get("meeting_comments", []):
if c["status"] != "pending":
continue
if meeting_id and c["meeting_id"] != meeting_id:
continue
# 转换 JSON 格式from -> from_user
c_dict = c.copy()
c_dict["from_user"] = c_dict.pop("from", "")
comments.append(MeetingComment(**c_dict))
return comments
async def mark_comment_addressed(self, comment_id: str) -> bool:
"""标记评论为已处理"""
async with self._lock:
data = await self._load_humans()
for comment in data["meeting_comments"]:
if comment["id"] == comment_id:
comment["status"] = "addressed"
data["last_updated"] = datetime.now().isoformat()
await self._save_humans(data)
return True
return False
async def get_participants(self) -> List[HumanParticipant]:
"""获取所有人类参与者"""
await self._ensure_structure()
data = await self._load_humans()
participants = []
for p in data.get("participants", {}).values():
participants.append(HumanParticipant(**p))
return participants
async def update_user_status(
self,
user_id: str,
status: str,
current_focus: str = ""
) -> bool:
"""更新用户状态"""
await self._ensure_structure()
async with self._lock:
data = await self._load_humans()
if user_id not in data.get("participants", {}):
return False
data["participants"][user_id]["status"] = status
if "human_states" not in data:
data["human_states"] = {}
data["human_states"][user_id] = {
"status": status,
"current_focus": current_focus,
"last_update": datetime.now().isoformat()
}
data["last_updated"] = datetime.now().isoformat()
await self._save_humans(data)
return True
async def get_summary(self) -> Dict:
"""获取人类输入服务的摘要信息"""
await self._ensure_structure()
data = await self._load_humans()
pending_tasks = await self.get_pending_tasks()
urgent_tasks = await self.get_urgent_tasks()
pending_comments = await self.get_pending_comments()
participants = await self.get_participants()
return {
"participants": len(participants),
"online_users": len([p for p in participants if p.status == "online"]),
"pending_tasks": len(pending_tasks),
"urgent_tasks": len(urgent_tasks),
"pending_comments": len(pending_comments),
"last_updated": data.get("last_updated", "")
}
# 全局单例
_human_input_service: Optional[HumanInputService] = None
def get_human_input_service() -> HumanInputService:
"""获取人类输入服务单例"""
global _human_input_service
if _human_input_service is None:
_human_input_service = HumanInputService()
return _human_input_service

View File

@@ -0,0 +1,669 @@
"""
LLM 服务层
提供统一的 LLM 调用接口,支持多个提供商:
- Anthropic (Claude)
- OpenAI (GPT)
- DeepSeek
- Ollama (本地模型)
- Google (Gemini)
"""
import os
import json
import asyncio
import logging
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Any, Union
from dataclasses import dataclass
from enum import Enum
import time
logger = logging.getLogger(__name__)
class TaskType(Enum):
"""任务类型分类"""
COMPLEX_REASONING = "complex_reasoning"
CODE_GENERATION = "code_generation"
CODE_REVIEW = "code_review"
SIMPLE_TASK = "simple_task"
COST_SENSITIVE = "cost_sensitive"
LOCAL_PRIVACY = "local_privacy"
MULTIMODAL = "multimodal"
ARCHITECTURE_DESIGN = "architecture_design"
TEST_GENERATION = "test_generation"
@dataclass
class LLMMessage:
"""LLM 消息"""
role: str # system, user, assistant
content: str
images: Optional[List[str]] = None
@dataclass
class LLMResponse:
"""LLM 响应"""
content: str
model: str
provider: str
tokens_used: int = 0
finish_reason: str = ""
latency: float = 0.0
@dataclass
class LLMConfig:
"""LLM 配置"""
# Anthropic
anthropic_api_key: Optional[str] = None
anthropic_base_url: str = "https://api.anthropic.com"
# OpenAI
openai_api_key: Optional[str] = None
openai_base_url: str = "https://api.openai.com/v1"
# DeepSeek
deepseek_api_key: Optional[str] = None
deepseek_base_url: str = "https://api.deepseek.com"
# Google
google_api_key: Optional[str] = None
# Ollama
ollama_base_url: str = "http://localhost:11434"
# 通用设置
default_timeout: int = 120
max_retries: int = 3
temperature: float = 0.7
max_tokens: int = 4096
@classmethod
def from_env(cls) -> "LLMConfig":
"""从环境变量加载配置"""
return cls(
anthropic_api_key=os.getenv("ANTHROPIC_API_KEY"),
openai_api_key=os.getenv("OPENAI_API_KEY"),
deepseek_api_key=os.getenv("DEEPSEEK_API_KEY"),
google_api_key=os.getenv("GOOGLE_API_KEY"),
ollama_base_url=os.getenv("OLLAMA_BASE_URL", "http://localhost:11434"),
default_timeout=int(os.getenv("LLM_TIMEOUT", "120")),
max_retries=int(os.getenv("LLM_MAX_RETRIES", "3")),
temperature=float(os.getenv("LLM_TEMPERATURE", "0.7")),
max_tokens=int(os.getenv("LLM_MAX_TOKENS", "4096"))
)
class LLMProvider(ABC):
"""LLM 提供商抽象基类"""
def __init__(self, config: LLMConfig):
self.config = config
@property
@abstractmethod
def provider_name(self) -> str:
"""提供商名称"""
pass
@abstractmethod
async def chat_completion(
self,
model: str,
messages: List[LLMMessage],
temperature: float = None,
max_tokens: int = None,
**kwargs
) -> LLMResponse:
"""聊天补全"""
pass
@abstractmethod
async def stream_completion(
self,
model: str,
messages: List[LLMMessage],
**kwargs
):
"""流式补全"""
pass
@abstractmethod
def get_available_models(self) -> List[str]:
"""获取可用模型列表"""
pass
async def _retry_with_backoff(self, func, *args, **kwargs):
"""带退避的重试机制"""
last_error = None
for attempt in range(self.config.max_retries):
try:
return await func(*args, **kwargs)
except Exception as e:
last_error = e
if attempt < self.config.max_retries - 1:
wait_time = 2 ** attempt
logger.warning(f"Attempt {attempt + 1} failed, retrying in {wait_time}s: {e}")
await asyncio.sleep(wait_time)
else:
logger.error(f"All {self.config.max_retries} attempts failed")
raise last_error
class AnthropicProvider(LLMProvider):
"""Anthropic Claude 提供商"""
@property
def provider_name(self) -> str:
return "anthropic"
def __init__(self, config: LLMConfig):
super().__init__(config)
self._client = None
def _get_client(self):
"""懒加载客户端"""
if self._client is None:
try:
import anthropic
self._client = anthropic.AsyncAnthropic(
api_key=self.config.anthropic_api_key,
base_url=self.config.anthropic_base_url,
timeout=self.config.default_timeout
)
except ImportError:
raise ImportError("请安装 anthropic 包: pip install anthropic")
return self._client
async def chat_completion(
self,
model: str,
messages: List[LLMMessage],
temperature: float = None,
max_tokens: int = None,
**kwargs
) -> LLMResponse:
start_time = time.time()
# 分离系统消息
system_message = ""
user_messages = []
for msg in messages:
if msg.role == "system":
system_message = msg.content
else:
user_messages.append({
"role": msg.role,
"content": msg.content
})
client = self._get_client()
response = await self._retry_with_backoff(
client.messages.create,
model=model,
system=system_message if system_message else None,
messages=user_messages,
temperature=temperature or self.config.temperature,
max_tokens=max_tokens or self.config.max_tokens,
**kwargs
)
latency = time.time() - start_time
return LLMResponse(
content=response.content[0].text,
model=model,
provider=self.provider_name,
tokens_used=response.usage.input_tokens + response.usage.output_tokens,
finish_reason=response.stop_reason,
latency=latency
)
async def stream_completion(self, model: str, messages: List[LLMMessage], **kwargs):
client = self._get_client()
system_message = ""
user_messages = []
for msg in messages:
if msg.role == "system":
system_message = msg.content
else:
user_messages.append({"role": msg.role, "content": msg.content})
async with client.messages.stream(
model=model,
system=system_message if system_message else None,
messages=user_messages,
max_tokens=self.config.max_tokens,
**kwargs
) as stream:
async for text in stream.text_stream:
yield text
def get_available_models(self) -> List[str]:
return [
"claude-opus-4.6",
"claude-sonnet-4.6",
"claude-haiku-4.6",
"claude-3-5-sonnet-20241022",
"claude-3-5-haiku-20241022"
]
class OpenAIProvider(LLMProvider):
"""OpenAI GPT 提供商"""
@property
def provider_name(self) -> str:
return "openai"
def __init__(self, config: LLMConfig):
super().__init__(config)
self._client = None
def _get_client(self):
if self._client is None:
try:
import openai
self._client = openai.AsyncOpenAI(
api_key=self.config.openai_api_key,
base_url=self.config.openai_base_url,
timeout=self.config.default_timeout
)
except ImportError:
raise ImportError("请安装 openai 包: pip install openai")
return self._client
async def chat_completion(
self,
model: str,
messages: List[LLMMessage],
temperature: float = None,
max_tokens: int = None,
**kwargs
) -> LLMResponse:
start_time = time.time()
client = self._get_client()
api_messages = [
{"role": msg.role, "content": msg.content}
for msg in messages
]
response = await self._retry_with_backoff(
client.chat.completions.create,
model=model,
messages=api_messages,
temperature=temperature or self.config.temperature,
max_tokens=max_tokens or self.config.max_tokens,
**kwargs
)
latency = time.time() - start_time
return LLMResponse(
content=response.choices[0].message.content,
model=model,
provider=self.provider_name,
tokens_used=response.usage.total_tokens,
finish_reason=response.choices[0].finish_reason,
latency=latency
)
async def stream_completion(self, model: str, messages: List[LLMMessage], **kwargs):
client = self._get_client()
api_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
stream = await client.chat.completions.create(
model=model,
messages=api_messages,
stream=True,
**kwargs
)
async for chunk in stream:
if chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
def get_available_models(self) -> List[str]:
return [
"gpt-4o",
"gpt-4o-mini",
"gpt-4-turbo",
"gpt-3.5-turbo"
]
class DeepSeekProvider(LLMProvider):
"""DeepSeek 提供商"""
@property
def provider_name(self) -> str:
return "deepseek"
def __init__(self, config: LLMConfig):
super().__init__(config)
self._client = None
def _get_client(self):
if self._client is None:
try:
import openai
self._client = openai.AsyncOpenAI(
api_key=self.config.deepseek_api_key,
base_url=self.config.deepseek_base_url,
timeout=self.config.default_timeout
)
except ImportError:
raise ImportError("请安装 openai 包: pip install openai")
return self._client
async def chat_completion(
self,
model: str,
messages: List[LLMMessage],
temperature: float = None,
max_tokens: int = None,
**kwargs
) -> LLMResponse:
start_time = time.time()
client = self._get_client()
api_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
response = await self._retry_with_backoff(
client.chat.completions.create,
model=model,
messages=api_messages,
temperature=temperature or self.config.temperature,
max_tokens=max_tokens or self.config.max_tokens,
**kwargs
)
latency = time.time() - start_time
return LLMResponse(
content=response.choices[0].message.content,
model=model,
provider=self.provider_name,
tokens_used=response.usage.total_tokens,
finish_reason=response.choices[0].finish_reason,
latency=latency
)
async def stream_completion(self, model: str, messages: List[LLMMessage], **kwargs):
client = self._get_client()
api_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
stream = await client.chat.completions.create(
model=model,
messages=api_messages,
stream=True,
**kwargs
)
async for chunk in stream:
if chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
def get_available_models(self) -> List[str]:
return [
"deepseek-chat",
"deepseek-coder"
]
class OllamaProvider(LLMProvider):
"""Ollama 本地模型提供商"""
@property
def provider_name(self) -> str:
return "ollama"
def __init__(self, config: LLMConfig):
super().__init__(config)
self._base_url = config.ollama_base_url
async def chat_completion(
self,
model: str,
messages: List[LLMMessage],
temperature: float = None,
max_tokens: int = None,
**kwargs
) -> LLMResponse:
import aiohttp
start_time = time.time()
api_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
payload = {
"model": model,
"messages": api_messages,
"stream": False,
"options": {
"temperature": temperature or self.config.temperature,
"num_predict": max_tokens or self.config.max_tokens
}
}
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self._base_url}/api/chat",
json=payload,
timeout=self.config.default_timeout
) as response:
result = await response.json()
latency = time.time() - start_time
return LLMResponse(
content=result.get("message", {}).get("content", ""),
model=model,
provider=self.provider_name,
tokens_used=result.get("prompt_eval_count", 0) + result.get("eval_count", 0),
latency=latency
)
async def stream_completion(self, model: str, messages: List[LLMMessage], **kwargs):
import aiohttp
api_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
payload = {
"model": model,
"messages": api_messages,
"stream": True
}
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self._base_url}/api/chat",
json=payload
) as response:
async for line in response.content:
if line:
data = json.loads(line)
if "message" in data:
yield data["message"].get("content", "")
def get_available_models(self) -> List[str]:
return ["llama3", "llama3.2", "mistral", "codellama", "deepseek-coder"]
class ModelRouter:
"""
智能模型路由器
根据任务类型和需求自动选择最合适的模型
"""
# 默认路由规则
ROUTING_RULES = {
TaskType.COMPLEX_REASONING: ("anthropic", "claude-opus-4.6"),
TaskType.CODE_GENERATION: ("anthropic", "claude-sonnet-4.6"),
TaskType.CODE_REVIEW: ("anthropic", "claude-sonnet-4.6"),
TaskType.ARCHITECTURE_DESIGN: ("anthropic", "claude-opus-4.6"),
TaskType.TEST_GENERATION: ("anthropic", "claude-sonnet-4.6"),
TaskType.SIMPLE_TASK: ("anthropic", "claude-haiku-4.6"),
TaskType.COST_SENSITIVE: ("deepseek", "deepseek-chat"),
TaskType.LOCAL_PRIVACY: ("ollama", "llama3"),
}
def __init__(self, config: LLMConfig = None):
self.config = config or LLMConfig.from_env()
self.providers: Dict[str, LLMProvider] = {}
self._initialize_providers()
def _initialize_providers(self):
"""初始化可用的提供商"""
if self.config.anthropic_api_key:
self.providers["anthropic"] = AnthropicProvider(self.config)
if self.config.openai_api_key:
self.providers["openai"] = OpenAIProvider(self.config)
if self.config.deepseek_api_key:
self.providers["deepseek"] = DeepSeekProvider(self.config)
# Ollama 总是可用(本地服务)
self.providers["ollama"] = OllamaProvider(self.config)
def classify_task(self, task_description: str) -> TaskType:
"""
分析任务描述,分类任务类型
使用关键词匹配和启发式规则
"""
task_lower = task_description.lower()
# 检查关键词
keywords_map = {
TaskType.ARCHITECTURE_DESIGN: ["架构", "设计", "系统设计", "技术选型", "架构图"],
TaskType.CODE_GENERATION: ["实现", "编写", "生成代码", "开发", "创建函数"],
TaskType.CODE_REVIEW: ["审查", "review", "检查", "分析代码"],
TaskType.TEST_GENERATION: ["测试", "单元测试", "测试用例"],
TaskType.COMPLEX_REASONING: ["分析", "推理", "判断", "复杂", "评估"],
}
# 计算匹配分数
scores = {}
for task_type, keywords in keywords_map.items():
score = sum(1 for kw in keywords if kw in task_lower)
if score > 0:
scores[task_type] = score
# 返回最高分的类型
if scores:
return max(scores, key=scores.get)
# 默认返回简单任务
return TaskType.SIMPLE_TASK
def get_route(self, task_type: TaskType, preferred_provider: str = None) -> tuple:
"""
获取路由决策
返回: (provider_name, model_name)
"""
# 如果指定了提供商,尝试使用
if preferred_provider and preferred_provider in self.providers:
provider = self.providers[preferred_provider]
models = provider.get_available_models()
if models:
return preferred_provider, models[0]
# 使用路由规则
if task_type in self.ROUTING_RULES:
provider_name, model_name = self.ROUTING_RULES[task_type]
if provider_name in self.providers:
return provider_name, model_name
# 回退到第一个可用的提供商
for provider_name, provider in self.providers.items():
models = provider.get_available_models()
if models:
return provider_name, models[0]
raise RuntimeError("没有可用的 LLM 提供商")
async def route_task(
self,
task: str,
messages: List[LLMMessage] = None,
preferred_model: str = None,
preferred_provider: str = None,
**kwargs
) -> LLMResponse:
"""
智能路由任务到合适的模型
参数:
task: 任务描述
messages: 消息列表(如果为 None会自动从 task 创建)
preferred_model: 首选模型
preferred_provider: 首选提供商
"""
# 如果指定了首选模型,尝试直接使用
if preferred_model:
if "-" in preferred_model:
# 从模型名推断提供商
if preferred_model.startswith("claude"):
provider_name = "anthropic"
elif preferred_model.startswith("gpt"):
provider_name = "openai"
elif preferred_model.startswith("deepseek"):
provider_name = "deepseek"
else:
provider_name = preferred_provider or "anthropic"
if provider_name in self.providers:
provider = self.providers[provider_name]
return await provider.chat_completion(
preferred_model,
messages or [LLMMessage(role="user", content=task)],
**kwargs
)
# 分类任务类型
task_type = self.classify_task(task)
provider_name, model_name = self.get_route(task_type, preferred_provider)
logger.info(f"路由任务: {task_type.value} -> {provider_name}/{model_name}")
provider = self.providers[provider_name]
return await provider.chat_completion(
model_name,
messages or [LLMMessage(role="user", content=task)],
**kwargs
)
def get_available_providers(self) -> List[str]:
"""获取所有可用的提供商"""
return list(self.providers.keys())
def get_provider_models(self, provider_name: str) -> List[str]:
"""获取指定提供商的可用模型"""
if provider_name in self.providers:
return self.providers[provider_name].get_available_models()
return []
# 单例获取函数
_llm_service: Optional[ModelRouter] = None
def get_llm_service(config: LLMConfig = None) -> ModelRouter:
"""获取 LLM 服务单例"""
global _llm_service
if _llm_service is None:
_llm_service = ModelRouter(config or LLMConfig.from_env())
return _llm_service
def reset_llm_service():
"""重置 LLM 服务(主要用于测试)"""
global _llm_service
_llm_service = None

View File

@@ -0,0 +1,404 @@
"""
会议记录服务 - 记录会议内容、讨论和共识
将会议记录保存为 Markdown 文件,按日期组织
"""
import asyncio
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
from dataclasses import dataclass, field, asdict
from .storage import get_storage
@dataclass
class DiscussionEntry:
"""单条讨论记录"""
agent_id: str
agent_name: str
content: str
timestamp: str = ""
step: str = ""
def __post_init__(self):
if not self.timestamp:
self.timestamp = datetime.now().isoformat()
@dataclass
class ProgressStep:
"""会议进度步骤"""
step_id: str
label: str
status: str = "pending" # pending, active, completed
completed_at: str = ""
def mark_active(self):
self.status = "active"
def mark_completed(self):
self.status = "completed"
self.completed_at = datetime.now().isoformat()
@dataclass
class MeetingInfo:
"""会议信息"""
meeting_id: str
title: str
date: str # YYYY-MM-DD
attendees: List[str] = field(default_factory=list)
steps: List[ProgressStep] = field(default_factory=list)
discussions: List[DiscussionEntry] = field(default_factory=list)
status: str = "in_progress" # in_progress, completed
created_at: str = ""
ended_at: str = ""
consensus: str = "" # 最终共识
def __post_init__(self):
if not self.created_at:
self.created_at = datetime.now().isoformat()
if not self.date:
self.date = datetime.now().strftime("%Y-%m-%d")
@property
def current_step(self) -> Optional[ProgressStep]:
"""获取当前活跃的步骤"""
for step in self.steps:
if step.status == "active":
return step
return None
@property
def completed_steps(self) -> List[ProgressStep]:
"""获取已完成的步骤"""
return [s for s in self.steps if s.status == "completed"]
@property
def progress_summary(self) -> str:
"""进度摘要"""
total = len(self.steps)
if total == 0:
return "0/0"
completed = len(self.completed_steps)
active = 1 if self.current_step else 0
return f"{completed}/{total}" + (" (active)" if active else "")
class MeetingRecorder:
"""
会议记录服务
记录会议的讨论内容、进度和共识,保存为 Markdown 文件
"""
def __init__(self):
self._storage = get_storage()
self._lock = asyncio.Lock()
def _parse_meeting_data(self, data: dict) -> MeetingInfo:
"""将字典数据转换为 MeetingInfo 对象"""
# 转换 steps
steps = [
ProgressStep(**s) if isinstance(s, dict) else s
for s in data.get("steps", [])
]
# 转换 discussions
discussions = [
DiscussionEntry(**d) if isinstance(d, dict) else d
for d in data.get("discussions", [])
]
# 创建 MeetingInfo
data["steps"] = steps
data["discussions"] = discussions
return MeetingInfo(**data)
def _get_meeting_dir(self, date: str) -> str:
"""获取会议目录路径"""
return f"meetings/{date}"
def _get_meeting_file(self, meeting_id: str, date: str) -> str:
"""获取会议文件路径"""
return f"{self._get_meeting_dir(date)}/{meeting_id}.md"
async def create_meeting(
self,
meeting_id: str,
title: str,
attendees: List[str],
steps: List[str] = None
) -> MeetingInfo:
"""
创建新会议记录
Args:
meeting_id: 会议 ID
title: 会议标题
attendees: 参会者列表
steps: 会议步骤列表
Returns:
创建的会议信息
"""
async with self._lock:
date = datetime.now().strftime("%Y-%m-%d")
# 创建进度步骤
progress_steps = []
if steps:
for i, step_label in enumerate(steps):
progress_steps.append(ProgressStep(
step_id=f"step_{i+1}",
label=step_label,
status="pending"
))
meeting = MeetingInfo(
meeting_id=meeting_id,
title=title,
date=date,
attendees=attendees,
steps=progress_steps
)
# 保存为 Markdown
await self._save_meeting_markdown(meeting)
return meeting
async def _save_meeting_markdown(self, meeting: MeetingInfo) -> None:
"""将会议保存为 Markdown 文件"""
lines = [
f"# {meeting.title}",
"",
f"**会议 ID**: {meeting.meeting_id}",
f"**日期**: {meeting.date}",
f"**状态**: {meeting.status}",
f"**参会者**: {', '.join(meeting.attendees)}",
"",
"## 会议进度",
"",
]
# 进度步骤
for step in meeting.steps:
status_icon = {
"pending": "",
"active": "",
"completed": ""
}.get(step.status, "")
time_str = f" ({step.completed_at})" if step.completed_at else ""
lines.append(f"- {status_icon} **{step.label}**{time_str}")
lines.append("")
lines.append("## 讨论记录")
lines.append("")
# 讨论内容
for discussion in meeting.discussions:
lines.append(f"### {discussion.agent_name} - {discussion.timestamp[:19]}")
if discussion.step:
lines.append(f"*步骤: {discussion.step}*")
lines.append("")
lines.append(discussion.content)
lines.append("")
# 共识
if meeting.consensus:
lines.append("## 共识")
lines.append("")
lines.append(meeting.consensus)
lines.append("")
# 元数据
lines.append("---")
lines.append("")
lines.append(f"**创建时间**: {meeting.created_at}")
if meeting.ended_at:
lines.append(f"**结束时间**: {meeting.ended_at}")
content = "\n".join(lines)
file_path = self._get_meeting_file(meeting.meeting_id, meeting.date)
# 使用存储服务保存
await self._storage.ensure_dir(self._get_meeting_dir(meeting.date))
await self._storage.write_json(file_path.replace(".md", ".json"), asdict(meeting))
# 同时保存 Markdown
import aiofiles
full_path = Path(self._storage.base_path) / file_path
async with aiofiles.open(full_path, mode="w", encoding="utf-8") as f:
await f.write(content)
async def add_discussion(
self,
meeting_id: str,
agent_id: str,
agent_name: str,
content: str,
step: str = ""
) -> None:
"""
添加讨论记录
Args:
meeting_id: 会议 ID
agent_id: Agent ID
agent_name: Agent 名称
content: 讨论内容
step: 当前步骤
"""
async with self._lock:
# 加载会议信息
date = datetime.now().strftime("%Y-%m-%d")
file_path = self._get_meeting_file(meeting_id, date)
json_path = file_path.replace(".md", ".json")
data = await self._storage.read_json(json_path)
if not data:
return # 会议不存在
meeting = self._parse_meeting_data(data)
meeting.discussions.append(DiscussionEntry(
agent_id=agent_id,
agent_name=agent_name,
content=content,
step=step
))
# 保存
await self._save_meeting_markdown(meeting)
async def update_progress(
self,
meeting_id: str,
step_label: str
) -> None:
"""
更新会议进度
Args:
meeting_id: 会议 ID
step_label: 步骤名称
"""
async with self._lock:
date = datetime.now().strftime("%Y-%m-%d")
json_path = self._get_meeting_file(meeting_id, date).replace(".md", ".json")
data = await self._storage.read_json(json_path)
if not data:
return
meeting = self._parse_meeting_data(data)
# 查找并更新步骤
step_found = False
for step in meeting.steps:
if step.label == step_label:
# 将之前的活跃步骤标记为完成
if meeting.current_step:
meeting.current_step.mark_completed()
step.mark_active()
step_found = True
break
if not step_found and meeting.steps:
# 如果找不到,将第一个 pending 步骤设为活跃
for step in meeting.steps:
if step.status == "pending":
if meeting.current_step:
meeting.current_step.mark_completed()
step.mark_active()
break
await self._save_meeting_markdown(meeting)
async def get_meeting(self, meeting_id: str, date: str = None) -> Optional[MeetingInfo]:
"""
获取会议信息
Args:
meeting_id: 会议 ID
date: 日期,默认为今天
Returns:
会议信息
"""
if date is None:
date = datetime.now().strftime("%Y-%m-%d")
json_path = self._get_meeting_file(meeting_id, date).replace(".md", ".json")
data = await self._storage.read_json(json_path)
if data:
return self._parse_meeting_data(data)
return None
async def list_meetings(self, date: str = None) -> List[MeetingInfo]:
"""
列出指定日期的会议
Args:
date: 日期,默认为今天
Returns:
会议列表
"""
if date is None:
date = datetime.now().strftime("%Y-%m-%d")
meetings_dir = Path(self._storage.base_path) / self._get_meeting_dir(date)
if not meetings_dir.exists():
return []
meetings = []
for json_file in meetings_dir.glob("*.json"):
data = await self._storage.read_json(f"meetings/{date}/{json_file.name}")
if data:
meetings.append(self._parse_meeting_data(data))
return sorted(meetings, key=lambda m: m.created_at)
async def end_meeting(self, meeting_id: str, consensus: str = "") -> bool:
"""
结束会议
Args:
meeting_id: 会议 ID
consensus: 最终共识
Returns:
是否成功
"""
async with self._lock:
date = datetime.now().strftime("%Y-%m-%d")
json_path = self._get_meeting_file(meeting_id, date).replace(".md", ".json")
data = await self._storage.read_json(json_path)
if not data:
return False
meeting = self._parse_meeting_data(data)
meeting.status = "completed"
meeting.ended_at = datetime.now().isoformat()
if consensus:
meeting.consensus = consensus
# 完成当前步骤
if meeting.current_step:
meeting.current_step.mark_completed()
await self._save_meeting_markdown(meeting)
return True
# 全局单例
_recorder_instance: Optional[MeetingRecorder] = None
def get_meeting_recorder() -> MeetingRecorder:
"""获取会议记录服务单例"""
global _recorder_instance
if _recorder_instance is None:
_recorder_instance = MeetingRecorder()
return _recorder_instance

View File

@@ -0,0 +1,172 @@
"""
会议调度器 - 实现栅栏同步Barrier Synchronization
"""
import asyncio
from datetime import datetime
from typing import Dict, List, Optional, Set
from dataclasses import dataclass, asdict
from .storage import get_storage
@dataclass
class MeetingQueue:
"""会议等待队列"""
meeting_id: str
title: str
expected_attendees: List[str]
arrived_attendees: List[str]
status: str = "waiting"
created_at: str = ""
started_at: str = ""
min_required: int = 2
def __post_init__(self):
if not self.created_at:
self.created_at = datetime.now().isoformat()
@property
def is_ready(self) -> bool:
expected = set(self.expected_attendees)
arrived = set(self.arrived_attendees)
return expected.issubset(arrived) and len(arrived) >= self.min_required
@property
def missing_attendees(self) -> List[str]:
return list(set(self.expected_attendees) - set(self.arrived_attendees))
@property
def progress(self) -> str:
return f"{len(self.arrived_attendees)}/{len(self.expected_attendees)}"
class MeetingScheduler:
"""会议调度器 - 栅栏同步实现"""
QUEUES_FILE = "cache/meeting_queue.json"
def __init__(self):
self._storage = get_storage()
self._lock = asyncio.Lock()
self._events: Dict[str, asyncio.Event] = {}
async def _load_queues(self) -> Dict[str, Dict]:
return await self._storage.read_json(self.QUEUES_FILE)
async def _save_queues(self, queues: Dict[str, Dict]) -> None:
await self._storage.write_json(self.QUEUES_FILE, queues)
async def create_meeting(
self,
meeting_id: str,
title: str,
expected_attendees: List[str],
min_required: int = None
) -> MeetingQueue:
async with self._lock:
queue = MeetingQueue(
meeting_id=meeting_id,
title=title,
expected_attendees=expected_attendees,
arrived_attendees=[],
min_required=min_required or len(expected_attendees)
)
queues = await self._load_queues()
queues[meeting_id] = asdict(queue)
await self._save_queues(queues)
return queue
async def get_queue(self, meeting_id: str) -> Optional[MeetingQueue]:
queues = await self._load_queues()
return MeetingQueue(**queues[meeting_id]) if meeting_id in queues else None
async def wait_for_meeting(
self,
agent_id: str,
meeting_id: str,
timeout: int = 300
) -> str:
async with self._lock:
queues = await self._load_queues()
if meeting_id not in queues:
await self.create_meeting(
meeting_id=meeting_id,
title=f"Meeting: {meeting_id}",
expected_attendees=[agent_id],
min_required=1
)
return "started"
queue_data = queues[meeting_id]
if agent_id not in queue_data.get("arrived_attendees", []):
queue_data["arrived_attendees"].append(agent_id)
queue_data["arrived_attendees"].sort()
await self._save_queues(queues)
queue = MeetingQueue(**queue_data)
is_ready = queue.is_ready
if is_ready:
await self._start_meeting(meeting_id)
return "started"
# 等待会议开始
event = self._events.setdefault(meeting_id, asyncio.Event())
try:
await asyncio.wait_for(event.wait(), timeout=timeout)
return "started"
except asyncio.TimeoutError:
return "timeout"
async def _start_meeting(self, meeting_id: str) -> None:
async with self._lock:
queues = await self._load_queues()
if meeting_id in queues:
queues[meeting_id]["status"] = "ready"
queues[meeting_id]["started_at"] = datetime.now().isoformat()
await self._save_queues(queues)
# 唤醒所有等待者
event = self._events.get(meeting_id)
if event:
event.set()
async def end_meeting(self, meeting_id: str) -> bool:
async with self._lock:
queues = await self._load_queues()
if meeting_id not in queues:
return False
queues[meeting_id]["status"] = "ended"
await self._save_queues(queues)
self._events.pop(meeting_id, None)
return True
async def get_all_queues(self) -> List[MeetingQueue]:
queues = await self._load_queues()
return [MeetingQueue(**data) for data in queues.values()]
async def add_attendee(self, meeting_id: str, agent_id: str) -> bool:
async with self._lock:
queues = await self._load_queues()
if meeting_id not in queues:
return False
if agent_id not in queues[meeting_id]["expected_attendees"]:
queues[meeting_id]["expected_attendees"].append(agent_id)
await self._save_queues(queues)
return True
return False
# 简化单例实现
_scheduler_instance: Optional[MeetingScheduler] = None
def get_meeting_scheduler() -> MeetingScheduler:
global _scheduler_instance
if _scheduler_instance is None:
_scheduler_instance = MeetingScheduler()
return _scheduler_instance

View File

@@ -0,0 +1,436 @@
"""
Agent 进程管理器
负责启动、停止和监控 Agent 进程/任务。
支持两种类型的 Agent:
1. NativeLLMAgent - 异步任务,无需外部进程
2. ProcessWrapperAgent - 外部 CLI 工具,需要进程管理
"""
import asyncio
import logging
import signal
import subprocess
import psutil
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from ..adapters.native_llm_agent import NativeLLMAgent, NativeLLMAgentFactory
from .heartbeat import get_heartbeat_service
from .agent_registry import get_agent_registry
logger = logging.getLogger(__name__)
class AgentStatus(Enum):
"""Agent 状态"""
STOPPED = "stopped"
STARTING = "starting"
RUNNING = "running"
STOPPING = "stopping"
CRASHED = "crashed"
UNKNOWN = "unknown"
@dataclass
class AgentProcess:
"""Agent 进程信息"""
agent_id: str
agent_type: str # native_llm, process_wrapper
status: AgentStatus = AgentStatus.STOPPED
process: Optional[subprocess.Popen] = None
task: Optional[asyncio.Task] = None
agent: Optional[NativeLLMAgent] = None
started_at: Optional[datetime] = None
stopped_at: Optional[datetime] = None
restart_count: int = 0
config: Dict[str, Any] = field(default_factory=dict)
@property
def uptime(self) -> Optional[float]:
"""运行时长(秒)"""
if self.started_at:
end = self.stopped_at or datetime.now()
return (end - self.started_at).total_seconds()
return None
@property
def is_alive(self) -> bool:
"""是否存活"""
if self.agent_type == "native_llm":
return self.status == AgentStatus.RUNNING and self.task and not self.task.done()
else:
return self.process and self.process.poll() is None
class ProcessManager:
"""
Agent 进程管理器
功能:
1. 启动/停止 Agent
2. 监控 Agent 健康状态
3. 自动重启崩溃的 Agent
4. 管理 Agent 生命周期
"""
def __init__(self):
self.processes: Dict[str, AgentProcess] = {}
self.heartbeat_service = get_heartbeat_service()
self.registry = get_agent_registry()
self._monitor_task: Optional[asyncio.Task] = None
self._running = False
async def start_agent(
self,
agent_id: str,
agent_type: str = "native_llm",
config: Dict[str, Any] = None
) -> bool:
"""
启动 Agent
参数:
agent_id: Agent 唯一标识
agent_type: Agent 类型 (native_llm, process_wrapper)
config: Agent 配置
返回:
是否成功启动
"""
if agent_id in self.processes and self.processes[agent_id].is_alive:
logger.warning(f"Agent {agent_id} 已在运行")
return False
logger.info(f"启动 Agent: {agent_id} (类型: {agent_type})")
process_info = AgentProcess(
agent_id=agent_id,
agent_type=agent_type,
status=AgentStatus.STARTING,
config=config or {}
)
try:
if agent_type == "native_llm":
success = await self._start_native_agent(process_info)
elif agent_type == "process_wrapper":
success = await self._start_process_wrapper(process_info)
else:
logger.error(f"不支持的 Agent 类型: {agent_type}")
return False
if success:
process_info.status = AgentStatus.RUNNING
process_info.started_at = datetime.now()
self.processes[agent_id] = process_info
# 启动监控任务
if not self._running:
await self.start_monitor()
return True
return False
except Exception as e:
logger.error(f"启动 Agent 失败: {agent_id}: {e}", exc_info=True)
process_info.status = AgentStatus.CRASHED
return False
async def _start_native_agent(self, process_info: AgentProcess) -> bool:
"""启动原生 LLM Agent"""
try:
# 创建 Agent 实例
agent = await NativeLLMAgentFactory.create(
agent_id=process_info.agent_id,
name=process_info.config.get("name"),
role=process_info.config.get("role", "developer"),
model=process_info.config.get("model", "claude-sonnet-4.6"),
config=process_info.config
)
process_info.agent = agent
# 创建后台任务保持 Agent 运行
async def agent_loop():
try:
# Agent 定期发送心跳
while True:
await asyncio.sleep(30)
if await agent.health_check():
await agent.update_heartbeat("idle")
else:
logger.warning(f"Agent {process_info.agent_id} 健康检查失败")
break
except asyncio.CancelledError:
logger.info(f"Agent {process_info.agent_id} 任务被取消")
except Exception as e:
logger.error(f"Agent {process_info.agent_id} 循环出错: {e}")
task = asyncio.create_task(agent_loop())
process_info.task = task
logger.info(f"原生 LLM Agent 启动成功: {process_info.agent_id}")
return True
except Exception as e:
logger.error(f"启动原生 Agent 失败: {e}")
return False
async def _start_process_wrapper(self, process_info: AgentProcess) -> bool:
"""启动进程包装 Agent"""
command = process_info.config.get("command")
args = process_info.config.get("args", [])
if not command:
logger.error("进程包装 Agent 需要指定 command")
return False
try:
# 启动子进程
proc = subprocess.Popen(
[command] + args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
process_info.process = proc
# 创建监控任务
async def process_monitor():
try:
while True:
if proc.poll() is not None:
logger.warning(f"进程 {process_info.agent_id} 已退出: {proc.returncode}")
break
await asyncio.sleep(5)
except asyncio.CancelledError:
# 尝试优雅关闭
try:
proc.terminate()
proc.wait(timeout=5)
except:
proc.kill()
task = asyncio.create_task(process_monitor())
process_info.task = task
logger.info(f"进程包装 Agent 启动成功: {process_info.agent_id} (PID: {proc.pid})")
return True
except Exception as e:
logger.error(f"启动进程失败: {e}")
return False
async def stop_agent(self, agent_id: str, graceful: bool = True) -> bool:
"""
停止 Agent
参数:
agent_id: Agent ID
graceful: 是否优雅关闭
返回:
是否成功停止
"""
if agent_id not in self.processes:
logger.warning(f"Agent {agent_id} 未运行")
return False
process_info = self.processes[agent_id]
logger.info(f"停止 Agent: {agent_id} (优雅: {graceful})")
process_info.status = AgentStatus.STOPPING
try:
if process_info.agent:
# 关闭原生 Agent
await process_info.agent.shutdown()
if process_info.task:
# 取消后台任务
process_info.task.cancel()
try:
await process_info.task
except asyncio.CancelledError:
pass
if process_info.process:
# 终止外部进程
if graceful:
process_info.process.terminate()
try:
process_info.process.wait(timeout=5)
except subprocess.TimeoutExpired:
process_info.process.kill()
else:
process_info.process.kill()
process_info.status = AgentStatus.STOPPED
process_info.stopped_at = datetime.now()
# 从进程列表移除
del self.processes[agent_id]
return True
except Exception as e:
logger.error(f"停止 Agent 失败: {agent_id}: {e}")
process_info.status = AgentStatus.CRASHED
return False
async def restart_agent(self, agent_id: str) -> bool:
"""重启 Agent"""
logger.info(f"重启 Agent: {agent_id}")
if agent_id in self.processes:
process_info = self.processes[agent_id]
config = process_info.config
agent_type = process_info.agent_type
await self.stop_agent(agent_id)
process_info.restart_count += 1
return await self.start_agent(agent_id, agent_type, config)
return False
def get_agent_status(self, agent_id: str) -> Optional[AgentStatus]:
"""获取 Agent 状态"""
if agent_id in self.processes:
return self.processes[agent_id].status
return AgentStatus.UNKNOWN
def get_all_agents(self) -> Dict[str, AgentProcess]:
"""获取所有 Agent 信息"""
return self.processes.copy()
def get_running_agents(self) -> List[str]:
"""获取正在运行的 Agent ID 列表"""
return [
pid for pid, proc in self.processes.items()
if proc.is_alive
]
async def monitor_agent_health(self) -> Dict[str, bool]:
"""
监控所有 Agent 健康状态
返回:
{agent_id: is_healthy}
"""
results = {}
for agent_id, process_info in self.processes.items():
if process_info.agent_type == "native_llm" and process_info.agent:
# 检查原生 Agent 健康状态
results[agent_id] = await process_info.agent.health_check()
else:
# 检查进程是否存活
results[agent_id] = process_info.is_alive
return results
async def start_monitor(self, interval: int = 30):
"""启动监控任务"""
if self._running:
return
self._running = True
async def monitor_loop():
while self._running:
try:
await self._check_agents()
await asyncio.sleep(interval)
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"监控循环出错: {e}", exc_info=True)
await asyncio.sleep(interval)
self._monitor_task = asyncio.create_task(monitor_loop())
logger.info("监控任务已启动")
async def stop_monitor(self):
"""停止监控任务"""
self._running = False
if self._monitor_task:
self._monitor_task.cancel()
try:
await self._monitor_task
except asyncio.CancelledError:
pass
logger.info("监控任务已停止")
async def _check_agents(self):
"""检查所有 Agent 状态"""
health_results = await self.monitor_agent_health()
for agent_id, is_healthy in health_results.items():
if not is_healthy:
logger.warning(f"Agent {agent_id} 不健康")
# 检查是否需要自动重启
process_info = self.processes.get(agent_id)
if process_info and process_info.config.get("auto_restart", False):
if process_info.restart_count < process_info.config.get("max_restarts", 3):
logger.info(f"自动重启 Agent: {agent_id}")
await self.restart_agent(agent_id)
else:
logger.error(f"Agent {agent_id} 重启次数超限,标记为崩溃")
process_info.status = AgentStatus.CRASHED
async def shutdown_all(self):
"""关闭所有 Agent"""
logger.info("关闭所有 Agent...")
agent_ids = list(self.processes.keys())
for agent_id in agent_ids:
await self.stop_agent(agent_id)
await self.stop_monitor()
logger.info("所有 Agent 已关闭")
def get_summary(self) -> Dict[str, Any]:
"""获取进程管理器摘要"""
running = self.get_running_agents()
total = len(self.processes)
status_counts = {}
for proc in self.processes.values():
status = proc.status.value
status_counts[status] = status_counts.get(status, 0) + 1
return {
"total_agents": total,
"running_agents": len(running),
"running_agent_ids": running,
"status_counts": status_counts,
"monitor_running": self._running
}
# 单例获取函数
_process_manager: Optional[ProcessManager] = None
def get_process_manager() -> ProcessManager:
"""获取进程管理器单例"""
global _process_manager
if _process_manager is None:
_process_manager = ProcessManager()
return _process_manager
def reset_process_manager():
"""重置进程管理器(主要用于测试)"""
global _process_manager
_process_manager = None

View File

@@ -0,0 +1,232 @@
"""
资源管理器 - 整合文件锁和心跳服务
提供声明式的任务执行接口,自动管理资源获取和释放
"""
import asyncio
import re
from typing import List, Dict, Optional
from dataclasses import dataclass
from .storage import get_storage
from .file_lock import get_file_lock_service
from .heartbeat import get_heartbeat_service
from .agent_registry import get_agent_registry
@dataclass
class TaskResult:
"""任务执行结果"""
success: bool
message: str
files_locked: List[str] = None
duration_seconds: float = 0.0
def __post_init__(self):
if self.files_locked is None:
self.files_locked = []
class ResourceManager:
"""
资源管理器
整合文件锁和心跳服务,提供声明式的任务执行接口:
- 自动解析任务中的文件路径
- 自动获取文件锁
- 自动更新心跳
- 任务完成后自动释放资源
"""
def __init__(self):
self._lock_service = get_file_lock_service()
self._heartbeat_service = get_heartbeat_service()
self._agent_registry = get_agent_registry()
# 文件路径正则模式
FILE_PATTERNS = [
r'[\w/]+\.(py|js|ts|tsx|jsx|java|go|rs|c|cpp|h|hpp|cs|swift|kt|rb|php|sh|bash|zsh|yaml|yml|json|xml|html|css|scss|md|txt|sql)',
r'[\w/]+/(?:src|lib|app|components|services|utils|tests|test|spec|config|assets|static|views|controllers|models|routes)/[\w./]+',
]
def _extract_files_from_task(self, task_description: str) -> List[str]:
"""
从任务描述中提取文件路径
Args:
task_description: 任务描述
Returns:
提取的文件路径列表
"""
files = []
for pattern in self.FILE_PATTERNS:
matches = re.findall(pattern, task_description)
files.extend(matches)
# 去重并过滤
seen = set()
result = []
for f in files:
# 标准化路径
normalized = f.strip().replace('\\', '/')
if normalized and normalized not in seen and len(normalized) > 3:
seen.add(normalized)
result.append(normalized)
return result
async def execute_task(
self,
agent_id: str,
task_description: str,
timeout: int = 300
) -> TaskResult:
"""
执行任务(声明式接口)
内部流程:
1. 解析任务需要的文件
2. 获取所有文件锁
3. 更新心跳状态
4. 执行任务(这里是模拟)
5. finally: 释放所有锁
Args:
agent_id: Agent ID
task_description: 任务描述
timeout: 超时时间(秒)
Returns:
任务执行结果
"""
import time
start_time = time.time()
# 1. 解析文件
files = self._extract_files_from_task(task_description)
# 2. 获取文件锁
acquired_files = []
for file_path in files:
success = await self._lock_service.acquire_lock(
file_path, agent_id, agent_id[:3].upper()
)
if success:
acquired_files.append(file_path)
try:
# 3. 更新心跳
await self._heartbeat_service.update_heartbeat(
agent_id,
status="working",
current_task=task_description,
progress=0
)
# 4. 执行任务(这里只是模拟,实际需要调用 Agent
# 实际实现中,这里会通过 CLIPluginAdapter 调用 Agent
await asyncio.sleep(0.1) # 模拟执行
duration = time.time() - start_time
return TaskResult(
success=True,
message=f"Task executed: {task_description}",
files_locked=acquired_files,
duration_seconds=duration
)
finally:
# 5. 释放所有锁
for file_path in acquired_files:
await self._lock_service.release_lock(file_path, agent_id)
# 更新心跳为 idle
await self._heartbeat_service.update_heartbeat(
agent_id,
status="idle",
current_task="",
progress=100
)
async def parse_task_files(self, task_description: str) -> List[str]:
"""
解析任务中的文件路径
Args:
task_description: 任务描述
Returns:
文件路径列表
"""
return self._extract_files_from_task(task_description)
async def get_agent_status(self, agent_id: str) -> Dict:
"""
获取 Agent 状态(整合锁和心跳信息)
Args:
agent_id: Agent ID
Returns:
Agent 状态信息
"""
# 获取心跳信息
heartbeat = await self._heartbeat_service.get_heartbeat(agent_id)
# 获取持有的锁
locks = await self._lock_service.get_agent_locks(agent_id)
# 获取注册信息
agent_info = await self._agent_registry.get_agent(agent_id)
# 获取运行时状态
agent_state = await self._agent_registry.get_state(agent_id)
return {
"agent_id": agent_id,
"info": {
"name": agent_info.name if agent_info else "",
"role": agent_info.role if agent_info else "",
"model": agent_info.model if agent_info else "",
},
"heartbeat": {
"status": heartbeat.status if heartbeat else "unknown",
"current_task": heartbeat.current_task if heartbeat else "",
"progress": heartbeat.progress if heartbeat else 0,
"elapsed": heartbeat.elapsed_display if heartbeat else "",
},
"locks": [
{"file": lock.file_path, "elapsed": lock.elapsed_display}
for lock in locks
],
"state": {
"task": agent_state.current_task if agent_state else "",
"progress": agent_state.progress if agent_state else 0,
"working_files": agent_state.working_files if agent_state else [],
}
}
async def get_all_status(self) -> List[Dict]:
"""
获取所有 Agent 的状态
Returns:
所有 Agent 状态列表
"""
agents = await self._agent_registry.list_agents()
statuses = []
for agent in agents:
status = await self.get_agent_status(agent.agent_id)
statuses.append(status)
return statuses
# 全局单例
_manager_instance: Optional[ResourceManager] = None
def get_resource_manager() -> ResourceManager:
"""获取资源管理器单例"""
global _manager_instance
if _manager_instance is None:
_manager_instance = ResourceManager()
return _manager_instance

View File

@@ -0,0 +1,199 @@
"""
角色分配器 - AI 驱动的角色分配
分析任务描述,自动为 Agent 分配最适合的角色
"""
import asyncio
from typing import Dict, List, Optional
from dataclasses import dataclass
from enum import Enum
from .agent_registry import AgentRegistry, AgentInfo
class AgentRole(str, Enum):
"""Agent 角色枚举"""
ARCHITECT = "architect"
PRODUCT_MANAGER = "pm"
DEVELOPER = "developer"
QA = "qa"
REVIEWER = "reviewer"
@dataclass
class RoleWeight:
"""角色权重配置"""
role: str
weight: float
keywords: List[str]
def matches(self, text: str) -> int:
"""计算匹配分数"""
score = 0
text_lower = text.lower()
for keyword in self.keywords:
if keyword.lower() in text_lower:
score += 1
return score
class RoleAllocator:
"""
角色分配器
分析任务描述,为 Agent 分配最适合的角色
"""
# 角色权重配置(来自 design-spec.md
ROLE_WEIGHTS = {
"pm": RoleWeight("pm", 1.5, ["需求", "产品", "规划", "用户", "功能", "priority", "requirement", "product"]),
"architect": RoleWeight("architect", 1.5, ["架构", "设计", "方案", "技术", "系统", "design", "architecture"]),
"developer": RoleWeight("developer", 1.0, ["开发", "实现", "编码", "代码", "function", "implement", "code"]),
"reviewer": RoleWeight("reviewer", 1.3, ["审查", "review", "检查", "验证", "校对", "check"]),
"qa": RoleWeight("qa", 1.2, ["测试", "test", "质量", "bug", "验证", "quality"]),
}
def __init__(self):
pass
def _analyze_task_roles(self, task: str) -> Dict[str, float]:
"""
分析任务需要的角色及其权重
Args:
task: 任务描述
Returns:
角色权重字典
"""
scores = {}
for role_name, role_weight in self.ROLE_WEIGHTS.items():
match_score = role_weight.matches(task)
if match_score > 0:
scores[role_name] = match_score * role_weight.weight
else:
# 即使没有匹配关键词,也给予基础权重
scores[role_name] = 0.1 * role_weight.weight
return scores
async def allocate_roles(
self,
task: str,
available_agents: List[str]
) -> Dict[str, str]:
"""
为任务分配角色
Args:
task: 任务描述
available_agents: 可用的 Agent ID 列表
Returns:
Agent ID -> 角色映射
"""
# 获取所有 Agent 信息
# 注意:在实际实现中,这会从 AgentRegistry 获取
# 这里简化处理,假设已有 Agent 信息
# 分析任务需要的角色
role_scores = self._analyze_task_roles(task)
# 按分数排序角色
sorted_roles = sorted(role_scores.items(), key=lambda x: -x[1])
# 简单分配:将可用 Agent 按顺序分配给角色
allocation = {}
for i, agent_id in enumerate(available_agents):
if i < len(sorted_roles):
allocation[agent_id] = sorted_roles[i][0]
else:
allocation[agent_id] = "developer" # 默认角色
return allocation
def get_primary_role(self, task: str) -> str:
"""
获取任务的主要角色
Args:
task: 任务描述
Returns:
主要角色名称
"""
role_scores = self._analyze_task_roles(task)
if not role_scores:
return "developer"
return max(role_scores.items(), key=lambda x: x[1])[0]
async def suggest_agents_for_task(
self,
task: str,
all_agents: List[AgentInfo],
count: int = 3
) -> List[AgentInfo]:
"""
为任务推荐合适的 Agent
Args:
task: 任务描述
all_agents: 所有可用 Agent 列表
count: 推荐数量
Returns:
推荐的 Agent 列表
"""
primary_role = self.get_primary_role(task)
# 按角色匹配度排序
scored_agents = []
for agent in all_agents:
if agent.role == primary_role:
scored_agents.append((agent, 10)) # 完全匹配高分
elif agent.role in ["architect", "developer", "reviewer"]:
scored_agents.append((agent, 5)) # 相关角色中分
else:
scored_agents.append((agent, 1)) # 其他角色低分
# 按分数排序
scored_agents.sort(key=lambda x: -x[1])
return [agent for agent, _ in scored_agents[:count]]
def explain_allocation(self, task: str, allocation: Dict[str, str]) -> str:
"""
解释角色分配的原因
Args:
task: 任务描述
allocation: 分配结果
Returns:
解释文本
"""
role_scores = self._analyze_task_roles(task)
primary = self.get_primary_role(task)
lines = [f"任务分析: {task}", f"主要角色: {primary}"]
lines.append("角色权重:")
for role, score in sorted(role_scores.items(), key=lambda x: -x[1]):
lines.append(f" - {role}: {score:.2f}")
lines.append("分配结果:")
for agent_id, role in allocation.items():
lines.append(f" - {agent_id}: {role}")
return "\n".join(lines)
# 全局单例
_allocator_instance: Optional[RoleAllocator] = None
def get_role_allocator() -> RoleAllocator:
"""获取角色分配器单例"""
global _allocator_instance
if _allocator_instance is None:
_allocator_instance = RoleAllocator()
return _allocator_instance

View File

@@ -0,0 +1,146 @@
"""
基础存储服务 - 提供 JSON 文件的异步读写操作
所有服务共享的底层存储抽象
"""
import json
import asyncio
from pathlib import Path
from typing import Any, Dict, Optional
import aiofiles
import aiofiles.os
class StorageService:
"""异步 JSON 文件存储服务"""
def __init__(self, base_path: str = ".doc"):
"""
初始化存储服务
Args:
base_path: 基础存储路径,默认为 .doc
"""
self.base_path = Path(base_path)
self._lock = asyncio.Lock() # 简单的内存锁,防止并发写入
async def ensure_dir(self, path: str) -> None:
"""
确保目录存在,不存在则创建
Args:
path: 目录路径(相对于 base_path 或绝对路径)
"""
dir_path = self.base_path / path if not Path(path).is_absolute() else Path(path)
await aiofiles.os.makedirs(dir_path, exist_ok=True)
async def read_json(self, path: str) -> Dict[str, Any]:
"""
读取 JSON 文件
Args:
path: 文件路径(相对于 base_path 或绝对路径)
Returns:
解析后的 JSON 字典,文件不存在或为空时返回空字典
"""
file_path = self.base_path / path if not Path(path).is_absolute() else Path(path)
if not await aiofiles.os.path.exists(file_path):
return {}
async with aiofiles.open(file_path, mode="r", encoding="utf-8") as f:
content = await f.read()
if not content.strip():
return {}
return json.loads(content)
async def write_json(self, path: str, data: Dict[str, Any]) -> None:
"""
写入 JSON 文件
Args:
path: 文件路径(相对于 base_path 或绝对路径)
data: 要写入的 JSON 数据
"""
file_path = self.base_path / path if not Path(path).is_absolute() else Path(path)
# 确保父目录存在
await self.ensure_dir(str(file_path.parent))
# 使用锁防止并发写入冲突
async with self._lock:
async with aiofiles.open(file_path, mode="w", encoding="utf-8") as f:
await f.write(json.dumps(data, ensure_ascii=False, indent=2))
async def append_json_list(self, path: str, item: Any) -> None:
"""
向 JSON 数组文件追加一项
Args:
path: 文件路径
item: 要追加的项
"""
data = await self.read_json(path)
if not isinstance(data, list):
data = []
data.append(item)
await self.write_json(path, {"items": data})
async def delete(self, path: str) -> bool:
"""
删除文件
Args:
path: 文件路径
Returns:
是否成功删除
"""
file_path = self.base_path / path if not Path(path).is_absolute() else Path(path)
if await aiofiles.os.path.exists(file_path):
await aiofiles.os.remove(file_path)
return True
return False
async def exists(self, path: str) -> bool:
"""
检查文件是否存在
Args:
path: 文件路径
Returns:
文件是否存在
"""
file_path = self.base_path / path if not Path(path).is_absolute() else Path(path)
return await aiofiles.os.path.exists(file_path)
# 全局单例实例
_storage_instance: Optional[StorageService] = None
def _find_project_root() -> Path:
"""查找项目根目录(包含 CLAUDE.md 的目录)"""
current = Path.cwd()
# 向上查找项目根目录
for parent in [current] + list(current.parents):
if (parent / "CLAUDE.md").exists():
return parent
# 如果找不到,使用当前目录的父目录(假设从 backend/ 运行)
if current.name == "backend":
return current.parent
# 默认使用当前目录
return current
def get_storage() -> StorageService:
"""获取存储服务单例,使用项目根目录下的 .doc 目录"""
global _storage_instance
if _storage_instance is None:
project_root = _find_project_root()
doc_path = project_root / ".doc"
_storage_instance = StorageService(str(doc_path))
return _storage_instance

View File

@@ -0,0 +1,473 @@
"""
工作流引擎 - 管理和执行工作流
支持从 YAML 文件加载工作流定义,并跟踪进度
"""
import asyncio
from pathlib import Path
from typing import Dict, List, Optional
from dataclasses import dataclass, field, asdict
from datetime import datetime
import yaml
from .storage import get_storage
from .meeting_recorder import get_meeting_recorder
@dataclass
class WorkflowMeeting:
"""工作流中的节点"""
meeting_id: str
title: str
attendees: List[str]
depends_on: List[str] = field(default_factory=list)
completed: bool = False
node_type: str = "meeting" # meeting | execution
min_required: int = None # 最少完成人数execution 节点用)
on_failure: str = None # 失败时跳转的节点 ID
# 执行状态追踪execution 节点专用)
completed_attendees: List[str] = field(default_factory=list)
@property
def is_ready(self) -> bool:
"""检查节点是否可以开始(依赖已完成)"""
return all(dep in self.depends_on for dep in self.depends_on)
@property
def is_execution_ready(self) -> bool:
"""检查执行节点是否所有人都完成了"""
if self.node_type != "execution":
return False
required = self.min_required or len(self.attendees)
return len(self.completed_attendees) >= required
@property
def missing_attendees(self) -> List[str]:
"""获取未完成的人员列表"""
if self.node_type != "execution":
return []
return [a for a in self.attendees if a not in self.completed_attendees]
@property
def progress(self) -> str:
"""执行进度"""
if self.node_type != "execution":
return "N/A"
return f"{len(self.completed_attendees)}/{len(self.attendees)}"
@dataclass
class Workflow:
"""工作流定义"""
workflow_id: str
name: str
description: str
meetings: List[WorkflowMeeting]
created_at: str = ""
status: str = "pending" # pending, in_progress, completed
def __post_init__(self):
if not self.created_at:
self.created_at = datetime.now().isoformat()
@property
def progress(self) -> str:
"""进度摘要"""
total = len(self.meetings)
completed = sum(1 for m in self.meetings if m.completed)
return f"{completed}/{total}"
@property
def current_meeting(self) -> Optional[WorkflowMeeting]:
"""获取当前应该进行的会议(第一个未完成的)"""
for meeting in self.meetings:
if not meeting.completed:
return meeting
return None
@property
def is_completed(self) -> bool:
"""工作流是否完成"""
return all(m.completed for m in self.meetings)
class WorkflowEngine:
"""
工作流引擎
管理工作流的加载、执行和进度跟踪
"""
WORKFLOWS_DIR = "workflow"
def __init__(self):
self._storage = get_storage()
self._recorder = get_meeting_recorder()
self._loaded_workflows: Dict[str, Workflow] = {}
# 注册的工作流文件路径
self._workflow_files: Dict[str, str] = {}
async def load_workflow(self, workflow_path: str) -> Workflow:
"""
从 YAML 文件加载工作流
Args:
workflow_path: YAML 文件路径(相对于 .doc/workflow/
Returns:
加载的工作流
"""
import aiofiles
# 构建完整路径
full_path = f"{self.WORKFLOWS_DIR}/{workflow_path}"
yaml_path = Path(self._storage.base_path) / full_path
if not yaml_path.exists():
raise ValueError(f"Workflow file not found: {workflow_path}")
# 读取 YAML 内容
async with aiofiles.open(yaml_path, mode="r", encoding="utf-8") as f:
yaml_content = await f.read()
content = yaml.safe_load(yaml_content)
# 解析工作流
meetings = []
for m in content.get("meetings", []):
meetings.append(WorkflowMeeting(
meeting_id=m["meeting_id"],
title=m["title"],
attendees=m["attendees"],
depends_on=m.get("depends_on", []),
node_type=m.get("node_type", "meeting"),
min_required=m.get("min_required"),
on_failure=m.get("on_failure")
))
workflow = Workflow(
workflow_id=content["workflow_id"],
name=content["name"],
description=content.get("description", ""),
meetings=meetings
)
self._loaded_workflows[workflow.workflow_id] = workflow
# 保存源文件路径,以便后续可以重新加载
self._workflow_files[workflow.workflow_id] = workflow_path
return workflow
async def get_next_meeting(self, workflow_id: str) -> Optional[WorkflowMeeting]:
"""
获取工作流中下一个应该进行的会议
Args:
workflow_id: 工作流 ID
Returns:
下一个会议,如果没有或已完成返回 None
"""
workflow = self._loaded_workflows.get(workflow_id)
if not workflow:
return None
return workflow.current_meeting
async def complete_meeting(self, workflow_id: str, meeting_id: str) -> bool:
"""
标记会议为已完成
Args:
workflow_id: 工作流 ID
meeting_id: 会议 ID
Returns:
是否成功标记
"""
workflow = self._loaded_workflows.get(workflow_id)
if not workflow:
return False
for meeting in workflow.meetings:
if meeting.meeting_id == meeting_id:
meeting.completed = True
# 更新工作流状态
if workflow.is_completed:
workflow.status = "completed"
else:
workflow.status = "in_progress"
return True
return False
async def create_workflow_meeting(
self,
workflow_id: str,
meeting_id: str
) -> bool:
"""
创建工作流中的会议记录
Args:
workflow_id: 工作流 ID
meeting_id: 会议 ID
Returns:
是否成功创建
"""
workflow = self._loaded_workflows.get(workflow_id)
if not workflow:
return False
meeting = None
for m in workflow.meetings:
if m.meeting_id == meeting_id:
meeting = m
break
if not meeting:
return False
# 创建会议记录
await self._recorder.create_meeting(
meeting_id=meeting.meeting_id,
title=f"{workflow.name}: {meeting.title}",
attendees=meeting.attendees,
steps=["收集初步想法", "讨论与迭代", "生成共识版本"]
)
return True
async def get_workflow_status(self, workflow_id: str) -> Optional[Dict]:
"""
获取工作流状态
Args:
workflow_id: 工作流 ID
Returns:
工作流状态信息
"""
# 如果不在内存中,尝试重新加载
if workflow_id not in self._loaded_workflows and workflow_id in self._workflow_files:
await self.load_workflow(self._workflow_files[workflow_id])
workflow = self._loaded_workflows.get(workflow_id)
if not workflow:
return None
return {
"workflow_id": workflow.workflow_id,
"name": workflow.name,
"description": workflow.description,
"status": workflow.status,
"progress": workflow.progress,
"meetings": [
{
"meeting_id": m.meeting_id,
"title": m.title,
"completed": m.completed
}
for m in workflow.meetings
]
}
async def list_workflows(self) -> List[Dict]:
"""
列出所有加载的工作流
Returns:
工作流列表
"""
return [
{
"workflow_id": w.workflow_id,
"name": w.name,
"status": w.status,
"progress": w.progress
}
for w in self._loaded_workflows.values()
]
# ========== 执行节点相关方法 ==========
async def join_execution_node(
self,
workflow_id: str,
meeting_id: str,
agent_id: str
) -> Dict:
"""
Agent 加入执行节点(标记完成)
Args:
workflow_id: 工作流 ID
meeting_id: 执行节点 ID
agent_id: Agent ID
Returns:
状态信息 {"status": "waiting"|"ready"|"completed", "progress": "2/3"}
"""
workflow = self._loaded_workflows.get(workflow_id)
if not workflow:
return {"status": "error", "message": "Workflow not found"}
for meeting in workflow.meetings:
if meeting.meeting_id == meeting_id:
if meeting.node_type != "execution":
return {"status": "error", "message": "Not an execution node"}
if agent_id not in meeting.completed_attendees:
meeting.completed_attendees.append(agent_id)
if meeting.is_execution_ready:
return {
"status": "ready",
"progress": meeting.progress,
"message": "所有 Agent 已完成,可以进入下一节点"
}
return {
"status": "waiting",
"progress": meeting.progress,
"missing": meeting.missing_attendees,
"message": f"等待其他 Agent 完成: {meeting.missing_attendees}"
}
return {"status": "error", "message": "Meeting not found"}
async def get_execution_status(
self,
workflow_id: str,
meeting_id: str
) -> Optional[Dict]:
"""
获取执行节点的状态
Returns:
执行状态信息
"""
workflow = self._loaded_workflows.get(workflow_id)
if not workflow:
return None
for meeting in workflow.meetings:
if meeting.meeting_id == meeting_id:
return {
"meeting_id": meeting.meeting_id,
"title": meeting.title,
"node_type": meeting.node_type,
"attendees": meeting.attendees,
"completed_attendees": meeting.completed_attendees,
"progress": meeting.progress,
"is_ready": meeting.is_execution_ready,
"missing": meeting.missing_attendees
}
return None
# ========== 条件跳转相关方法 ==========
async def jump_to_node(
self,
workflow_id: str,
target_meeting_id: str
) -> bool:
"""
强制跳转到指定节点(重置后续所有节点)
Args:
workflow_id: 工作流 ID
target_meeting_id: 目标节点 ID
Returns:
是否成功跳转
"""
workflow = self._loaded_workflows.get(workflow_id)
if not workflow:
return False
# 找到目标节点并重置从它开始的所有后续节点
target_found = False
for meeting in workflow.meetings:
if meeting.meeting_id == target_meeting_id:
target_found = True
meeting.completed = False
meeting.completed_attendees = []
elif target_found:
# 目标节点之后的所有节点都重置
meeting.completed = False
meeting.completed_attendees = []
workflow.status = "in_progress"
return target_found
async def handle_failure(
self,
workflow_id: str,
meeting_id: str
) -> Optional[str]:
"""
处理节点失败,根据 on_failure 配置跳转
Args:
workflow_id: 工作流 ID
meeting_id: 失败的节点 ID
Returns:
跳转目标节点 ID如果没有配置则返回 None
"""
workflow = self._loaded_workflows.get(workflow_id)
if not workflow:
return None
for meeting in workflow.meetings:
if meeting.meeting_id == meeting_id and meeting.on_failure:
await self.jump_to_node(workflow_id, meeting.on_failure)
return meeting.on_failure
return None
async def get_workflow_detail(self, workflow_id: str) -> Optional[Dict]:
"""
获取工作流详细信息(包含所有节点状态)
Returns:
工作流详细信息
"""
workflow = self._loaded_workflows.get(workflow_id)
if not workflow:
return None
return {
"workflow_id": workflow.workflow_id,
"name": workflow.name,
"description": workflow.description,
"status": workflow.status,
"progress": workflow.progress,
"current_node": workflow.current_meeting.meeting_id if workflow.current_meeting else None,
"meetings": [
{
"meeting_id": m.meeting_id,
"title": m.title,
"node_type": m.node_type,
"attendees": m.attendees,
"depends_on": m.depends_on,
"completed": m.completed,
"on_failure": m.on_failure,
"progress": m.progress if m.node_type == "execution" else None
}
for m in workflow.meetings
]
}
# 全局单例
_engine_instance: Optional[WorkflowEngine] = None
def get_workflow_engine() -> WorkflowEngine:
"""获取工作流引擎单例"""
global _engine_instance
if _engine_instance is None:
_engine_instance = WorkflowEngine()
return _engine_instance

View File

@@ -0,0 +1,58 @@
"""
单例模式工具模块 - 统一管理全局服务实例
"""
from typing import TypeVar, Type, Dict, Callable, Optional
T = TypeVar('T')
class SingletonRegistry:
"""单例注册表 - 统一管理所有服务实例"""
_instances: Dict[str, object] = {}
_factories: Dict[str, Callable[[], object]] = {}
@classmethod
def register(cls, name: str, factory: Callable[[], T]) -> None:
"""注册服务工厂函数"""
cls._factories[name] = factory
@classmethod
def get(cls, name: str, instance_type: Type[T]) -> T:
"""获取或创建服务实例"""
if name not in cls._instances:
if name not in cls._factories:
raise KeyError(f"未注册的服务: {name}")
cls._instances[name] = cls._factories[name]()
return cls._instances[name]
@classmethod
def reset(cls, name: Optional[str] = None) -> None:
"""重置服务实例(用于测试)"""
if name:
cls._instances.pop(name, None)
else:
cls._instances.clear()
def singleton_factory(factory: Callable[[], T]) -> Callable[[], T]:
"""
单例工厂装饰器
用法:
@singleton_factory
def get_service():
return Service()
service = get_service() # 始终返回同一实例
"""
instance: Optional[T] = None
def wrapper() -> T:
nonlocal instance
if instance is None:
instance = factory()
return instance
return wrapper

1115
backend/cli.py Normal file

File diff suppressed because it is too large Load Diff

35
backend/requirements.txt Normal file
View File

@@ -0,0 +1,35 @@
# FastAPI 核心依赖
fastapi>=0.110.0
uvicorn[standard]>=0.27.0
pydantic>=2.0.0
# CLI 工具
typer>=0.9.0
rich>=13.0.0
# 异步文件操作
aiofiles>=23.0.0
# 文件锁
filelock>=3.13.0
# YAML 解析
pyyaml>=6.0
# HTTP 客户端(调用 LLM API
httpx>=0.25.0
# Anthropic APIClaude
anthropic>=0.18.0
# OpenAI API可选
openai>=1.0.0
# WebSocket 支持
websockets>=12.0
# 进程管理
psutil>=5.9.0
# APscheduler定时任务
apscheduler>=3.10.0

View File

@@ -0,0 +1,385 @@
#!/usr/bin/env python3
"""
后端服务完整测试脚本
测试所有 10 个步骤的服务是否正常工作
"""
import asyncio
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from app.services.storage import get_storage
from app.services.file_lock import get_file_lock_service
from app.services.heartbeat import get_heartbeat_service
from app.services.agent_registry import get_agent_registry
from app.services.meeting_scheduler import get_meeting_scheduler
from app.services.meeting_recorder import get_meeting_recorder
from app.services.resource_manager import get_resource_manager
from app.services.workflow_engine import get_workflow_engine
from app.services.role_allocator import get_role_allocator
async def test_storage_service():
"""测试存储服务"""
print("\n=== 测试 StorageService ===")
storage = get_storage()
# 测试写入
await storage.write_json("cache/test_storage.json", {"test": "data", "number": 42})
print("[PASS] 写入 JSON 文件")
# 测试读取
data = await storage.read_json("cache/test_storage.json")
assert data["test"] == "data", "读取数据不匹配"
print("[PASS] 读取 JSON 文件")
# 测试存在检查
exists = await storage.exists("cache/test_storage.json")
assert exists, "文件应该存在"
print("[PASS] 文件存在检查")
# 测试删除
await storage.delete("cache/test_storage.json")
exists = await storage.exists("cache/test_storage.json")
assert not exists, "文件应该已被删除"
print("[PASS] 删除文件")
print("StorageService 测试通过")
return True
async def test_file_lock_service():
"""测试文件锁服务"""
print("\n=== 测试 FileLockService ===")
service = get_file_lock_service()
# 测试获取锁
success = await service.acquire_lock("src/test/file.py", "agent-001", "TestAgent")
assert success, "应该成功获取锁"
print("[PASS] 获取文件锁")
# 测试检查锁定
locked_by = await service.check_locked("src/test/file.py")
assert locked_by == "agent-001", "锁持有者应该匹配"
print("[PASS] 检查文件锁定状态")
# 测试其他 Agent 无法获取
success = await service.acquire_lock("src/test/file.py", "agent-002", "OtherAgent")
assert not success, "其他 Agent 不应该能获取已被锁定的文件"
print("[PASS] 冲突检测正常工作")
# 测试获取所有锁
locks = await service.get_locks()
assert len(locks) >= 1, "应该有至少一个锁"
print("[PASS] 获取所有锁列表")
# 测试释放锁
success = await service.release_lock("src/test/file.py", "agent-001")
assert success, "应该成功释放锁"
print("[PASS] 释放文件锁")
print("FileLockService 测试通过")
return True
async def test_heartbeat_service():
"""测试心跳服务"""
print("\n=== 测试 HeartbeatService ===")
service = get_heartbeat_service()
# 测试更新心跳
await service.update_heartbeat("agent-001", "working", "测试任务", 50)
print("[PASS] 更新心跳")
# 测试获取心跳
hb = await service.get_heartbeat("agent-001")
assert hb is not None, "心跳信息应该存在"
assert hb.status == "working", "状态应该匹配"
assert hb.progress == 50, "进度应该匹配"
print("[PASS] 获取心跳信息")
# 测试获取所有心跳
all_hbs = await service.get_all_heartbeats()
assert "agent-001" in all_hbs, "应该在所有心跳列表中"
print("[PASS] 获取所有心跳")
# 测试活跃 Agent
active = await service.get_active_agents(within_seconds=10)
assert "agent-001" in active, "应该是活跃 Agent"
print("[PASS] 获取活跃 Agent")
print("HeartbeatService 测试通过")
return True
async def test_agent_registry():
"""测试 Agent 注册服务"""
print("\n=== 测试 AgentRegistry ===")
registry = get_agent_registry()
# 测试注册 Agent
agent = await registry.register_agent(
"test-agent-001",
"Test Agent",
"developer",
"claude-opus-4.6",
"测试用的 Agent"
)
assert agent.agent_id == "test-agent-001", "ID 应该匹配"
print("[PASS] 注册 Agent")
# 测试获取 Agent
fetched = await registry.get_agent("test-agent-001")
assert fetched is not None, "应该能获取到 Agent"
assert fetched.name == "Test Agent", "名称应该匹配"
print("[PASS] 获取 Agent 信息")
# 测试更新状态
await registry.update_state("test-agent-001", "修复 bug", 75)
print("[PASS] 更新 Agent 状态")
# 测试获取状态
state = await registry.get_state("test-agent-001")
assert state is not None, "状态应该存在"
assert state.progress == 75, "进度应该匹配"
print("[PASS] 获取 Agent 状态")
# 测试列出所有 Agent
agents = await registry.list_agents()
assert len(agents) >= 1, "应该至少有一个 Agent"
print("[PASS] 列出所有 Agent")
print("AgentRegistry 测试通过")
return True
async def test_meeting_scheduler():
"""测试会议调度器"""
print("\n=== 测试 MeetingScheduler ===")
scheduler = get_meeting_scheduler()
# 测试创建会议
queue = await scheduler.create_meeting(
"test-meeting-001",
"测试会议",
["agent-001", "agent-002"],
min_required=2
)
assert queue.meeting_id == "test-meeting-001", "ID 应该匹配"
print("[PASS] 创建会议")
# 测试获取队列
fetched = await scheduler.get_queue("test-meeting-001")
assert fetched is not None, "队列应该存在"
print("[PASS] 获取会议队列")
# 测试等待会议(模拟两个 Agent 到达)
result1 = await scheduler.wait_for_meeting("agent-001", "test-meeting-001", timeout=1)
print(f" Agent-1 到达: {result1}")
result2 = await scheduler.wait_for_meeting("agent-002", "test-meeting-001", timeout=1)
print(f" Agent-2 到达: {result2}")
# 最后一个到达者应该触发会议开始
assert result2 == "started", "最后一个到达者应该触发会议开始"
print("[PASS] 栅栏同步工作正常")
# 测试结束会议
success = await scheduler.end_meeting("test-meeting-001")
assert success, "应该成功结束会议"
print("[PASS] 结束会议")
print("MeetingScheduler 测试通过")
return True
async def test_meeting_recorder():
"""测试会议记录服务"""
print("\n=== 测试 MeetingRecorder ===")
recorder = get_meeting_recorder()
# 测试创建会议记录
meeting = await recorder.create_meeting(
"test-record-001",
"测试记录会议",
["agent-001", "agent-002"],
["步骤1", "步骤2", "步骤3"]
)
assert meeting.meeting_id == "test-record-001", "ID 应该匹配"
assert len(meeting.steps) == 3, "应该有 3 个步骤"
print("[PASS] 创建会议记录")
# 测试添加讨论
await recorder.add_discussion("test-record-001", "agent-001", "Agent1", "这是第一条讨论")
await recorder.add_discussion("test-record-001", "agent-002", "Agent2", "这是第二条讨论")
print("[PASS] 添加讨论记录")
# 测试更新进度
await recorder.update_progress("test-record-001", "步骤1")
print("[PASS] 更新会议进度")
# 测试获取会议
fetched = await recorder.get_meeting("test-record-001")
assert fetched is not None, "会议应该存在"
assert len(fetched.discussions) == 2, "应该有 2 条讨论"
print("[PASS] 获取会议详情")
# 测试结束会议
success = await recorder.end_meeting("test-record-001", "达成共识:继续开发")
assert success, "应该成功结束会议"
print("[PASS] 结束会议并保存共识")
print("MeetingRecorder 测试通过")
return True
async def test_resource_manager():
"""测试资源管理器"""
print("\n=== 测试 ResourceManager ===")
manager = get_resource_manager()
# 测试解析任务文件
files = await manager.parse_task_files("修复 src/auth/login.py 和 src/utils/helper.js 中的 bug")
assert "src/auth/login.py" in files or "src/utils/helper.js" in files, "应该能解析出文件路径"
print(f"[PASS] 解析任务文件: {files}")
# 测试获取 Agent 状态(需要先有注册的 Agent
try:
status = await manager.get_agent_status("test-agent-001")
print(f"[PASS] 获取 Agent 状态: {status['agent_id']}")
except Exception as e:
print(f" [WARN] 获取状态警告: {e}")
print("ResourceManager 测试通过")
return True
async def test_workflow_engine():
"""测试工作流引擎"""
print("\n=== 测试 WorkflowEngine ===")
engine = get_workflow_engine()
# 确保测试工作流文件存在
workflow_content = """
workflow_id: "test-workflow"
name: "测试工作流"
description: "用于测试的工作流"
meetings:
- meeting_id: "step1"
title: "第一步"
attendees: ["agent-001"]
depends_on: []
- meeting_id: "step2"
title: "第二步"
attendees: ["agent-001", "agent-002"]
depends_on: ["step1"]
"""
import aiofiles
from pathlib import Path
workflow_path = Path(engine._storage.base_path) / "workflow" / "test.yaml"
async with aiofiles.open(workflow_path, mode="w", encoding="utf-8") as f:
await f.write(workflow_content)
# 测试加载工作流
workflow = await engine.load_workflow("test.yaml")
assert workflow.workflow_id == "test-workflow", "ID 应该匹配"
assert len(workflow.meetings) == 2, "应该有 2 个会议"
print("[PASS] 加载工作流")
# 测试获取下一个会议
next_meeting = await engine.get_next_meeting("test-workflow")
assert next_meeting is not None, "应该有下一个会议"
assert next_meeting.meeting_id == "step1", "第一个会议应该是 step1"
print("[PASS] 获取下一个会议")
# 测试完成会议
success = await engine.complete_meeting("test-workflow", "step1")
assert success, "应该成功标记会议完成"
print("[PASS] 标记会议完成")
# 测试获取工作流状态
status = await engine.get_workflow_status("test-workflow")
assert status is not None, "状态应该存在"
assert status["progress"] == "1/2", "进度应该是 1/2"
print("[PASS] 获取工作流状态")
print("WorkflowEngine 测试通过")
return True
async def test_role_allocator():
"""测试角色分配器"""
print("\n=== 测试 RoleAllocator ===")
allocator = get_role_allocator()
# 测试获取主要角色
primary = allocator.get_primary_role("实现登录功能并编写测试用例")
assert primary in ["pm", "developer", "qa", "architect", "reviewer"], "应该是有效角色"
print(f"[PASS] 获取主要角色: {primary}")
# 测试角色分配
allocation = await allocator.allocate_roles(
"设计数据库架构并实现 API",
["claude-001", "kimi-002", "opencode-003"]
)
assert len(allocation) == 3, "应该为 3 个 Agent 分配角色"
print(f"[PASS] 角色分配: {allocation}")
# 测试解释分配
explanation = allocator.explain_allocation("修复 bug", allocation)
assert "主要角色" in explanation, "解释应该包含主要角色"
print("[PASS] 解释角色分配")
print("RoleAllocator 测试通过")
return True
async def run_all_tests():
"""运行所有测试"""
print("=" * 60)
print("Swarm Command Center - 后端服务完整测试")
print("=" * 60)
tests = [
("StorageService", test_storage_service),
("FileLockService", test_file_lock_service),
("HeartbeatService", test_heartbeat_service),
("AgentRegistry", test_agent_registry),
("MeetingScheduler", test_meeting_scheduler),
("MeetingRecorder", test_meeting_recorder),
("ResourceManager", test_resource_manager),
("WorkflowEngine", test_workflow_engine),
("RoleAllocator", test_role_allocator),
]
results = []
for name, test_func in tests:
try:
success = await test_func()
results.append((name, success, None))
except Exception as e:
print(f"[FAIL] {name} 测试失败: {e}")
import traceback
traceback.print_exc()
results.append((name, False, str(e)))
# 打印总结
print("\n" + "=" * 60)
print("测试结果总结")
print("=" * 60)
passed = sum(1 for _, success, _ in results if success)
total = len(results)
for name, success, error in results:
status = "[PASS]" if success else f"[FAIL: {error}]"
print(f"{name:20s} {status}")
print("=" * 60)
print(f"总计: {passed}/{total} 通过")
print("=" * 60)
return passed == total
if __name__ == "__main__":
success = asyncio.run(run_all_tests())
sys.exit(0 if success else 1)

35
current-state.md Normal file
View File

@@ -0,0 +1,35 @@
- generic [ref=e3]:
- complementary [ref=e4]:
- generic [ref=e6]:
- generic [ref=e8]: S
- generic [ref=e9]:
- heading "Swarm Center" [level=1] [ref=e10]
- paragraph [ref=e11]: 多智能体协作系统
- navigation [ref=e12]:
- link "仪表盘" [ref=e13] [cursor=pointer]:
- /url: /
- img [ref=e14]
- generic [ref=e19]: 仪表盘
- link "Agent 管理" [ref=e20] [cursor=pointer]:
- /url: /agents
- img [ref=e21]
- generic [ref=e26]: Agent 管理
- link "会议管理" [ref=e27] [cursor=pointer]:
- /url: /meetings
- img [ref=e28]
- generic [ref=e30]: 会议管理
- link "资源监控" [ref=e31] [cursor=pointer]:
- /url: /resources
- img [ref=e32]
- generic [ref=e34]: 资源监控
- link "工作流" [ref=e35] [cursor=pointer]:
- /url: /workflow
- img [ref=e36]
- generic [ref=e40]: 工作流
- link "配置" [ref=e41] [cursor=pointer]:
- /url: /settings
- img [ref=e42]
- generic [ref=e45]: 配置
- generic [ref=e46]: v0.1.0
- main [ref=e47]:
- generic [ref=e216]: 加载中...

686
docs/api-reference.md Normal file
View File

@@ -0,0 +1,686 @@
# Swarm Command Center - API 接口文档
后端服务已完整实现并通过测试。本文档描述前端与后端交互的 API 接口。
## 基础信息
- **API 基础地址**: `http://localhost:8000/api`
- **数据格式**: JSON
- **字符编码**: UTF-8
## 服务状态
| 服务 | 状态 | 说明 |
|------|------|------|
| StorageService | ✅ 已测试 | JSON 文件存储 |
| FileLockService | ✅ 已测试 | 文件锁管理 |
| HeartbeatService | ✅ 已测试 | Agent 心跳与超时检测 |
| AgentRegistry | ✅ 已测试 | Agent 注册与状态管理 |
| MeetingScheduler | ✅ 已测试 | 会议栅栏同步 |
| MeetingRecorder | ✅ 已测试 | 会议记录与 Markdown |
| ResourceManager | ✅ 已测试 | 资源整合管理 |
| WorkflowEngine | ✅ 已测试 | YAML 工作流 |
| RoleAllocator | ✅ 已测试 | AI 角色分配 |
---
## 1. Agent 管理接口
### 1.1 列出所有 Agent
```http
GET /api/agents
```
**响应示例**:
```json
{
"agents": [
{
"agent_id": "claude-001",
"name": "Claude Code",
"role": "architect",
"model": "claude-opus-4.6",
"status": "idle",
"created_at": "2026-03-05T10:30:00"
}
]
}
```
### 1.2 注册新 Agent
```http
POST /api/agents/register
Content-Type: application/json
{
"agent_id": "claude-001",
"name": "Claude Code",
"role": "architect",
"model": "claude-opus-4.6",
"description": ""
}
```
**角色可选值**: `architect`, `pm`, `developer`, `qa`, `reviewer`, `human`
### 1.3 获取 Agent 详情
```http
GET /api/agents/:agent_id
```
**响应示例**:
```json
{
"agent_id": "claude-001",
"name": "Claude Code",
"role": "architect",
"model": "claude-opus-4.6",
"status": "working",
"created_at": "2026-03-05T10:30:00"
}
```
### 1.4 获取 Agent 状态
```http
GET /api/agents/:agent_id/state
```
**响应示例**:
```json
{
"agent_id": "claude-001",
"current_task": "修复登录bug",
"progress": 75,
"working_files": ["src/auth/login.py"],
"last_update": "2026-03-05T10:35:00"
}
```
### 1.5 更新 Agent 状态
```http
POST /api/agents/:agent_id/state
Content-Type: application/json
{
"task": "",
"progress": 50,
"working_files": ["tests/test_auth.py"]
}
```
---
## 2. 文件锁接口
### 2.1 获取所有锁
```http
GET /api/locks
```
**响应示例**:
```json
{
"locks": [
{
"file_path": "src/auth/login.py",
"agent_id": "claude-001",
"agent_name": "CLA",
"acquired_at": "2026-03-05T10:30:00",
"elapsed_display": "5m 30s"
}
]
}
```
### 2.2 获取文件锁
```http
POST /api/locks/acquire
Content-Type: application/json
{
"file_path": "src/auth/login.py",
"agent_id": "claude-001",
"agent_name": "CLA"
}
```
**响应示例**:
```json
{
"success": true,
"file_path": "src/auth/login.py",
"agent_id": "claude-001"
}
```
### 2.3 释放文件锁
```http
POST /api/locks/release
Content-Type: application/json
{
"file_path": "src/auth/login.py",
"agent_id": "claude-001"
}
```
### 2.4 检查文件锁定状态
```http
GET /api/locks/check?file_path=src/auth/login.py
```
**响应示例**:
```json
{
"file_path": "src/auth/login.py",
"locked": true,
"locked_by": "claude-001"
}
```
---
## 3. 心跳接口
### 3.1 获取所有心跳
```http
GET /api/heartbeats
```
**响应示例**:
```json
{
"heartbeats": {
"claude-001": {
"agent_id": "claude-001",
"status": "working",
"current_task": "修复bug",
"progress": 50,
"elapsed_display": "2m 15s",
"is_timeout": false
}
}
}
```
### 3.2 更新心跳
```http
POST /api/heartbeats/:agent_id
Content-Type: application/json
{
"status": "working",
"current_task": "",
"progress": 75
}
```
**状态可选值**: `working`, `waiting`, `idle`, `error`
### 3.3 检查超时 Agent
```http
GET /api/heartbeats/timeouts?timeout_seconds=60
```
**响应示例**:
```json
{
"timeout_seconds": 60,
"timeout_agents": ["agent-002", "agent-003"]
}
```
---
## 4. 会议调度接口
### 4.1 创建会议
```http
POST /api/meetings/create
Content-Type: application/json
{
"meeting_id": "design-review-001",
"title": "",
"expected_attendees": ["claude-001", "kimi-002", "opencode-003"],
"min_required": 2
}
```
### 4.2 获取会议队列
```http
GET /api/meetings/:meeting_id/queue
```
**响应示例**:
```json
{
"meeting_id": "design-review-001",
"title": "设计评审会议",
"status": "waiting",
"expected_attendees": ["claude-001", "kimi-002", "opencode-003"],
"arrived_attendees": ["claude-001", "kimi-002"],
"missing_attendees": ["opencode-003"],
"progress": "2/3",
"is_ready": false
}
```
### 4.3 等待会议开始(栅栏同步)
```http
POST /api/meetings/:meeting_id/wait
Content-Type: application/json
{
"agent_id": "claude-001",
"timeout": 300
}
```
**响应示例**:
```json
{
"result": "started",
"meeting_id": "design-review-001",
"agent_id": "claude-001"
}
```
**result 说明**:
- `started`: 会议已开始(最后一个到达者触发)
- `timeout`: 等待超时
- `error`: 发生错误
### 4.4 结束会议
```http
POST /api/meetings/:meeting_id/end
```
---
## 5. 会议记录接口
### 5.1 创建会议记录
```http
POST /api/meetings/record/create
Content-Type: application/json
{
"meeting_id": "design-review-001",
"title": "",
"attendees": ["claude-001", "kimi-002"],
"steps": ["", "", ""]
}
```
### 5.2 添加讨论记录
```http
POST /api/meetings/:meeting_id/discuss
Content-Type: application/json
{
"agent_id": "claude-001",
"agent_name": "Claude",
"content": "使 JWT ",
"step": ""
}
```
### 5.3 更新会议进度
```http
POST /api/meetings/:meeting_id/progress
Content-Type: application/json
{
"step": ""
}
```
### 5.4 获取会议详情
```http
GET /api/meetings/:meeting_id?date=2026-03-05
```
**响应示例**:
```json
{
"meeting_id": "design-review-001",
"title": "设计评审",
"date": "2026-03-05",
"status": "in_progress",
"attendees": ["claude-001", "kimi-002"],
"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",
"content": "我建议使用 JWT",
"timestamp": "2026-03-05T10:30:00",
"step": "讨论迭代"
}
],
"progress_summary": "1/3",
"consensus": ""
}
```
### 5.5 完成会议
```http
POST /api/meetings/:meeting_id/finish
Content-Type: application/json
{
"consensus": " JWT + Redis "
}
```
---
## 6. 资源管理接口
### 6.1 执行任务
```http
POST /api/execute
Content-Type: application/json
{
"agent_id": "claude-001",
"task": " src/auth/login.py bug",
"timeout": 300
}
```
**响应示例**:
```json
{
"success": true,
"message": "Task executed: 修复 src/auth/login.py 中的 bug",
"files_locked": ["src/auth/login.py"],
"duration_seconds": 1.25
}
```
### 6.2 获取所有 Agent 状态
```http
GET /api/status
```
**响应示例**:
```json
{
"agents": [
{
"agent_id": "claude-001",
"info": {
"name": "Claude Code",
"role": "architect",
"model": "claude-opus-4.6"
},
"heartbeat": {
"status": "working",
"current_task": "修复bug",
"progress": 50,
"elapsed": "3m 20s"
},
"locks": [
{"file": "src/auth/login.py", "elapsed": "2m 10s"}
],
"state": {
"task": "修复bug",
"progress": 50,
"working_files": ["src/auth/login.py"]
}
}
]
}
```
### 6.3 解析任务文件
```http
POST /api/parse-task
Content-Type: application/json
{
"task": " src/auth/login.py src/utils/helper.js"
}
```
**响应示例**:
```json
{
"task": "修复 src/auth/login.py 和 src/utils/helper.js",
"files": ["src/auth/login.py", "src/utils/helper.js"]
}
```
---
## 7. 工作流接口
### 7.1 加载工作流
```http
POST /api/workflows/load
Content-Type: application/json
{
"path": "example.yaml"
}
```
**响应示例**:
```json
{
"workflow_id": "example_project",
"name": "示例项目工作流",
"description": "展示多智能体协作的典型工作流",
"status": "pending",
"progress": "0/4",
"meetings": [
{"meeting_id": "requirements-review", "title": "需求评审", "completed": false},
{"meeting_id": "design-review", "title": "设计评审", "completed": false}
]
}
```
### 7.2 获取工作流状态
```http
GET /api/workflows/:workflow_id/status
```
### 7.3 获取下一个会议
```http
GET /api/workflows/:workflow_id/next
```
### 7.4 标记会议完成
```http
POST /api/workflows/:workflow_id/complete
Content-Type: application/json
{
"meeting_id": "requirements-review"
}
```
### 7.5 列出可用工作流文件
```http
GET /api/workflows/files
```
**响应示例**:
```json
{
"files": ["example.yaml", "project-a.yaml"]
}
```
---
## 8. 角色分配接口
### 8.1 获取任务主要角色
```http
POST /api/roles/primary
Content-Type: application/json
{
"task": ""
}
```
**响应示例**:
```json
{
"task": "实现登录功能并编写测试用例",
"primary_role": "pm",
"role_scores": {
"pm": 1.5,
"qa": 1.2,
"developer": 1.0,
"architect": 0.15,
"reviewer": 0.13
}
}
```
### 8.2 分配角色
```http
POST /api/roles/allocate
Content-Type: application/json
{
"task": " API",
"agents": ["claude-001", "kimi-002", "opencode-003"]
}
```
**响应示例**:
```json
{
"task": "设计数据库架构并实现 API",
"primary_role": "architect",
"allocation": {
"claude-001": "architect",
"kimi-002": "developer",
"opencode-003": "pm"
}
}
```
### 8.3 解释角色分配
```http
POST /api/roles/explain
Content-Type: application/json
{
"task": "bug",
"agents": ["claude-001", "kimi-002"]
}
```
---
## 9. 系统接口
### 9.1 健康检查
```http
GET /health
```
**响应示例**:
```json
{
"status": "healthy",
"version": "0.1.0",
"services": {
"api": "ok",
"storage": "ok"
}
}
```
---
## 错误处理
所有 API 错误返回统一格式:
```json
{
"error": true,
"code": "RESOURCE_NOT_FOUND",
"message": "Agent not found: claude-001"
}
```
**常见错误码**:
- `400` - Bad Request (请求参数错误)
- `404` - Not Found (资源不存在)
- `409` - Conflict (资源冲突,如文件已被锁定)
- `500` - Internal Server Error (服务器内部错误)
---
## 前端集成建议
### 实时更新
- 使用轮询polling获取 Agent 状态和会议进度
- 建议轮询间隔: 5-10 秒
### 错误处理
- 所有 API 调用应包含错误处理
- 网络错误时显示友好提示
### 状态管理
- 建议使用 React Query / SWR 管理服务器状态
- 本地状态使用 Zustand / Redux Toolkit
---
## 测试验证
后端所有服务已通过自动化测试:
```bash
cd backend
python test_all_services.py
```
**测试结果**: 9/9 通过 ✅

470
docs/backend-steps.md Normal file
View File

@@ -0,0 +1,470 @@
# 后端开发步骤
每一步完成后都有验证方法,前端逐步替换 mock 数据。
---
## 第一步:项目初始化
**操作**
```bash
cd backend
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install fastapi uvicorn pydantic typer aiofiles
mkdir -p .doc/agents .doc/cache .doc/meetings .doc/resources .doc/workflow
```
**创建文件**
- `backend/main.py` - FastAPI 入口
- `backend/cli.py` - CLI 入口
**验证**
```bash
python cli.py hello # 输出: Hello Swarm
python -m uvicorn main:app --reload
curl http://localhost:8000/ # 输出: {"status":"ok"}
```
---
## 第二步:基础存储服务
**操作** - 创建 `backend/services/storage.py`
```python
class StorageService:
async def read_json(self, path: str) -> dict
async def write_json(self, path: str, data: dict)
async def ensure_dir(self, path: str)
```
**验证**
```bash
python cli.py storage write .doc/test.json '{"foo":"bar"}'
python cli.py storage read .doc/test.json
# 输出: {"foo":"bar"}
```
---
## 第三步:文件锁服务
**操作** - 创建 `backend/services/file_lock.py`
```python
class FileLockService:
async def acquire_lock(self, file_path: str, agent_id: str) -> bool
async def release_lock(self, file_path: str, agent_id: str) -> bool
async def get_locks(self) -> List[LockInfo]
async def check_locked(self, file_path: str) -> Optional[str]
```
存储到 `.doc/cache/file_locks.json`
**验证**
```bash
# CLI 测试
python cli.py lock acquire src/auth/login.py claude-001
# 输出: Lock acquired
python cli.py lock status
# 输出: src/auth/login.py -> claude-001 (2s ago)
python cli.py lock release src/auth/login.py claude-001
# 输出: Lock released
```
**API 测试**
```bash
curl http://localhost:8000/api/locks
# 输出: {"locks": []}
curl -X POST http://localhost:8000/api/locks/acquire \
-H "Content-Type: application/json" \
-d '{"file_path":"src/auth/login.py","agent_id":"claude-001"}'
# 输出: {"acquired":true}
```
**前端对接** - 修改 `ResourceMonitorCard.tsx`
```typescript
// 替换 lockedFiles mock
const response = await fetch('http://localhost:8000/api/locks');
const data = await response.json();
setLockedFiles(data.locks);
```
---
## 第四步:心跳服务
**操作** - 创建 `backend/services/heartbeat.py`
```python
class HeartbeatService:
async def update_heartbeat(self, agent_id: str, status: dict) -> None
async def get_heartbeat(self, agent_id: str) -> Optional[dict]
async def get_all_heartbeats(self) -> dict
async def check_timeout(self, timeout_seconds: int) -> List[str]
```
存储到 `.doc/cache/heartbeats.json`
**验证**
```bash
# 模拟 Agent 发送心跳
python cli.py heartbeat ping claude-001 '{"status":"working","task":"fixing bug"}'
# 输出: Heartbeat updated
python cli.py heartbeat list
# 输出: claude-001: working (5s ago)
python cli.py heartbeat check-timeout 30
# 输出: No timed out agents
```
**API 测试**
```bash
curl http://localhost:8000/api/heartbeats
# 输出: {"heartbeats": {"claude-001": {...}}}
```
---
## 第五步Agent 注册服务
**操作** - 创建 `backend/services/agent_registry.py`
```python
class AgentRegistry:
async def register_agent(self, agent_info: AgentInfo) -> None
async def get_agent(self, agent_id: str) -> Optional[AgentInfo]
async def list_agents(self) -> List[AgentInfo]
async def update_state(self, agent_id: str, state: dict) -> None
async def get_state(self, agent_id: str) -> Optional[dict]
```
存储到 `.doc/agents/{agent_id}/` 目录
**验证**
```bash
python cli.py agent register claude-001 \
--name "Claude Code" \
--role "architect" \
--model "claude-opus-4.6"
# 输出: Agent registered
python cli.py agent list
# 输出: claude-001 | Claude Code | architect | claude-opus-4.6
python cli.py agent state claude-001 set '{"task":"fixing bug","progress":50}'
python cli.py agent state claude-001 get
# 输出: {"task":"fixing bug","progress":50}
```
**API 测试**
```bash
curl http://localhost:8000/api/agents
# 输出: {"agents": [...]}
curl http://localhost:8000/api/agents/claude-001/state
# 输出: {"task":"fixing bug","progress":50}
```
**前端对接** - 修改 `AgentStatusCard.tsx`
```typescript
// 替换 agents mock
const response = await fetch('http://localhost:8000/api/agents');
const data = await response.json();
setAgents(data.agents);
```
---
## 第六步:会议调度器(栅栏同步)
**操作** - 创建 `backend/services/meeting_scheduler.py`
```python
class MeetingScheduler:
async def wait_for_meeting(self, agent_id: str, meeting_id: str) -> str
async def get_queue(self, meeting_id: str) -> List[str]
async def start_meeting(self, meeting_id: str) -> None
async def end_meeting(self, meeting_id: str) -> None
```
存储到 `.doc/cache/meeting_queue.json`
**验证**
```bash
# 终端1 - Agent 1 等待
python cli.py meeting wait design-review claude-001
# 阻塞等待...
# 终端2 - Agent 2 等待
python cli.py meeting wait design-review kimi-002
# 触发会议,输出: Meeting started!
python cli.py meeting queue design-review
# 输出: [claude-001, kimi-002]
```
**API 测试**
```bash
curl -X POST http://localhost:8000/api/meetings/design-review/wait \
-H "Content-Type: application/json" \
-d '{"agent_id":"claude-001"}'
# 输出: {"status":"waiting"}
curl http://localhost:8000/api/meetings/design-review/queue
# 输出: {"waiting_agents":["claude-001"]}
```
---
## 第七步:会议记录服务
**操作** - 创建 `backend/services/meeting_recorder.py`
```python
class MeetingRecorder:
async def create_meeting(self, meeting_info: MeetingInfo) -> str
async def add_discussion(self, meeting_id: str, agent_id: str, content: str) -> None
async def update_progress(self, meeting_id: str, step: str) -> None
async def get_meeting(self, meeting_id: str) -> MeetingInfo
async def list_meetings(self, date: str) -> List[MeetingInfo]
```
存储到 `.doc/meetings/{YYYY-MM-DD}/{meeting_id}.md`
**验证**
```bash
python cli.py meeting create design-review \
--title "认证方案设计评审" \
--attendees claude-001,kimi-002,opencode-003
# 输出: Meeting created: design_review_20260304_141000
python cli.py meeting discuss design_review_20260304_141000 \
--agent claude-001 \
--content "建议使用 JWT"
# 输出: Discussion added
python cli.py meeting progress design_review_20260304_141000 "收集初步想法"
# 输出: Progress updated
```
**API 测试**
```bash
curl http://localhost:8000/api/meetings/2026-03-04
# 输出: {"meetings": [...]}
curl http://localhost:8000/api/meetings/design_review_20260304_141000
# 输出: {...}
```
**前端对接** - 修改 `MeetingProgressCard.tsx``RecentMeetingsCard.tsx`
```typescript
// 替换 steps mock
const response = await fetch('http://localhost:8000/api/meetings/active');
const data = await response.json);
setSteps(data.meeting.steps);
```
---
## 第八步:资源管理器(整合锁 + 心跳)
**操作** - 创建 `backend/services/resource_manager.py`
```python
class ResourceManager:
async def execute_task(self, agent_id: str, task_description: str) -> str
# 内部逻辑:
# 1. 解析任务需要的文件
# 2. 获取所有文件锁
# 3. 执行任务
# 4. finally: 释放所有锁
```
**验证**
```bash
python cli.py execute claude-001 "修改 src/auth/login.py 修复登录 bug"
# 输出: Task executing...
# 输出: Locks acquired: [src/auth/login.py, src/utils/crypto.py]
# 输出: Task completed
# 输出: Locks released
```
---
## 第九步:工作流引擎
**操作** - 创建 `backend/services/workflow_engine.py`
```python
class WorkflowEngine:
async def load_workflow(self, workflow_path: str) -> Workflow
async def get_next_meeting(self, workflow_id: str) -> Optional[MeetingInfo]
async def complete_meeting(self, workflow_id: str, meeting_id: str) -> None
```
**验证**
```bash
python cli.py workflow load .doc/workflow/project_workflow.yaml
# 输出: Workflow loaded: my_project
python cli.py workflow next my_project
# 输出: Next meeting: design_review (attendees: claude-001,kimi-002)
```
---
## 第十步角色分配器AI 路由)
**操作** - 创建 `backend/services/role_allocator.py`
```python
class RoleAllocator:
async def allocate_roles(self, task: str, agents: List[str]) -> Dict[str, str]
# 内部调用 LLM 分析任务,返回角色分配
```
**验证**
```bash
python cli.py role allocate "重构认证模块" claude-001,kimi-002,opencode-003
# 输出: claude-001 -> architect
# 输出: kimi-002 -> pm
# 输出: opencode-003 -> developer
```
---
## API 接口汇总
| 路径 | 方法 | 说明 |
|------|------|------|
| `/api/agents` | GET | 获取所有 Agent |
| `/api/agents/{id}/state` | GET/POST | Agent 状态 |
| `/api/locks` | GET | 获取所有锁 |
| `/api/locks/acquire` | POST | 获取锁 |
| `/api/locks/release` | POST | 释放锁 |
| `/api/heartbeats` | GET | 获取心跳列表 |
| `/api/heartbeats/{id}` | POST | 更新心跳 |
| `/api/meetings/{date}` | GET | 获取会议列表 |
| `/api/meetings/{id}` | GET | 获取会议详情 |
| `/api/meetings/{id}/wait` | POST | 加入会议等待 |
| `/api/workflows/{id}/next` | GET | 获取下一步 |
---
## 前端 Mock 替换顺序
| 步骤 | 组件 | API 端点 |
|------|------|----------|
| 3 | ResourceMonitorCard | `/api/locks` |
| 5 | AgentStatusCard | `/api/agents` |
| 7 | MeetingProgressCard | `/api/meetings/{id}` |
| 7 | RecentMeetingsCard | `/api/meetings/{date}` |
| - | WorkflowCard | `/api/workflows/{id}/next` |
---
## 参考开源项目
### 多智能体协作框架
| 项目 | 链接 | 参考点 |
|------|------|--------|
| **AutoGen** | [github.com/microsoft/autogen](https://github.com/microsoft/autogen) | 对话驱动协作、异步消息传递、Agent 交接机制 |
| **CrewAI** | [github.com/CrewAIInc/crewAI](https://github.com/CrewAIInc/crewAI) | 角色扮演式 Agent、任务编排、Crew 结构 |
| **AgentScope** | [阿里通义实验室](https://github.com/modelscope/agentscope) | Agent 编排、安全沙箱、跨框架互操作 |
| **MetaGPT** | [github.com/FoundationAgents/MetaGPT](https://github.com/FoundationAgents/MetaGPT) | AI 软件公司、多角色协作、文档生成 |
### FastAPI 项目结构
| 项目 | 链接 | 参考点 |
|------|------|--------|
| **FastAPI Project Structure** | [github.com/brianobot/fastapi_project_structure](https://github.com/brianobot/fastapi_project_structure) | 分层架构、services/routers/schemas 分离 |
| **FastAPI Best Architecture** | [github.com](搜索) | 伪三层架构、插件系统 |
| **FastAPI Tips** | [github.com](搜索) | WebSocket 实时通信、性能优化 |
```
推荐目录结构:
backend/
├── app/
│ ├── main.py # FastAPI 入口
│ ├── settings.py # 配置管理
│ ├── dependencies.py # 依赖注入
│ ├── middlewares.py # 中间件
│ ├── api_router.py # 路由汇总
│ ├── routers/ # API 路由
│ │ ├── agents.py
│ │ ├── locks.py
│ │ └── meetings.py
│ ├── services/ # 业务逻辑
│ │ ├── storage.py
│ │ ├── file_lock.py
│ │ ├── heartbeat.py
│ │ ├── agent_registry.py
│ │ └── meeting_scheduler.py
│ ├── schemas/ # Pydantic 模型
│ │ ├── agent.py
│ │ ├── lock.py
│ │ └── meeting.py
│ └── models/ # 数据模型(如用数据库)
├── cli.py # CLI 入口 (Typer)
└── requirements.txt
```
### 文件锁实现
| 库 | 链接 | 特点 |
|-----|------|------|
| **filelock** | [github.com/tox-dev/filelock](https://github.com/tox-dev/filelock) | 跨平台、上下文管理器、超时机制 |
| **fasteners** | [github.com/harlowja/fasteners](https://github.com/harlowja/fasteners) | 进程间读写锁 |
**推荐使用 filelock**
```python
from filelock import FileLock, Timeout
lock = FileLock("/path/to/file.lock", timeout=10)
with lock:
# 临界区代码
pass
```
### WebSocket 实时更新
FastAPI 原生支持 WebSocket参考官方文档[fastapi.tiangolo.com/zh/websockets](https://fastapi.tiangolo.com/zh/advanced/websockets/)
```python
from fastapi import WebSocket
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
async for data in websocket.iter_text():
await websocket.send_text(f"Received: {data}")
```
### 技术栈推荐
```txt
# requirements.txt
fastapi>=0.110.0
uvicorn[standard]>=0.27.0
pydantic>=2.0.0
typer>=0.9.0
aiofiles>=23.0.0
filelock>=3.13.0
pyyaml>=6.0
anthropic>=0.18.0 # Claude API
openai>=1.0.0 # OpenAI API
```
### 设计文档参考
| 文档 | 链接 |
|------|------|
| AutoGen 编程模型 | [github.com/microsoft/autogen/docs/design](https://github.com/microsoft/autogen/tree/main/docs/design) |
| Agent Worker Protocol | [github.com/microsoft/autogen/docs/design](https://github.com/microsoft/autogen/blob/main/docs/design/03%20-%20Agent%20Worker%20Protocol.md) |

693
docs/design-spec.md Normal file
View File

@@ -0,0 +1,693 @@
# Swarm - 多 Agent 协作系统设计文档
> 版本1.0
> 日期2026-03-04
> 作者Swarm Team
---
## 目录
1. [项目概述](#1-项目概述)
2. [系统架构](#2-系统架构)
3. [插件化 CLI 工具适配](#3-插件化-cli-工具适配)
4. [资源管理与文件锁](#4-资源管理与文件锁)
5. [会议与共识机制](#5-会议与共识机制)
6. [动态角色分配](#6-动态角色分配)
7. [多模型支持](#7-多模型支持)
8. [共享存储结构](#8-共享存储结构)
9. [技术栈](#9-技术栈)
---
## 1. 项目概述
### 1.1 核心目标
构建一个通用的多 Agent 协作框架,支持:
- **多工具接入**Claude Code CLI、Kimi CLI、OpenCode 等命令行 AI 工具
- **会议驱动协作**:通过会议流程组织 Agent 间的协作
- **资源自动管理**声明式资源管理Agent 无需手动处理锁
- **协作共识**Agent 之间通过讨论达成共识,而非简单投票
- **动态角色**AI 根据任务自动分析并分配角色
- **多模型支持**:支持多种 LLM智能路由与故障转移
### 1.2 设计原则
| 原则 | 说明 |
|-----|------|
| 插件化 | CLI 工具通过适配器层接入,实现统一接口 |
| 声明式 | Agent 只声明意图,系统自动管理资源 |
| 会议驱动 | Workflow 定义会议节点,栅栏同步触发 |
| 协作共识 | 多轮讨论、迭代提案、AI 收敛判断 |
| AI 分配 | LLM 分析任务自动分配最优角色 |
| 可靠性 | 超时 + 心跳 + finally + 看门狗多层保障 |
---
## 2. 系统架构
### 2.1 整体架构图
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 用户界面层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Web UI │ │ CLI 工具 │ │ API 接口 │ │ VSCode 插件 │ │
│ │ (React/TS) │ │ (Node.js) │ │ (Express) │ │ (TypeScript)│ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 协调层 (Python) │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Workflow Engine │ │ Meeting Scheduler│ │ Resource Manager │ │
│ │ (工作流编排) │ │ (栅栏同步) │ │ (文件锁+心跳) │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Role Allocator │ │ Consensus Engine │ │ Model Router │ │
│ │ (AI角色分配) │ │ (协作共识) │ │ (多模型路由) │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Agent 适配层 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ CLIPluginAdapter Interface │ │
│ │ + execute() + join_meeting() + write_state() + read_others() │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ ClaudeCode │ │ KimiCLI │ │ OpenCode │ │ 自定义... │ │
│ │ Adapter │ │ Adapter │ │ Adapter │ │ Adapter │ │
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 模型层 │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Anthropic │ │ OpenAI │ │ Google │ │ DeepSeek │ │
│ │ API │ │ API │ │ API │ │ API │ │
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Ollama (本地模型) │ │
│ └────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 共享存储层 (.doc/) │
│ ├── agents/ ├── dialogues/ ├── progress/ │
│ ├── resources/ ├── meetings/ ├── cache/ │
│ └── workflow/ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## 3. 插件化 CLI 工具适配
### 3.1 统一接口设计
```python
interface CLIPluginAdapter:
"""CLI 工具适配器统一接口"""
id: str
name: str
version: str
# 核心能力
def execute(task: Task) -> Result:
"""执行任务"""
pass
def join_meeting(meeting_id: str) -> None:
"""加入会议等待队列"""
pass
def write_state(state: dict) -> None:
"""写入自己的状态文件"""
pass
def read_others(agent_id: str) -> dict:
"""读取其他 Agent 的状态"""
pass
def update_heartbeat() -> None:
"""更新心跳"""
pass
```
### 3.2 预置适配器
| 适配器 | CLI 工具 | 特点 |
|--------|---------|------|
| ClaudeCodeAdapter | Claude Code CLI | MCP 支持,代码审查强 |
| KimiCLIAdapter | Kimi CLI | ACP 协议Zsh 集成 |
| OpenCodeAdapter | OpenCode | 快速代码生成 |
| CustomAdapter | 自定义 | 扩展接口 |
### 3.3 适配器实现示例
```python
class ClaudeCodeAdapter(CLIPluginAdapter):
def __init__(self, config: dict):
self.id = "claude-code-001"
self.name = "Claude Code"
self.config = config
self.executor = TaskExecutor(self.id, resource_manager)
def execute(self, task: Task) -> Result:
# 通过 TaskExecutor 自动管理资源
return self.executor.execute(task.description)
def join_meeting(self, meeting_id: str):
return meeting_scheduler.wait_for_meeting(self.id, meeting_id)
```
---
## 4. 资源管理与文件锁
### 4.1 核心设计
**声明式资源管理**Agent 只需要声明"我要修改这个文件",系统自动获取锁、执行任务、释放锁。
### 4.2 TaskExecutor 包装层
```python
class TaskExecutor:
"""任务执行器 - 自动管理资源生命周期"""
def execute(self, task_description: str) -> str:
# 1. 解析意图,识别需要的文件
required_files = self._parse_required_files(task_description)
# 2. 获取所有需要的锁
self._acquire_all_locks(required_files, task_description)
try:
# 3. 执行实际任务
result = self._do_execute(task_description)
return result
finally:
# 4. 无论发生什么,都释放锁
self._release_all_locks()
```
### 4.3 可靠性保障机制
| 机制 | 解决问题 | 触发方式 |
|-----|---------|---------|
| 超时释放 | 锁忘记释放 | 定时器扫描 |
| 心跳机制 | Agent 挂了检测 | 定时器 + 心跳超时 |
| Lease 上下文管理器 | 代码异常退出 | Python `with` / `finally` |
| 生命周期 Hooks | 进程崩溃清理 | `atexit` + 看门狗 |
| 看门狗进程 | 整个系统挂了 | 独立进程监控 |
| 原子操作 | 并发冲突 | 文件锁 |
### 4.4 Agent 使用方式
```python
# Agent 代码 - 完全不需要关心锁
class ClaudeCodeAgent:
def fix_login_bug(self):
# 只需要描述任务,锁完全透明
result = self.executor.execute(
"修改 src/main.py 修复登录 bug"
)
return result
```
---
## 5. 会议与共识机制
### 5.1 栅栏同步
```python
class MeetingScheduler:
"""会议调度器 - 实现栅栏同步"""
def wait_for_meeting(self, agent_id: str, meeting_id: str):
"""
Agent 调用此方法表示"我准备好了,等会议"
最后一个到达的 Agent 触发会议
"""
queue = self._load_queue()
# 加入等待队列
queue[meeting_id]["waiting_agents"].append(agent_id)
# 检查是否所有人都准备好了
if self._is_everyone_ready(meeting_id):
self._start_meeting(meeting_id)
return "meeting_started"
return "waiting"
```
### 5.2 协作式共识流程
```
┌─────────────────────────────────────────────────────────────┐
│ 协作共识流程 │
│ │
│ 第一阶段:收集初步想法 │
│ Agent A: "建议用 JWT因为..." │
│ Agent B: "我建议用 Session因为..." │
│ Agent C: "两者结合可能更好..." │
│ │
│ 第二阶段讨论与迭代最多5轮
│ → Agent 互相质疑、补充、融合 │
│ → 根据讨论更新提案 │
│ → 检查收敛性 │
│ │
│ 第三阶段:生成共识版本 │
│ → 合并相似提案 │
│ → 记录讨论过程 │
│ → 生成会议文件 │
└─────────────────────────────────────────────────────────────┘
```
### 5.3 会议文件结构
```markdown
---
meeting_id: requirement_review_20260304_103000
date: 2026-03-04
attendees: [claude-code-001, kimi-cli-002, opencode-003]
consensus_type: collaborative
iterations: 3
---
# 需求评审会议
## 参会者
- `claude-code-001` (权重: 1.5)
- `kimi-cli-002` (权重: 1.0)
- `opencode-003` (权重: 1.2)
## 讨论过程
### 第一轮:初步想法
[各 Agent 的初步提案...]
### 第二轮:讨论
[互相质疑、补充的过程...]
### 第三轮:迭代
[根据讨论更新的提案...]
## 最终共识
[达成的共识内容...]
## 贡献记录
| Agent | 主要贡献 |
|-------|---------|
| claude-code-001 | ... |
| kimi-cli-002 | ... |
| opencode-003 | ... |
## 后续行动
1. [ ] ...
2. [ ] ...
```
---
## 6. 动态角色分配
### 6.1 AI 驱动的角色分配
```python
class AIRoleAllocator:
"""AI 驱动的角色分配器"""
def allocate_roles(self, task: Task, available_agents: List[str]) -> dict:
"""
分析任务,自动分配角色给各 Agent
"""
# 获取所有 Agent 的能力描述
agent_capabilities = self._get_agent_capabilities(available_agents)
# 构造提示词,让 AI 分析
prompt = f"""
你是一个团队协调 AI需要为以下任务分配角色。
## 任务描述
{task.description}
## 可用的 Agent 及其能力
{self._format_capabilities(agent_capabilities)}
请分析任务需求,为每个 Agent 分配最合适的角色。
"""
# 调用 LLM 分析
response = self.llm_client.complete(prompt)
allocation = json.loads(response)
# 应用角色分配
for assignment in allocation["role_assignments"]:
self._apply_role(
assignment["agent_id"],
assignment["role"],
assignment["reason"]
)
return allocation
```
### 6.2 可用角色
| 角色 | 名称 | 能力 | 权重 |
|-----|------|------|------|
| pm | 产品经理 | 需求分析、优先级排序、用户故事 | 1.5 |
| architect | 架构师 | 系统设计、技术选型、性能规划 | 1.5 |
| developer | 开发者 | 编码、单元测试、代码审查 | 1.0 |
| qa | 测试工程师 | 测试用例、自动化测试、bug报告 | 1.2 |
| reviewer | 代码审查者 | 代码审查、安全检查、性能分析 | 1.3 |
---
## 7. 多模型支持
### 7.1 智能路由策略
```python
class MultiModelRouter:
"""多模型路由器"""
def route(self, task: Task, context: dict) -> str:
"""
智能路由:根据任务类型选择最合适的模型
"""
task_type = self._classify_task(task)
routing_rules = {
"complex_reasoning": ("anthropic", "claude-opus-4.6"),
"code_generation": ("anthropic", "claude-sonnet-4.6"),
"simple_task": ("anthropic", "claude-haiku-4.6"),
"cost_sensitive": ("deepseek", "deepseek-chat"),
"local_privacy": ("ollama", "llama3"),
"multimodal": ("google", "gemini-2.5-flash")
}
provider, model = routing_rules.get(task_type, default)
return self.providers[provider].complete(model, task, context)
```
### 7.2 故障转移
```python
def route_with_failover(self, task: Task, context: dict) -> str:
"""带故障转移的路由"""
primary_route = self._get_primary_route(task)
fallback_routes = self._get_fallback_routes()
# 尝试主路由
try:
return self._execute_route(primary_route, task, context)
except Exception:
pass
# 尝试备选路由
for route in fallback_routes:
try:
return self._execute_route(route, task, context)
except Exception:
pass
# 全部失败,使用最简单的本地模型
return self._execute_local_fallback(task, context)
```
---
## 8. 共享存储结构
```
.doc/
├── agents/ # Agent 注册与状态
│ ├── registry.json # 所有已注册 Agent 列表
│ ├── claude-code-001/ # 每个 Agent 的独立目录
│ │ ├── config.json # Agent 配置
│ │ ├── state.json # 当前状态
│ │ ├── history.json # 历史记录
│ │ └── capabilities.json # 能力声明
│ └── ...
├── dialogues/ # Agent 间对话记录
│ └── [agent_a]_[agent_b]_[timestamp].json
├── progress/ # 工程进度
│ ├── project_state.json # 项目整体状态
│ ├── milestones.json # 里程碑
│ └── tasks.json # 任务列表
├── resources/ # 资源分配
│ ├── resource_pool.json # 资源池状态
│ ├── file_locks.json # 文件锁
│ └── allocation_log.json # 分配日志
├── meetings/ # 会议记录与共识
│ ├── [YYYY-MM-DD]/
│ │ └── [HHMMSS]_[meeting_name].md
│ └── meeting_template.md
├── cache/ # 实时缓存
│ ├── meeting_queue.json # 会议等待队列
│ ├── message_bus.json # 消息总线快照
│ ├── lock_state.json # 锁状态缓存
│ └── heartbeats.json # Agent 心跳
├── workflow/ # 工作流定义
│ ├── project_workflow.yaml # 项目工作流
│ ├── meeting_definitions.yaml # 会议定义
│ └── consensus_rules.yaml # 共识规则
└── shared/ # 共享资源
├── knowledge_base/ # 知识库
├── templates/ # 模板文件
└── conventions/ # 编码规范
```
---
## 9. 技术栈
### 9.1 后端 (Python)
| 组件 | 技术选择 |
|-----|---------|
| 核心框架 | Python 3.12+ |
| 异步运行时 | asyncio |
| LLM 调用 | anthropic, openai, langchain |
| 文件操作 | pathlib, aiofiles |
| 进程管理 | subprocess, multiprocessing |
| 定时任务 | APScheduler |
### 9.2 前端 (TypeScript)
| 组件 | 技术选择 |
|-----|---------|
| 框架 | React 18 + TypeScript |
| UI 库 | shadcn/ui + Tailwind CSS |
| 状态管理 | Zustand |
| 实时通信 | WebSocket |
| HTTP 客户端 | fetch / axios |
### 9.3 API 层 (Node.js)
| 组件 | 技术选择 |
|-----|---------|
| 框架 | Express / Fastify |
| 类型检查 | TypeScript |
| 实时通信 | Socket.IO |
| 进程通信 | child_process |
---
## 附录
### A. 参考项目
详见 [reference-projects.md](reference-projects.md)
### B. 更新日志
| 版本 | 日期 | 变更 |
|-----|------|------|
| 1.0 | 2026-03-04 | 初始版本 |
| 1.1 | 2026-03-04 | 添加人类参与系统设计 |
### C. 人类参与系统
#### C.1 设计概述
人类参与者通过 `humans.json` 文件参与协作,无需 API 或 UI 调用。
**核心特点**
- 单一共享文件 `humans.json`
- 事件驱动通知机制
- 按优先级区分处理
- 自动合并到会议讨论
#### C.2 文件结构
```
.doc/
├── agents/ # Agent 目录
├── humans.json # 人类输入共享文件
├── meetings/ # 会议文件
├── events/ # 事件通知
└── task_queue.json # 任务队列
```
#### C.3 humans.json 结构
```json
{
"version": "1.0",
"last_updated": "2026-03-04T14:30:00Z",
"participants": {
"user001": {
"name": "张三",
"role": "tech_lead",
"status": "online",
"avatar": "👤"
}
},
// 任务需求
"task_requests": [
{
"id": "req_001",
"from": "user001",
"timestamp": "2026-03-04T14:28:00Z",
"priority": "high", // high | medium | low
"type": "correction",
"title": "登录验证方式调整",
"content": "建议改用 Session + Redis",
"target_files": ["src/auth/login.py"],
"suggested_agent": "claude-code-001",
"urgent": true,
"status": "pending"
}
],
// 会议发言
"meeting_comments": [
{
"id": "comment_001",
"from": "user001",
"meeting_id": "design_review",
"timestamp": "2026-03-04T14:25:00Z",
"type": "proposal",
"priority": "normal",
"content": "建议使用 SQLite保持简单",
"status": "pending"
}
],
// 人类状态
"human_states": {
"user001": {
"status": "online",
"current_focus": "reviewing",
"preferred_contact": "async"
}
}
}
```
#### C.4 事件驱动流程
```
人类写入 humans.json
文件监听器触发
写入 events/notification_stream.json
┌───────┴────────┐
│ │
↓ ↓
会议通知 任务通知
│ │
↓ ↓
Agent 收到 Agent 收到
会议通知 任务通知
│ │
↓ ↓
读取会议 读取任务
评论部分 requests
│ │
↓ ↓
参与讨论 按优先级
处理任务
```
#### C.5 任务优先级处理
| 优先级 | 处理方式 | 示例 |
|-------|---------|------|
| **high** + urgent | 立即中断当前任务 | 安全漏洞修复、生产故障 |
| **high** | 尽快处理,完成当前任务后 | 功能调整、架构变更 |
| **medium** | 加入队列,按序处理 | 功能增强、文档补充 |
| **low** | 空闲时处理 | 优化建议、非紧急任务 |
#### C.6 会议中的人类发言显示
```markdown
## 讨论记录
### Agent 提案
**claude-code-001 (14:20)**:
> 建议使用 JWT + Refresh Token...
### 人类发言(高优先级)
**user001 [HUMAN] ⚠️ (14:22)**:
> 我反对使用 JWT。我们的数据量不大SQLite + Session 就够了。
>
> **优先级HIGH**
### Agent 响应
**kimi-cli-002 (14:23)**:
> 收到用户反馈,重新评估方案...
```
#### C.7 前端输入实现
前端提供任务输入栏,输入后:
1. 写入 `humans.json``task_requests`
2. 记录发送时间戳
3. 触发文件监听事件
4. Agent 读取并处理
```javascript
// 提交任务
function submitTask(content, priority = "medium") {
const request = {
id: `req_${Date.now()}`,
from: "user001",
timestamp: new Date().toISOString(),
priority: priority,
type: "user_task",
content: content,
status: "pending"
};
// 写入 humans.json
humans.task_requests.push(request);
// 保存文件...
}
```
---
*文档结束*

187
docs/frontend-steps.md Normal file
View File

@@ -0,0 +1,187 @@
# 前端开发步骤
后端已完成并验证通过,现在重新设计前端为多页面应用。
---
## 后端 CLI 验证结果
| 服务 | 状态 | 测试命令 |
|------|------|----------|
| 项目初始化 | ✅ | `python cli.py hello`, `python cli.py version` |
| 存储服务 | ✅ | `python cli.py storage write/read/delete` |
| 文件锁 | ✅ | `python cli.py lock acquire/status/release/check` |
| 心跳服务 | ✅ | `python cli.py heartbeat ping/list` |
| Agent 注册 | ✅ | `python cli.py agent register/list/info/state` |
| 会议调度 | ✅ | `python cli.py meeting create/wait/queue` |
| 会议记录 | ✅ | `python cli.py meeting record-create/discuss/progress` |
| 资源管理 | ✅ | `python cli.py execute`, `python cli.py status` |
| 工作流引擎 | ✅ | `python cli.py workflow show` |
| 角色分配 | ✅ | `python cli.py role allocate/primary` |
---
## 前端架构重新设计
### 当前问题
- 单页面应用,所有组件挤在一起
- 没有导航结构
- 难以扩展
### 新架构设计
```
frontend/
├── src/
│ ├── pages/ # 页面组件
│ │ ├── DashboardPage.tsx # 仪表盘首页
│ │ ├── AgentsPage.tsx # Agent 管理页
│ │ ├── MeetingsPage.tsx # 会议管理页
│ │ ├── ResourcesPage.tsx # 资源监控页
│ │ ├── SettingsPage.tsx # 配置页面
│ │ └── WorkflowPage.tsx # 工作流页面
│ ├── components/ # 共享组件
│ │ ├── layout/
│ │ │ ├── AppLayout.tsx # 主布局(导航栏)
│ │ │ └── Sidebar.tsx # 侧边栏
│ │ ├── ui/ # UI 组件
│ │ └── dashboard/ # 仪表盘组件
│ ├── lib/ # API 客户端
│ │ └── api.ts # 统一 API 调用
│ └── App.tsx # 路由入口
```
---
## 页面设计规范
### 1. 仪表盘 (DashboardPage)
- 组件网格Agent 状态卡片、会议进度、资源监控、最近会议
- 实时更新(轮询或 WebSocket
- 快捷操作按钮
### 2. Agent 管理页 (AgentsPage)
- Agent 列表(表格或卡片)
- 注册新 Agent 表单
- Agent 详情面板(可展开)
- 状态指示器
### 3. 会议管理页 (MeetingsPage)
- 会议列表(按日期分组)
- 创建会议按钮
- 会议详情(步骤进度、讨论记录)
- 参会者状态
### 4. 资源监控页 (ResourcesPage)
- 文件锁列表
- CPU/内存使用情况
- Agent 心跳状态
- 实时刷新
### 5. 配置页 (SettingsPage)
- 后端 API 地址配置
- Agent 配置
- 工作流上传
- 系统设置
### 6. 工作流页 (WorkflowPage)
- 工作流列表
- YAML 编辑器
- 工作流执行状态
- 进度追踪
---
## 开发步骤
### 第一步:路由和布局
- 安装 React Router
- 创建 AppLayout 和 Sidebar
- 配置路由
### 第二步:仪表盘页面
- 从现有组件迁移到 DashboardPage
- 添加导航链接
### 第三步API 客户端
- 创建 `lib/api.ts`
- 实现所有 API 调用函数
- 替换 mock 数据
### 第四步Agent 管理页
- 创建 AgentsPage
- 注册表单
- Agent 列表和详情
### 第五步:会议管理页
- 创建 MeetingsPage
- 会议列表和详情
- 创建会议模态框
### 第六步:资源监控页
- 创建 ResourcesPage
- 实时数据刷新
- 图表展示
### 第七步:配置页面
- 创建 SettingsPage
- 表单输入
- 配置保存
### 第八步:工作流页面
- 创建 WorkflowPage
- YAML 上传和编辑
- 执行状态展示
---
## 路由结构
```typescript
const routes = [
{ path: '/', element: <DashboardPage /> },
{ path: '/agents', element: <AgentsPage /> },
{ path: '/meetings', element: <MeetingsPage /> },
{ path: '/resources', element: <ResourcesPage /> },
{ path: '/settings', element: <SettingsPage /> },
{ path: '/workflow', element: <WorkflowPage /> },
]
```
---
## API 端点对接
```typescript
// API 基础配置
const API_BASE = 'http://localhost:8000/api';
// Agents
GET /api/agents
POST /api/agents/register
GET /api/agents/:id
GET /api/agents/:id/state
POST /api/agents/:id/state
// Locks
GET /api/locks
POST /api/locks/acquire
POST /api/locks/release
# Heartbeats
GET /api/heartbeats
POST /api/heartbeats/:id
# Meetings
GET /api/meetings/:date
GET /api/meetings/:id
POST /api/meetings/create
POST /api/meetings/:id/discuss
POST /api/meetings/:id/progress
```
---
## 下一步
开始实施前端开发步骤,从路由和布局开始。

305
docs/reference-projects.md Normal file
View File

@@ -0,0 +1,305 @@
# 多 Agent 协作系统 - 参考项目分析
> 本文档分析相关开源项目的优点,用于设计整合
---
## 一、插件化架构参考
### 1. OpenClaw ⭐⭐⭐⭐⭐
**GitHub**: [MindDock/OpenClaw](https://github.com/MindDock/OpenClaw)
**核心优点**
- **Channel Plugin 接口**统一的插件架构支持消息通道、Agent 工具、CLI 扩展
- **Plugin SDK**:完整的插件开发框架,包含扩展点、配置系统、发布流程
- **Gateway 控制平面**WebSocket/HTTP 控制平面,支持 RPC 方法、事件系统
- **适配器模式**:每个插件实现标准接口,易于扩展
```typescript
// OpenClaw Channel Plugin 接口
interface ChannelPlugin {
id: string;
meta: ChannelMeta;
capabilities: ChannelCapabilities;
config: ChannelConfigAdapter;
outbound: ChannelOutboundAdapter;
inbound?: ChannelInboundAdapter;
messaging?: ChannelMessagingAdapter;
}
```
**适用场景**CLI 工具插件化、多通道适配
---
### 2. everything-claude-code ⭐⭐⭐⭐⭐
**GitHub**: [affaan-m/everything-claude-code](https://github.com/affaan-m/everything-claude-code)
**核心优点**
- **跨工具兼容**:设计为可在 Claude Code、Codex、Cursor、OpenCode 等多种工具运行
- **DRY Adapter**:适配器模式实现跨工具通用描述
- **插件系统**:完整的 Claude Code 插件,支持 agents、commands、skills、hooks
- **多语言规则架构**按语言分组的规则系统common + typescript + python + golang
```json
{
"extraKnownMarketplaces": {
"everything-claude-code": {
"source": {
"source": "github",
"repo": "affaan-m/everything-claude-code"
}
}
},
"enabledPlugins": {
"everything-claude-code@everything-claude-code": true
}
}
```
**适用场景**:跨 CLI 工具的 Agent 适配
---
### 3. Kimi CLI ⭐⭐⭐⭐
**GitHub**: [MoonshotAI/kimi-cli](https://github.com/MoonshotAI/kimi-cli)
**核心优点**
- **Agent Client Protocol (ACP)**:标准的 Agent 客户端协议
- **MCP 支持**:完整的 Model Context Protocol 支持
- **Zsh 集成**Shell 集成支持模式切换Agent/Shell
```bash
# MCP 配置示例
kimi --mcp-config-file /path/to/mcp.json
```
**适用场景**CLI 工具的 MCP 集成参考
---
## 二、共识机制参考
### 1. W-5 Multi-Agent Consensus Framework ⭐⭐⭐⭐⭐
**GitHub**: [Winner12-AI/w5-football-prediction](https://github.com/Winner12-AI/w5-football-prediction)
**核心优点**
- **概率再平衡器**:使用 Gemini 3 作为"概率再平衡器"
- **动态提示注入**:根据任务动态调整提示词
- **多 Agent 辩论**Agent 之间通过辩论达成共识
- **准确率提升**86.3% 准确率,通过共识机制实现
```
共识流程:
1. 各 Agent 独立分析
2. 提出初步结论
3. Agent 之间辩论
4. 概率再平衡
5. 达成共识
```
**适用场景**:多 Agent 决策共识
---
### 2. Claude-Flow ⭐⭐⭐⭐⭐
**项目**: Enterprise multi-agent orchestration for Claude Code
**核心优点**
- **容错共识**:支持 Byzantine、Weighted、Majority 三种共识机制
- **Queen-led Swarms**:层级化协调,防止 Agent 漂移
- **SONA 学习**:自优化神经架构,<0.05ms 适应速度
- **HNSW Memory**150x-12,500x 更快的模式检索
| 共识类型 | 特点 | 适用场景 |
|---------|------|---------|
| Byzantine | 2/3 多数,抗恶意节点 | 高安全要求 |
| Weighted | 按权重投票 | 专家系统 |
| Majority | 简单多数 | 快速决策 |
| Raft | 领导者选举 | 分布式协调 |
| Gossip | 流言传播 | 大规模系统 |
**适用场景**:企业级 Agent 编排、容错共识
---
### 3. Multi-Agent Consensus Seeking via LLMs
**论文**: [Multi-Agent Consensus Seeking Via Large Language Models](https://www.aminer.cn/pub/6541a83c939a5f40824d000c/multi-agent-consensus-seeking-via-large-language-models)
**核心优点**
- **平均策略**LLM Agent 主要使用平均策略寻求共识
- **网络拓扑影响**:分析 Agent 数量、个性、网络拓扑对谈判的影响
- **零样本自主规划**:应用于多机器人聚合任务
**适用场景**Agent 谈判、共识算法设计
---
## 三、协作协议参考
### 1. Co-TAP: Three-Layer Agent Interaction Protocol ⭐⭐⭐⭐⭐
**项目**: Co-TAP 三层 Agent 交互协议
**核心优点**
- **HAI (Human-Agent Interaction)**:人机交互标准化
- **UAP (Unified Agent Protocol)**:异构 Agent 统一通信
- **MEK (Memory-Extraction-Knowledge)**:认知链标准化
```
┌─────────────────────────────────────────────────┐
│ HAI 层:用户-界面-Agent 信息流标准化 │
├─────────────────────────────────────────────────┤
│ UAP 层:异构 Agent 无缝互联 │
├─────────────────────────────────────────────────┤
│ MEK 层:记忆-提取-知识认知链 │
└─────────────────────────────────────────────────┘
```
**适用场景**:跨平台 Agent 通信
---
### 2. Internet of Agents (IoA) ⭐⭐⭐⭐
**论文**: Internet of Agents: Fundamentals, Applications, and Challenges
**核心优点**
- **能力通知与发现**Agent 动态发现机制
- **自适应通信协议**:协议转换机制
- **动态任务匹配**:任务自动分配
- **共识与冲突解决**:内置冲突解决机制
- **激励模型**:经济激励机制
**适用场景**:大规模 Agent 网络
---
### 3. Agent Name Service (ANS) ⭐⭐⭐⭐
**项目**: Universal Directory for AI Agent Discovery
**核心优点**
- **PKI 证书**:可验证的 Agent 身份
- **DNS 命名**DNS 风格的命名约定
- **协议适配器层**:支持 A2A、MCP、ACP 等多种协议
- **安全解析**:正式化的解析算法
**适用场景**Agent 发现与互操作
---
## 四、MCP 集成参考
### 1. MCP 协议完整生态
**标准**: Model Context Protocol (Anthropic)
**核心优点**
- **三种传输模式**stdio、http、sse
- **三大核心原语**Tools、Resources、Prompts
- **动态发现**:运行时工具发现
- **16000+ Server**:丰富的生态
```json
{
"mcpServers": {
"github": {
"type": "http",
"url": "https://github-mcp.example.com",
"headers": {
"Authorization": "Bearer ${GITHUB_TOKEN}"
}
},
"filesystem": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "~/Projects"]
}
}
}
```
---
## 五、整合设计建议
基于以上分析,建议整合以下优点:
### 1. 插件化架构(来自 OpenClaw + everything-claude-code
```python
interface CLIPluginAdapter {
id: string
name: string
version: string
# 适配能力
def execute(task: Task) -> Result
def get_status() -> Status
def join_meeting(meeting_id: str) -> None
def write_state(state: dict) -> None
def read_others(agent_id: str) -> dict
}
# 预置适配器
class ClaudeCodeAdapter(CLIPluginAdapter): ...
class KimiCodeAdapter(CLIPluginAdapter): ...
class OpenCodeAdapter(CLIPluginAdapter): ...
```
### 2. 共识机制(来自 Claude-Flow + W-5
```python
class ConsensusMechanism:
def propose(self, agent_id: str, proposal: Proposal) -> None
def vote(self, agent_id: str, vote: Vote) -> None
def resolve(self) -> Decision
# 共识类型
class ByzantineConsensus(ConsensusMechanism): ...
class WeightedConsensus(ConsensusMechanism): ...
class MajorityConsensus(ConsensusMechanism): ...
```
### 3. 会议驱动(栅栏同步)
```python
class MeetingBarrier:
def wait(self, agent_id: str) -> None
def trigger_when_all_ready(self) -> None
def start_conensus_process(self) -> Decision
```
### 4. .doc 文件夹结构
```
.doc/
├── agents/ # Agent 注册与状态
│ ├── claude-code.json
│ ├── kimi-cli.json
│ └── opencode.json
├── dialogues/ # Agent 间对话记录
│ └── agent_a_agent_b_20250304.json
├── progress/ # 工程进度
│ └── project_state.json
├── resources/ # 资源分配与锁
│ └── resource_pool.json
├── meetings/ # 会议记录与共识
│ └── meeting_20250304.md
├── cache/ # 实时缓存
│ └── meeting_queue.json
└── workflow/ # 工作流定义
└── project_workflow.yaml
```
---
## 六、参考项目链接汇总
| 项目 | GitHub/链接 | 核心价值 |
|-----|------------|---------|
| OpenClaw | [MindDock/OpenClaw](https://github.com/MindDock/OpenClaw) | 插件化架构 |
| everything-claude-code | [affaan-m/everything-claude-code](https://github.com/affaan-m/everything-claude-code) | 跨工具兼容 |
| Kimi CLI | [MoonshotAI/kimi-cli](https://github.com/MoonshotAI/kimi-cli) | MCP + ACP |
| W-5 Framework | [Winner12-AI/w5-football-prediction](https://github.com/Winner12-AI/w5-football-prediction) | 共识机制 |
| Claude-Flow | [ruvnet/claude-flow](https://github.com/ruvnet/claude-flow) | 企业级编排 |
| RuVector | [ruvnet/ruvector](https://github.com/ruvnet/ruvector) | 自学习向量库 |
| template-repo | [AndrewAltimit/template-repo](https://github.com/AndrewAltimit/template-repo) | 6 Agents + 14 MCP |
| MCP Protocol | [modelcontextprotocol](https://modelcontextprotocol.io/) | 统一协议标准 |
---
*更新日期2026-03-04*

63
frontend/README.md Normal file
View File

@@ -0,0 +1,63 @@
# Swarm Command Center
Sci-Fi 风格的多智能体协作系统前端界面。
## 技术栈
- **React 18** - UI 框架
- **TypeScript** - 类型安全
- **Vite** - 构建工具
- **Tailwind CSS** - 样式框架
- **Lucide React** - 图标库
## 开发
```bash
# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 构建生产版本
npm run build
```
## 设计系统
### 颜色
- 主色: Cyan `#00f0ff`
- 辅助: Purple `#8b5cf6`
- 成功: Green `#00ff9d`
- 警告: Amber `#ff9500`
- 错误: Pink `#ff006e`
### 字体
- **Orbitron** - 标题/显示字体
- **Noto Sans SC** - 中文正文字体
- **JetBrains Mono** - 代码/数据字体
- **Rajdhani** - 辅助字体
### 组件
- Header - 顶部导航栏
- TaskInput - 任务输入组件
- AgentStatusCard - 智能体状态卡片
- DiscussionCard - 讨论区卡片
- StatisticsCard - 统计数据卡片
- WorkflowCard - 工作流卡片
- MeetingProgressCard - 会议进度卡片
- ResourceMonitorCard - 资源监控卡片
- ConsensusCard - 共识状态卡片
- BarrierSyncCard - 屏障同步卡片
- RecentMeetingsCard - 最近会议卡片
- ActionBar - 操作栏
## 布局
采用 Bento Grid 响应式布局:
- 桌面端 (1400px+): 6 列
- 平板端 (768px-1400px): 4 列
- 移动端 (<768px): 1 列

16
frontend/index.html Normal file
View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Swarm Command Center</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Noto+Sans+SC:wght@300;400;500;600;700&family=Orbitron:wght@400;500;600;700&family=Rajdhani:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

2681
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
frontend/package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "swarm-command-center",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"test": "playwright test",
"test:ui": "playwright test --ui"
},
"dependencies": {
"clsx": "^2.1.1",
"lucide-react": "^0.487.0",
"motion": "^12.23.24",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^7.13.1",
"tailwind-merge": "^3.2.0"
},
"devDependencies": {
"@playwright/test": "^1.58.2",
"@tailwindcss/vite": "^4.1.12",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.7.0",
"tailwindcss": "^4.1.12",
"typescript": "^5.8.3",
"vite": "^6.3.5"
}
}

View File

@@ -0,0 +1,31 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Playwright 配置文件
* 用于端到端测试前端页面和 API 接口
*/
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
});

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="8" fill="url(#gradient)"/>
<path d="M10 16C10 12.6863 12.6863 10 16 10C19.3137 10 22 12.6863 22 16C22 19.3137 19.3137 22 16 22" stroke="#030712" stroke-width="2" stroke-linecap="round"/>
<circle cx="16" cy="16" r="3" fill="#030712"/>
<circle cx="22" cy="10" r="2" fill="#030712"/>
<circle cx="10" cy="22" r="2" fill="#030712"/>
<defs>
<linearGradient id="gradient" x1="0" y1="0" x2="32" y2="32" gradientUnits="userSpaceOnUse">
<stop stop-color="#00f0ff"/>
<stop offset="1" stop-color="#8b5cf6"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 669 B

32
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,32 @@
import {
createBrowserRouter,
RouterProvider,
Navigate,
} from 'react-router-dom';
import { AppLayout } from './components/layout/AppLayout';
import { DashboardPage } from './pages/DashboardPage';
import { AgentsPage } from './pages/AgentsPage';
import { MeetingsPage } from './pages/MeetingsPage';
import { ResourcesPage } from './pages/ResourcesPage';
import { WorkflowPage } from './pages/WorkflowPage';
import { SettingsPage } from './pages/SettingsPage';
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
children: [
{ index: true, element: <DashboardPage /> },
{ path: 'agents', element: <AgentsPage /> },
{ path: 'meetings', element: <MeetingsPage /> },
{ path: 'resources', element: <ResourcesPage /> },
{ path: 'workflow', element: <WorkflowPage /> },
{ path: 'settings', element: <SettingsPage /> },
{ path: '*', element: <Navigate to="/" replace /> },
],
},
]);
export default function App() {
return <RouterProvider router={router} />;
}

View File

@@ -0,0 +1,230 @@
import { Play, Pause, RotateCcw, Settings, Download, Terminal, Bell, RefreshCw } from "lucide-react";
import { useState } from "react";
export function ActionBar() {
const [running, setRunning] = useState(true);
const [notif, setNotif] = useState(3);
return (
<div
style={{
background: "rgba(10,15,30,0.85)",
border: "1px solid rgba(0,240,255,0.1)",
borderRadius: 16,
backdropFilter: "blur(20px)",
WebkitBackdropFilter: "blur(20px)",
padding: "14px 24px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 16,
flexWrap: "wrap",
position: "relative",
overflow: "hidden",
}}
>
{/* Bottom gradient line */}
<div
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: 1,
background: "linear-gradient(90deg,transparent,rgba(0,240,255,0.3),transparent)",
}}
/>
{/* Left: Status & Controls */}
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
{/* System status */}
<div
style={{
padding: "6px 14px",
background: running ? "rgba(0,255,157,0.08)" : "rgba(255,149,0,0.08)",
border: `1px solid ${running ? "rgba(0,255,157,0.25)" : "rgba(255,149,0,0.25)"}`,
borderRadius: 10,
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<span
style={{
width: 8,
height: 8,
borderRadius: "50%",
background: running ? "#00ff9d" : "#ff9500",
color: running ? "#00ff9d" : "#ff9500",
}}
className={running ? "animate-status-working" : "animate-pulse-glow"}
/>
<span className="font-mono-code" style={{ fontSize: 11, color: running ? "#00ff9d" : "#ff9500" }}>
{running ? "SYSTEM RUNNING" : "SYSTEM PAUSED"}
</span>
</div>
{/* Play/Pause */}
<button
onClick={() => setRunning(!running)}
className="btn-secondary"
style={{
padding: "8px 16px",
display: "flex",
alignItems: "center",
gap: 6,
color: running ? "#ff9500" : "#00ff9d",
borderColor: running ? "rgba(255,149,0,0.3)" : "rgba(0,255,157,0.3)",
background: running ? "rgba(255,149,0,0.08)" : "rgba(0,255,157,0.08)",
}}
>
{running ? <Pause size={14} /> : <Play size={14} />}
{running ? "暂停" : "继续"}
</button>
<button
className="btn-secondary"
style={{ padding: "8px 14px", display: "flex", alignItems: "center", gap: 6 }}
>
<RotateCcw size={14} />
</button>
<button
className="btn-secondary"
style={{ padding: "8px 14px", display: "flex", alignItems: "center", gap: 6 }}
>
<RefreshCw size={14} />
</button>
</div>
{/* Center: Quick stats */}
<div style={{ display: "flex", alignItems: "center", gap: 20 }}>
{[
{ label: "任务队列", value: "5", color: "#00f0ff" },
{ label: "当前会议", value: "1", color: "#ff9500" },
{ label: "锁数量", value: "5", color: "#8b5cf6" },
{ label: "错误", value: "0", color: "#00ff9d" },
].map(s => (
<div key={s.label} style={{ textAlign: "center" }}>
<div
className="font-orbitron"
style={{ fontSize: 16, fontWeight: 700, color: s.color, lineHeight: 1 }}
>
{s.value}
</div>
<div className="font-mono-code" style={{ fontSize: 9, color: "#6b7280", marginTop: 2 }}>
{s.label}
</div>
</div>
))}
</div>
{/* Right: Actions */}
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
{/* Notification bell */}
<button
onClick={() => setNotif(0)}
style={{
position: "relative",
width: 40,
height: 40,
background: "rgba(0,240,255,0.05)",
border: "1px solid rgba(0,240,255,0.15)",
borderRadius: 10,
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
color: "#9ca3af",
transition: "all 0.2s ease",
}}
>
<Bell size={16} />
{notif > 0 && (
<span
style={{
position: "absolute",
top: 6,
right: 6,
width: 14,
height: 14,
borderRadius: "50%",
background: "#ff006e",
fontSize: 8,
color: "#fff",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontFamily: "JetBrains Mono, monospace",
fontWeight: 700,
}}
>
{notif}
</span>
)}
</button>
<button
style={{
width: 40,
height: 40,
background: "rgba(0,240,255,0.05)",
border: "1px solid rgba(0,240,255,0.15)",
borderRadius: 10,
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
color: "#9ca3af",
transition: "all 0.2s ease",
}}
>
<Terminal size={16} />
</button>
<button
style={{
width: 40,
height: 40,
background: "rgba(0,240,255,0.05)",
border: "1px solid rgba(0,240,255,0.15)",
borderRadius: 10,
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
color: "#9ca3af",
transition: "all 0.2s ease",
}}
>
<Download size={16} />
</button>
<button
style={{
width: 40,
height: 40,
background: "rgba(0,240,255,0.05)",
border: "1px solid rgba(0,240,255,0.15)",
borderRadius: 10,
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
color: "#9ca3af",
transition: "all 0.2s ease",
}}
>
<Settings size={16} />
</button>
<button className="btn-primary" style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Play size={13} />
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,255 @@
import { Cpu, Zap, MoreHorizontal } from "lucide-react";
type AgentStatus = "working" | "waiting" | "idle";
interface Agent {
id: string;
name: string;
fullName: string;
role: string;
status: AgentStatus;
task: string;
progress: number;
model: string;
tokens: number;
gradient: string;
}
const agents: Agent[] = [
{
id: "claude-001",
name: "CLA",
fullName: "Claude Code",
role: "架构师",
status: "working",
task: "重构认证模块 src/auth/",
progress: 68,
model: "claude-opus-4.6",
tokens: 14203,
gradient: "linear-gradient(135deg, #8b5cf6, #6366f1)",
},
{
id: "kimi-002",
name: "KIM",
fullName: "Kimi CLI",
role: "产品经理",
status: "waiting",
task: "等待需求评审会议",
progress: 100,
model: "moonshot-v1-8k",
tokens: 8921,
gradient: "linear-gradient(135deg, #f59e0b, #d97706)",
},
{
id: "opencode-003",
name: "OPC",
fullName: "OpenCode",
role: "开发者",
status: "working",
task: "生成 API 单元测试用例",
progress: 34,
model: "gpt-4o",
tokens: 6744,
gradient: "linear-gradient(135deg, #10b981, #059669)",
},
{
id: "human-001",
name: "USR",
fullName: "Tech Lead",
role: "技术负责人",
status: "idle",
task: "等待 Agent 完成评审",
progress: 0,
model: "human",
tokens: 0,
gradient: "linear-gradient(135deg, #f59e0b, #d97706)",
},
];
const statusConfig = {
working: { color: "#00ff9d", bg: "rgba(0,255,157,0.1)", label: "Working", border: "rgba(0,255,157,0.4)" },
waiting: { color: "#ff9500", bg: "rgba(255,149,0,0.1)", label: "Meeting", border: "rgba(255,149,0,0.4)" },
idle: { color: "#6b7280", bg: "rgba(107,114,128,0.1)", label: "Idle", border: "rgba(107,114,128,0.2)" },
};
function AgentItem({ agent }: { agent: Agent }) {
const cfg = statusConfig[agent.status];
return (
<div
style={{
background: "rgba(0,0,0,0.3)",
border: `1px solid ${cfg.border}`,
borderRadius: 12,
padding: 14,
transition: "all 0.3s ease",
}}
className={
agent.status === "working"
? "agent-working"
: agent.status === "waiting"
? "agent-waiting"
: ""
}
>
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 10 }}>
{/* Avatar */}
<div
className="agent-avatar"
style={{ background: agent.gradient, flexShrink: 0 }}
>
{agent.name}
</div>
{/* Name & Role */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<span
className="font-orbitron"
style={{ fontSize: 12, color: "#e5e7eb", fontWeight: 600 }}
>
{agent.fullName}
</span>
</div>
<div
className="font-mono-code"
style={{ fontSize: 10, color: "#6b7280", marginTop: 1 }}
>
{agent.role} · {agent.model}
</div>
</div>
{/* Status badge */}
<div
className="badge"
style={{
color: cfg.color,
borderColor: `${cfg.color}40`,
background: cfg.bg,
flexShrink: 0,
}}
>
<span
style={{ display: "inline-flex", alignItems: "center", gap: 4 }}
>
<span
style={{ width: 5, height: 5, borderRadius: "50%", background: cfg.color }}
className={agent.status !== "idle" ? "animate-pulse-glow" : ""}
/>
{cfg.label}
</span>
</div>
</div>
{/* Task */}
<div
className="font-rajdhani"
style={{ fontSize: 12, color: "#9ca3af", marginBottom: 8, lineHeight: 1.4 }}
>
{agent.task}
</div>
{/* Progress */}
{agent.status !== "idle" && (
<div>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: 4,
}}
>
<span className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>
</span>
<span className="font-mono-code" style={{ fontSize: 10, color: cfg.color }}>
{agent.progress}%
</span>
</div>
<div
style={{
height: 4,
background: "#111827",
borderRadius: 2,
overflow: "hidden",
}}
>
<div
style={{
height: "100%",
width: `${agent.progress}%`,
background:
agent.status === "working"
? "linear-gradient(90deg,#00f0ff,#00ff9d)"
: "linear-gradient(90deg,#00f0ff,#ff9500)",
borderRadius: 2,
transition: "width 0.5s ease",
}}
/>
</div>
</div>
)}
{/* Tokens */}
{agent.tokens > 0 && (
<div
style={{ display: "flex", alignItems: "center", gap: 4, marginTop: 8 }}
>
<Zap size={10} color="#6b7280" />
<span className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>
{agent.tokens.toLocaleString()} tokens
</span>
</div>
)}
</div>
);
}
export function AgentStatusCard() {
return (
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
{/* Header */}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 16 }}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div
style={{
width: 28,
height: 28,
borderRadius: 8,
background: "rgba(0,240,255,0.1)",
border: "1px solid rgba(0,240,255,0.2)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Cpu size={14} color="#00f0ff" />
</div>
<span className="card-title">Agent </span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span className="font-mono-code" style={{ fontSize: 10, color: "#00ff9d" }}>
3 / 4
</span>
<button
style={{
background: "transparent",
border: "none",
color: "#6b7280",
cursor: "pointer",
padding: 4,
}}
>
<MoreHorizontal size={16} />
</button>
</div>
</div>
{/* Agent list */}
<div style={{ display: "flex", flexDirection: "column", gap: 10, flex: 1, overflowY: "auto" }}>
{agents.map(agent => (
<AgentItem key={agent.id} agent={agent} />
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,279 @@
import { Shield, Zap } from "lucide-react";
type NodeState = "ready" | "waiting" | "pending";
interface BarrierNode {
id: string;
name: string;
gradient: string;
state: NodeState;
waitingFor?: string;
}
const nodes: BarrierNode[] = [
{
id: "n1",
name: "CLA",
gradient: "linear-gradient(135deg,#8b5cf6,#6366f1)",
state: "ready",
},
{
id: "n2",
name: "KIM",
gradient: "linear-gradient(135deg,#f59e0b,#d97706)",
state: "waiting",
waitingFor: "架构设计完成",
},
{
id: "n3",
name: "OPC",
gradient: "linear-gradient(135deg,#10b981,#059669)",
state: "ready",
},
{
id: "n4",
name: "USR",
gradient: "linear-gradient(135deg,#f59e0b,#b45309)",
state: "pending",
},
];
const stateColors: Record<NodeState, string> = {
ready: "#00ff9d",
waiting: "#ff9500",
pending: "#374151",
};
const stateLabels: Record<NodeState, string> = {
ready: "READY",
waiting: "WAIT",
pending: "IDLE",
};
const syncPoints = [
{ name: "INIT", completed: true },
{ name: "REVIEW", completed: true },
{ name: "DESIGN", completed: false, active: true },
{ name: "IMPL", completed: false },
{ name: "DEPLOY", completed: false },
];
export function BarrierSyncCard() {
const readyCount = nodes.filter(n => n.state === "ready").length;
return (
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
{/* Header */}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 14, flexShrink: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div
style={{
width: 28,
height: 28,
borderRadius: 8,
background: "rgba(0,240,255,0.1)",
border: "1px solid rgba(0,240,255,0.2)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Shield size={14} color="#00f0ff" />
</div>
<span className="card-title"></span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<span className="font-mono-code" style={{ fontSize: 11, color: "#ff9500" }}>
{readyCount}/{nodes.length}
</span>
<div
style={{
padding: "3px 10px",
background: "rgba(255,149,0,0.1)",
border: "1px solid rgba(255,149,0,0.3)",
borderRadius: 6,
}}
>
<span className="font-mono-code" style={{ fontSize: 10, color: "#ff9500" }}>
<span className="animate-pulse-fast"></span>
</span>
</div>
</div>
</div>
<div style={{ display: "flex", gap: 20, flex: 1, minHeight: 0 }}>
{/* Left: Agent nodes */}
<div style={{ display: "flex", flexDirection: "column", gap: 8, minWidth: 200 }}>
{nodes.map(node => (
<div
key={node.id}
style={{
display: "flex",
alignItems: "center",
gap: 10,
padding: "8px 12px",
background: "rgba(0,0,0,0.3)",
border: `1px solid ${stateColors[node.state]}30`,
borderRadius: 10,
transition: "all 0.3s ease",
}}
>
<div
className="agent-avatar"
style={{ background: node.gradient, width: 32, height: 32, fontSize: 10 }}
>
{node.name}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div className="font-mono-code" style={{ fontSize: 11, color: "#9ca3af" }}>
{node.name === "CLA" ? "Claude Code" : node.name === "KIM" ? "Kimi CLI" : node.name === "OPC" ? "OpenCode" : "Tech Lead"}
</div>
{node.waitingFor && (
<div className="font-mono-code" style={{ fontSize: 9, color: "#ff9500", marginTop: 1 }}>
: {node.waitingFor}
</div>
)}
</div>
<div
className="barrier-node"
style={{
width: 32,
height: 32,
borderColor: stateColors[node.state],
color: stateColors[node.state],
background: `${stateColors[node.state]}10`,
fontSize: 8,
}}
title={stateLabels[node.state]}
>
{node.state === "ready" ? (
<svg width="12" height="12" viewBox="0 0 12 12">
<path d="M2 6L5 9L10 3" stroke="#00ff9d" strokeWidth="1.5" strokeLinecap="round" fill="none" />
</svg>
) : node.state === "waiting" ? (
<Zap size={12} />
) : (
<span style={{ fontSize: 8 }}></span>
)}
</div>
</div>
))}
</div>
{/* Right: Sync points */}
<div style={{ flex: 1, minWidth: 0 }}>
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280", marginBottom: 10 }}>
</div>
{/* Progress line */}
<div style={{ position: "relative", marginBottom: 16 }}>
<div
style={{
height: 3,
background: "#111827",
borderRadius: 2,
position: "relative",
overflow: "visible",
}}
>
{/* Completed portion */}
<div
style={{
position: "absolute",
left: 0,
top: 0,
height: "100%",
width: "42%",
background: "linear-gradient(90deg,#00f0ff,#00ff9d)",
borderRadius: 2,
}}
/>
{/* Active flowing portion */}
<div
style={{
position: "absolute",
left: "42%",
top: 0,
height: "100%",
width: "16%",
background: "linear-gradient(90deg,#ff9500,rgba(255,149,0,0.3))",
borderRadius: 2,
overflow: "hidden",
}}
>
<div
style={{
position: "absolute",
inset: 0,
background: "linear-gradient(90deg,transparent,rgba(255,255,255,0.3),transparent)",
animation: "line-flow 1.5s linear infinite",
}}
/>
</div>
{/* Dots */}
{syncPoints.map((sp, i) => {
const left = `${(i / (syncPoints.length - 1)) * 100}%`;
return (
<div
key={sp.name}
style={{
position: "absolute",
top: "50%",
left,
transform: "translate(-50%,-50%)",
width: 12,
height: 12,
borderRadius: "50%",
border: `2px solid ${sp.completed ? "#00ff9d" : sp.active ? "#ff9500" : "#374151"}`,
background: sp.completed ? "#00ff9d" : sp.active ? "#ff9500" : "#111827",
zIndex: 2,
boxShadow: sp.active ? "0 0 8px rgba(255,149,0,0.6)" : sp.completed ? "0 0 6px rgba(0,255,157,0.4)" : "none",
}}
className={sp.active ? "animate-scale-pulse" : ""}
/>
);
})}
</div>
{/* Labels */}
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 12 }}>
{syncPoints.map(sp => (
<div
key={sp.name}
className="font-mono-code"
style={{
fontSize: 9,
color: sp.completed ? "#00ff9d" : sp.active ? "#ff9500" : "#374151",
textAlign: "center",
flex: 1,
}}
>
{sp.name}
</div>
))}
</div>
</div>
{/* Status panel */}
<div
style={{
padding: "10px 12px",
background: "rgba(255,149,0,0.05)",
border: "1px solid rgba(255,149,0,0.15)",
borderRadius: 10,
}}
>
<div className="font-mono-code" style={{ fontSize: 10, color: "#ff9500", marginBottom: 4 }}>
DESIGN
</div>
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>
Agent wait_for_meeting("design_review") · {readyCount}/{nodes.length}
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,196 @@
import { TrendingUp } from "lucide-react";
const agentVotes = [
{ name: "Claude Code", agree: true, weight: 1.5, comment: "Session+Redis 方案合理" },
{ name: "Kimi CLI", agree: true, weight: 1.0, comment: "支持简化方案" },
{ name: "OpenCode", agree: true, weight: 1.2, comment: "符合项目需求" },
{ name: "Tech Lead", agree: true, weight: 2.0, comment: "简单优先原则" },
];
interface ConsensusRingProps {
value: number;
size?: number;
}
function ConsensusRing({ value, size = 100 }: ConsensusRingProps) {
const r = (size - 8) / 2;
const circ = 2 * Math.PI * r;
const offset = circ - (value / 100) * circ;
return (
<svg width={size} height={size} style={{ transform: "rotate(-90deg)" }}>
{/* Track */}
<circle
cx={size / 2}
cy={size / 2}
r={r}
fill="none"
stroke="#111827"
strokeWidth={4}
/>
{/* Progress */}
<circle
cx={size / 2}
cy={size / 2}
r={r}
fill="none"
stroke="#00f0ff"
strokeWidth={4}
strokeDasharray={circ}
strokeDashoffset={offset}
strokeLinecap="round"
style={{ transition: "stroke-dashoffset 1s ease", filter: "drop-shadow(0 0 6px rgba(0,240,255,0.5))" }}
/>
</svg>
);
}
export function ConsensusCard() {
const pct = 78;
const totalWeight = agentVotes.reduce((s, a) => s + a.weight, 0);
const agreeWeight = agentVotes.filter(a => a.agree).reduce((s, a) => s + a.weight, 0);
return (
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
{/* Header */}
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 14, flexShrink: 0 }}>
<div
style={{
width: 28,
height: 28,
borderRadius: 8,
background: "rgba(0,240,255,0.1)",
border: "1px solid rgba(0,240,255,0.2)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<TrendingUp size={14} color="#00f0ff" />
</div>
<span className="card-title"></span>
<span
className="badge"
style={{
color: "#00ff9d",
borderColor: "rgba(0,255,157,0.3)",
background: "rgba(0,255,157,0.1)",
}}
>
</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 20, flex: 1 }}>
{/* Ring */}
<div style={{ position: "relative", flexShrink: 0 }}>
<ConsensusRing value={pct} size={96} />
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
<span
className="font-orbitron"
style={{ fontSize: 20, fontWeight: 700, color: "#00f0ff", lineHeight: 1 }}
>
{pct}%
</span>
<span
className="font-mono-code"
style={{ fontSize: 8, color: "#6b7280", marginTop: 2 }}
>
</span>
</div>
</div>
{/* Agent votes */}
<div style={{ flex: 1, display: "flex", flexDirection: "column", gap: 6 }}>
{agentVotes.map(v => (
<div
key={v.name}
style={{
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<div
style={{
width: 16,
height: 16,
borderRadius: "50%",
background: v.agree ? "rgba(0,255,157,0.2)" : "rgba(255,0,110,0.2)",
border: `1px solid ${v.agree ? "#00ff9d" : "#ff006e"}`,
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
<span style={{ fontSize: 9, color: v.agree ? "#00ff9d" : "#ff006e" }}>
{v.agree ? "✓" : "✗"}
</span>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<span
className="font-mono-code"
style={{ fontSize: 10, color: "#9ca3af" }}
>
{v.name}
</span>
<span
className="font-mono-code"
style={{
fontSize: 9,
color: "#6b7280",
background: "rgba(0,0,0,0.3)",
padding: "1px 4px",
borderRadius: 3,
}}
>
w:{v.weight}
</span>
</div>
</div>
{/* Weight bar */}
<div style={{ width: 40, height: 3, background: "#111827", borderRadius: 2, overflow: "hidden" }}>
<div
style={{
height: "100%",
width: `${(v.weight / 2) * 100}%`,
background: v.agree ? "#00ff9d" : "#ff006e",
borderRadius: 2,
}}
/>
</div>
</div>
))}
</div>
</div>
{/* Summary */}
<div
style={{
marginTop: 12,
padding: "8px 12px",
background: "rgba(0,255,157,0.05)",
border: "1px solid rgba(0,255,157,0.15)",
borderRadius: 10,
flexShrink: 0,
}}
>
<span className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>
{agreeWeight.toFixed(1)} / {totalWeight.toFixed(1)} ·
</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,285 @@
import { useState } from "react";
import { MessageSquare, CheckCircle, Send } from "lucide-react";
type MessageType = "proposal" | "comment" | "consensus" | "human";
interface Message {
id: string;
agent: string;
agentFull: string;
gradient: string;
type: MessageType;
content: string;
time: string;
round?: number;
}
const initMessages: Message[] = [
{
id: "m1",
agent: "CLA",
agentFull: "Claude Code",
gradient: "linear-gradient(135deg,#8b5cf6,#6366f1)",
type: "proposal",
content: "建议使用 JWT + Refresh Token 方案。优点无状态、易于水平扩展支持跨域。实现方案Access Token 15分钟过期Refresh Token 7天过期。",
time: "14:20:03",
round: 1,
},
{
id: "m2",
agent: "KIM",
agentFull: "Kimi CLI",
gradient: "linear-gradient(135deg,#f59e0b,#d97706)",
type: "comment",
content: "需要考虑 Token 撤销问题。JWT 无状态特性导致主动撤销困难,建议结合 Redis 黑名单机制,在注销时将 Token 加入黑名单。",
time: "14:21:17",
round: 1,
},
{
id: "m3",
agent: "USR",
agentFull: "Tech Lead [HUMAN]",
gradient: "linear-gradient(135deg,#f59e0b,#b45309)",
type: "human",
content: "⚠️ 我们的数据量不大SQLite + Session 就够了。不要过度设计,保持简单。",
time: "14:22:05",
},
{
id: "m4",
agent: "OPC",
agentFull: "OpenCode",
gradient: "linear-gradient(135deg,#10b981,#059669)",
type: "comment",
content: "收到 Tech Lead 反馈。重新评估方案Session + Redis 的确更简单,且符合当前规模。支持调整为 Session 方案。",
time: "14:23:31",
round: 2,
},
{
id: "m5",
agent: "CLA",
agentFull: "Claude Code",
gradient: "linear-gradient(135deg,#8b5cf6,#6366f1)",
type: "consensus",
content: "✅ 共识达成:采用 Session + Redis 方案。简单可靠,符合当前项目规模。后续如有扩展需求可迁移至 JWT。",
time: "14:25:44",
round: 2,
},
];
const typeConfig = {
proposal: { border: "#00f0ff", label: "提案", bg: "rgba(0,240,255,0.05)" },
comment: { border: "#ff9500", label: "评论", bg: "rgba(255,149,0,0.05)" },
consensus: { border: "#00ff9d", label: "共识", bg: "linear-gradient(135deg,rgba(0,255,157,0.1),rgba(0,240,255,0.1))" },
human: { border: "#f59e0b", label: "人类", bg: "rgba(245,158,11,0.05)" },
};
export function DiscussionCard() {
const [messages, setMessages] = useState(initMessages);
const [filter, setFilter] = useState<MessageType | "all">("all");
const [newMsg, setNewMsg] = useState("");
const filtered = filter === "all" ? messages : messages.filter(m => m.type === filter);
const sendMsg = () => {
if (!newMsg.trim()) return;
const m: Message = {
id: `m${Date.now()}`,
agent: "USR",
agentFull: "Tech Lead [HUMAN]",
gradient: "linear-gradient(135deg,#f59e0b,#b45309)",
type: "human",
content: newMsg,
time: new Date().toLocaleTimeString("zh-CN", { hour12: false }),
};
setMessages(prev => [...prev, m]);
setNewMsg("");
};
return (
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
{/* Header */}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 14, flexShrink: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div
style={{
width: 28,
height: 28,
borderRadius: 8,
background: "rgba(0,240,255,0.1)",
border: "1px solid rgba(0,240,255,0.2)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<MessageSquare size={14} color="#00f0ff" />
</div>
<span className="card-title"></span>
<span
className="badge"
style={{ color: "#00f0ff", borderColor: "rgba(0,240,255,0.3)", background: "rgba(0,240,255,0.1)" }}
>
2
</span>
</div>
<div style={{ display: "flex", gap: 4 }}>
{(["all", "proposal", "comment", "consensus", "human"] as const).map(f => (
<button
key={f}
onClick={() => setFilter(f)}
className="font-mono-code"
style={{
padding: "3px 8px",
fontSize: 10,
borderRadius: 6,
border: `1px solid ${filter === f ? "rgba(0,240,255,0.4)" : "rgba(0,240,255,0.1)"}`,
background: filter === f ? "rgba(0,240,255,0.1)" : "transparent",
color: filter === f ? "#00f0ff" : "#6b7280",
cursor: "pointer",
textTransform: "uppercase",
transition: "all 0.2s",
}}
>
{f === "all" ? "全部" : f === "proposal" ? "提案" : f === "comment" ? "评论" : f === "consensus" ? "共识" : "人类"}
</button>
))}
</div>
</div>
{/* Consensus indicator */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
padding: "8px 12px",
background: "rgba(0,255,157,0.05)",
border: "1px solid rgba(0,255,157,0.2)",
borderRadius: 10,
marginBottom: 12,
flexShrink: 0,
}}
>
<CheckCircle size={13} color="#00ff9d" />
<span className="font-mono-code" style={{ fontSize: 11, color: "#00ff9d" }}>
· 78% · 2/5
</span>
</div>
{/* Messages */}
<div style={{ flex: 1, overflowY: "auto", display: "flex", flexDirection: "column", gap: 10, paddingRight: 4 }}>
{filtered.map((msg, i) => {
const cfg = typeConfig[msg.type];
return (
<div
key={msg.id}
className="animate-message-slide"
style={{
animationDelay: `${i * 0.05}s`,
display: "flex",
gap: 10,
alignItems: "flex-start",
}}
>
{/* Avatar */}
<div
className="agent-avatar"
style={{ background: msg.gradient, flexShrink: 0, marginTop: 2 }}
>
{msg.agent}
</div>
{/* Content */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
<span
className="font-orbitron"
style={{ fontSize: 11, color: "#e5e7eb", fontWeight: 600 }}
>
{msg.agentFull}
</span>
<span
className="badge"
style={{
color: cfg.border,
borderColor: `${cfg.border}40`,
background: `${cfg.border}15`,
fontSize: 9,
}}
>
{cfg.label}
{msg.round ? ` · R${msg.round}` : ""}
</span>
<span
className="font-mono-code"
style={{ fontSize: 10, color: "#4b5563", marginLeft: "auto" }}
>
{msg.time}
</span>
</div>
<div
className="font-rajdhani"
style={{
background: msg.type === "consensus"
? "linear-gradient(135deg,rgba(0,255,157,0.1),rgba(0,240,255,0.08))"
: "rgba(0,0,0,0.3)",
borderLeft: `3px solid ${cfg.border}`,
borderRadius: "0 10px 10px 0",
padding: "10px 14px",
fontSize: 13,
color: "#d1d5db",
lineHeight: 1.5,
border: msg.type === "consensus" ? `1px solid rgba(0,255,157,0.2)` : undefined,
borderLeftColor: cfg.border,
borderLeftWidth: 3,
}}
>
{msg.content}
</div>
</div>
</div>
);
})}
</div>
{/* Input */}
<div
style={{
marginTop: 12,
display: "flex",
gap: 8,
padding: "10px 12px",
background: "rgba(0,0,0,0.3)",
border: "1px solid rgba(0,240,255,0.1)",
borderRadius: 10,
flexShrink: 0,
}}
>
<div
className="agent-avatar"
style={{ background: "linear-gradient(135deg,#f59e0b,#d97706)", flexShrink: 0 }}
>
USR
</div>
<input
value={newMsg}
onChange={e => setNewMsg(e.target.value)}
onKeyDown={e => e.key === "Enter" && sendMsg()}
placeholder="参与讨论... (Enter 发送)"
style={{
flex: 1,
background: "transparent",
border: "none",
outline: "none",
color: "#e5e7eb",
fontSize: 13,
fontFamily: "'Noto Sans SC',sans-serif",
}}
/>
<button onClick={sendMsg} style={{ background: "none", border: "none", cursor: "pointer", color: "#00f0ff", padding: 4 }}>
<Send size={14} />
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,198 @@
import { useState, useEffect } from "react";
import { Activity, Wifi, Clock, Zap } from "lucide-react";
const agents = [
{ id: "claude-001", name: "Claude", status: "working" as const },
{ id: "kimi-002", name: "Kimi", status: "waiting" as const },
{ id: "opencode-003", name: "OpenCode", status: "working" as const },
{ id: "human-001", name: "Human", status: "idle" as const },
];
const statusColors = {
working: "#00ff9d",
waiting: "#ff9500",
idle: "#6b7280",
};
export function Header() {
const [time, setTime] = useState(new Date());
const [uptime, setUptime] = useState(7432);
useEffect(() => {
const t = setInterval(() => {
setTime(new Date());
setUptime(prev => prev + 1);
}, 1000);
return () => clearInterval(t);
}, []);
const formatTime = (d: Date) =>
d.toLocaleTimeString("zh-CN", { hour12: false });
const formatUptime = (s: number) => {
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
};
return (
<header
style={{
background: "rgba(10,15,30,0.85)",
borderBottom: "1px solid rgba(0,240,255,0.1)",
backdropFilter: "blur(20px)",
WebkitBackdropFilter: "blur(20px)",
position: "sticky",
top: 0,
zIndex: 100,
}}
>
<div
style={{
maxWidth: 1800,
margin: "0 auto",
padding: "0 30px",
height: 72,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 20,
}}
>
{/* Logo */}
<div style={{ display: "flex", alignItems: "center", gap: 16, flexShrink: 0 }}>
<div style={{ position: "relative" }}>
{/* Rotating halo */}
<div
className="animate-rotate-logo"
style={{
position: "absolute",
inset: -3,
borderRadius: 14,
background: "conic-gradient(from 0deg, #00f0ff, #8b5cf6, transparent, #00f0ff)",
opacity: 0.6,
}}
/>
{/* Logo box */}
<div
style={{
position: "relative",
width: 48,
height: 48,
borderRadius: 12,
background: "linear-gradient(135deg, #00f0ff, #8b5cf6)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1,
}}
>
<span
className="font-orbitron"
style={{ fontSize: 22, fontWeight: 900, color: "#030712" }}
>
S
</span>
</div>
</div>
<div>
<div
className="font-orbitron"
style={{
fontSize: 22,
fontWeight: 700,
letterSpacing: 4,
background: "linear-gradient(90deg, #00f0ff, #8b5cf6)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
textTransform: "uppercase",
}}
>
SWARM
</div>
<div
className="font-mono-code"
style={{ fontSize: 10, color: "#6b7280", letterSpacing: 2, marginTop: -2 }}
>
MULTI-AGENT COMMAND CENTER
</div>
</div>
</div>
{/* Agent Status Pills */}
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
{agents.map((agent) => (
<div
key={agent.id}
style={{
display: "flex",
alignItems: "center",
gap: 6,
background: "rgba(0,0,0,0.3)",
border: `1px solid ${statusColors[agent.status]}33`,
borderRadius: 20,
padding: "5px 12px",
transition: "all 0.3s ease",
}}
>
<div
style={{
width: 7,
height: 7,
borderRadius: "50%",
background: statusColors[agent.status],
color: statusColors[agent.status],
flexShrink: 0,
}}
className={agent.status !== "idle" ? "animate-pulse-glow" : ""}
/>
<span
className="font-mono-code"
style={{ fontSize: 11, color: statusColors[agent.status], fontWeight: 500 }}
>
{agent.name}
</span>
</div>
))}
</div>
{/* System Stats */}
<div style={{ display: "flex", alignItems: "center", gap: 20, flexShrink: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Wifi size={14} color="#6b7280" />
<div>
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>LATENCY</div>
<div className="font-mono-code" style={{ fontSize: 12, color: "#00ff9d" }}>42ms</div>
</div>
</div>
<div style={{ width: 1, height: 32, background: "rgba(0,240,255,0.1)" }} />
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Activity size={14} color="#6b7280" />
<div>
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>UPTIME</div>
<div className="font-mono-code" style={{ fontSize: 12, color: "#00f0ff" }}>{formatUptime(uptime)}</div>
</div>
</div>
<div style={{ width: 1, height: 32, background: "rgba(0,240,255,0.1)" }} />
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Zap size={14} color="#6b7280" />
<div>
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>ACTIVE</div>
<div className="font-mono-code" style={{ fontSize: 12, color: "#ff9500" }}>3 / 4</div>
</div>
</div>
<div style={{ width: 1, height: 32, background: "rgba(0,240,255,0.1)" }} />
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Clock size={14} color="#6b7280" />
<div>
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>LOCAL TIME</div>
<div className="font-mono-code" style={{ fontSize: 12, color: "#e5e7eb" }}>{formatTime(time)}</div>
</div>
</div>
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,228 @@
import { Users, Clock } from "lucide-react";
type NodeStatus = "completed" | "active" | "pending";
interface MeetingStep {
id: string;
label: string;
status: NodeStatus;
time?: string;
}
const steps: MeetingStep[] = [
{ id: "s1", label: "收集初步想法", status: "completed", time: "14:10" },
{ id: "s2", label: "讨论与迭代", status: "active", time: "14:18" },
{ id: "s3", label: "生成共识版本", status: "pending" },
{ id: "s4", label: "记录会议文件", status: "pending" },
];
const statusColors: Record<NodeStatus, string> = {
completed: "#00ff9d",
active: "#ff9500",
pending: "#374151",
};
const attendees = [
{ name: "CLA", gradient: "linear-gradient(135deg,#8b5cf6,#6366f1)" },
{ name: "KIM", gradient: "linear-gradient(135deg,#f59e0b,#d97706)" },
{ name: "OPC", gradient: "linear-gradient(135deg,#10b981,#059669)" },
{ name: "USR", gradient: "linear-gradient(135deg,#f59e0b,#b45309)" },
];
export function MeetingProgressCard() {
return (
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
{/* Header */}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 14, flexShrink: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div
style={{
width: 28,
height: 28,
borderRadius: 8,
background: "rgba(255,149,0,0.1)",
border: "1px solid rgba(255,149,0,0.3)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Users size={14} color="#ff9500" />
</div>
<span className="card-title"></span>
<span
className="badge"
style={{
color: "#ff9500",
borderColor: "rgba(255,149,0,0.4)",
background: "rgba(255,149,0,0.1)",
}}
>
<span className="animate-pulse-fast" style={{ color: "#ff9500" }}> </span>
</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<Clock size={12} color="#6b7280" />
<span className="font-mono-code" style={{ fontSize: 11, color: "#ff9500" }}>
08:23
</span>
</div>
</div>
{/* Meeting name */}
<div
style={{
padding: "8px 12px",
background: "rgba(255,149,0,0.05)",
border: "1px solid rgba(255,149,0,0.2)",
borderRadius: 10,
marginBottom: 14,
flexShrink: 0,
}}
>
<div className="font-orbitron" style={{ fontSize: 11, color: "#e5e7eb" }}>
</div>
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280", marginTop: 2 }}>
design_review_20260304_141000 ·
</div>
</div>
{/* Timeline (horizontal) */}
<div style={{ marginBottom: 14, flexShrink: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 0 }}>
{steps.map((step, i) => (
<div
key={step.id}
style={{
flex: 1,
display: "flex",
flexDirection: "column",
alignItems: "center",
position: "relative",
}}
>
{/* Line before dot (except first) */}
{i > 0 && (
<div
style={{
position: "absolute",
left: 0,
right: "50%",
top: 7,
height: 3,
background:
steps[i - 1].status === "completed"
? "linear-gradient(90deg,#00f0ff,#00ff9d)"
: steps[i - 1].status === "active"
? "linear-gradient(90deg,#00f0ff,#ff9500)"
: "#111827",
overflow: "hidden",
}}
>
{steps[i - 1].status === "active" && (
<div
style={{
position: "absolute",
inset: 0,
background: "linear-gradient(90deg,transparent,rgba(0,240,255,0.6),transparent)",
animation: "line-flow 2s linear infinite",
}}
/>
)}
</div>
)}
{/* Line after dot (except last) */}
{i < steps.length - 1 && (
<div
style={{
position: "absolute",
left: "50%",
right: 0,
top: 7,
height: 3,
background:
step.status === "completed"
? "linear-gradient(90deg,#00ff9d,#00f0ff)"
: step.status === "active"
? "linear-gradient(90deg,#ff9500,rgba(0,240,255,0.2))"
: "#111827",
}}
/>
)}
{/* Dot */}
<div
style={{
width: 16,
height: 16,
borderRadius: "50%",
border: `2px solid ${statusColors[step.status]}`,
background:
step.status === "completed"
? "#00ff9d"
: step.status === "active"
? "#ff9500"
: "#111827",
zIndex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: step.status !== "pending" ? `0 0 8px ${statusColors[step.status]}80` : "none",
flexShrink: 0,
transition: "all 0.3s",
}}
className={step.status === "active" ? "animate-scale-pulse" : ""}
>
{step.status === "completed" && (
<svg width="8" height="8" viewBox="0 0 8 8">
<path d="M1.5 4L3.5 6L6.5 2" stroke="#030712" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
</svg>
)}
</div>
{/* Label */}
<div
className="font-mono-code"
style={{
fontSize: 9,
color: statusColors[step.status],
marginTop: 6,
textAlign: "center",
lineHeight: 1.3,
}}
>
{step.label}
{step.time && (
<div style={{ color: "#4b5563", marginTop: 1 }}>{step.time}</div>
)}
</div>
</div>
))}
</div>
</div>
{/* Attendees */}
<div style={{ display: "flex", alignItems: "center", gap: 10, flexShrink: 0 }}>
<span className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>
</span>
<div style={{ display: "flex", gap: 6 }}>
{attendees.map(a => (
<div
key={a.name}
className="agent-avatar"
style={{ background: a.gradient, width: 24, height: 24, fontSize: 9 }}
>
{a.name}
</div>
))}
</div>
<span className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>
2/5
</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,235 @@
import { Calendar, CheckCircle, Clock } from "lucide-react";
interface Meeting {
id: string;
name: string;
date: string;
attendees: string[];
iterations: number;
consensus: string;
status: "completed" | "ongoing" | "scheduled";
tags: string[];
}
const meetings: Meeting[] = [
{
id: "m1",
name: "项目启动会议",
date: "2026-03-04 09:00",
attendees: ["CLA", "KIM", "OPC", "USR"],
iterations: 2,
consensus: "确定技术栈React+Python+FastAPI",
status: "completed",
tags: ["架构", "启动"],
},
{
id: "m2",
name: "需求评审会议",
date: "2026-03-04 11:30",
attendees: ["CLA", "KIM", "USR"],
iterations: 3,
consensus: "MVP 功能范围确定,优先级排序完成",
status: "completed",
tags: ["需求", "PM"],
},
{
id: "m3",
name: "认证方案设计评审",
date: "2026-03-04 14:10",
attendees: ["CLA", "KIM", "OPC", "USR"],
iterations: 2,
consensus: "Session+Redis 方案,简单优先",
status: "ongoing",
tags: ["设计", "安全"],
},
{
id: "m4",
name: "代码审查会议",
date: "2026-03-04 17:00",
attendees: ["CLA", "OPC"],
iterations: 0,
consensus: "—",
status: "scheduled",
tags: ["代码", "审查"],
},
];
const statusConfig = {
completed: { color: "#00ff9d", bg: "rgba(0,255,157,0.1)", label: "已完成" },
ongoing: { color: "#ff9500", bg: "rgba(255,149,0,0.1)", label: "进行中" },
scheduled: { color: "#6b7280", bg: "rgba(107,114,128,0.1)", label: "计划中" },
};
const agentGradients: Record<string, string> = {
CLA: "linear-gradient(135deg,#8b5cf6,#6366f1)",
KIM: "linear-gradient(135deg,#f59e0b,#d97706)",
OPC: "linear-gradient(135deg,#10b981,#059669)",
USR: "linear-gradient(135deg,#f59e0b,#b45309)",
};
export function RecentMeetingsCard() {
return (
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
{/* Header */}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 14, flexShrink: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div
style={{
width: 28,
height: 28,
borderRadius: 8,
background: "rgba(0,240,255,0.1)",
border: "1px solid rgba(0,240,255,0.2)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Calendar size={14} color="#00f0ff" />
</div>
<span className="card-title"></span>
</div>
<span className="font-mono-code" style={{ fontSize: 11, color: "#6b7280" }}>
4 · 2
</span>
</div>
{/* Meeting list */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, flex: 1 }}>
{meetings.map(m => {
const cfg = statusConfig[m.status];
return (
<div
key={m.id}
style={{
background: "rgba(0,0,0,0.3)",
border: `1px solid ${cfg.color}20`,
borderRadius: 12,
padding: 12,
display: "flex",
flexDirection: "column",
gap: 8,
transition: "all 0.3s ease",
cursor: "pointer",
}}
onMouseEnter={e => {
(e.currentTarget as HTMLDivElement).style.borderColor = `${cfg.color}50`;
(e.currentTarget as HTMLDivElement).style.boxShadow = `0 0 15px ${cfg.color}15`;
}}
onMouseLeave={e => {
(e.currentTarget as HTMLDivElement).style.borderColor = `${cfg.color}20`;
(e.currentTarget as HTMLDivElement).style.boxShadow = "none";
}}
>
{/* Top row */}
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 8 }}>
<div style={{ flex: 1, minWidth: 0 }}>
<div
className="font-orbitron"
style={{ fontSize: 11, color: "#e5e7eb", fontWeight: 600, lineHeight: 1.3 }}
>
{m.name}
</div>
<div
style={{ display: "flex", alignItems: "center", gap: 4, marginTop: 3 }}
>
<Clock size={9} color="#4b5563" />
<span className="font-mono-code" style={{ fontSize: 9, color: "#4b5563" }}>
{m.date}
</span>
</div>
</div>
<span
className="badge"
style={{
color: cfg.color,
borderColor: `${cfg.color}40`,
background: cfg.bg,
flexShrink: 0,
fontSize: 9,
}}
>
{cfg.label}
</span>
</div>
{/* Attendees */}
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<div style={{ display: "flex", gap: -4 }}>
{m.attendees.map(a => (
<div
key={a}
className="agent-avatar"
style={{
background: agentGradients[a],
width: 20,
height: 20,
fontSize: 7,
marginLeft: -4,
border: "1px solid #0a0f1e",
}}
>
{a}
</div>
))}
</div>
{m.iterations > 0 && (
<span className="font-mono-code" style={{ fontSize: 9, color: "#6b7280" }}>
{m.iterations}
</span>
)}
</div>
{/* Consensus */}
{m.status !== "scheduled" && (
<div
style={{
padding: "5px 8px",
background: m.status === "completed" ? "rgba(0,255,157,0.05)" : "rgba(255,149,0,0.05)",
border: `1px solid ${m.status === "completed" ? "rgba(0,255,157,0.15)" : "rgba(255,149,0,0.15)"}`,
borderRadius: 8,
display: "flex",
alignItems: "flex-start",
gap: 5,
}}
>
<CheckCircle
size={10}
color={m.status === "completed" ? "#00ff9d" : "#ff9500"}
style={{ flexShrink: 0, marginTop: 1 }}
/>
<span
className="font-rajdhani"
style={{ fontSize: 11, color: "#9ca3af", lineHeight: 1.3 }}
>
{m.consensus}
</span>
</div>
)}
{/* Tags */}
<div style={{ display: "flex", gap: 5, flexWrap: "wrap" }}>
{m.tags.map(tag => (
<span
key={tag}
className="font-mono-code"
style={{
fontSize: 9,
color: "#6b7280",
background: "rgba(0,240,255,0.05)",
border: "1px solid rgba(0,240,255,0.1)",
borderRadius: 4,
padding: "1px 6px",
}}
>
#{tag}
</span>
))}
</div>
</div>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,183 @@
import { Server, Lock, HardDrive } from "lucide-react";
interface Resource {
label: string;
value: number;
max: number;
unit: string;
type: "cyan" | "green" | "amber";
icon: React.ReactNode;
detail?: string;
}
const resources: Resource[] = [
{
label: "CPU 占用",
value: 67,
max: 100,
unit: "%",
type: "green",
icon: <Server size={13} />,
detail: "4核 / 8线程 · 负载 2.7",
},
{
label: "内存使用",
value: 58,
max: 100,
unit: "%",
type: "amber",
icon: <HardDrive size={13} />,
detail: "9.3GB / 16GB",
},
{
label: "文件锁",
value: 5,
max: 12,
unit: "/12",
type: "cyan",
icon: <Lock size={13} />,
detail: "src/auth/ · src/api/ · src/utils/",
},
];
const barColors = {
cyan: "linear-gradient(90deg,#00f0ff,#8b5cf6)",
green: "linear-gradient(90deg,#00ff9d,#00f0ff)",
amber: "linear-gradient(90deg,#ff9500,#ff006e)",
};
const textColors = {
cyan: "#00f0ff",
green: "#00ff9d",
amber: "#ff9500",
};
const lockedFiles = [
{ path: "src/auth/login.py", agent: "CLA", time: "3m 21s" },
{ path: "src/api/routes.py", agent: "OPC", time: "1m 05s" },
{ path: "src/utils/crypto.py", agent: "CLA", time: "3m 21s" },
];
export function ResourceMonitorCard() {
return (
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
{/* Header */}
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 14, flexShrink: 0 }}>
<div
style={{
width: 28,
height: 28,
borderRadius: 8,
background: "rgba(0,240,255,0.1)",
border: "1px solid rgba(0,240,255,0.2)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Server size={14} color="#00f0ff" />
</div>
<span className="card-title"></span>
</div>
{/* Resource bars */}
<div style={{ display: "flex", flexDirection: "column", gap: 12, marginBottom: 14, flexShrink: 0 }}>
{resources.map(r => (
<div key={r.label}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 5 }}>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<span style={{ color: textColors[r.type] }}>{r.icon}</span>
<span className="font-mono-code" style={{ fontSize: 11, color: "#9ca3af" }}>
{r.label}
</span>
</div>
<span
className="font-orbitron"
style={{ fontSize: 12, fontWeight: 600, color: textColors[r.type] }}
>
{r.value}{r.unit}
</span>
</div>
<div
style={{
height: 6,
background: "#111827",
borderRadius: 3,
overflow: "hidden",
}}
>
<div
style={{
height: "100%",
width: `${(r.value / r.max) * 100}%`,
background: barColors[r.type],
borderRadius: 3,
transition: "width 0.5s ease",
}}
/>
</div>
{r.detail && (
<div className="font-mono-code" style={{ fontSize: 9, color: "#4b5563", marginTop: 3 }}>
{r.detail}
</div>
)}
</div>
))}
</div>
{/* File locks */}
<div style={{ flex: 1 }}>
<div
className="font-mono-code"
style={{ fontSize: 10, color: "#6b7280", marginBottom: 8 }}
>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
{lockedFiles.map((f, i) => (
<div
key={i}
style={{
display: "flex",
alignItems: "center",
gap: 8,
padding: "6px 10px",
background: "rgba(0,0,0,0.3)",
border: "1px solid rgba(0,240,255,0.08)",
borderRadius: 8,
}}
>
<Lock size={10} color="rgba(0,240,255,0.4)" style={{ flexShrink: 0 }} />
<span
className="font-mono-code"
style={{ fontSize: 10, color: "#9ca3af", flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
>
{f.path}
</span>
<span
className="font-mono-code"
style={{
fontSize: 9,
color: "#00f0ff",
background: "rgba(0,240,255,0.1)",
border: "1px solid rgba(0,240,255,0.2)",
borderRadius: 4,
padding: "1px 6px",
flexShrink: 0,
}}
>
{f.agent}
</span>
<span
className="font-mono-code"
style={{ fontSize: 9, color: "#4b5563", flexShrink: 0 }}
>
{f.time}
</span>
</div>
))}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,151 @@
import { BarChart2, TrendingUp, CheckCircle, Clock } from "lucide-react";
const stats = [
{ label: "已完成任务", value: "47", unit: "", color: "#00ff9d", icon: <CheckCircle size={14} /> },
{ label: "进行中", value: "3", unit: "", color: "#00f0ff", icon: <Clock size={14} /> },
{ label: "共识次数", value: "12", unit: "", color: "#8b5cf6", icon: <TrendingUp size={14} /> },
{ label: "Token 消耗", value: "98.2", unit: "k", color: "#ff9500", icon: <BarChart2 size={14} /> },
];
const weekData = [40, 65, 48, 80, 62, 75, 90];
const weekLabels = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
export function StatisticsCard() {
const maxVal = Math.max(...weekData);
return (
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
{/* Header */}
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16, flexShrink: 0 }}>
<div
style={{
width: 28,
height: 28,
borderRadius: 8,
background: "rgba(0,240,255,0.1)",
border: "1px solid rgba(0,240,255,0.2)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<BarChart2 size={14} color="#00f0ff" />
</div>
<span className="card-title"></span>
</div>
{/* Main stat */}
<div style={{ textAlign: "center", marginBottom: 16, flexShrink: 0 }}>
<div
className="font-orbitron"
style={{
fontSize: 40,
fontWeight: 700,
color: "#00f0ff",
textShadow: "0 0 20px rgba(0,240,255,0.4)",
lineHeight: 1,
}}
>
47
</div>
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280", marginTop: 4 }}>
</div>
</div>
{/* Stats grid */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8, marginBottom: 16, flexShrink: 0 }}>
{stats.map(stat => (
<div
key={stat.label}
style={{
background: "rgba(0,0,0,0.3)",
border: `1px solid ${stat.color}20`,
borderRadius: 10,
padding: "10px",
textAlign: "center",
}}
>
<div style={{ color: stat.color, marginBottom: 4 }}>{stat.icon}</div>
<div
className="font-orbitron"
style={{ fontSize: 18, fontWeight: 700, color: stat.color }}
>
{stat.value}
<span style={{ fontSize: 12 }}>{stat.unit}</span>
</div>
<div className="font-mono-code" style={{ fontSize: 9, color: "#6b7280", marginTop: 2 }}>
{stat.label}
</div>
</div>
))}
</div>
{/* Weekly chart */}
<div style={{ flex: 1 }}>
<div className="font-mono-code" style={{ fontSize: 10, color: "#6b7280", marginBottom: 8 }}>
</div>
<div
style={{
display: "flex",
alignItems: "flex-end",
gap: 5,
height: 60,
}}
>
{weekData.map((v, i) => (
<div
key={i}
style={{
flex: 1,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 4,
}}
>
<div
style={{
width: "100%",
height: `${(v / maxVal) * 52}px`,
background: i === 6
? "linear-gradient(180deg,#00f0ff,#8b5cf6)"
: "rgba(0,240,255,0.2)",
borderRadius: 3,
border: i === 6 ? "1px solid rgba(0,240,255,0.3)" : "none",
transition: "height 0.5s ease",
}}
/>
<span className="font-mono-code" style={{ fontSize: 8, color: "#4b5563" }}>
{weekLabels[i].slice(1)}
</span>
</div>
))}
</div>
</div>
{/* Success rate */}
<div
style={{
marginTop: 12,
padding: "10px 12px",
background: "rgba(0,255,157,0.05)",
border: "1px solid rgba(0,255,157,0.15)",
borderRadius: 10,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexShrink: 0,
}}
>
<span className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>
</span>
<span className="font-orbitron" style={{ fontSize: 14, fontWeight: 700, color: "#00ff9d" }}>
94.7%
</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,65 @@
// 状态徽章组件 - 统一的状态显示
import { statusColors, statusBgColors } from '../styles/dashboard';
interface StatusBadgeProps {
status: string;
label?: string;
size?: 'sm' | 'md';
}
const statusLabels: Record<string, string> = {
completed: '已完成',
in_progress: '进行中',
pending: '等待中',
waiting: '等待中',
working: '工作中',
idle: '空闲',
error: '错误',
};
export function StatusBadge({ status, label, size = 'md' }: StatusBadgeProps) {
const color = statusColors[status] || '#666';
const bgColor = statusBgColors[status] || '#66666620';
const displayLabel = label || statusLabels[status] || status;
const padding = size === 'sm' ? '2px 8px' : '4px 10px';
const fontSize = size === 'sm' ? 10 : 11;
return (
<span
style={{
fontSize,
padding,
borderRadius: 12,
background: bgColor,
color,
textTransform: 'capitalize',
}}
>
{displayLabel}
</span>
);
}
// 状态点组件
interface StatusDotProps {
status: string;
size?: number;
}
export function StatusDot({ status, size = 8 }: StatusDotProps) {
const color = statusColors[status] || '#666';
return (
<div
style={{
width: size,
height: size,
borderRadius: '50%',
background: color,
boxShadow: `0 0 8px ${color}`,
}}
/>
);
}

View File

@@ -0,0 +1,183 @@
import { useState, useRef } from "react";
import { Send, Zap, Code, FileText, Users, Cpu } from "lucide-react";
const quickTags = [
{ icon: <Code size={11} />, label: "代码审查" },
{ icon: <FileText size={11} />, label: "需求分析" },
{ icon: <Zap size={11} />, label: "快速修复" },
{ icon: <Users size={11} />, label: "团队会议" },
{ icon: <Cpu size={11} />, label: "架构设计" },
];
export function TaskInput() {
const [value, setValue] = useState("");
const [shaking, setShaking] = useState(false);
const [priority, setPriority] = useState<"high" | "medium" | "low">("medium");
const [submitted, setSubmitted] = useState(false);
const inputRef = useRef<HTMLTextAreaElement>(null);
const handleSubmit = () => {
if (!value.trim()) {
setShaking(true);
setTimeout(() => setShaking(false), 500);
inputRef.current?.focus();
return;
}
setSubmitted(true);
setTimeout(() => {
setSubmitted(false);
setValue("");
}, 2000);
};
const handleTag = (label: string) => {
setValue(prev => (prev ? `${prev} [${label}]` : `[${label}] `));
inputRef.current?.focus();
};
const priorityColors = {
high: "#ff006e",
medium: "#ff9500",
low: "#6b7280",
};
return (
<div
className={shaking ? "animate-shake" : ""}
style={{
background: "rgba(10,15,30,0.7)",
border: "1px solid rgba(0,240,255,0.1)",
borderRadius: 16,
backdropFilter: "blur(20px)",
WebkitBackdropFilter: "blur(20px)",
overflow: "hidden",
position: "relative",
}}
>
{/* Top gradient line */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: 2,
background: "linear-gradient(90deg, transparent, #00f0ff, #8b5cf6, transparent)",
}}
/>
<div style={{ padding: "20px 24px" }}>
{/* Input row */}
<div style={{ display: "flex", alignItems: "flex-start", gap: 12 }}>
{/* Human avatar */}
<div
className="agent-avatar"
style={{
background: "linear-gradient(135deg, #f59e0b, #d97706)",
marginTop: 6,
flexShrink: 0,
}}
>
H
</div>
{/* Textarea */}
<textarea
ref={inputRef}
value={value}
onChange={e => setValue(e.target.value)}
onKeyDown={e => {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) handleSubmit();
}}
placeholder="描述你的任务... Agent 将自动分析并协同完成(⌘+Enter 提交)"
rows={2}
style={{
flex: 1,
background: "transparent",
border: "none",
outline: "none",
resize: "none",
color: "#e5e7eb",
fontSize: 15,
fontFamily: "'Noto Sans SC', sans-serif",
lineHeight: 1.6,
caretColor: "#00f0ff",
}}
/>
</div>
{/* Bottom bar */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 12,
marginTop: 14,
flexWrap: "wrap",
}}
>
{/* Quick tags */}
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
{quickTags.map(tag => (
<button
key={tag.label}
className="quick-tag"
onClick={() => handleTag(tag.label)}
style={{ display: "flex", alignItems: "center", gap: 5 }}
>
{tag.icon}
{tag.label}
</button>
))}
</div>
{/* Right controls */}
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
{/* Priority selector */}
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
{(["high", "medium", "low"] as const).map(p => (
<button
key={p}
onClick={() => setPriority(p)}
className="font-mono-code"
style={{
fontSize: 10,
padding: "4px 10px",
borderRadius: 6,
border: `1px solid ${priority === p ? priorityColors[p] : "rgba(0,240,255,0.1)"}`,
background: priority === p ? `${priorityColors[p]}20` : "transparent",
color: priority === p ? priorityColors[p] : "#6b7280",
cursor: "pointer",
textTransform: "uppercase",
transition: "all 0.2s ease",
}}
>
{p === "high" ? "高优先" : p === "medium" ? "中优先" : "低优先"}
</button>
))}
</div>
{/* Submit button */}
<button
className="btn-primary"
onClick={handleSubmit}
style={{
padding: "10px 20px",
display: "flex",
alignItems: "center",
gap: 8,
background: submitted
? "linear-gradient(135deg, #00ff9d, #00f0ff)"
: undefined,
}}
>
<Send size={13} />
{submitted ? "已提交!" : "提交任务"}
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,211 @@
import { GitBranch } from "lucide-react";
type NodeStatus = "completed" | "active" | "pending";
interface WorkflowNode {
id: string;
label: string;
status: NodeStatus;
duration?: string;
agent?: string;
}
const nodes: WorkflowNode[] = [
{ id: "n1", label: "需求收集", status: "completed", duration: "2m 14s", agent: "KIM" },
{ id: "n2", label: "角色分配", status: "completed", duration: "0m 32s", agent: "SYS" },
{ id: "n3", label: "需求评审会议", status: "completed", duration: "5m 03s", agent: "ALL" },
{ id: "n4", label: "架构设计", status: "active", agent: "CLA" },
{ id: "n5", label: "代码实现", status: "pending", agent: "OPC" },
{ id: "n6", label: "代码审查会议", status: "pending", agent: "ALL" },
{ id: "n7", label: "测试验证", status: "pending", agent: "OPC" },
{ id: "n8", label: "部署上线", status: "pending", agent: "CLA" },
];
const statusColors = {
completed: "#00ff9d",
active: "#ff9500",
pending: "#374151",
};
const statusBg = {
completed: "#00ff9d",
active: "#ff9500",
pending: "#111827",
};
export function WorkflowCard() {
const completedCount = nodes.filter(n => n.status === "completed").length;
return (
<div className="glass-card" style={{ padding: 20, height: "100%", display: "flex", flexDirection: "column" }}>
{/* Header */}
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 14, flexShrink: 0 }}>
<div
style={{
width: 28,
height: 28,
borderRadius: 8,
background: "rgba(0,240,255,0.1)",
border: "1px solid rgba(0,240,255,0.2)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<GitBranch size={14} color="#00f0ff" />
</div>
<span className="card-title"></span>
</div>
{/* Progress summary */}
<div style={{ marginBottom: 14, flexShrink: 0 }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}>
<span className="font-mono-code" style={{ fontSize: 10, color: "#6b7280" }}>
</span>
<span className="font-mono-code" style={{ fontSize: 10, color: "#00f0ff" }}>
{completedCount}/{nodes.length}
</span>
</div>
<div style={{ height: 5, background: "#111827", borderRadius: 3, overflow: "hidden" }}>
<div
style={{
height: "100%",
width: `${(completedCount / nodes.length) * 100}%`,
background: "linear-gradient(90deg,#00f0ff,#8b5cf6)",
borderRadius: 3,
transition: "width 0.5s ease",
}}
/>
</div>
</div>
{/* Node list */}
<div style={{ flex: 1, overflowY: "auto", display: "flex", flexDirection: "column", gap: 0 }}>
{nodes.map((node, i) => (
<div
key={node.id}
style={{ display: "flex", gap: 10, alignItems: "stretch" }}
>
{/* Left: dot + line */}
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
width: 20,
flexShrink: 0,
}}
>
{/* Dot */}
<div
style={{
width: 14,
height: 14,
borderRadius: "50%",
border: `2px solid ${statusColors[node.status]}`,
background: statusBg[node.status],
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
marginTop: 6,
transition: "all 0.3s ease",
boxShadow: node.status !== "pending" ? `0 0 8px ${statusColors[node.status]}60` : "none",
}}
className={node.status === "active" ? "animate-scale-pulse" : ""}
>
{node.status === "completed" && (
<svg width="7" height="7" viewBox="0 0 7 7">
<path d="M1 3.5L3 5.5L6 1.5" stroke="#030712" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
</svg>
)}
</div>
{/* Line */}
{i < nodes.length - 1 && (
<div
style={{
flex: 1,
width: 2,
margin: "2px 0",
borderRadius: 1,
position: "relative",
overflow: "hidden",
background: node.status === "completed"
? "linear-gradient(180deg,#00f0ff,#00ff9d)"
: node.status === "active"
? "linear-gradient(180deg,#00f0ff,#ff9500)"
: "#111827",
}}
>
{node.status === "active" && (
<div
className="timeline-line-flow"
style={{ position: "absolute", inset: 0, background: "transparent" }}
/>
)}
</div>
)}
</div>
{/* Right: content */}
<div
style={{
flex: 1,
paddingBottom: i < nodes.length - 1 ? 10 : 0,
paddingTop: 4,
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<span
className="font-rajdhani"
style={{
fontSize: 13,
color:
node.status === "completed"
? "#e5e7eb"
: node.status === "active"
? "#fff"
: "#6b7280",
fontWeight: node.status === "active" ? 600 : 400,
}}
>
{node.label}
</span>
{node.agent && (
<span
className="font-mono-code"
style={{
fontSize: 9,
color: statusColors[node.status],
background: `${statusColors[node.status]}15`,
border: `1px solid ${statusColors[node.status]}30`,
borderRadius: 4,
padding: "1px 5px",
}}
>
{node.agent}
</span>
)}
</div>
{node.duration && (
<div className="font-mono-code" style={{ fontSize: 9, color: "#4b5563", marginTop: 2 }}>
{node.duration}
</div>
)}
{node.status === "active" && (
<div
className="font-mono-code"
style={{ fontSize: 9, color: "#ff9500", marginTop: 2 }}
>
<span className="animate-pulse-fast"> </span>...
</div>
)}
</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,50 @@
// Agent 状态列表组件
import type { Agent } from '../../types';
import { StatusBadge, StatusDot } from '../StatusBadge';
interface AgentStatusListProps {
agents: Agent[];
}
export function AgentStatusList({ agents }: AgentStatusListProps) {
if (agents.length === 0) {
return (
<p style={{ textAlign: 'center', color: 'rgba(255, 255, 255, 0.4)', padding: 24 }}>
Agent
</p>
);
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{agents.slice(0, 5).map((agent) => (
<div
key={agent.agent_id}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 16px',
background: 'rgba(0, 0, 0, 0.2)',
borderRadius: 8,
border: '1px solid rgba(255, 255, 255, 0.05)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<StatusDot status={agent.status} />
<div>
<p style={{ fontSize: 14, fontWeight: 500, color: '#fff', margin: 0 }}>
{agent.name}
</p>
<p style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.4)', margin: 0 }}>
{agent.role} · {agent.model}
</p>
</div>
</div>
<StatusBadge status={agent.status} />
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,44 @@
// 最近会议列表组件
import type { Meeting } from '../../types';
import { StatusBadge } from '../StatusBadge';
interface RecentMeetingsListProps {
meetings: Meeting[];
}
export function RecentMeetingsList({ meetings }: RecentMeetingsListProps) {
if (meetings.length === 0) {
return (
<p style={{ textAlign: 'center', color: 'rgba(255, 255, 255, 0.4)', padding: 24 }}>
</p>
);
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{meetings.slice(0, 5).map((meeting) => (
<div
key={meeting.meeting_id}
style={{
padding: '12px 16px',
background: 'rgba(0, 0, 0, 0.2)',
borderRadius: 8,
border: '1px solid rgba(255, 255, 255, 0.05)',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<p style={{ fontSize: 14, fontWeight: 500, color: '#fff', margin: 0 }}>
{meeting.title}
</p>
<StatusBadge status={meeting.status} />
</div>
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: '4px 0 0 0' }}>
: {meeting.progress_summary} · : {meeting.attendees.length}
</p>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,86 @@
// 统计卡片组件
import { ArrowRight } from 'lucide-react';
import { Link } from 'react-router-dom';
import { colors, transitions } from '../../styles/dashboard';
interface StatCardProps {
icon: React.ElementType;
title: string;
value: string | number;
subtitle?: string;
color: string;
to: string;
}
export function StatCard({ icon: Icon, title, value, subtitle, color, to }: StatCardProps) {
return (
<Link
to={to}
style={{
display: 'block',
padding: 24,
background: colors.background.card,
borderRadius: 12,
border: colors.border.default,
textDecoration: 'none',
color: 'inherit',
transition: transitions.default,
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = color;
e.currentTarget.style.transform = 'translateY(-2px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'rgba(0, 240, 255, 0.1)';
e.currentTarget.style.transform = 'translateY(0)';
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<div
style={{
width: 48,
height: 48,
borderRadius: 10,
background: `${color}20`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color,
}}
>
<Icon size={24} />
</div>
<div style={{ flex: 1 }}>
<p
style={{
fontSize: 12,
color: colors.text.secondary,
margin: 0,
marginBottom: 4,
}}
>
{title}
</p>
<p
style={{
fontSize: 28,
fontWeight: 700,
color: colors.text.primary,
margin: 0,
marginBottom: 2,
}}
>
{value}
</p>
{subtitle && (
<p style={{ fontSize: 12, color: colors.text.muted, margin: 0 }}>
{subtitle}
</p>
)}
</div>
<ArrowRight size={18} color="rgba(255, 255, 255, 0.3)" />
</div>
</Link>
);
}

View File

@@ -0,0 +1,13 @@
export { Header } from './Header';
export { TaskInput } from './TaskInput';
export { AgentStatusCard } from './AgentStatusCard';
export { DiscussionCard } from './DiscussionCard';
export { StatisticsCard } from './StatisticsCard';
export { WorkflowCard } from './WorkflowCard';
export { MeetingProgressCard } from './MeetingProgressCard';
export { ResourceMonitorCard } from './ResourceMonitorCard';
export { ConsensusCard } from './ConsensusCard';
export { BarrierSyncCard } from './BarrierSyncCard';
export { RecentMeetingsCard } from './RecentMeetingsCard';
export { ActionBar } from './ActionBar';
export { StatusBadge, StatusDot } from './StatusBadge';

View File

@@ -0,0 +1,69 @@
import { Outlet } from 'react-router-dom';
import { Sidebar } from './Sidebar';
export function AppLayout() {
return (
<div style={{ display: 'flex', minHeight: '100vh', background: '#030712' }}>
{/* Sidebar */}
<Sidebar />
{/* Main Content */}
<main
style={{
flex: 1,
marginLeft: 240,
position: 'relative',
minHeight: '100vh',
}}
>
{/* Background Grid */}
<div className="bg-grid" />
<div className="bg-scanline" />
{/* Ambient glow orbs */}
<div
style={{
position: 'fixed',
top: '20%',
left: '10%',
width: 700,
height: 700,
borderRadius: '50%',
background:
'radial-gradient(circle,rgba(0,240,255,0.04) 0%,transparent 70%)',
pointerEvents: 'none',
zIndex: 0,
}}
/>
<div
style={{
position: 'fixed',
top: '60%',
right: '5%',
width: 600,
height: 600,
borderRadius: '50%',
background:
'radial-gradient(circle,rgba(139,92,246,0.05) 0%,transparent 70%)',
pointerEvents: 'none',
zIndex: 0,
}}
/>
{/* Page Content */}
<div
style={{
position: 'relative',
zIndex: 1,
maxWidth: 1600,
margin: '0 auto',
padding: '24px',
minHeight: '100vh',
}}
>
<Outlet />
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,135 @@
import { NavLink } from 'react-router-dom';
import {
LayoutDashboard,
Users,
Calendar,
HardDrive,
Workflow,
Settings,
} from 'lucide-react';
const navItems = [
{ path: '/', icon: LayoutDashboard, label: '仪表盘' },
{ path: '/agents', icon: Users, label: 'Agent 管理' },
{ path: '/meetings', icon: Calendar, label: '会议管理' },
{ path: '/resources', icon: HardDrive, label: '资源监控' },
{ path: '/workflow', icon: Workflow, label: '工作流' },
{ path: '/settings', icon: Settings, label: '配置' },
];
export function Sidebar() {
return (
<aside
style={{
width: 240,
height: '100vh',
background: 'rgba(3, 7, 18, 0.95)',
borderRight: '1px solid rgba(0, 240, 255, 0.1)',
display: 'flex',
flexDirection: 'column',
position: 'fixed',
left: 0,
top: 0,
zIndex: 100,
}}
>
{/* Logo */}
<div
style={{
padding: '24px 20px',
borderBottom: '1px solid rgba(0, 240, 255, 0.1)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div
style={{
width: 40,
height: 40,
borderRadius: 8,
background: 'linear-gradient(135deg, #00f0ff 0%, #8b5cf6 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 0 20px rgba(0, 240, 255, 0.3)',
}}
>
<span
style={{
fontSize: 20,
fontWeight: 700,
color: '#000',
}}
>
S
</span>
</div>
<div>
<h1
style={{
fontSize: 16,
fontWeight: 700,
color: '#fff',
margin: 0,
letterSpacing: '0.5px',
}}
>
Swarm Center
</h1>
<p
style={{
fontSize: 11,
color: 'rgba(0, 240, 255, 0.7)',
margin: 0,
marginTop: 2,
}}
>
</p>
</div>
</div>
</div>
{/* Navigation */}
<nav style={{ flex: 1, padding: '16px 12px' }}>
{navItems.map((item) => (
<NavLink
key={item.path}
to={item.path}
style={({ isActive }) => ({
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '12px 16px',
marginBottom: 4,
borderRadius: 8,
textDecoration: 'none',
color: isActive ? '#00f0ff' : 'rgba(255, 255, 255, 0.7)',
background: isActive
? 'rgba(0, 240, 255, 0.1)'
: 'transparent',
borderLeft: isActive
? '3px solid #00f0ff'
: '3px solid transparent',
transition: 'all 0.2s ease',
})}
>
<item.icon size={20} />
<span style={{ fontSize: 14, fontWeight: 500 }}>{item.label}</span>
</NavLink>
))}
</nav>
{/* Version */}
<div
style={{
padding: '16px 20px',
borderTop: '1px solid rgba(0, 240, 255, 0.1)',
fontSize: 12,
color: 'rgba(255, 255, 255, 0.4)',
}}
>
v0.1.0
</div>
</aside>
);
}

View File

@@ -0,0 +1,2 @@
export { AppLayout } from './AppLayout';
export { Sidebar } from './Sidebar';

428
frontend/src/lib/api.ts Normal file
View File

@@ -0,0 +1,428 @@
// API 客户端 - 与后端交互
import type {
Agent,
AgentState,
FileLock,
Heartbeat,
Meeting,
MeetingQueue,
Workflow,
AgentResourceStatus,
} from '../types';
// API 基础地址
const API_BASE = 'http://localhost:8000/api';
// 通用请求函数
async function request<T>(
endpoint: string,
options?: RequestInit
): Promise<T> {
const url = `${API_BASE}${endpoint}`;
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
},
...options,
});
if (!response.ok) {
const error = await response.json().catch(() => ({
error: {
code: 'UNKNOWN_ERROR',
message: `HTTP ${response.status}: ${response.statusText}`,
},
}));
throw new Error(error.error?.message || '请求失败');
}
return response.json();
}
// ==================== Agent API ====================
export const agentApi = {
// 列出所有 Agent
list: () => request<{ agents: Agent[] }>('/agents'),
// 注册新 Agent
register: (agent: Omit<Agent, 'status' | 'created_at'>) =>
request<Agent>('/agents/register', {
method: 'POST',
body: JSON.stringify(agent),
}),
// 获取 Agent 详情
get: (agentId: string) => request<Agent>(`/agents/${agentId}`),
// 获取 Agent 状态
getState: (agentId: string) =>
request<AgentState>(`/agents/${agentId}/state`),
// 更新 Agent 状态
updateState: (
agentId: string,
state: { task: string; progress: number; working_files?: string[] }
) =>
request<{ success: boolean }>(`/agents/${agentId}/state`, {
method: 'POST',
body: JSON.stringify(state),
}),
};
// ==================== 文件锁 API ====================
export const lockApi = {
// 获取所有锁
list: () => request<{ locks: FileLock[] }>('/locks'),
// 获取文件锁
acquire: (filePath: string, agentId: string, agentName: string) =>
request<{ success: boolean }>('/locks/acquire', {
method: 'POST',
body: JSON.stringify({ file_path: filePath, agent_id: agentId, agent_name: agentName }),
}),
// 释放文件锁
release: (filePath: string, agentId: string) =>
request<{ success: boolean }>('/locks/release', {
method: 'POST',
body: JSON.stringify({ file_path: filePath, agent_id: agentId }),
}),
// 检查文件锁定状态
check: (filePath: string) =>
request<{ file_path: string; locked: boolean; locked_by?: string }>(
`/locks/check?file_path=${encodeURIComponent(filePath)}`
),
};
// ==================== 心跳 API ====================
export const heartbeatApi = {
// 获取所有心跳
list: () => request<{ heartbeats: Record<string, Heartbeat> }>('/heartbeats'),
// 更新心跳
update: (
agentId: string,
data: { status: Heartbeat['status']; current_task: string; progress: number }
) =>
request<{ success: boolean }>(`/heartbeats/${agentId}`, {
method: 'POST',
body: JSON.stringify(data),
}),
// 检查超时 Agent
checkTimeouts: (timeoutSeconds: number = 60) =>
request<{ timeout_seconds: number; timeout_agents: string[] }>(
`/heartbeats/timeouts?timeout_seconds=${timeoutSeconds}`
),
};
// ==================== 会议 API ====================
export const meetingApi = {
// 创建会议
create: (data: {
meeting_id: string;
title: string;
expected_attendees: string[];
min_required?: number;
}) =>
request<{ success: boolean }>('/meetings/create', {
method: 'POST',
body: JSON.stringify(data),
}),
// 获取会议队列
getQueue: (meetingId: string) =>
request<MeetingQueue>(`/meetings/${meetingId}/queue`),
// 等待会议开始
wait: (meetingId: string, agentId: string, timeout?: number) =>
request<{ result: 'started' | 'timeout' | 'error'; meeting_id: string; agent_id: string }>(
`/meetings/${meetingId}/wait`,
{
method: 'POST',
body: JSON.stringify({ agent_id: agentId, timeout }),
}
),
// 结束会议
end: (meetingId: string) =>
request<{ success: boolean }>(`/meetings/${meetingId}/end`, {
method: 'POST',
}),
// 创建会议记录
createRecord: (data: {
meeting_id: string;
title: string;
attendees: string[];
steps?: string[];
}) =>
request<Meeting>('/meetings/record/create', {
method: 'POST',
body: JSON.stringify(data),
}),
// 添加讨论
addDiscussion: (
meetingId: string,
data: { agent_id: string; agent_name: string; content: string; step?: string }
) =>
request<{ success: boolean }>(`/meetings/${meetingId}/discuss`, {
method: 'POST',
body: JSON.stringify(data),
}),
// 更新进度
updateProgress: (meetingId: string, step: string) =>
request<{ success: boolean }>(`/meetings/${meetingId}/progress`, {
method: 'POST',
body: JSON.stringify({ step }),
}),
// 获取会议详情
get: (meetingId: string, date?: string) => {
const query = date ? `?date=${date}` : '';
return request<Meeting>(`/meetings/${meetingId}${query}`);
},
// 完成会议
finish: (meetingId: string, consensus: string) =>
request<{ success: boolean }>(`/meetings/${meetingId}/finish`, {
method: 'POST',
body: JSON.stringify({ consensus }),
}),
// 列出今日会议
listToday: () => {
const today = new Date().toISOString().split('T')[0];
return request<{ meetings: Meeting[] }>(`/meetings?date=${today}`);
},
};
// ==================== 资源管理 API ====================
export const resourceApi = {
// 执行任务
execute: (agentId: string, task: string, timeout?: number) =>
request<{
success: boolean;
message: string;
files_locked: string[];
duration_seconds: number;
}>('/execute', {
method: 'POST',
body: JSON.stringify({ agent_id: agentId, task, timeout }),
}),
// 获取所有 Agent 状态
getAllStatus: () => request<{ agents: AgentResourceStatus[] }>('/status'),
// 解析任务文件
parseTask: (task: string) =>
request<{ task: string; files: string[] }>('/parse-task', {
method: 'POST',
body: JSON.stringify({ task }),
}),
};
// ==================== 工作流 API ====================
export const workflowApi = {
// 列出工作流文件
listFiles: () =>
request<{ files: Array<{ name: string; path: string; size: number; modified: number }> }>('/workflows/files'),
// 列出已加载的工作流
list: () =>
request<{ workflows: Array<{ workflow_id: string; name: string; status: string; progress: string }> }>('/workflows/list'),
// 启动工作流
start: (path: string) =>
request<Workflow>(`/workflows/start/${path}`, {
method: 'POST',
}),
// 获取工作流详情
get: (workflowId: string) =>
request<Workflow>(`/workflows/${workflowId}`),
// 获取工作流状态
getStatus: (workflowId: string) =>
request<Workflow>(`/workflows/${workflowId}/status`),
// 获取下一个节点
getNext: (workflowId: string) =>
request<{ meeting: { meeting_id: string; title: string; node_type: string; attendees: string[] } | null; message?: string }>(`/workflows/${workflowId}/next`),
// 标记节点完成
complete: (workflowId: string, meetingId: string) =>
request<{ success: boolean; message: string }>(`/workflows/${workflowId}/complete/${meetingId}`, {
method: 'POST',
}),
// Agent 加入执行节点
join: (workflowId: string, meetingId: string, agentId: string) =>
request<{ status: 'ready' | 'waiting' | 'error'; progress: string; message: string; missing?: string[] }>(`/workflows/${workflowId}/join/${meetingId}`, {
method: 'POST',
body: JSON.stringify({ agent_id: agentId }),
}),
// 获取执行节点状态
getExecutionStatus: (workflowId: string, meetingId: string) =>
request<{ meeting_id: string; title: string; node_type: string; progress: string; is_ready: boolean; completed_attendees: string[]; missing: string[] }>(`/workflows/${workflowId}/execution/${meetingId}`),
// 强制跳转到指定节点
jump: (workflowId: string, targetMeetingId: string) =>
request<{ success: boolean; message: string; detail?: Workflow }>(`/workflows/${workflowId}/jump`, {
method: 'POST',
body: JSON.stringify({ target_meeting_id: targetMeetingId }),
}),
// 处理节点失败
handleFailure: (workflowId: string, meetingId: string) =>
request<{ success: boolean; message: string; target?: string; detail?: Workflow }>(`/workflows/${workflowId}/fail/${meetingId}`, {
method: 'POST',
}),
};
// ==================== 角色分配 API ====================
export const roleApi = {
// 获取任务主要角色
getPrimary: (task: string) =>
request<{
task: string;
primary_role: string;
role_scores: Record<string, number>;
}>('/roles/primary', {
method: 'POST',
body: JSON.stringify({ task }),
}),
// 分配角色
allocate: (task: string, agents: string[]) =>
request<{
task: string;
primary_role: string;
allocation: Record<string, string>;
}>('/roles/allocate', {
method: 'POST',
body: JSON.stringify({ task, agents }),
}),
// 解释角色分配
explain: (task: string, agents: string[]) =>
request<{ explanation: string }>('/roles/explain', {
method: 'POST',
body: JSON.stringify({ task, agents }),
}),
};
// ==================== 系统 API ====================
export const systemApi = {
// 健康检查
health: () =>
request<{
status: string;
version: string;
services: Record<string, string>;
}>('/health'),
};
// ==================== 人类输入 API ====================
export const humanApi = {
// 获取摘要
summary: () =>
request<{ participants: number; online_users: number; pending_tasks: number; urgent_tasks: number; pending_comments: number; last_updated: string }>('/humans/summary'),
// 注册参与者
register: (userId: string, name: string, role?: string, avatar?: string) =>
request<{ success: boolean; user_id: string }>('/humans/register', {
method: 'POST',
body: JSON.stringify({ user_id: userId, name, role, avatar }),
}),
// 获取参与者列表
getParticipants: () =>
request<{ participants: Array<{ id: string; name: string; role: string; status: string; avatar: string }> }>('/humans/participants'),
// 添加任务请求
addTask: (content: string, options?: { from_user?: string; priority?: string; title?: string; target_files?: string[]; suggested_agent?: string; urgent?: boolean }) =>
request<{ success: boolean; task_id: string }>('/humans/tasks', {
method: 'POST',
body: JSON.stringify({ content, ...options }),
}),
// 获取待处理任务
getPendingTasks: (options?: { priority?: string; agent?: string }) => {
const params = new URLSearchParams();
if (options?.priority) params.append('priority', options.priority);
if (options?.agent) params.append('agent', options.agent);
return request<{ tasks: Array<{ id: string; from_user: string; timestamp: string; priority: string; title: string; content: string; suggested_agent: string; urgent: boolean; is_urgent: boolean }>; count: number }>(`/humans/tasks?${params}`);
},
// 获取紧急任务
getUrgentTasks: () =>
request<{ tasks: Array<{ id: string; from_user: string; content: string; title: string; suggested_agent: string }>; count: number }>('/humans/tasks/urgent'),
// 标记任务处理中
markTaskProcessing: (taskId: string) =>
request<{ success: boolean }>(`/humans/tasks/${taskId}/processing`, {
method: 'PUT',
}),
// 标记任务完成
markTaskComplete: (taskId: string) =>
request<{ success: boolean }>(`/humans/tasks/${taskId}/complete`, {
method: 'PUT',
}),
// 添加会议评论
addComment: (meetingId: string, content: string, options?: { from_user?: string; comment_type?: string; priority?: string }) =>
request<{ success: boolean; comment_id: string }>('/humans/comments', {
method: 'POST',
body: JSON.stringify({ meeting_id: meetingId, content, ...options }),
}),
// 获取待处理评论
getPendingComments: (meetingId?: string) => {
const params = meetingId ? `?meeting_id=${meetingId}` : '';
return request<{ comments: Array<{ id: string; from_user: string; meeting_id: string; timestamp: string; type: string; priority: string; content: string }>; count: string }>(`/humans/comments${params}`);
},
// 标记评论已处理
markCommentAddressed: (commentId: string) =>
request<{ success: boolean }>(`/humans/comments/${commentId}/addressed`, {
method: 'PUT',
}),
// 更新用户状态
updateUserStatus: (userId: string, status: string, currentFocus?: string) =>
request<{ success: boolean }>(`/humans/users/${userId}/status`, {
method: 'PUT',
body: JSON.stringify({ status, current_focus: currentFocus }),
}),
};
// 导出所有 API
export const api = {
agent: agentApi,
lock: lockApi,
heartbeat: heartbeatApi,
meeting: meetingApi,
resource: resourceApi,
workflow: workflowApi,
role: roleApi,
system: systemApi,
human: humanApi,
};
export default api;

10
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './styles/index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);

View File

@@ -0,0 +1,972 @@
import { useState, useEffect } from 'react';
import { Plus, Users, Activity, Cpu, RefreshCw, Play, Square, Power } from 'lucide-react';
import { api } from '../lib/api';
import type { Agent, AgentState } from '../types';
// 注册 Agent 模态框
function RegisterModal({
isOpen,
onClose,
onSubmit,
}: {
isOpen: boolean;
onClose: () => void;
onSubmit: (data: {
agent_id: string;
name: string;
role: string;
model: string;
description: string;
}) => void;
}) {
const [form, setForm] = useState({
agent_id: '',
name: '',
role: 'developer',
model: 'claude-opus-4.6',
description: '',
});
if (!isOpen) return null;
return (
<div
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0, 0, 0, 0.7)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
}}
onClick={onClose}
>
<div
style={{
width: 480,
background: 'rgba(17, 24, 39, 0.95)',
borderRadius: 12,
border: '1px solid rgba(0, 240, 255, 0.2)',
padding: 24,
}}
onClick={(e) => e.stopPropagation()}
>
<h2
style={{
fontSize: 20,
fontWeight: 700,
color: '#fff',
margin: 0,
marginBottom: 20,
}}
>
Agent
</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<label
style={{
display: 'block',
fontSize: 12,
color: 'rgba(255, 255, 255, 0.6)',
marginBottom: 6,
}}
>
Agent ID
</label>
<input
type="text"
value={form.agent_id}
onChange={(e) => setForm({ ...form, agent_id: e.target.value })}
placeholder="例如: claude-001"
style={{
width: '100%',
padding: '10px 14px',
background: 'rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(0, 240, 255, 0.2)',
borderRadius: 8,
color: '#fff',
fontSize: 14,
outline: 'none',
}}
/>
</div>
<div>
<label
style={{
display: 'block',
fontSize: 12,
color: 'rgba(255, 255, 255, 0.6)',
marginBottom: 6,
}}
>
</label>
<input
type="text"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="例如: Claude Code"
style={{
width: '100%',
padding: '10px 14px',
background: 'rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(0, 240, 255, 0.2)',
borderRadius: 8,
color: '#fff',
fontSize: 14,
outline: 'none',
}}
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<div>
<label
style={{
display: 'block',
fontSize: 12,
color: 'rgba(255, 255, 255, 0.6)',
marginBottom: 6,
}}
>
</label>
<select
value={form.role}
onChange={(e) => setForm({ ...form, role: e.target.value })}
style={{
width: '100%',
padding: '10px 14px',
background: 'rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(0, 240, 255, 0.2)',
borderRadius: 8,
color: '#fff',
fontSize: 14,
outline: 'none',
}}
>
<option value="architect"> (architect)</option>
<option value="pm"> (pm)</option>
<option value="developer"> (developer)</option>
<option value="qa"> (qa)</option>
<option value="reviewer"> (reviewer)</option>
<option value="human"> (human)</option>
</select>
</div>
<div>
<label
style={{
display: 'block',
fontSize: 12,
color: 'rgba(255, 255, 255, 0.6)',
marginBottom: 6,
}}
>
</label>
<input
type="text"
value={form.model}
onChange={(e) => setForm({ ...form, model: e.target.value })}
placeholder="模型名称"
style={{
width: '100%',
padding: '10px 14px',
background: 'rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(0, 240, 255, 0.2)',
borderRadius: 8,
color: '#fff',
fontSize: 14,
outline: 'none',
}}
/>
</div>
</div>
<div>
<label
style={{
display: 'block',
fontSize: 12,
color: 'rgba(255, 255, 255, 0.6)',
marginBottom: 6,
}}
>
</label>
<textarea
value={form.description}
onChange={(e) => setForm({ ...form, description: e.target.value })}
placeholder="Agent 的职责描述"
rows={3}
style={{
width: '100%',
padding: '10px 14px',
background: 'rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(0, 240, 255, 0.2)',
borderRadius: 8,
color: '#fff',
fontSize: 14,
outline: 'none',
resize: 'none',
}}
/>
</div>
<div style={{ display: 'flex', gap: 12, marginTop: 8 }}>
<button
onClick={onClose}
style={{
flex: 1,
padding: '12px 20px',
background: 'rgba(255, 255, 255, 0.05)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: 8,
color: 'rgba(255, 255, 255, 0.7)',
fontSize: 14,
cursor: 'pointer',
}}
>
</button>
<button
onClick={() => {
if (form.agent_id && form.name) {
onSubmit(form);
onClose();
setForm({
agent_id: '',
name: '',
role: 'developer',
model: 'claude-opus-4.6',
description: '',
});
}
}}
style={{
flex: 1,
padding: '12px 20px',
background: 'linear-gradient(135deg, #00f0ff 0%, #8b5cf6 100%)',
border: 'none',
borderRadius: 8,
color: '#000',
fontSize: 14,
fontWeight: 600,
cursor: 'pointer',
}}
>
</button>
</div>
</div>
</div>
</div>
);
}
// Agent 详情面板
function AgentDetailPanel({
agent,
state,
onClose,
}: {
agent: Agent;
state: AgentState | null;
onClose: () => void;
}) {
const getStatusColor = (status: string) => {
switch (status) {
case 'working':
return '#00ff9d';
case 'idle':
return '#00f0ff';
case 'waiting':
return '#ff9500';
case 'error':
return '#ff006e';
default:
return '#666';
}
};
return (
<div
style={{
position: 'fixed',
right: 0,
top: 0,
bottom: 0,
width: 400,
background: 'rgba(17, 24, 39, 0.98)',
borderLeft: '1px solid rgba(0, 240, 255, 0.1)',
padding: 24,
zIndex: 100,
overflow: 'auto',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 24 }}>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#fff', margin: 0 }}>
Agent
</h2>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
color: 'rgba(255, 255, 255, 0.5)',
fontSize: 24,
cursor: 'pointer',
}}
>
×
</button>
</div>
<div
style={{
padding: 20,
background: 'rgba(0, 0, 0, 0.3)',
borderRadius: 12,
marginBottom: 20,
}}
>
<div
style={{
width: 64,
height: 64,
borderRadius: 12,
background: `linear-gradient(135deg, ${getStatusColor(agent.status)}40 0%, ${getStatusColor(agent.status)}10 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
}}
>
<Users size={32} color={getStatusColor(agent.status)} />
</div>
<h3 style={{ fontSize: 18, fontWeight: 600, color: '#fff', margin: '0 0 4px 0' }}>
{agent.name}
</h3>
<p style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.5)', margin: 0 }}>
{agent.agent_id}
</p>
<div
style={{
display: 'flex',
gap: 8,
marginTop: 12,
}}
>
<span
style={{
padding: '4px 12px',
borderRadius: 12,
background: `${getStatusColor(agent.status)}20`,
color: getStatusColor(agent.status),
fontSize: 12,
textTransform: 'capitalize',
}}
>
{agent.status}
</span>
<span
style={{
padding: '4px 12px',
borderRadius: 12,
background: 'rgba(139, 92, 246, 0.2)',
color: '#8b5cf6',
fontSize: 12,
}}
>
{agent.role}
</span>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div
style={{
padding: 16,
background: 'rgba(0, 0, 0, 0.2)',
borderRadius: 8,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<Cpu size={16} color="#00f0ff" />
<span style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.6)' }}></span>
</div>
<p style={{ fontSize: 14, color: '#fff', margin: 0 }}>{agent.model}</p>
</div>
{state && (
<>
<div
style={{
padding: 16,
background: 'rgba(0, 0, 0, 0.2)',
borderRadius: 8,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<Activity size={16} color="#00ff9d" />
<span style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.6)' }}></span>
</div>
<p style={{ fontSize: 14, color: '#fff', margin: 0 }}>{state.current_task || '无'}</p>
</div>
<div
style={{
padding: 16,
background: 'rgba(0, 0, 0, 0.2)',
borderRadius: 8,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<span style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.6)' }}></span>
</div>
<div
style={{
height: 8,
background: 'rgba(255, 255, 255, 0.1)',
borderRadius: 4,
overflow: 'hidden',
}}
>
<div
style={{
width: `${state.progress}%`,
height: '100%',
background: 'linear-gradient(90deg, #00f0ff, #8b5cf6)',
borderRadius: 4,
}}
/>
</div>
<p style={{ fontSize: 12, color: '#00f0ff', margin: '8px 0 0 0' }}>
{state.progress}%
</p>
</div>
{state.working_files.length > 0 && (
<div
style={{
padding: 16,
background: 'rgba(0, 0, 0, 0.2)',
borderRadius: 8,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<span style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.6)' }}></span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{state.working_files.map((file, i) => (
<span
key={i}
style={{
fontSize: 13,
color: '#00f0ff',
fontFamily: 'monospace',
}}
>
{file}
</span>
))}
</div>
</div>
)}
<div
style={{
padding: 16,
background: 'rgba(0, 0, 0, 0.2)',
borderRadius: 8,
}}
>
<span style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.6)' }}></span>
<p style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.4)', margin: '4px 0 0 0' }}>
{new Date(state.last_update).toLocaleString()}
</p>
</div>
</>
)}
</div>
</div>
);
}
// 运行中的 Agent 状态
interface RunningAgent {
agent_id: string;
status: string;
is_alive: boolean;
uptime: number | null;
restart_count: number;
}
export function AgentsPage() {
const [agents, setAgents] = useState<Agent[]>([]);
const [runningAgents, setRunningAgents] = useState<Record<string, RunningAgent>>({});
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
const [agentState, setAgentState] = useState<AgentState | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// API 基础 URL
const API_BASE = 'http://localhost:8000/api';
// 加载 Agent 列表
const loadAgents = async () => {
try {
setLoading(true);
const res = await api.agent.list();
setAgents(res.agents);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : '加载失败');
} finally {
setLoading(false);
}
};
// 加载运行中的 Agent
const loadRunningAgents = async () => {
try {
const res = await fetch(`${API_BASE}/agents/control/list`);
if (res.ok) {
const data = await res.json();
const runningMap: Record<string, RunningAgent> = {};
data.forEach((agent: RunningAgent) => {
runningMap[agent.agent_id] = agent;
});
setRunningAgents(runningMap);
}
} catch (err) {
console.error('加载运行状态失败:', err);
}
};
// 启动 Agent
const startAgent = async (agentId: string, agent: Agent) => {
try {
const res = await fetch(`${API_BASE}/agents/control/start`, {
method: 'POST',
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) {
alert(`启动失败: ${err instanceof Error ? err.message : '未知错误'}`);
}
};
// 停止 Agent
const stopAgent = async (agentId: string) => {
try {
const res = await fetch(`${API_BASE}/agents/control/stop`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_id: agentId,
graceful: true
})
});
if (res.ok) {
await loadRunningAgents();
} else {
alert('停止失败');
}
} catch (err) {
alert(`停止失败: ${err instanceof Error ? err.message : '未知错误'}`);
}
};
// 加载 Agent 状态
const loadAgentState = async (agentId: string) => {
try {
const state = await api.agent.getState(agentId);
setAgentState(state);
} catch (err) {
setAgentState(null);
}
};
useEffect(() => {
loadAgents();
loadRunningAgents();
const interval = setInterval(() => {
loadAgents();
loadRunningAgents();
}, 10000);
return () => clearInterval(interval);
}, []);
// 当选中 Agent 时加载状态
useEffect(() => {
if (selectedAgent) {
loadAgentState(selectedAgent.agent_id);
}
}, [selectedAgent]);
// 注册 Agent
const handleRegister = async (data: {
agent_id: string;
name: string;
role: string;
model: string;
description: string;
}) => {
try {
await api.agent.register(data as Omit<Agent, 'status' | 'created_at'>);
loadAgents();
} catch (err) {
alert(err instanceof Error ? err.message : '注册失败');
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'working':
return '#00ff9d';
case 'idle':
return '#00f0ff';
case 'waiting':
return '#ff9500';
case 'error':
return '#ff006e';
default:
return '#666';
}
};
const getRoleLabel = (role: string) => {
const labels: Record<string, string> = {
architect: '架构师',
pm: '产品经理',
developer: '开发者',
qa: '测试工程师',
reviewer: '审查者',
human: '人类',
};
return labels[role] || role;
};
if (loading && agents.length === 0) {
return (
<div style={{ display: 'flex', justifyContent: 'center', padding: 100 }}>
<div style={{ color: '#00f0ff' }}>...</div>
</div>
);
}
return (
<div>
{/* 添加脉动动画样式 */}
<style>{`
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
`}</style>
{/* Header */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24,
}}
>
<div>
<h1 style={{ fontSize: 28, fontWeight: 700, color: '#fff', margin: 0 }}>Agent </h1>
<p style={{ fontSize: 14, color: 'rgba(255, 255, 255, 0.5)', margin: '8px 0 0 0' }}>
</p>
</div>
<div style={{ display: 'flex', gap: 12 }}>
<button
onClick={loadAgents}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '10px 16px',
background: 'rgba(255, 255, 255, 0.05)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: 8,
color: 'rgba(255, 255, 255, 0.7)',
fontSize: 14,
cursor: 'pointer',
}}
>
<RefreshCw size={16} />
</button>
<button
onClick={() => setIsModalOpen(true)}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '10px 16px',
background: 'linear-gradient(135deg, #00f0ff 0%, #8b5cf6 100%)',
border: 'none',
borderRadius: 8,
color: '#000',
fontSize: 14,
fontWeight: 600,
cursor: 'pointer',
}}
>
<Plus size={18} />
Agent
</button>
</div>
</div>
{error && (
<div
style={{
padding: '12px 16px',
background: '#ff006e20',
border: '1px solid #ff006e50',
borderRadius: 8,
color: '#ff006e',
marginBottom: 24,
}}
>
: {error}
</div>
)}
{/* Agent Grid */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
gap: 16,
}}
>
{agents.map((agent) => (
<div
key={agent.agent_id}
onClick={() => setSelectedAgent(agent)}
style={{
padding: 20,
background: 'rgba(17, 24, 39, 0.7)',
borderRadius: 12,
border: '1px solid rgba(0, 240, 255, 0.1)',
cursor: 'pointer',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = 'rgba(0, 240, 255, 0.3)';
e.currentTarget.style.transform = 'translateY(-2px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'rgba(0, 240, 255, 0.1)';
e.currentTarget.style.transform = 'translateY(0)';
}}
>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
<div
style={{
width: 48,
height: 48,
borderRadius: 10,
background: `linear-gradient(135deg, ${getStatusColor(agent.status)}40 0%, ${getStatusColor(agent.status)}10 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}
>
<Users size={24} color={getStatusColor(agent.status)} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<h3
style={{
fontSize: 16,
fontWeight: 600,
color: '#fff',
margin: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{agent.name}
</h3>
<p
style={{
fontSize: 12,
color: 'rgba(255, 255, 255, 0.4)',
margin: '4px 0 0 0',
}}
>
{agent.agent_id}
</p>
</div>
<div
style={{
width: 8,
height: 8,
borderRadius: '50%',
background: getStatusColor(agent.status),
boxShadow: `0 0 8px ${getStatusColor(agent.status)}`,
flexShrink: 0,
}}
/>
</div>
<div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
<span
style={{
padding: '4px 10px',
borderRadius: 12,
background: `${getStatusColor(agent.status)}20`,
color: getStatusColor(agent.status),
fontSize: 11,
}}
>
{agent.status}
</span>
<span
style={{
padding: '4px 10px',
borderRadius: 12,
background: 'rgba(139, 92, 246, 0.2)',
color: '#8b5cf6',
fontSize: 11,
}}
>
{getRoleLabel(agent.role)}
</span>
</div>
<div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid rgba(255, 255, 255, 0.05)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: 0 }}>
: {agent.model}
</p>
{/* 运行状态指示 */}
{runningAgents[agent.agent_id] ? (
<span style={{ fontSize: 11, color: '#00ff9d', display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#00ff9d', animation: 'pulse 2s infinite' }} />
</span>
) : (
<span style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.3)' }}>
</span>
)}
</div>
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.3)', margin: '4px 0 0 0' }}>
: {new Date(agent.created_at).toLocaleDateString()}
</p>
{/* 启动/停止按钮 */}
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
{runningAgents[agent.agent_id] ? (
<button
onClick={(e) => {
e.stopPropagation();
stopAgent(agent.agent_id);
}}
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
padding: '8px 12px',
background: 'rgba(255, 0, 110, 0.2)',
border: '1px solid rgba(255, 0, 110, 0.3)',
borderRadius: 6,
color: '#ff006e',
fontSize: 12,
cursor: 'pointer',
}}
>
<Square size={14} />
</button>
) : (
<button
onClick={(e) => {
e.stopPropagation();
startAgent(agent.agent_id, agent);
}}
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
padding: '8px 12px',
background: 'rgba(0, 255, 157, 0.2)',
border: '1px solid rgba(0, 255, 157, 0.3)',
borderRadius: 6,
color: '#00ff9d',
fontSize: 12,
cursor: 'pointer',
}}
>
<Play size={14} />
</button>
)}
</div>
{/* 显示运行时长 */}
{runningAgents[agent.agent_id]?.uptime && (
<p style={{ fontSize: 11, color: 'rgba(0, 255, 157, 0.6)', margin: '8px 0 0 0' }}>
: {Math.floor(runningAgents[agent.agent_id].uptime! / 60)}
</p>
)}
</div>
</div>
))}
</div>
{agents.length === 0 && !loading && (
<div style={{ textAlign: 'center', padding: 80 }}>
<Users size={48} color="rgba(255, 255, 255, 0.2)" style={{ marginBottom: 16 }} />
<p style={{ color: 'rgba(255, 255, 255, 0.5)', margin: 0 }}> Agent</p>
<p style={{ color: 'rgba(255, 255, 255, 0.3)', fontSize: 14 }}>"注册 Agent"</p>
</div>
)}
{/* Modals */}
<RegisterModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} onSubmit={handleRegister} />
{selectedAgent && (
<AgentDetailPanel
agent={selectedAgent}
state={agentState}
onClose={() => {
setSelectedAgent(null);
setAgentState(null);
}}
/>
)}
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More