Files
sanguo_moziplus_v2/docs/design/v2.7-three-tier-hierarchy.md
T
2026-05-17 22:11:22 +08:00

10 KiB
Raw Blame History

v2.7 三级层次结构设计:Project / Card / Task

日期:2026-05-17 作者:庞统 + 司马懿(共识方案) 状态:待用户确认


一、核心设计决策

1.1 三层定义

Project(项目)
  └── Card(卡片/策略)
        ├── Task(任务)
        ├── Output(产出)
        ├── Review(审查)
        ├── Comment(评论)
        ├── Event(事件)
        └── routing_decisions(路由审计)
层级 语义 例子 生命周期
Project 业务领域/产品线 sanguo_quant_live、sanguo_vnpy 长期(月/年)
Card 独立的工作流/策略 动量策略v1、数据平台P3 中期(天/周)
Task 具体执行单元 编写回测脚本、清洗分钟线 短期(小时/天)

1.2 Card = DB 隔离边界

双方共识,司马懿论证:

  • Project = DBsanguo_quant_live 会有几十个策略,几百个 task + outputs + events 混一起,Ticker 扫描爆炸,Agent prompt 噪音大
  • Task = DB:太碎,同策略的任务有 outputs 引用和 depends_on 依赖,拆 DB 失去事务一致性
  • Card = DB:一个策略的所有 task 在同一个 SQLite,事务一致、JOIN 快、黑板上下文干净

调研支撑(Context Folding / ByteDance 2025):

Card 完成后,整个黑板的详细数据"折叠"为摘要,只保留对上层有用的结论。这天然对应 Card 级别的归档。

1.3 归档/恢复 — 只改 metadata,不移动文件

双方共识:

  • cards 的 status 字段:active / closed / archived / deleted
  • 归档 = 改 status → Ticker 跳过 → DB 不再被打开 → 内存释放
  • 恢复 = 改回 active
  • 磁盘不够时手动脚本批量移 archived cards 到冷存储
  • 删除 = status=deleted + 7 天后异步清理文件

1.4 跨 Card 依赖 — 不支持

双方共识:

  • 打破 DB 边界 = 跨库事务 = SQLite 做不了
  • 引入分布式依赖 = 一堆 edge case
  • 业务上策略之间应该独立;如果策略B 用策略A 的数据,那是数据层面的事,不是编排层面的事

二、数据模型

2.1 Registry DB(全局唯一,管理 Project + Card 元数据)

路径:data/registry.db

CREATE TABLE IF NOT EXISTS projects (
    id          TEXT PRIMARY KEY,   -- 项目标识,如 'sanguo_quant_live'
    name        TEXT NOT NULL,      -- 显示名称
    description TEXT DEFAULT '',
    status      TEXT DEFAULT 'active',  -- active/archived/deleted
    config_json TEXT DEFAULT '{}',      -- 项目级配置(agents、routing 等)
    created_at  TEXT NOT NULL,
    updated_at  TEXT
);

CREATE TABLE IF NOT EXISTS cards (
    id          TEXT PRIMARY KEY,   -- 卡片标识,如 'strategy-momentum-v1'
    project_id  TEXT NOT NULL REFERENCES projects(id),
    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 '[]',      -- 标签
    created_at  TEXT NOT NULL,
    updated_at  TEXT,
    archived_at TEXT,
    FOREIGN KEY (project_id) REFERENCES projects(id)
);

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 DBper-card,当前 blackboard.db 不变)

每个 Card 独立一个 SQLite,内含当前所有表(tasks、outputs、reviews、comments、events、task_attempts、routing_decisions)。

无任何表结构变更——Task/Output/Review 等模型完全不变。

2.3 目录结构

data/
├── registry.db                          ← 全局注册表
├── sanguo_quant_live/                   ← Project 目录
│   ├── cards/
│   │   ├── strategy-momentum-v1/
│   │   │   ├── blackboard.db           ← Card 级黑板
│   │   │   └── artifacts/              ← 产出物文件
│   │   ├── strategy-mean-reversion/
│   │   │   ├── blackboard.db
│   │   │   └── artifacts/
│   │   └── ...
│   └── project.json                    ← 项目级配置(可选)
├── sanguo_vnpy/
│   └── cards/
│       └── ...
└── 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.dbprojects + cards 表)

三、API 路由变更

3.1 新路由

# Project 级
GET    /api/projects                          → 列出所有项目
POST   /api/projects                          → 创建项目
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

# 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}/tasks              → 遍历该 project 所有 active cards 的 tasks

3.2 向后兼容

  • GET /api/projects/{pid}/tasks 保留,内部遍历该 project 的所有 active cards 合并返回
  • 前端迁移期间,旧 API 继续工作

四、Ticker 变更

4.1 当前逻辑

# 遍历 registry 中的所有 project,每个 project 打开一个 DB
for project_id, project_info in registry.projects.items():
    db_path = data_root / project_id / "blackboard.db"
    # _tick_project(db_path, ...)

4.2 变更后

# 从 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, ...)

4.3 性能

  • SQLite 打开/关闭很快(ms 级)
  • 如果 active cards > 50,分批扫描(per-tick 限制 max_cards_per_tick=20
  • Ticker 只扫 active cardsclosed/archived 完全不打开 DB

五、前端变更

5.1 左侧导航

当前:  Project List → Task List
变更:  Project List → Card List → Task List

5.2 新增 CardList 组件

  • 卡片式展示每个 Card(名称、状态、任务数、进度)
  • 新建 Card 按钮
  • 归档/恢复操作

5.3 TaskList 适配

  • 切换 Card 时重新加载 Task 列表
  • 便利模式:显示当前 Project 所有 active cards 的 tasks(按 card 分组)

六、Registry API

class ProjectRegistry:
    """管理 registry.db 中的 projects + cards"""

    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

    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

七、路由方案兼容性

完全兼容(司马懿 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 天

前置条件

  • v2.6.1 路由方案已落地并验证
  • 司马懿代码评审通过
  • E2E 验证通过(review 正确路由到司马懿)

不在 v2.7.0 范围内

  • 跨 Card 依赖(不支持)
  • Card 级向量检索/摘要(Phase 2
  • Card 模板(Phase 3
  • 冷存储迁移脚本(按需)

九、风险评估

风险 影响 缓解
数据迁移丢失 迁移脚本 + 旧数据备份 + 回滚方案
前端三层导航体验复杂 保留便利 API,前端渐进迁移
active cards 过多导致 Ticker 慢 max_cards_per_tick 限制 + 分批
registry.db 并发写冲突 WAL + busy_timeout + 单 Ticker 写入

十、与调研报告的对齐

调研发现 方案对齐
Jira:单表 + 类型字段 + project_id 隔离 Task 保持单表,Card 做 DB 隔离(比 Jira 粒度更细)
LangGraphSubgraph 独立 State + 映射回传 Card 独立黑板,未来 Card 完成后摘要写入 Project 级
Context Folding:子任务折叠为摘要 Card 归档时折叠,保留元数据索引
Hot/Cold Memory 分离 active=热、closed=温、archived=冷,按需加载