# v2.7 数据模型设计：Project → Task → SubTask

> 日期：2026-05-18
> 版本：v2.1（经 08:02 ~ 10:56 讨论 + 评审修正定稿）
> 作者：庞统
> 状态：评审通过
> 司马懿评审：Mail #298（2 必修 + 4 OBS，全部采纳）

---

## 一、设计推导过程

### 1.1 起因：Card 层为什么被回滚

v2.7 原设计是 `Project → Card → Task` 三级层次结构。经过讨论发现 Card 是一个位置尴尬的中间层：

| 概念 | 用户视角 | Card 层的位置 |
|------|---------|-------------|
| Project | 仓库/组织（sanguo_quant_live） | ✅ 一致 |
| Task | 用户的一条需求/目标（"动量策略v1"） | Card 吃掉了 Task 的语义 |
| 原子步骤 | Agent 执行的具体动作（张飞编码） | Card 下的 Task 才是真正的原子步骤 |

**Card 既不是 Task（用户不会说"帮我建一个 Card"），也不是 SubTask（太粗了），更不是 Campaign（被锁在 Project 内）。** 它是一个多余抽象。

正确模型应该是用户自然使用的三层：

```
Project（项目/仓库）
  └── Task（用户需求/意图/目标）
        └── SubTask（Agent 执行的原子任务/Stage）
```

### 1.2 SubTask 表的讨论：为什么最终没建

**提出**：最初设计新建 `subtasks` 表，包含独立的状态、指派、依赖。

**质疑**：
1. **调度意义**：没有 SubTask 表也能调度——当前 Ticker 扫描 Task 就调度了。SubTask 作为调度单位的好处理论上是并行，但 Task 自引用（parent_task）已经能做到。
2. **和已有设计的重叠**：v2.6 的 Task 表已有 `parent_task` 字段，拆解结果就是子 Task（Task 自引用）。课题4 的 planner.md 已定义了拆解流程（四步+组件库+PlanChecker）。新建 SubTask 表和这些重叠。
3. **plan_json 也重叠**：已有 planner.md 模板指导庞统拆解，产出是黑板上的子 Task，不需要额外的 plan_json 字段。

**结论**：**不建 SubTask 表，复用 Task 自引用（parent_task）。** 子 Task 和父 Task 用同一张表，通过 `parent_task` 字段形成父子关系。

### 1.3 跨项目协作的讨论

**最初设计**：SubTask 级跨项目（姜维在 vnpy 为 quant_live 的某个 SubTask 工作）。

**修正**：
- 跨项目应该在 Task 级，不是 SubTask 级
- 把 Task 整体信息（outputs、comments、黑板讨论）共享出去，具体用哪部分由 Agent 自己判断
- 通过 bootstrap 注入实现（build_bootstrap 在 Agent 启动前把依赖 Task 的产出摘要拼装进 prompt）

**结论**：Task 级跨项目，不改 API，通过 bootstrap 注入。

### 1.4 关系模型的讨论

任务之间存在三种可能的关系：

| 关系 | 字段 | 语义 | 例子 |
|------|------|------|------|
| **父子** | `parent_task` | 组成归属——"我是谁的一部分" | 因子研究 **属于** 动量策略v1 |
| **依赖** | `depends_on` | 执行顺序——"我必须等谁完成" | 策略编码 **必须等** 因子研究完成 |
| **引用** | 新字段？ | 成果物关联——"我用了谁的产出" | 动量策略v1 **用了** 回测引擎搭建的产出 |

**背靠背讨论**（Mail #302 → #297）：

司马懿的独立判断：
- **父子**：有必要，提供聚合进度 + 前端分组。Card 回滚后 parent_task 是唯一的分组聚合手段
- **依赖**：已确定必须保留，调度引擎消费
- **引用**：当前没必要，没有消费者。和依赖有本质区别（依赖是调度的前置条件，引用是审计的溯源链），但当前审计场景不存在
- **补充**：第三种关系可能是"模板/实例"（多个策略用相同 stages 流程），但 stages_json 已能表达

**最终结论**：

| 字段 | 保留 | 用途 |
|------|------|------|
| `parent_task` | ✅ | 聚合进度 + 前端分组 |
| `depends_on` | ✅ | 调度顺序 |
| 引用 | ❌ | 未来按需加 `references TEXT` |
| `stages_json` | ✅ 新增 | 动态 Stage 定义 |

