# 专题-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 请求体 ```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` - `流程-02B-普通用户系统处理流程(系统视角).md` - `后端-04-统一数据库Schema-v1.md` - `后端-05-统一API契约-v1.md` - `前端-02-编辑器与影子层交互.md`