Files
mnote/bugs/07-ai/done/7-25-reasonix-block-edit-workflow-empty-block-ops-after-markdown-match-v1.md
T
lix-2026 a2cb1338c8 chore: 保存当前架构收口与 bug 修复快照
归档本轮 P0/P1 bug 修复、设计审查迁移、AI selection scope 收口与 stream contract 调整,并保留当前 05 主线迁移起点。
2026-05-18 17:01:35 +08:00

6.0 KiB
Raw Blame History

7-25 [done][bug] ACP Reasonix 页面 AI block_edit_workflow 在 markdown 命中后生成空 block_ops v1

发现时间:2026-05-17

状态:[done]

关联主线:07-ai

关联缺陷:7-24 在线 markdown_edit 写回不以最终 Markdown 为真源

1. 用户可见症状

在页面 AI 中切换到 ACP · Reasonix,输入:

你
检查你是否能读取到本页第一段,同时请修改第二段为:测试123

工具调用失败:

mnote.page_ai.block_edit_workflow 失败 · page-ai-fast-mp9wm0qo
结果 mnote.doc.apply_block_ops operations 不能为空

2. 问题定义

page_ai.block_edit_workflow 当前已切到:

模型输出 search/replace
-> mnote.doc.markdown_edit
-> 在线文档再转换为 mnote.doc.apply_block_ops

但在线写回层没有以最终 Markdown 为真源。它在 markdown 字符串中 search/replace 成功后,又用原始 operations 去原始 blocks 中反查目标块。如果反查不到块,就生成空 block_ops,随后仍调用 mnote.doc.apply_block_ops,最终抛出“operations 不能为空”。

3. 证据

  • page_ai_workflow.rs 调用模型生成 markdown operations。
  • page_ai_workflow.rs 构造 mnote.doc.markdown_edit 输入。
  • doc.rsmd 字符串上执行 search/replace。
  • doc.rs 调用 build_block_ops_from_markdown_edit
  • doc.rs 只用 block.text.contains(search) 反查 block。
  • doc.rs 可能返回空 block_ops
  • doc.rs 即使 block_ops 为空,也继续调用 doc_apply_block_ops
  • block.rs 对空 operations 报错:mnote.doc.apply_block_ops operations 不能为空

4. 根因判断

这条错误不是最终 root cause,只是下游暴露出来的症状。

真正根因是 markdown 层与 block ops 写回层存在二次定位:

  1. search_replace(&md, search, replace) 可以通过精确、归一化或 fuzzy 匹配成功。
  2. build_block_ops_from_markdown_edit 却只支持 block.text.contains(search)
  3. 两套匹配规则不一致时,markdown 层显示已命中,block 层却生成空 operations。
  4. 空 operations 没有在 markdown_edit 层转成语义化错误,而是继续传给 apply_block_ops

5. 影响

  • ACP Reasonix 的页面编辑会向用户暴露底层 apply_block_ops 错误,而不是说明“markdown 命中但无法映射到块”。
  • “读取第一段 + 修改第二段”这类混合任务可能进入 fast workflow,但该 workflow 只能表达写入,不保证先读后答。
  • 如果模型给出的 search 是段落序号、fuzzy 片段、带格式文本或跨块文本,markdown 层可能成功,block 写回层仍失败。

6. 建议修复:建议以长期,彻底的修复优先,建议参考/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/cli-main中对全局替换和单块替换的判断逻辑。

短期:

  • doc_markdown_edit 在调用 doc_apply_block_ops 前,如果 applied > 0block_ops.is_empty(),应返回明确错误,例如 mnote_doc_markdown_edit_block_mapping_empty,并包含 failed operation 的 search 摘要。
  • page_ai_workflow 应把该错误翻译为用户可理解的提示,不要暴露 apply_block_ops operations 不能为空

中期:

  • build_block_ops_from_markdown_edit 必须复用 search_replace 的定位结果,或让 search_replace 返回原始命中范围 / paragraph / block 映射。
  • 对“第一段/第二段”等序号型指令,应由模型输出对应段落原文作为 search,并增加回读校验。

长期:

  • 7-24 收口:在线 markdown_edit 应以最终 Markdown 为写回真源,或明确限制只支持可安全映射的单块精确替换。

7. 复现 / 验证建议

  • 浏览器 smokeACP Reasonix,页面含至少两段正文,输入“检查第一段并修改第二段为:测试123”。
  • 断言失败时错误码不能是 mnote.doc.apply_block_ops operations 不能为空,应是 markdown 到 block 映射失败的明确错误。
  • 修复后回读 Page Aggregate,断言第二段实际变为 测试123,同时 AI 回复能正常说明已读取到第一段。

8. 修复

已修复:

  • doc.rsmnote.doc.markdown_edit 调用 doc_apply_block_ops 前拦截 applied > 0 && block_ops.is_empty(),返回 mnote_doc_markdown_edit_block_mapping_empty,不再泄露 mnote.doc.apply_block_ops operations 不能为空
  • doc.rs 抽出 search_replace_exact_or_normalized,让 markdown 层与 block 映射层共享“精确 / 忽略空白 / 全半角归一化”规则。
  • doc.rs 在线 block ops 构建不再用 block.text.contains(search),改为对单块文本执行同一套安全匹配;fuzzy 仍只保留在 markdown 预处理层,避免写错块。
  • hermes_tools.rs 增加回归测试,覆盖空映射语义错误与忽略空白后正确映射到 p_2

9. 验证

cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_markdown_edit -- --nocapture

结果:4 个相关测试通过。

10. 剩余风险

本修复解决单块 search/replace 映射和空 ops 下沉;跨块 Markdown 重写、完整最终 Markdown 作为写回真源仍归属于 7-24