11 KiB
11 KiB
专题-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 - 普通用户系统链路:
流程-02B-普通用户系统处理流程(系统视角).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 请求体
{
"expectedRevision": 5,
"contentOverride": {
"type": "doc",
"content": []
}
}
规则:
expectedRevision必填。contentOverride可选。- 未传
contentOverride表示原样接受。 - 传了
contentOverride表示修改后合并。
5.3 Accept 响应语义
响应必须至少说明三件事:
- 正文 Block 已成功合并。
- Suggestion 已离开 Active,进入 Archive。
- 当前请求走的是哪条知识草稿路径:
- 原样接受:已同步确认当前草稿。
- 修改后合并:旧草稿已失效,新的重新提取异步链路已启动。
可接受的响应结构示例:
{
"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 原样接受事务
单个事务内完成:
- 加载 Active Suggestion 并校验归属、状态与过期时间。
- 加载目标 Block,并用
expectedRevision做并发保护。 - 校验关联草稿是否存在、是否已校验完成、是否 stale。
- 更新 Block 内容与 revision。
- 将 Suggestion 迁入
suggestion_archive(disposition=accepted)。 - 批量确认关联知识草稿,写入正式知识与 change log。
- 写
audit_log。 - 写
projection_outbox。
任何一步失败,整笔事务回滚。
6.2 修改后合并事务
单个事务内完成:
- 加载 Active Suggestion 并校验归属、状态与过期时间。
- 加载目标 Block,并用
expectedRevision做并发保护。 - 用
contentOverride更新 Block 内容与 revision。 - 将 Suggestion 迁入
suggestion_archive(disposition=accepted),必要时记录final_content。 - 将旧知识草稿标记为失效或删除,不允许继续确认。
- 写
audit_log,标记这是 modified merge。 - AFTER_COMMIT 发布重新提取事件或创建异步任务记录。
这里的关键不是任务名字,而是语义:重新提取只能发生在提交之后,不能把外部调用塞进正文主事务。
6.3 Reject 事务
单个事务内完成:
- 加载 Active Suggestion。
- 校验归属与状态。
- 迁入
suggestion_archive(disposition=rejected)。 - 丢弃关联知识草稿。
- 写
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 原样接受
- 用户点击“接受”。
- 正文进入乐观更新状态。
- 服务端完成主事务。
- 正文切换为已保存态。
- Suggestion 卡片从待审区移除。
- UI 显示“已合并,关联知识草稿已确认”。
- 知识视图与历史视图最终一致刷新。
8.2 修改后合并
- 用户点击“修改”。
- 调整最终入正文的结构化内容。
- 点击“修改后合并”。
- 服务端完成正文主事务,并让旧草稿失效。
- UI 显示“已合并,旧知识草稿已失效,正在重新提取”。
- 前端进入等待新草稿状态。
8.3 Reject
- 用户点击“拒绝”。
- Suggestion 离开待审区。
- 正文无变化。
- 关联草稿一起丢弃。
9. Acceptance Criteria
Must-Have(P0)
- Accept 成功后,目标 Block 正确更新,revision 正确递增。
- Suggestion 成功迁入 Archive,不再留在 Active。
- 原样接受时,关联且未 stale 的知识草稿同步确认入库,并写 change log 与 outbox。
- 修改后合并时,旧草稿立即失效,不允许继续确认。
- 修改后合并只启动重新提取异步链路,不在主事务里做外部调用。
- Reject 对正文零副作用。
- 409 冲突必须有显式恢复路径。
- 成功提示必须区分原样接受与修改后合并两条路径。
Should-Have(P1)
- 成功后正文有短暂高亮反馈。
- 修改后合并能显示“等待新草稿”的明确状态。
- 同一 Block 下重复触发 Accept 时有临时互斥保护。
Nice-to-Have(P2)
- 更细粒度的对比视图。
- 建议卡片快捷键。
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流程-02B-普通用户系统处理流程(系统视角).md后端-04-统一数据库Schema-v1.md后端-05-统一API契约-v1.md前端-02-编辑器与影子层交互.md