Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
337 lines
11 KiB
Markdown
337 lines
11 KiB
Markdown
# 专题-01:正文建议接受(Accept Suggestion)实现规范
|
||
|
||
- 版本:v4
|
||
- 更新日期:2026-04-24
|
||
- 目标读者:产品 / 架构 / 前端 / 后端 / 测试
|
||
- 阅读时间:20–35 分钟
|
||
- 边界说明:本文档只收束“接受建议”这条跨文档主链路:用户怎么把建议写入正文、知识草稿怎么处理、事务边界怎么切、前端怎么反馈。精确 Schema、状态机和统一错误模型仍分别以 `后端-04`、`架构-04`、`后端-05` 为准。
|
||
|
||
## 1. 目标与范围
|
||
|
||
Accept Suggestion 是正文从待审层进入正式事实层的唯一合法入口。
|
||
|
||
这个能力要同时解决四件事:
|
||
|
||
- 把候选文本安全写入目标 Block。
|
||
- 在原样接受时,把当前绑定且未 stale 的知识草稿一并确认入库。
|
||
- 在修改后合并时,立即废弃旧知识草稿,并启动重新提取链路。
|
||
- 把正文、知识、历史与投影边界切干净,避免“看起来成功,其实系统已经脏了”。
|
||
|
||
### 1.1 In Scope
|
||
|
||
- 接受建议(原样接受、修改后合并)
|
||
- 拒绝建议
|
||
- 关联知识草稿的确认 / 失效语义
|
||
- 前端正文乐观更新、回滚与冲突处理
|
||
- 审计、归档、outbox 写入与最终一致刷新
|
||
|
||
### 1.2 Out of Scope
|
||
|
||
- 建议生成本身
|
||
- 知识提取算法本身
|
||
- 全书解析的章节批量确认
|
||
- 一致性检查面板本身
|
||
- AST 级精细 diff
|
||
|
||
## 2. 统一语义(先把话说死)
|
||
|
||
### 2.1 三条用户结果路径
|
||
|
||
| 路径 | 正文结果 | 知识草稿结果 | 历史结果 |
|
||
|---|---|---|---|
|
||
| 原样接受 | Suggestion 内容进入目标 Block | 与当前 Suggestion 绑定、且未 stale 的草稿同步确认入库 | Suggestion 归档为 accepted |
|
||
| 修改后合并 | `contentOverride` 进入目标 Block | 旧草稿立即失效;AFTER_COMMIT 重新提取新的草稿 | Suggestion 归档为 accepted,并保留 `final_content` 快照(如需要) |
|
||
| 拒绝 | 正文不变 | 关联草稿一起丢弃 | Suggestion 归档为 rejected |
|
||
|
||
### 2.2 为什么“修改后合并”不是新状态
|
||
|
||
- 用户决策仍然是“接受这条建议,只是我改了最终入正文的文本”。
|
||
- 它不应该发明新的 Active 状态,也不应该生成第二套历史状态机。
|
||
- 归档层统一记为 `accepted`,但必须保留“这是 modified merge”的审计语义。
|
||
|
||
### 2.3 Stale Draft 规则
|
||
|
||
只要源文本发生变化,旧知识草稿就必须被视为 stale:
|
||
|
||
- 不能继续确认旧草稿。
|
||
- 不能把旧草稿偷偷并入正式知识。
|
||
- 必须重新走提取 -> 校验 -> 风险分级链路,生成新的草稿。
|
||
|
||
## 3. 相关 SSOT
|
||
|
||
- 双轨模型:`架构-02-核心数据结构与双轨模型.md`
|
||
- 状态机与端点前后置条件:`架构-04-状态机与约束清单.md`
|
||
- 系统链路:`流程-02-系统处理流程(系统视角).md`
|
||
- 数据库 Schema:`后端-04-统一数据库Schema-v1.md`
|
||
- API 契约:`后端-05-统一API契约-v1.md`
|
||
- 前端协作规则:`前端-02-编辑器与影子层交互.md`
|
||
|
||
本文档只做一件事:把这些分散约束收束成一条可落地的 Accept 链路。
|
||
|
||
## 4. 核心不变式
|
||
|
||
### 4.1 正文与建议
|
||
|
||
- `blocks.content` 是正式正文的唯一真相。
|
||
- `blocks.revision` 是写入锚点。
|
||
- `suggestions` 只保存待审对象;进入终态后必须迁入 `suggestion_archive`。
|
||
|
||
### 4.2 关联知识草稿
|
||
|
||
- 生成场景下的知识草稿通过 `proposals.suggestion_id` 关联到 Suggestion。
|
||
- 原样接受时,只允许确认“当前 Suggestion 绑定、且未 stale、且已完成校验”的草稿。
|
||
- 修改后合并时,旧草稿统一失效,不允许继续沿用。
|
||
|
||
### 4.3 事务边界
|
||
|
||
- Accept 主事务内不得发起外部 AI / 提取 / 校验调用。
|
||
- 原样接受时,正文写入、Suggestion 归档、知识草稿确认、change log、outbox 写入必须放在同一事务里完成。
|
||
- 修改后合并时,正文写入、Suggestion 归档、旧草稿失效与审计仍在主事务;重新提取只能走 AFTER_COMMIT 异步链路。
|
||
|
||
### 4.4 历史与投影
|
||
|
||
- Accept / Reject 后,Active 区不能再看到这条 Suggestion。
|
||
- 正式写入知识后,必须产生日志和 outbox。
|
||
- 投影失败不能反向打断已经成功的 Accept 主事务。
|
||
|
||
## 5. API 收束
|
||
|
||
### 5.1 接口
|
||
|
||
- Accept:`POST /works/{workId}/suggestions/{suggestionId}/accept`
|
||
- Reject:`POST /works/{workId}/suggestions/{suggestionId}/reject`
|
||
|
||
### 5.2 Accept 请求体
|
||
|
||
```json
|
||
{
|
||
"expectedRevision": 5,
|
||
"contentOverride": {
|
||
"type": "doc",
|
||
"content": []
|
||
}
|
||
}
|
||
```
|
||
|
||
规则:
|
||
|
||
- `expectedRevision` 必填。
|
||
- `contentOverride` 可选。
|
||
- 未传 `contentOverride` 表示原样接受。
|
||
- 传了 `contentOverride` 表示修改后合并。
|
||
|
||
### 5.3 Accept 响应语义
|
||
|
||
响应必须至少说明三件事:
|
||
|
||
1. 正文 Block 已成功合并。
|
||
2. Suggestion 已离开 Active,进入 Archive。
|
||
3. 当前请求走的是哪条知识草稿路径:
|
||
- 原样接受:已同步确认当前草稿。
|
||
- 修改后合并:旧草稿已失效,新的重新提取异步链路已启动。
|
||
|
||
可接受的响应结构示例:
|
||
|
||
```json
|
||
{
|
||
"block": {
|
||
"id": "uuid",
|
||
"revision": 6,
|
||
"content": {
|
||
"type": "doc",
|
||
"content": []
|
||
}
|
||
},
|
||
"suggestion": {
|
||
"id": "uuid",
|
||
"disposition": "accepted",
|
||
"archivedAt": "ISO8601"
|
||
},
|
||
"extractionJob": {
|
||
"id": "uuid",
|
||
"status": "queued"
|
||
}
|
||
}
|
||
```
|
||
|
||
说明:
|
||
|
||
- `extractionJob` 只在“修改后合并”路径下有意义。
|
||
- 原样接受场景不要求返回即时 `proposalCount`;数量刷新按最终一致处理。
|
||
|
||
### 5.4 Reject 语义
|
||
|
||
Reject 只做三件事:
|
||
|
||
- 迁出 Suggestion
|
||
- 丢弃关联草稿
|
||
- 写历史 / 审计
|
||
|
||
Reject 不得修改正文,不得触发新的提取。
|
||
|
||
## 6. 后端实现规范
|
||
|
||
### 6.1 原样接受事务
|
||
|
||
单个事务内完成:
|
||
|
||
1. 加载 Active Suggestion 并校验归属、状态与过期时间。
|
||
2. 加载目标 Block,并用 `expectedRevision` 做并发保护。
|
||
3. 校验关联草稿是否存在、是否已校验完成、是否 stale。
|
||
4. 更新 Block 内容与 revision。
|
||
5. 将 Suggestion 迁入 `suggestion_archive(disposition=accepted)`。
|
||
6. 批量确认关联知识草稿,写入正式知识与 change log。
|
||
7. 写 `audit_log`。
|
||
8. 写 `projection_outbox`。
|
||
|
||
任何一步失败,整笔事务回滚。
|
||
|
||
### 6.2 修改后合并事务
|
||
|
||
单个事务内完成:
|
||
|
||
1. 加载 Active Suggestion 并校验归属、状态与过期时间。
|
||
2. 加载目标 Block,并用 `expectedRevision` 做并发保护。
|
||
3. 用 `contentOverride` 更新 Block 内容与 revision。
|
||
4. 将 Suggestion 迁入 `suggestion_archive(disposition=accepted)`,必要时记录 `final_content`。
|
||
5. 将旧知识草稿标记为失效或删除,不允许继续确认。
|
||
6. 写 `audit_log`,标记这是 modified merge。
|
||
7. AFTER_COMMIT 发布重新提取事件或创建异步任务记录。
|
||
|
||
这里的关键不是任务名字,而是语义:**重新提取只能发生在提交之后,不能把外部调用塞进正文主事务。**
|
||
|
||
### 6.3 Reject 事务
|
||
|
||
单个事务内完成:
|
||
|
||
1. 加载 Active Suggestion。
|
||
2. 校验归属与状态。
|
||
3. 迁入 `suggestion_archive(disposition=rejected)`。
|
||
4. 丢弃关联知识草稿。
|
||
5. 写 `audit_log`。
|
||
|
||
Reject 不得更新 Block,不得创建重新提取链路。
|
||
|
||
## 7. 前端实现规范
|
||
|
||
### 7.1 状态拥有
|
||
|
||
- 正文 Block:Query Cache 持有。
|
||
- Active Suggestion:ShadowStore 持有。
|
||
- 关联知识草稿提示与角标:以最终一致方式刷新,不把瞬时数字当真相。
|
||
|
||
### 7.2 Accept 成功反馈
|
||
|
||
- 原样接受:`已合并,关联知识草稿已确认`
|
||
- 修改后合并:`已合并,旧知识草稿已失效,正在重新提取`
|
||
|
||
禁止反馈:
|
||
|
||
- “已合并,发现 N 条提案待审核”
|
||
- “已合并,请稍后看系统是否成功”
|
||
|
||
### 7.3 冲突处理
|
||
|
||
当服务端返回 `409 REVISION_CONFLICT`:
|
||
|
||
- 停止当前乐观链路。
|
||
- 如果本地没有后续编辑,回滚到上一个稳定状态。
|
||
- 如果本地已经继续编辑,进入显式冲突处理态,禁止静默覆盖。
|
||
- 给用户清晰的下一步:放弃本次合并,或基于新 revision 重试。
|
||
|
||
### 7.4 成功后的刷新策略
|
||
|
||
- 成功后移除当前 Suggestion 卡片。
|
||
- 失效相关正文、知识、历史和角标查询。
|
||
- 原样接受优先刷新“已确认结果”;修改后合并优先进入“等待新草稿”状态。
|
||
|
||
## 8. 用户可观察流程
|
||
|
||
### 8.1 原样接受
|
||
|
||
1. 用户点击“接受”。
|
||
2. 正文进入乐观更新状态。
|
||
3. 服务端完成主事务。
|
||
4. 正文切换为已保存态。
|
||
5. Suggestion 卡片从待审区移除。
|
||
6. UI 显示“已合并,关联知识草稿已确认”。
|
||
7. 知识视图与历史视图最终一致刷新。
|
||
|
||
### 8.2 修改后合并
|
||
|
||
1. 用户点击“修改”。
|
||
2. 调整最终入正文的结构化内容。
|
||
3. 点击“修改后合并”。
|
||
4. 服务端完成正文主事务,并让旧草稿失效。
|
||
5. UI 显示“已合并,旧知识草稿已失效,正在重新提取”。
|
||
6. 前端进入等待新草稿状态。
|
||
|
||
### 8.3 Reject
|
||
|
||
1. 用户点击“拒绝”。
|
||
2. Suggestion 离开待审区。
|
||
3. 正文无变化。
|
||
4. 关联草稿一起丢弃。
|
||
|
||
## 9. Acceptance Criteria
|
||
|
||
### Must-Have(P0)
|
||
|
||
1. Accept 成功后,目标 Block 正确更新,revision 正确递增。
|
||
2. Suggestion 成功迁入 Archive,不再留在 Active。
|
||
3. 原样接受时,关联且未 stale 的知识草稿同步确认入库,并写 change log 与 outbox。
|
||
4. 修改后合并时,旧草稿立即失效,不允许继续确认。
|
||
5. 修改后合并只启动重新提取异步链路,不在主事务里做外部调用。
|
||
6. Reject 对正文零副作用。
|
||
7. 409 冲突必须有显式恢复路径。
|
||
8. 成功提示必须区分原样接受与修改后合并两条路径。
|
||
|
||
### Should-Have(P1)
|
||
|
||
1. 成功后正文有短暂高亮反馈。
|
||
2. 修改后合并能显示“等待新草稿”的明确状态。
|
||
3. 同一 Block 下重复触发 Accept 时有临时互斥保护。
|
||
|
||
### Nice-to-Have(P2)
|
||
|
||
1. 更细粒度的对比视图。
|
||
2. 建议卡片快捷键。
|
||
|
||
## 10. Test Plan
|
||
|
||
### 10.1 Manual
|
||
|
||
- 原样接受:正文更新,Suggestion 消失,知识视图可看到已确认结果。
|
||
- 修改后合并:正文更新,旧草稿失效,系统稍后出现新草稿。
|
||
- Reject:正文不变,Suggestion 消失,知识不被污染。
|
||
- 409 冲突:前端进入可恢复状态,不覆盖用户新输入。
|
||
- 重复点击已接受对象:幂等,不重复污染正文与历史。
|
||
|
||
### 10.2 Automated
|
||
|
||
**Backend Unit / Integration**
|
||
|
||
- 原样接受正常路径
|
||
- 原样接受 stale draft 拦截
|
||
- 修改后合并触发旧草稿失效
|
||
- Reject 零正文副作用
|
||
- 409 冲突返回必要信息
|
||
- 投影失败不阻塞主事务
|
||
|
||
**Frontend E2E**
|
||
|
||
- 原样接受成功链路
|
||
- 修改后合并成功链路
|
||
- Reject 成功链路
|
||
- 409 冲突链路
|
||
- 本地脏状态下的失败恢复
|
||
|
||
## 11. 关联阅读
|
||
|
||
- `架构-02-核心数据结构与双轨模型.md`
|
||
- `架构-04-状态机与约束清单.md`
|
||
- `流程-02-系统处理流程(系统视角).md`
|
||
- `后端-04-统一数据库Schema-v1.md`
|
||
- `后端-05-统一API契约-v1.md`
|
||
- `前端-02-编辑器与影子层交互.md`
|