### 1.5 状态的讨论

**最初提出** `planning` / `challenging` 两个新状态。

**修正**：课题3 已定义了完整的审查流水线（plan_review / output_review / guardrail / final_review），课题4 已定义了拆解流程。不需要额外加这两个状态。

**结论**：不加新状态，复用已有机制。

### 1.6 前端展示的讨论

**Stage 进度条**：和当前的固化 5 阶段（pending→claimed→working→review→done）一个意思，只是变成动态的 stages_json 定义的阶段。

**百分比问题**：Stage 是动态的，后续节点还没执行到，百分比不可靠。改用 `■■□□□ 2/5` + 当前 Stage 名。

---

## 二、最终设计

### 2.1 数据模型

**不建新表，只改现有 Task 表：**

```sql
-- 新增字段
ALTER TABLE tasks ADD COLUMN stages_json TEXT DEFAULT '[]';
```

**stages_json 格式**：

```json
[
  {"id": "research", "label": "因子研究", "order": 1},
  {"id": "data_prep", "label": "数据准备", "order": 2},
  {"id": "coding", "label": "策略编码", "order": 3},
  {"id": "backtest", "label": "回测验证", "order": 4},
  {"id": "optimize", "label": "参数优化", "order": 5}
]
```

**父子关系**（已有字段，启用使用）：

```sql
-- 已存在，不需要 ALTER
parent_task TEXT  -- 子 Task 填父 Task 的 id，顶层 Task 为 NULL
```

**关系总结**：

| 字段 | 语义 | 消费者 |
|------|------|--------|
| `parent_task` | 组成归属 | 前端分组、进度聚合 |
| `depends_on` | 执行顺序 | Ticker 调度引擎 |
| `stages_json` | 动态阶段定义（仅顶层 Task） | 前端 Stage 进度展示 |

### 2.2 父子关系行为规则

**子 Task 创建**：
- 庞统拆解时，为每个原子步骤创建子 Task（`parent_task=父Task.id`）
- 子 Task 有自己独立的 `status`、`assignee`、`depends_on`、`stage`
- 子 Task 的 `stage` 字段绑定到父 Task 的 `stages_json` 中的某个 stage id

**父 Task 状态 = 子 Task 聚合**（v2.8 更新）：

| 父 Task 状态 | 推导规则 |
|-------------|---------|
| `pending` | 所有子 Task 都是 pending，或无子 Task |
| `escalated` | 有子 Task escalated（v2.8 新增，视同 working） |
| `waiting_human` | 有子 Task waiting_human（v2.8/M3 新增，视同 working） |
| `working` | 有子 Task 在 claimed / working |
| `review` | 有子 Task 在 review |
| `done` | 所有子 Task 都是 done |
| `failed` | 有子 Task failed 且无 active 子 Task |
| `blocked` | 有子 Task blocked 且无 active 子 Task |
| `cancelled` | 用户/AI 取消（手动设置，**不参与聚合**） |
| `paused` | 用户暂停（手动设置，**不参与聚合**） |

**聚合优先级**（v2.8 更新）：
escalated > waiting_human > review > working > pending > failed > blocked

**不参与聚合**：paused, cancelled（手动状态）

**手动状态保护**：父 Task 被手动设为 cancelled 或 paused 后，聚合不覆盖。

**聚合刷新时机**：每次 Ticker tick 时，全量扫描有子 Task 的父 Task，刷新聚合状态。

**边界条件**：
1. 全量扫描（当前任务量 <100，性能无忧）
2. 手动状态不覆盖（cancelled 的父 Task 不参与聚合）

### 2.3 示例数据

```
tasks 表：

| id        | title        | parent_task | depends_on    | stage     | status  | assignee  |
|-----------|-------------|-------------|---------------|-----------|---------|-----------|
| task-001  | 动量策略v1   | NULL        | NULL          | NULL      | working | NULL      | ← 父 Task
| task-002  | 因子研究     | task-001    | []            | research  | done    | zhangfei  | ← 子 Task
| task-003  | 分钟线下载   | task-001    | []            | data_prep | done    | zhaoyun   | ← 子 Task
| task-004  | 策略编码     | task-001    | [task-002,task-003] | coding    | working | zhangfei  | ← 子 Task
| task-005  | 回测验证     | task-001    | [task-004]    | backtest  | pending | NULL      | ← 子 Task

task-001 的 stages_json：
[{"id":"research","label":"因子研究","order":1},
 {"id":"data_prep","label":"数据准备","order":2},
 {"id":"coding","label":"策略编码","order":3},
 {"id":"backtest","label":"回测验证","order":4},
 {"id":"optimize","label":"参数优化","order":5}]
```

