oh-my-muse/design-docs/专题-01-正文建议接受(Accept Suggestion)实现规范.md
zizi 0d0e1d4473 添加产品设计文档
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-20 10:38:31 +08:00

11 KiB
Raw Blame History

专题-01正文建议接受Accept Suggestion实现规范

  • 版本v4
  • 更新日期2026-04-24
  • 目标读者:产品 / 架构 / 前端 / 后端 / 测试
  • 阅读时间2035 分钟
  • 边界说明:本文档只收束“接受建议”这条跨文档主链路:用户怎么把建议写入正文、知识草稿怎么处理、事务边界怎么切、前端怎么反馈。精确 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 接口

  • AcceptPOST /works/{workId}/suggestions/{suggestionId}/accept
  • RejectPOST /works/{workId}/suggestions/{suggestionId}/reject

5.2 Accept 请求体

{
  "expectedRevision": 5,
  "contentOverride": {
    "type": "doc",
    "content": []
  }
}

规则:

  • expectedRevision 必填。
  • contentOverride 可选。
  • 未传 contentOverride 表示原样接受。
  • 传了 contentOverride 表示修改后合并。

5.3 Accept 响应语义

响应必须至少说明三件事:

  1. 正文 Block 已成功合并。
  2. Suggestion 已离开 Active进入 Archive。
  3. 当前请求走的是哪条知识草稿路径:
    • 原样接受:已同步确认当前草稿。
    • 修改后合并:旧草稿已失效,新的重新提取异步链路已启动。

可接受的响应结构示例:

{
  "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 状态拥有

  • 正文 BlockQuery Cache 持有。
  • Active SuggestionShadowStore 持有。
  • 关联知识草稿提示与角标:以最终一致方式刷新,不把瞬时数字当真相。

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-HaveP0

  1. Accept 成功后,目标 Block 正确更新revision 正确递增。
  2. Suggestion 成功迁入 Archive不再留在 Active。
  3. 原样接受时,关联且未 stale 的知识草稿同步确认入库,并写 change log 与 outbox。
  4. 修改后合并时,旧草稿立即失效,不允许继续确认。
  5. 修改后合并只启动重新提取异步链路,不在主事务里做外部调用。
  6. Reject 对正文零副作用。
  7. 409 冲突必须有显式恢复路径。
  8. 成功提示必须区分原样接受与修改后合并两条路径。

Should-HaveP1

  1. 成功后正文有短暂高亮反馈。
  2. 修改后合并能显示“等待新草稿”的明确状态。
  3. 同一 Block 下重复触发 Accept 时有临时互斥保护。

Nice-to-HaveP2

  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