Files
sanguo_moziplus_v2/docs/design/v2.7-subtask-model.md
T
2026-05-18 23:04:49 +08:00

349 lines
14 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.
# 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(用户需求/意图/目标)
└── SubTaskAgent 执行的原子任务/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 escalatedv2.8 新增,视同 working |
| `waiting_human` | 有子 Task waiting_humanv2.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 延后 |