### 2.4 跨项目协作

**方式**：通过 bootstrap 注入（不改 API）。

`build_bootstrap()` 在 spawn Agent 前：
1. 查黑板找到依赖 Task 的 outputs 和 comments
2. 拼装到 Agent 的 prompt 里
3. Agent 开箱就能看到依赖信息，不需要自己跨项目查询

### 2.5 Ticker 变更

**和 v2.6 基本一致，只加两步**：

```python
async def _tick_project(self, project_id, project_info):
    # 1. 扫描当前状态（不变）
    # 2. 依赖推进（不变）
    # 3. 僵尸/超时处理（不变）
    # 4. 调度 pending 子 Task（不变——和调度普通 Task 一样）
    # 5. 调度审查（不变）
    # 6. 【新增】聚合父 Task 状态
    #    - 全量扫描所有有子 Task 的父 Task
    #    - 跳过手动状态（cancelled）的父 Task
    refresh_parent_task_statuses(db_path)
    # 7. 写 daemon_tick 事件（不变）
```

**调度逻辑完全不变**——子 Task 和普通 Task 一样被扫描、调度。唯一的变更是 tick 末尾加一步聚合刷新。

### 2.6 前端改动

#### 2.6.1 Task 看板（EdictBoard.tsx）

- 任务列表只显示**顶层 Task**（`parent_task IS NULL`）
- Task 卡片新增：
  - `■■□□□ 2/5`（已完成子 Task 数 / 总子 Task 数）
  - `当前：策略编码`（当前活跃子 Task 的 stage label）

#### 2.6.2 Task 详情弹窗（TaskModal.tsx）

- **Stage 进度条**：从 `stages_json` 读取，按子 Task 状态标记每个 Stage（✅ done / 🔄 working / ⬜ pending）
- **子 Task 列表**：按 Stage 分组展示，每个子 Task 显示标题、Agent、状态、产出
- 和现有的进度条 + 实时动态是一个意思，只是从固化的 5 阶段变成动态 Stage

#### 2.6.3 Mail Tab

- 新增 Tab，列表展示 Sanguo Mail
- 后续独立实现

#### 2.6.4 不做的

- 不做独立的 SubTask 管理页
- 不做跨 Project 的 Product 视图
- 不做 Card 看板

---

## 三、回滚范围

### 3.1 代码回滚（开发目录 `~/.openclaw/sanguo_projects/sanguo_moziplus_v2/`）

| 文件 | 操作 |
|------|------|
| `src/blackboard/models.py` | 删除 Card dataclass + CARD_VALID_STATUSES + CARD_TYPE_SET + CARD_MANUAL_STATUSES |
| `src/blackboard/db.py` | 删除 `_CARDS_SCHEMA`、`_migrate_v27()`；tasks DDL 移除 `card_id`（**保留 `stage`**）；保留其他所有内容 |
| `src/blackboard/operations.py` | 删除 CardOps 类全部方法 |
| `src/blackboard/queries.py` | 移除 card_id 过滤参数 |
| `src/api/card_routes.py` | **删除整个文件** |
| `src/api/mail_routes.py` | **删除整个文件**（Mail Tab 延后，后续重新设计后端 API） |
| `src/daemon/ticker.py` | 移除 per-card 扫描逻辑（`_tick_card`、`CardOps` 引用、`from ... import CardOps`），恢复 per-project 直扫 |
| `src/main.py` | 移除 card_routes、mail_routes 注册；移除 v2.7 迁移调用；版本号保持 2.7.0 |
| `tests/test_v27_cards.py` | **删除整个文件** |

### 3.2 设计文档

| 文件 | 操作 |
|------|------|
| `docs/design/v2.7-three-tier-hierarchy.md` | 移至 `docs/design/archive/` |

