Files
sanguo_moziplus_v2/docs/design/18-api-refactor-and-toolchain-tab.md
T
cfdaily 33521b8b39
CI / lint (pull_request) Successful in 7s
CI / test (pull_request) Successful in 27s
CI / notify-on-failure (pull_request) Successful in 0s
[moz] docs: §18 职责分离 — 测试详细代码移入 18-test-design.md
- 主文档 §6 只保留概要表格 + 文件指向
- 测试 fixture/完整代码/覆盖矩阵 → 18-test-design.md
- 删除误加的 GATE/委派/wiki 章节
- CI 集成改为表格格式 + 引用
2026-06-14 13:56:47 +08:00

504 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-17814157630662026-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 | 初版设计 |