# 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 | 初版设计 |