### 3.3 保留

| 文件/功能 | 原因 |
|-----------|------|
| `registry.db` + ProjectRegistry + 自动发现 | Project 管理需要 |
| per-project `blackboard.db` | 多项目隔离需要 |
| Project API 路由 | 前端项目选择器需要 |
| `docs/design/product-direction-notes.md` | 新增，Product 方向备忘 |

---

## 四、新增内容

### 4.1 后端

| 序号 | 任务 | 文件 | 说明 |
|------|------|------|------|
| 1 | tasks DDL 加 `stages_json` | `db.py` | `ALTER TABLE tasks ADD COLUMN stages_json TEXT DEFAULT '[]'` |
| 2 | 子 Task 聚合函数 | `operations.py` | `refresh_parent_status(db_path, parent_task_id)` |
| 3 | 按 parent_task 查询 | `queries.py` | `list_subtasks(db_path, parent_task_id)` |
| 4 | Ticker 加聚合刷新 | `ticker.py` | tick 末尾调用聚合函数 |
| 5 | API 路由微调 | `blackboard_routes.py` | Task 详情接口返回子 Task 列表 + Stage 进度 |

### 4.2 前端

| 序号 | 任务 | 文件 | 说明 |
|------|------|------|------|
| 6 | Task 卡片加进度指示 | `EdictBoard.tsx` | `■■□□□ 2/5` + 当前 Stage 名 |
| 7 | Task 详情弹窗改造 | `TaskModal.tsx` | 动态 Stage 进度条 + 子 Task 列表 |
| 8 | store/api 适配 | `store.ts` + `api.ts` | 加载子 Task 数据、Stage 进度 |

### 4.3 测试

| 序号 | 任务 | 文件 |
|------|------|------|
| 9 | 子 Task CRUD + 聚合 | `test_v27_subtasks.py` |
| 10 | 现有测试不回归 | 全量 pytest |

---

## 五、实施顺序

| 阶段 | 任务 | 预计 |
|------|------|------|
| **Phase 1：回滚** | 删除 Card 层代码 + 归档设计文档 | 2h |
| **Phase 2：后端** | stages_json + 聚合 + 查询 + Ticker + API | 4h |
| **Phase 3：前端** | 卡片指示器 + 详情弹窗改造 | 4h |
| **Phase 4：测试** | 新测试 + 全量回归 | 2h |
| **Phase 5：评审** | 发司马懿评审 + 修复 | 2h |
| **合计** | | **~14h** |

---

## 六、Product 方向备忘（不实施，记录未来）

详见 `docs/design/product-direction-notes.md`。

核心思路：Product = 跨 Project 的 Task 聚合，多个 Task 的产出物组合成完整产品。和 Project 是正交维度。等有实际场景再建模。

---

## 七、讨论参与

| 时间 | 参与者 | 内容 |
|------|--------|------|
| 08:02 | 用户 | 质疑 Card 层存在必要性，提出 Project → Task → SubTask 三层 |
| 08:07 | 用户 | 提出 Campaign/Product 概念（跨项目 Task 聚合） |
| 08:13 | 用户 | 确认四层：Project → Task → SubTask → Product（未来），决定回滚 Card |
| 08:21 | 用户 | 确认三条：回滚 Card、完善三层、记录 Product 方向 |
| 08:47 | 用户 | 追问 SubTask 表用途、parent_project 归属、plan_json 重叠、状态必要性 |
| 09:55 | 用户 | 确认 Task 级跨项目、不需要 planning/challenging 状态、追问 bootstrap 注入含义 |
| 09:59 | 用户 | 确认进度展示方式，追问 parent_task 含义 |
| 10:00 | 用户 | 追问父子 vs 引用 vs 依赖的业务选择 |
| 10:03 | 用户 | 要求背靠背发给司马懿讨论 |
| 10:10 | 司马懿 | 回复：留父子+依赖，砍引用，补充模板/实例关系 |
| 10:50 | 用户 | 确认最终方案，要求更新设计文档并统一发评审 |
| 10:56 | 司马懿 | 评审：2 必修 + 4 OBS，方向正确 |
| 10:56+ | 庞统 | 全部采纳：claimed 归入 working、聚合优先级、手动状态保护、stage 保留、Mail 延后 |
