- 主文档 §6 只保留概要表格 + 文件指向 - 测试 fixture/完整代码/覆盖矩阵 → 18-test-design.md - 删除误加的 GATE/委派/wiki 章节 - CI 集成改为表格格式 + 引用
17 KiB
API 聚合重构 + 工具链 Tab 设计
编号: §18 状态: 设计中 日期: 2026-06-14 作者: 庞统(副军师)🐦 审查: 司马懿(mail-1781415763066 已回复,方案 B 调整版确认)
1. 背景与目标
1.1 问题
- blackboard_routes.py 膨胀:572 行、22 个路由,task/comment/output/review/event/decision/observation/archive 全堆一个文件,维护困难
- 前端 N+1 请求:打开 TaskModal 需要 5 次独立请求(task + events + subtasks + progress + comments),影响前端性能
- 工具链事件无前端展示:
_toolchainDB 隔离已完成,但前端无对应 Tab,工具链事件只能通过 Agent 收 Mail 感知
1.2 目标
- 按领域拆分 blackboard_routes.py → 3 个文件
- 实现细粒度 expand 聚合接口,前端 1-2 次请求拿全任务详情
- 新增工具链 Tab(列表 + 详情 + 搜索栏)
- 任务列表支持标题搜索
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 路由注册变更
# 拆分前
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 返回格式
{
"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 实现伪码
@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 实现
@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 定义
// store.ts TabKey 新增
| 'toolchain'
// TAB_DEFS 新增(插在 settings 前面)
{ key: 'toolchain', label: '工具链', icon: '⛓️' },
5.2 数据加载
// 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 不变 |
兼容性验证脚本:
#!/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 | 初版设计 |