diff --git a/docs/design/v2.7-three-tier-hierarchy.md b/docs/design/v2.7-three-tier-hierarchy.md index e2be0f5..a41d736 100644 --- a/docs/design/v2.7-three-tier-hierarchy.md +++ b/docs/design/v2.7-three-tier-hierarchy.md @@ -1,8 +1,8 @@ # v2.7 三级层次结构设计:Project / Card / Task > 日期:2026-05-17 -> 作者:庞统 + 司马懿(共识方案) -> 状态:待用户确认 +> 作者:庞统 + 司马懿(共识)→ 用户确认逻辑隔离方案 +> 状态:待用户确认最终版 --- @@ -27,112 +27,143 @@ Project(项目) | **Card** | 独立的工作流/策略 | 动量策略v1、数据平台P3 | 中期(天/周) | | **Task** | 具体执行单元 | 编写回测脚本、清洗分钟线 | 短期(小时/天) | -### 1.2 Card = DB 隔离边界 +### 1.2 隔离策略:一个 Project 一个 DB + card_id 逻辑隔离 -**双方共识,司马懿论证:** +**物理隔离**(一 Card 一 DB)vs **逻辑隔离**(一 Project 一 DB + card_id 字段): -- ❌ Project = DB:sanguo_quant_live 会有几十个策略,几百个 task + outputs + events 混一起,Ticker 扫描爆炸,Agent prompt 噪音大 -- ❌ Task = DB:太碎,同策略的任务有 outputs 引用和 depends_on 依赖,拆 DB 失去事务一致性 -- ✅ **Card = DB**:一个策略的所有 task 在同一个 SQLite,事务一致、JOIN 快、黑板上下文干净 +| 维度 | 物理隔离 | 逻辑隔离 ✅ 选定 | +|------|---------|----------------| +| 业界参考 | 无主流先例 | Jira(单表+类型字段)、Linear(单DB+team_id)、Asana(单DB+workspace_id) | +| DB 文件数 | Card 数量级(可能上百) | Project 数量级(个位数) | +| 跨 Card 查询 | 不可能(跨库) | 天然支持(同一 DB) | +| 事务 | Card 内完整,跨 Card 不可能 | Project 内任意事务 | +| 归档 | 关闭连接即可 | 按 card_id UPDATE status | +| Agent prompt | 天然干净 | 查询时加 WHERE card_id=? 过滤 | +| 复杂度 | DB 文件管理复杂 | 所有表加一列,查询统一 | +| 备份 | 按 Card 备份(碎片化) | 按 Project 备份(完整) | -**调研支撑(Context Folding / ByteDance 2025):** -> Card 完成后,整个黑板的详细数据"折叠"为摘要,只保留对上层有用的结论。这天然对应 Card 级别的归档。 +**选逻辑隔离的理由:** +1. **优秀实践一致**:Jira/Linear/Asana 都用逻辑隔离,说明这是成熟模式 +2. **调研报告建议**(5.4 节):"单实例 + Card 级命名空间" +3. **跨 Card 查询有需求**:用户看 Project 概览时需要聚合所有 Card 的 tasks +4. **运维简单**:一个 Project 一个文件,备份/迁移/清理都方便 +5. **Card 数量可控**:Ticker 查询 `WHERE card_id IN (active_cards)` 性能无问题 -### 1.3 归档/恢复 — 只改 metadata,不移动文件 +### 1.3 归档/恢复 — 只改 metadata -**双方共识:** - -- cards 的 status 字段:`active` / `closed` / `archived` / `deleted` -- 归档 = 改 status → Ticker 跳过 → DB 不再被打开 → 内存释放 -- 恢复 = 改回 active -- 磁盘不够时手动脚本批量移 archived cards 到冷存储 -- 删除 = status=deleted + 7 天后异步清理文件 +- cards 表 status 字段:`active` / `closed` / `archived` / `deleted` +- 归档 = `UPDATE cards SET status='archived'` → Ticker 跳过 → 不 dispatch 新任务 +- 恢复 = `UPDATE cards SET status='active'` +- 删除 = status=deleted + 7 天后异步清理关联数据 ### 1.4 跨 Card 依赖 — 不支持 -**双方共识:** - -- 打破 DB 边界 = 跨库事务 = SQLite 做不了 -- 引入分布式依赖 = 一堆 edge case -- 业务上策略之间应该独立;如果策略B 用策略A 的数据,那是数据层面的事,不是编排层面的事 +- 业务上策略之间应该独立 +- 如果策略B 用策略A 的数据,那是数据层面的事(artifacts 路径引用),不是编排层面的事 --- ## 二、数据模型 -### 2.1 Registry DB(全局唯一,管理 Project + Card 元数据) +### 2.1 Registry DB(全局唯一,管理 Project 列表) 路径:`data/registry.db` ```sql CREATE TABLE IF NOT EXISTS projects ( - id TEXT PRIMARY KEY, -- 项目标识,如 'sanguo_quant_live' - name TEXT NOT NULL, -- 显示名称 + id TEXT PRIMARY KEY, + name TEXT NOT NULL, description TEXT DEFAULT '', - status TEXT DEFAULT 'active', -- active/archived/deleted - config_json TEXT DEFAULT '{}', -- 项目级配置(agents、routing 等) + status TEXT DEFAULT 'active', -- active/archived/deleted + config_json TEXT DEFAULT '{}', -- agents、routing 等项目级配置 created_at TEXT NOT NULL, updated_at TEXT ); +``` +**职责**:只管 Project 列表 + 指向 DB 文件路径。Card 信息在 Project DB 内部管理。 + +### 2.2 Project DB(per-project,逻辑隔离) + +路径:`data/{pid}/blackboard.db` + +**新增 cards 表:** + +```sql CREATE TABLE IF NOT EXISTS cards ( - id TEXT PRIMARY KEY, -- 卡片标识,如 'strategy-momentum-v1' - project_id TEXT NOT NULL REFERENCES projects(id), + id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT DEFAULT '', - status TEXT DEFAULT 'active', -- active/closed/archived/deleted - db_path TEXT NOT NULL, -- 相对路径,如 'sanguo_quant_live/cards/strategy-momentum-v1/blackboard.db' - card_type TEXT DEFAULT 'default', -- default/strategy/data_pipeline/research - labels_json TEXT DEFAULT '[]', -- 标签 + status TEXT DEFAULT 'active', -- active/closed/archived/deleted + card_type TEXT DEFAULT 'default', -- default/strategy/data_pipeline/research + labels_json TEXT DEFAULT '[]', created_at TEXT NOT NULL, updated_at TEXT, - archived_at TEXT, - FOREIGN KEY (project_id) REFERENCES projects(id) + archived_at TEXT ); -CREATE INDEX IF NOT EXISTS idx_cards_project ON cards(project_id); CREATE INDEX IF NOT EXISTS idx_cards_status ON cards(status); ``` -### 2.2 Card DB(per-card,当前 blackboard.db 不变) +**现有所有表加 card_id 字段:** -每个 Card 独立一个 SQLite,内含当前所有表(tasks、outputs、reviews、comments、events、task_attempts、routing_decisions)。 +```sql +-- tasks 表 +ALTER TABLE tasks ADD COLUMN card_id TEXT; +CREATE INDEX IF NOT EXISTS idx_tasks_card ON tasks(card_id); -**无任何表结构变更**——Task/Output/Review 等模型完全不变。 +-- outputs 表 +ALTER TABLE outputs ADD COLUMN card_id TEXT; +CREATE INDEX IF NOT EXISTS idx_outputs_card ON outputs(card_id); -### 2.3 目录结构 +-- reviews 表 +ALTER TABLE reviews ADD COLUMN card_id TEXT; + +-- comments 表 +ALTER TABLE comments ADD COLUMN card_id TEXT; + +-- events 表 +ALTER TABLE events ADD COLUMN card_id TEXT; +CREATE INDEX IF NOT EXISTS idx_events_card ON events(card_id); + +-- task_attempts 表 +ALTER TABLE task_attempts ADD COLUMN card_id TEXT; + +-- routing_decisions 表 +ALTER TABLE routing_decisions ADD COLUMN card_id TEXT; +``` + +**DDL 变更策略:** +- CREATE TABLE 语句直接包含 card_id(新 DB) +- 旧 DB 通过 `_migrate_v27()` 添加列 + 创建默认 Card + 回填 card_id +- 每个查询都加 WHERE card_id=? 过滤 + +### 2.3 默认 Card + +迁移策略:每个旧 DB 自动创建一个默认 Card(id=`default`),将所有现有数据的 card_id 设为 `default`。 + +新建 Project 时也自动创建一个默认 Card,用户可以直接在默认 Card 上工作(不感知 Card 层),也可以手动创建新 Card。 + +### 2.4 目录结构 ``` data/ -├── registry.db ← 全局注册表 -├── sanguo_quant_live/ ← Project 目录 -│ ├── cards/ -│ │ ├── strategy-momentum-v1/ -│ │ │ ├── blackboard.db ← Card 级黑板 -│ │ │ └── artifacts/ ← 产出物文件 -│ │ ├── strategy-mean-reversion/ -│ │ │ ├── blackboard.db -│ │ │ └── artifacts/ -│ │ └── ... -│ └── project.json ← 项目级配置(可选) +├── registry.db ← Project 列表 +├── sanguo_quant_live/ +│ ├── blackboard.db ← Project 级 DB(含 cards 表 + card_id 隔离) +│ └── artifacts/ ← 产出物文件 +│ ├── strategy-momentum-v1/ +│ ├── strategy-mean-reversion/ +│ └── default/ ├── sanguo_vnpy/ -│ └── cards/ -│ └── ... -└── inbox/ ← 快捷创建(单 Card 项目) +│ ├── blackboard.db +│ └── artifacts/ +└── inbox/ ← 快捷单 Card 项目 └── fetch-data-001/ ├── blackboard.db └── artifacts/ ``` -### 2.4 迁移映射 - -| 当前 | 迁移后 | -|------|--------| -| `data/demo/blackboard.db` | `data/_migration/demo/default/blackboard.db`(默认 Card) | -| `data/projects/blackboard.db` | `data/_migration/projects/default/blackboard.db` | -| `data/e2e-routing-test/blackboard.db` | `data/_migration/e2e-routing-test/default/blackboard.db` | -| `data/_registry.yaml` | `data/registry.db`(projects + cards 表) | - --- ## 三、API 路由变更 @@ -141,36 +172,39 @@ data/ ``` # Project 级 -GET /api/projects → 列出所有项目 -POST /api/projects → 创建项目 -GET /api/projects/{pid} → 项目详情 -PATCH /api/projects/{pid} → 更新项目配置 -DELETE /api/projects/{pid} → 删除项目 +GET /api/projects → 列出所有项目 +POST /api/projects → 创建项目(自动创建默认 Card) +GET /api/projects/{pid} → 项目详情 +PATCH /api/projects/{pid} → 更新项目配置 +DELETE /api/projects/{pid} → 删除项目 # Card 级 -GET /api/projects/{pid}/cards → 列出项目下所有 Card -POST /api/projects/{pid}/cards → 创建 Card -GET /api/projects/{pid}/cards/{cid} → Card 详情 -PATCH /api/projects/{pid}/cards/{cid} → 更新 Card(改名/归档/恢复) -DELETE /api/projects/{pid}/cards/{cid} → 删除 Card +GET /api/projects/{pid}/cards → 列出项目下所有 Card +POST /api/projects/{pid}/cards → 创建 Card +GET /api/projects/{pid}/cards/{cid} → Card 详情(含聚合统计) +PATCH /api/projects/{pid}/cards/{cid} → 更新 Card(改名/归档/恢复) +DELETE /api/projects/{pid}/cards/{cid} → 删除 Card # Task 级(嵌套在 Card 下) -GET /api/projects/{pid}/cards/{cid}/tasks → 列出 Card 下所有 Task -POST /api/projects/{pid}/cards/{cid}/tasks → 创建 Task -GET /api/projects/{pid}/cards/{cid}/tasks/{tid} → Task 详情 -POST /api/projects/{pid}/cards/{tid}/status → 更新状态 -POST /api/projects/{pid}/cards/{tid}/outputs → 添加产出 -POST /api/projects/{pid}/cards/{tid}/reviews → 添加审查 -POST /api/projects/{pid}/cards/{tid}/comments → 添加评论 +GET /api/projects/{pid}/cards/{cid}/tasks → 列出 Card 下所有 Task +POST /api/projects/{pid}/cards/{cid}/tasks → 创建 Task +GET /api/projects/{pid}/cards/{cid}/tasks/{tid} → Task 详情 +POST /api/projects/{pid}/cards/{cid}/tasks/{tid}/status → 更新状态 +POST /api/projects/{pid}/cards/{cid}/tasks/{tid}/outputs → 添加产出 +POST /api/projects/{pid}/cards/{cid}/tasks/{tid}/reviews → 添加审查 +POST /api/projects/{pid}/cards/{cid}/tasks/{tid}/comments → 添加评论 +GET /api/projects/{pid}/cards/{cid}/events → Card 事件流 -# 便利路由(前端兼容) -GET /api/projects/{pid}/tasks → 遍历该 project 所有 active cards 的 tasks +# 便利路由(前端兼容/项目概览) +GET /api/projects/{pid}/tasks → 该 Project 所有 active cards 的 tasks(合并) +GET /api/projects/{pid}/events → 该 Project 所有 active cards 的事件(合并) ``` ### 3.2 向后兼容 -- `GET /api/projects/{pid}/tasks` 保留,内部遍历该 project 的所有 active cards 合并返回 -- 前端迁移期间,旧 API 继续工作 +- `GET /api/projects/{pid}/tasks` 保留,内部查所有 active cards 的 tasks +- 旧前端不传 card_id 时,自动使用 `default` Card +- 渐进迁移:前端可以先不感知 Card 层,按需升级 --- @@ -179,121 +213,157 @@ GET /api/projects/{pid}/tasks → 遍历该 project 所有 activ ### 4.1 当前逻辑 ```python -# 遍历 registry 中的所有 project,每个 project 打开一个 DB -for project_id, project_info in registry.projects.items(): +for project_id in registry.projects: db_path = data_root / project_id / "blackboard.db" - # _tick_project(db_path, ...) + _tick_project(db_path, ...) ``` ### 4.2 变更后 ```python -# 从 registry.db 查所有 status=active 的 cards -for card in registry.get_active_cards(project_id): - db_path = data_root / card.db_path - # _tick_card(card, db_path, ...) +for project_id in registry.list_projects(status='active'): + db_path = data_root / project_id / "blackboard.db" + # 查出所有 active cards + active_cards = queries.list_cards(db_path, status='active') + for card in active_cards: + _tick_card(db_path, card.id, ...) ``` ### 4.3 性能 -- SQLite 打开/关闭很快(ms 级) -- 如果 active cards > 50,分批扫描(per-tick 限制 max_cards_per_tick=20) -- Ticker 只扫 active cards,closed/archived 完全不打开 DB +- 单 DB 内按 card_id 查询,SQLite 索引命中,微秒级 +- 无打开/关闭多个 DB 的开销 +- active cards 过多时加 `LIMIT` 分批 --- -## 五、前端变更 +## 五、Blackboard 操作变更 -### 5.1 左侧导航 +### 5.1 现有接口签名变更 + +所有 Blackboard/Queries 方法加 `card_id` 参数: + +```python +# Before +bb.create_task(task) + +# After +bb.create_task(task, card_id="strategy-momentum-v1") + +# Before +q.tasks_by_status("pending") + +# After +q.tasks_by_status("pending", card_id="strategy-momentum-v1") +``` + +### 5.2 Card 管理 + +```python +class CardOps: + """Card CRUD(操作 Project DB 内的 cards 表)""" + def create_card(self, db_path, card_id, name, card_type="default") -> dict + def get_card(self, db_path, card_id) -> dict | None + def list_cards(self, db_path, status=None) -> list[dict] + def update_card(self, db_path, card_id, **kwargs) -> bool + def archive_card(self, db_path, card_id) -> bool + def restore_card(self, db_path, card_id) -> bool + def delete_card(self, db_path, card_id) -> bool + + def card_stats(self, db_path, card_id) -> dict: + """聚合统计:task数、完成率、活跃agent等""" +``` + +--- + +## 六、路由方案兼容性 + +**完全兼容:** + +- routing_decisions 表加 card_id,审计按 Card 隔离 +- current_agent / previous_agent 在 task 级别,通过 card_id 自然隔离 +- agent_profiles 和 routing 配置在 Project 级(registry.db 的 config_json 或项目级配置文件) +- AgentRouter 实例 per-Project,Card 只是查询过滤条件 + +--- + +## 七、前端变更 + +### 7.1 导航结构 ``` 当前: Project List → Task List 变更: Project List → Card List → Task List ``` -### 5.2 新增 CardList 组件 +### 7.2 新增组件 -- 卡片式展示每个 Card(名称、状态、任务数、进度) -- 新建 Card 按钮 -- 归档/恢复操作 +- **CardList**:卡片式展示(名称、状态 badge、task 进度条、操作按钮) +- **CardDetail**:单 Card 概览(聚合统计 + 事件流) +- 新建 Card 对话框 -### 5.3 TaskList 适配 +### 7.3 便利模式 -- 切换 Card 时重新加载 Task 列表 -- 便利模式:显示当前 Project 所有 active cards 的 tasks(按 card 分组) +Project 概览页:展示所有 active cards 的 tasks 合并视图(按 Card 分组折叠) + +### 7.4 渐进迁移 + +- Phase 1:后端支持 Card,前端不感知(所有操作走 default Card) +- Phase 2:前端加 CardList 导航 +- Phase 3:Card 模板、归档管理、可视化 --- -## 六、Registry API +## 八、数据迁移 + +### 8.1 迁移脚本 ```python -class ProjectRegistry: - """管理 registry.db 中的 projects + cards""" +def migrate_v27(db_path: Path): + """将旧 DB 升级到 v2.7 三级结构""" + # 1. 创建 cards 表 + # 2. 所有表加 card_id 列 + # 3. 创建默认 Card(id='default') + # 4. 回填所有现有数据的 card_id = 'default' + # 5. 创建索引 +``` - def create_project(self, project_id: str, name: str, ...) -> dict - def get_project(self, project_id: str) -> dict | None - def list_projects(self, status: str = None) -> list[dict] - def update_project(self, project_id: str, **kwargs) -> bool +### 8.2 Registry 迁移 - def create_card(self, card_id: str, project_id: str, name: str, ...) -> dict - def get_card(self, card_id: str) -> dict | None - def list_cards(self, project_id: str, status: str = None) -> list[dict] - def get_active_cards(self, project_id: str) -> list[dict] - def update_card(self, card_id: str, **kwargs) -> bool - def archive_card(self, card_id: str) -> bool - def restore_card(self, card_id: str) -> bool +```python +def migrate_registry(data_root: Path): + """将 _registry.yaml 迁移为 registry.db""" + # 读 _registry.yaml → 写 registry.db projects 表 + # 每个 project 目录下已有 blackboard.db,路径不变 ``` --- -## 七、路由方案兼容性 - -**完全兼容**(司马懿 Mail #289 确认): - -- routing_decisions 表在 Card 级 DB 内(每个 Card 独立审计) -- current_agent / previous_agent 在 task 级别,自然在 Card DB 内 -- agent_profiles 和 routing 配置在 Project 级(config_json 或 project.yaml) -- 唯一变化:Ticker 扫描从"per-project"变为"per-card" - ---- - -## 八、实施计划 +## 九、实施计划 ### v2.7.0 范围 -| 序号 | 任务 | 预计工作量 | -|------|------|-----------| +| 序号 | 任务 | 预计 | +|------|------|------| | 1 | registry.db + ProjectRegistry 类 | 0.5 天 | -| 2 | 数据迁移脚本(旧 project → Card) | 0.5 天 | -| 3 | API 路由变更(新增 Card 层) | 1 天 | -| 4 | Ticker 适配(per-card 扫描) | 0.5 天 | -| 5 | 前端 CardList + 导航适配 | 1 天 | -| 6 | 端到端测试 | 0.5 天 | -| **合计** | | **4 天** | +| 2 | cards 表 + 所有表加 card_id + 迁移 | 0.5 天 | +| 3 | CardOps + Blackboard/Queries 接口加 card_id | 1 天 | +| 4 | API 路由(Card CRUD + Task 嵌套) | 1 天 | +| 5 | Ticker 适配(per-card 扫描) | 0.5 天 | +| 6 | 前端 CardList + 导航 | 1 天 | +| 7 | 端到端测试 + 迁移验证 | 0.5 天 | +| **合计** | | **5 天** | ### 前置条件 -- ✅ v2.6.1 路由方案已落地并验证 -- ✅ 司马懿代码评审通过 -- ✅ E2E 验证通过(review 正确路由到司马懿) +- ✅ v2.6.1 路由方案已落地并 E2E 验证 -### 不在 v2.7.0 范围内 +### 不在 v2.7.0 范围 - 跨 Card 依赖(不支持) -- Card 级向量检索/摘要(Phase 2) -- Card 模板(Phase 3) -- 冷存储迁移脚本(按需) - ---- - -## 九、风险评估 - -| 风险 | 影响 | 缓解 | -|------|------|------| -| 数据迁移丢失 | 高 | 迁移脚本 + 旧数据备份 + 回滚方案 | -| 前端三层导航体验复杂 | 中 | 保留便利 API,前端渐进迁移 | -| active cards 过多导致 Ticker 慢 | 低 | max_cards_per_tick 限制 + 分批 | -| registry.db 并发写冲突 | 低 | WAL + busy_timeout + 单 Ticker 写入 | +- Card 模板 +- 向量检索/摘要(Phase 2) +- 冷存储迁移脚本 --- @@ -301,7 +371,8 @@ class ProjectRegistry: | 调研发现 | 方案对齐 | |---------|---------| -| Jira:单表 + 类型字段 + project_id 隔离 | Task 保持单表,Card 做 DB 隔离(比 Jira 粒度更细) | -| LangGraph:Subgraph 独立 State + 映射回传 | Card 独立黑板,未来 Card 完成后摘要写入 Project 级 | +| Jira:单表 + 类型字段 + project_id 隔离 | ✅ 单 DB + card_id 字段,完全一致 | +| Linear:单DB + team_id 过滤 | ✅ 单 DB + card_id 过滤,完全一致 | +| LangGraph:Subgraph 独立 State | Card 是查询隔离,非物理隔离 | | Context Folding:子任务折叠为摘要 | Card 归档时折叠,保留元数据索引 | -| Hot/Cold Memory 分离 | active=热、closed=温、archived=冷,按需加载 | +| Hot/Cold Memory 分离 | active=热、archived=冷,按需加载 |