33521b8b39
- 主文档 §6 只保留概要表格 + 文件指向 - 测试 fixture/完整代码/覆盖矩阵 → 18-test-design.md - 删除误加的 GATE/委派/wiki 章节 - CI 集成改为表格格式 + 引用
504 lines
17 KiB
Markdown
504 lines
17 KiB
Markdown
# API 聚合重构 + 工具链 Tab 设计
|
||
|
||
> **编号**: §18
|
||
> **状态**: 设计中
|
||
> **日期**: 2026-06-14
|
||
> **作者**: 庞统(副军师)🐦
|
||
> **审查**: 司马懿(mail-1781415763066 已回复,方案 B 调整版确认)
|
||
|
||
---
|
||
|
||
## 1. 背景与目标
|
||
|
||
### 1.1 问题
|
||
|
||
1. **blackboard_routes.py 膨胀**:572 行、22 个路由,task/comment/output/review/event/decision/observation/archive 全堆一个文件,维护困难
|
||
2. **前端 N+1 请求**:打开 TaskModal 需要 5 次独立请求(task + events + subtasks + progress + comments),影响前端性能
|
||
3. **工具链事件无前端展示**:`_toolchain` DB 隔离已完成,但前端无对应 Tab,工具链事件只能通过 Agent 收 Mail 感知
|
||
|
||
### 1.2 目标
|
||
|
||
1. 按领域拆分 blackboard_routes.py → 3 个文件
|
||
2. 实现细粒度 expand 聚合接口,前端 1-2 次请求拿全任务详情
|
||
3. 新增工具链 Tab(列表 + 详情 + 搜索栏)
|
||
4. 任务列表支持标题搜索
|
||
|
||
### 1.3 不做
|
||
|
||
- checkpoint_routes.py 不纳入拆分(已独立)
|
||
- mail_routes / toolchain_routes / project_routes 不动
|
||
- SQL JOIN / batch query 性能优化(当前 SQLite 单写下多次查询可接受)
|
||
|
||
---
|
||
|
||
## 2. 后端 API 文件拆分
|
||
|
||
### 2.1 拆分方案(方案 B 调整版,司马懿确认)
|
||
|
||
| 新文件 | 内容 | 预估行数 |
|
||
|--------|------|---------|
|
||
| `task_routes.py` | task CRUD + create(含 AI 标题) + patch + progress + claim + status(含广播) + archive + archive-done | ~280 |
|
||
| `task_relation_routes.py` | comments + outputs(含文件写入) + reviews + decisions + observations + events + experiences + summary | ~250 |
|
||
| `shared.py` | `_bb()` / `_q()` / `_validate_project()` / `_task_to_dict()` / `_init_agent_ids()` / `_extract_mentions()` / 常量导入 | ~30 |
|
||
|
||
### 2.2 路由分配明细
|
||
|
||
**task_routes.py**(10 个路由):
|
||
|
||
| 路由 | 方法 | 函数 | 说明 |
|
||
|------|------|------|------|
|
||
| `/tasks` | GET | `list_tasks` | 列表(新增 `q` 搜索参数) |
|
||
| `/tasks` | POST | `create_task` | 创建(含 `_generate_title`) |
|
||
| `/tasks/{tid}` | GET | `get_task` | 详情(含 expand 聚合) |
|
||
| `/tasks/{tid}` | PATCH | `patch_task` | 更新 |
|
||
| `/tasks/{tid}/progress` | GET | `task_progress` | 进度 |
|
||
| `/tasks/{tid}/claim` | POST | `claim_task` | 认领 |
|
||
| `/tasks/{tid}/status` | POST | `update_status` | 状态流转(含广播逻辑) |
|
||
| `/tasks/{tid}/archive` | POST | `archive_task` | 归档 |
|
||
| `/tasks/archive-done` | POST | `archive_done_tasks` | 批量归档 |
|
||
|
||
**task_relation_routes.py**(13 个路由):
|
||
|
||
| 路由 | 方法 | 函数 | 说明 |
|
||
|------|------|------|------|
|
||
| `/tasks/{tid}/comments` | GET | `get_comments` | 评论列表 |
|
||
| `/tasks/{tid}/comments` | POST | `add_comment` | 添加评论(含 @mention 提取) |
|
||
| `/tasks/{tid}/outputs` | GET | `get_outputs` | 产出列表 |
|
||
| `/tasks/{tid}/outputs` | POST | `write_output` | 写入产出(含文件写入逻辑) |
|
||
| `/tasks/{tid}/decisions` | GET | `get_decisions` | 决策列表 |
|
||
| `/tasks/{tid}/decisions` | POST | `add_decision` | 添加决策 |
|
||
| `/tasks/{tid}/observations` | POST | `add_observation` | 添加观察 |
|
||
| `/tasks/{tid}/reviews` | GET | `get_reviews` | 审查列表 |
|
||
| `/tasks/{tid}/reviews` | POST | `add_review` | 添加审查 |
|
||
| `/tasks/{tid}/events` | GET | `get_task_events` | 事件列表 |
|
||
| `/tasks/{tid}/experiences` | GET | `get_task_experiences` | 经验列表 |
|
||
| `/events` | GET | `get_events` | 项目级事件 |
|
||
| `/summary` | GET | `task_summary` | 任务汇总 |
|
||
|
||
### 2.3 shared.py 共享件
|
||
|
||
从 blackboard_routes.py 提取到 shared.py:
|
||
|
||
| 符号 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `_validate_project()` | function | 项目 ID 校验 |
|
||
| `_bb()` | function | Blackboard 实例获取 |
|
||
| `_q()` | function | Queries 实例获取 |
|
||
| `_task_to_dict()` | function | Task → dict 序列化 |
|
||
| `_init_agent_ids()` | function | Agent ID 初始化 |
|
||
| `_extract_mentions()` | function | @mention 提取 |
|
||
| `VALID_STATUSES` | import | 从 db.py 重导出 |
|
||
| `OUTPUT_TYPES` | import | 从 db.py 重导出 |
|
||
|
||
### 2.4 main.py 路由注册变更
|
||
|
||
```python
|
||
# 拆分前
|
||
from src.api.blackboard_routes import router as blackboard_router
|
||
app.include_router(blackboard_router)
|
||
|
||
# 拆分后
|
||
from src.api.task_routes import router as task_router
|
||
from src.api.task_relation_routes import router as task_relation_router
|
||
app.include_router(task_router)
|
||
app.include_router(task_relation_router)
|
||
```
|
||
|
||
URL prefix 不变:所有路由仍是 `/api/projects/{pid}/...`,前端 URL 零改动。
|
||
|
||
### 2.5 向后兼容
|
||
|
||
- 删除 `blackboard_routes.py`,所有引用指向新文件
|
||
- `expand=all` 保持兼容(内部映射为全量 expand)
|
||
- 不改变任何 API 的请求/响应格式(仅文件组织变化)
|
||
|
||
---
|
||
|
||
## 3. expand 聚合接口
|
||
|
||
### 3.1 设计
|
||
|
||
`GET /api/projects/{pid}/tasks/{tid}?expand=comments,outputs,reviews,events,decisions`
|
||
|
||
支持逗号分隔的细粒度选择,替代当前的 `expand=all`。
|
||
|
||
### 3.2 返回格式
|
||
|
||
```json
|
||
{
|
||
"task": { "id": "...", "title": "...", "status": "working", ... },
|
||
"comments": {
|
||
"items": [...],
|
||
"total_count": 45,
|
||
"limit": 20
|
||
},
|
||
"events": {
|
||
"items": [...],
|
||
"total_count": 120,
|
||
"limit": 30
|
||
},
|
||
"outputs": [...],
|
||
"reviews": [...],
|
||
"decisions": [...]
|
||
}
|
||
```
|
||
|
||
### 3.3 limit 策略
|
||
|
||
| 关联资源 | expand 返回 | 分页支持 | 理由 |
|
||
|----------|------------|---------|------|
|
||
| comments | 最新 20 条 + total_count | `GET /comments?limit=50&offset=0` | 高频资源,长任务可能积累几十条 |
|
||
| events | 最新 30 条 + total_count | `GET /events?limit=100&offset=0` | 运行几天可能上百条 |
|
||
| outputs | 全部 | 不需要 | 通常 <5 条 |
|
||
| reviews | 全部 | 不需要 | 通常 <5 条 |
|
||
| decisions | 全部 | 不需要 | 通常 <5 条 |
|
||
|
||
前端拿到 `total_count > items.length` 时显示"还有 N 条",按需点击加载。
|
||
|
||
### 3.4 实现伪码
|
||
|
||
```python
|
||
@router.get("/tasks/{task_id}")
|
||
async def get_task(project_id: str, task_id: str,
|
||
expand: Optional[str] = None):
|
||
bb = _bb(project_id)
|
||
task = bb.get_task(task_id)
|
||
if not task:
|
||
raise HTTPException(404, f"Task not found: {task_id}")
|
||
|
||
result = _task_to_dict(task)
|
||
|
||
if not expand:
|
||
return result
|
||
|
||
expand_list = expand.split(",") if expand != "all" else [
|
||
"comments", "outputs", "reviews", "events", "decisions"
|
||
]
|
||
|
||
q = _q(project_id)
|
||
|
||
if "comments" in expand_list:
|
||
all_comments = bb.get_comments(task_id)
|
||
result["comments"] = {
|
||
"items": [dict(c.__dict__) for c in all_comments[-20:]],
|
||
"total_count": len(all_comments),
|
||
"limit": 20,
|
||
}
|
||
|
||
if "events" in expand_list:
|
||
all_events = q.task_events(task_id)
|
||
result["events"] = {
|
||
"items": all_events[-30:],
|
||
"total_count": len(all_events),
|
||
"limit": 30,
|
||
}
|
||
|
||
if "outputs" in expand_list:
|
||
result["outputs"] = [dict(o.__dict__) for o in bb.get_outputs(task_id)]
|
||
|
||
if "reviews" in expand_list:
|
||
result["reviews"] = [dict(r.__dict__) for r in bb.get_reviews(task_id)]
|
||
|
||
if "decisions" in expand_list:
|
||
result["decisions"] = [dict(d.__dict__) for d in bb.get_decisions(task_id)]
|
||
|
||
return result
|
||
```
|
||
|
||
### 3.5 性能分析
|
||
|
||
| 场景 | 当前(无 expand) | expand 后 | 改善 |
|
||
|------|-----------------|-----------|------|
|
||
| 打开 TaskModal | 5 次 HTTP 请求 | 2 次(task+expand + subtasks) | -60% 请求 |
|
||
| 单次 expand 响应体 | — | ~5-15KB(典型) | 一次大请求 < 五次小请求 |
|
||
| DB 查询次数 | 5 次(各端点独立查) | 5 次(expand 内部循环) | 相同,暂不优化 |
|
||
|
||
---
|
||
|
||
## 4. 任务列表搜索
|
||
|
||
### 4.1 设计
|
||
|
||
`GET /api/projects/{pid}/tasks?q=关键词`
|
||
|
||
在现有 `list_tasks` 基础上增加 `q` 查询参数,支持标题模糊搜索(SQL LIKE)。
|
||
|
||
### 4.2 实现
|
||
|
||
```python
|
||
@router.get("/tasks")
|
||
async def list_tasks(project_id: str, q: Optional[str] = None, ...):
|
||
bb = _bb(project_id)
|
||
tasks = bb.list_tasks(status=status, ...)
|
||
|
||
if q:
|
||
q_lower = q.lower()
|
||
tasks = [t for t in tasks if q_lower in (t.title or "").lower()]
|
||
|
||
return {"tasks": [_task_to_dict(t) for t in tasks]}
|
||
```
|
||
|
||
**设计决策**:过滤在 Python 层做而非 SQL 层。
|
||
- 理由:当前 `list_tasks` 已在 Python 层做 status 筛选,加一层 title 过滤一致性更好
|
||
- 如果后续任务量大(>1000),再改为 SQL LIKE 查询
|
||
|
||
---
|
||
|
||
## 5. 前端:工具链 Tab
|
||
|
||
### 5.1 Tab 定义
|
||
|
||
```typescript
|
||
// store.ts TabKey 新增
|
||
| 'toolchain'
|
||
|
||
// TAB_DEFS 新增(插在 settings 前面)
|
||
{ key: 'toolchain', label: '工具链', icon: '⛓️' },
|
||
```
|
||
|
||
### 5.2 数据加载
|
||
|
||
```typescript
|
||
// store.ts 新增
|
||
toolchainTasks: any[];
|
||
loadToolchain: async () => {
|
||
const res = await fetch('/api/projects/_toolchain/tasks');
|
||
const data = await res.json();
|
||
set({ toolchainTasks: data.tasks || [] });
|
||
}
|
||
|
||
// Tab 切换时加载
|
||
if (tab === 'toolchain') s.loadToolchain();
|
||
```
|
||
|
||
### 5.3 ToolchainPanel 组件
|
||
|
||
仿 MailPanel 结构,三个区域:
|
||
|
||
**搜索栏**(顶部):
|
||
- 文本输入框,输入关键词实时过滤列表
|
||
- 调用 `GET /api/projects/_toolchain/tasks?q=关键词`
|
||
|
||
**列表区**(左侧):
|
||
- 工具链事件列表(时间倒序)
|
||
- 每条显示:标题 + 时间 + 状态标签
|
||
- 点击选中,高亮当前选中项
|
||
|
||
**详情区**(右侧):
|
||
- 选中事件的完整内容
|
||
- 调用 `GET /api/projects/_toolchain/tasks/{tid}?expand=comments` 获取详情
|
||
- 展示:标题、描述、状态、评论(action_report 等)
|
||
|
||
### 5.4 和 Mail 的隔离
|
||
|
||
| 维度 | Mail Tab | 工具链 Tab |
|
||
|------|---------|-----------|
|
||
| 数据源 | `_mail` 项目 | `_toolchain` 项目 |
|
||
| 事件类型 | Agent 间通信(inform/request) | 系统事件(CI/PR/部署/Review) |
|
||
| 搜索 | 无(邮件量不大) | 有(工具链事件频率高) |
|
||
|
||
---
|
||
|
||
## 6. 测试设计
|
||
|
||
### 6.1 后端 API 拆分测试
|
||
|
||
**目标**:验证拆分后所有路由 URL 不变、行为不变。
|
||
|
||
**测试文件**:`tests/integration/test_api.py`(扩展现有)+ 新增 `tests/unit/test_task_routes.py`
|
||
|
||
| 测试类 | 测试用例 | 验证点 |
|
||
|--------|---------|--------|
|
||
| TestTaskRoutes | test_list_tasks | GET /tasks 返回格式不变 |
|
||
| | test_list_tasks_with_search | q 参数过滤正确 |
|
||
| | test_list_tasks_empty_q | q 为空时返回全部 |
|
||
| | test_get_task | GET /tasks/{tid} 基本详情 |
|
||
| | test_get_task_expand_comments | expand=comments 返回带 total_count + limit |
|
||
| | test_get_task_expand_events | expand=events 返回带 total_count + limit |
|
||
| | test_get_task_expand_outputs | expand=outputs 全量返回 |
|
||
| | test_get_task_expand_multiple | expand=comments,outputs,reviews 组合 |
|
||
| | test_get_task_expand_all | expand=all 向后兼容 |
|
||
| | test_get_task_no_expand | 不传 expand 返回基本 task |
|
||
| | test_create_task | POST 格式不变 |
|
||
| | test_claim_task | 认领行为不变 |
|
||
| | test_update_status | 状态流转不变 |
|
||
| | test_patch_task | PATCH 不变 |
|
||
| | test_archive_task | 归档不变 |
|
||
|
||
| 测试类 | 测试用例 | 验证点 |
|
||
|--------|---------|--------|
|
||
| TestTaskRelationRoutes | test_comments_crud | GET/POST comments 不变 |
|
||
| | test_outputs_crud | GET/POST outputs 不变 |
|
||
| | test_write_output_file | 文件写入逻辑不变 |
|
||
| | test_reviews_crud | GET/POST reviews 不变 |
|
||
| | test_decisions_crud | GET/POST decisions 不变 |
|
||
| | test_observations_add | POST observations 不变 |
|
||
| | test_events_list | GET events 不变 |
|
||
| | test_experiences_list | GET experiences 不变 |
|
||
| | test_project_events | GET /events 不变 |
|
||
| | test_summary | GET /summary 不变 |
|
||
|
||
**兼容性验证脚本**:
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# tests/scripts/verify_api_compat.sh
|
||
# 对比拆分前后所有路由 URL 和方法,确保零变化
|
||
|
||
echo "=== 拆分前路由清单 ==="
|
||
# 从 git stash 或 main 分支提取
|
||
git stash
|
||
python -c "
|
||
from src.main import app
|
||
for route in app.routes:
|
||
if hasattr(route, 'methods') and hasattr(route, 'path'):
|
||
for m in sorted(route.methods):
|
||
if m in ('GET','POST','PATCH','DELETE','PUT'):
|
||
print(f'{m} {route.path}')
|
||
" | sort > /tmp/routes_before.txt
|
||
|
||
git stash pop
|
||
|
||
echo "=== 拆分后路由清单 ==="
|
||
python -c "
|
||
from src.main import app
|
||
for route in app.routes:
|
||
if hasattr(route, 'methods') and hasattr(route, 'path'):
|
||
for m in sorted(route.methods):
|
||
if m in ('GET','POST','PATCH','DELETE','PUT'):
|
||
print(f'{m} {route.path}')
|
||
" | sort > /tmp/routes_after.txt
|
||
|
||
echo "=== Diff ==="
|
||
diff /tmp/routes_before.txt /tmp/routes_after.txt
|
||
if [ $? -eq 0 ]; then
|
||
echo "✅ 路由完全一致"
|
||
else
|
||
echo "❌ 路由有差异"
|
||
exit 1
|
||
fi
|
||
```
|
||
|
||
### 6.2 expand 聚合测试
|
||
|
||
**测试文件**:`tests/unit/test_expand_api.py`
|
||
|
||
| 测试用例 | 验证点 |
|
||
|---------|--------|
|
||
| test_expand_comments_limit | comments 返回最新 20 条 + total_count=25 |
|
||
| test_expand_comments_are_latest | 验证返回的是最新 20 条(index 5-24) |
|
||
| test_expand_events_limit | events 返回最新 30 条 + total_count=35 |
|
||
| test_expand_outputs_full | outputs 全量返回(list 格式,不分页) |
|
||
| test_expand_reviews_full | reviews 全量返回 |
|
||
| test_expand_decisions_full | decisions 全量返回 |
|
||
| test_expand_multiple_fields | expand=comments,outputs,reviews 组合,未请求的不返回 |
|
||
| test_expand_all_compat | expand=all 向后兼容 |
|
||
| test_no_expand | 不传 expand 只返回基本 task |
|
||
| test_expand_invalid_field_ignored | 无效字段静默忽略 |
|
||
|
||
### 6.3 搜索测试
|
||
|
||
**测试文件**:`tests/unit/test_task_routes.py` 内 `TestTaskListRoutes`
|
||
|
||
| 测试用例 | 验证点 |
|
||
|---------|--------|
|
||
| test_list_tasks_with_search | q 参数标题模糊搜索 |
|
||
| test_list_tasks_search_case_insensitive | 大小写不敏感 |
|
||
| test_list_tasks_search_no_match | 无匹配返回空列表 |
|
||
| test_list_tasks_search_empty_q | q 为空返回全部 |
|
||
|
||
### 6.4 前端测试(手动验证)
|
||
|
||
| 验证点 | 操作 | 预期 |
|
||
|--------|------|------|
|
||
| 工具链 Tab 出现 | 打开前端 | Tab 栏有 ⛓️ 工具链 |
|
||
| 列表加载 | 点击工具链 Tab | 显示 _toolchain 事件列表 |
|
||
| 搜索过滤 | 输入关键词 | 列表实时过滤 |
|
||
| 详情展示 | 点击某条事件 | 右侧/弹窗显示完整内容 |
|
||
| Tab 切换不丢数据 | 切到其他 Tab 再回来 | 数据保持 |
|
||
|
||
### 6.5 CI 集成
|
||
|
||
| 命令 | 说明 |
|
||
|------|------|
|
||
| `bash tests/scripts/verify_api_compat.sh` | 路由兼容性验证(CI 必跑) |
|
||
| `pytest tests/unit/test_task_routes.py tests/unit/test_expand_api.py tests/integration/test_api.py -m "not e2e" -v` | 新增单元 + 集成测试 |
|
||
|
||
> 测试用例详细设计(fixture + 完整代码 + 覆盖矩阵)见 `docs/design/18-test-design.md`
|
||
|
||
---
|
||
|
||
## 7. 实施计划
|
||
|
||
### Phase 1: 后端 API 拆分(不含功能变更)
|
||
|
||
| 步骤 | 内容 | 验证 |
|
||
|------|------|------|
|
||
| 1.1 | 创建 `shared.py`,提取共享 helper | import 无报错 |
|
||
| 1.2 | 创建 `task_routes.py`,迁移 10 个路由 | 路由注册成功 |
|
||
| 1.3 | 创建 `task_relation_routes.py`,迁移 13 个路由 | 路由注册成功 |
|
||
| 1.4 | 更新 `main.py` router 注册 | app 启动无报错 |
|
||
| 1.5 | 删除 `blackboard_routes.py` | — |
|
||
| 1.6 | 运行 `verify_api_compat.sh` | 路由清单 diff = 0 |
|
||
| 1.7 | 运行现有测试 | 全量通过 |
|
||
|
||
### Phase 2: expand 聚合 + 搜索
|
||
|
||
| 步骤 | 内容 | 验证 |
|
||
|------|------|------|
|
||
| 2.1 | 重写 `get_task` expand 逻辑(细粒度) | TestExpandAPI 通过 |
|
||
| 2.2 | `list_tasks` 加 `q` 参数 | TestTaskSearch 通过 |
|
||
| 2.3 | 新增测试用例 | 覆盖率达标 |
|
||
|
||
### Phase 3: 前端工具链 Tab
|
||
|
||
| 步骤 | 内容 | 验证 |
|
||
|------|------|------|
|
||
| 3.1 | store.ts 新增 toolchain 数据加载 | — |
|
||
| 3.2 | api.ts 新增 expand 调用封装 | — |
|
||
| 3.3 | 创建 `ToolchainPanel.tsx` | 组件渲染正常 |
|
||
| 3.4 | App.tsx 注册新 Tab | Tab 显示正确 |
|
||
| 3.5 | TaskModal 改用 expand 减少 | 请求次数减少 |
|
||
|
||
### Phase 4: 联调 + 评审
|
||
|
||
| 步骤 | 内容 |
|
||
|------|------|
|
||
| 4.1 | 全量测试 `pytest -m "not e2e"` |
|
||
| 4.2 | 发评审给司马懿 |
|
||
| 4.3 | 前端手动验证 |
|
||
|
||
---
|
||
|
||
## 8. 风险评估
|
||
|
||
| 风险 | 级别 | 缓解 |
|
||
|------|------|------|
|
||
| 拆分后 import 路径断裂 | 中 | IDE 全局搜索 + 运行时验证 |
|
||
| expand 返回体过大 | 低 | comments/events 有 limit |
|
||
| 工具链事件量大影响前端 | 低 | 搜索栏 + 分页 |
|
||
| expand=all 向后兼容 | 低 | 单独兼容分支处理 |
|
||
|
||
---
|
||
|
||
## 9. 评审记录
|
||
|
||
### 司马懿 mail-1781415763066(2026-06-14)
|
||
|
||
| 项目 | 结论 |
|
||
|------|------|
|
||
| 文件拆分 | 方案 B 调整版(task_routes + task_relation_routes + shared) |
|
||
| expand | 细粒度,events/comments 带 limit+total_count |
|
||
| 性能 | 当前 SQLite 多次查询可接受 |
|
||
| checkpoint | 不纳入 |
|
||
| _generate_title | 留在 task_routes.py |
|
||
| write_output | 注意不是简单 CRUD |
|
||
|
||
---
|
||
|
||
## 10. 变更记录
|
||
|
||
| 日期 | 版本 | 内容 |
|
||
|------|------|------|
|
||
| 2026-06-14 | v1.0 | 初版设计 |
|