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

17 KiB
Raw Blame History

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.py10 个路由):

路由 方法 函数 说明
/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.py13 个路由):

路由 方法 函数 说明
/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.pyTestTaskListRoutes

测试用例 验证点
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_tasksq 参数 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 初版设计