2026-05-18 17:01:35 +08:00
|
|
|
|
# 7-25 [done][bug] ACP Reasonix 页面 AI block_edit_workflow 在 markdown 命中后生成空 block_ops v1
|
2026-05-17 23:16:37 +08:00
|
|
|
|
|
|
|
|
|
|
> 发现时间:2026-05-17
|
|
|
|
|
|
>
|
2026-05-18 17:01:35 +08:00
|
|
|
|
> 状态:`[done]`
|
2026-05-17 23:16:37 +08:00
|
|
|
|
>
|
|
|
|
|
|
> 关联主线:`07-ai`
|
|
|
|
|
|
>
|
|
|
|
|
|
> 关联缺陷:`7-24 在线 markdown_edit 写回不以最终 Markdown 为真源`
|
|
|
|
|
|
|
|
|
|
|
|
## 1. 用户可见症状
|
|
|
|
|
|
|
|
|
|
|
|
在页面 AI 中切换到 `ACP · Reasonix`,输入:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
你
|
|
|
|
|
|
检查你是否能读取到本页第一段,同时请修改第二段为:测试123
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
工具调用失败:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
mnote.page_ai.block_edit_workflow 失败 · page-ai-fast-mp9wm0qo
|
|
|
|
|
|
结果 mnote.doc.apply_block_ops operations 不能为空
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 2. 问题定义
|
|
|
|
|
|
|
|
|
|
|
|
`page_ai.block_edit_workflow` 当前已切到:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
模型输出 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](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:69) 调用模型生成 markdown operations。
|
|
|
|
|
|
- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:91) 构造 `mnote.doc.markdown_edit` 输入。
|
|
|
|
|
|
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:811) 在 `md` 字符串上执行 search/replace。
|
|
|
|
|
|
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:857) 调用 `build_block_ops_from_markdown_edit`。
|
|
|
|
|
|
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:989) 只用 `block.text.contains(search)` 反查 block。
|
|
|
|
|
|
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:1008) 可能返回空 `block_ops`。
|
|
|
|
|
|
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:878) 即使 `block_ops` 为空,也继续调用 `doc_apply_block_ops`。
|
|
|
|
|
|
- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs:405) 对空 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 写回层仍失败。
|
|
|
|
|
|
|
2026-05-18 17:01:35 +08:00
|
|
|
|
## 6. 建议修复:建议以长期,彻底的修复优先,建议参考/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/cli-main中对全局替换和单块替换的判断逻辑。
|
2026-05-17 23:16:37 +08:00
|
|
|
|
|
|
|
|
|
|
短期:
|
|
|
|
|
|
|
|
|
|
|
|
- `doc_markdown_edit` 在调用 `doc_apply_block_ops` 前,如果 `applied > 0` 但 `block_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. 复现 / 验证建议
|
|
|
|
|
|
|
|
|
|
|
|
- 浏览器 smoke:ACP Reasonix,页面含至少两段正文,输入“检查第一段并修改第二段为:测试123”。
|
|
|
|
|
|
- 断言失败时错误码不能是 `mnote.doc.apply_block_ops operations 不能为空`,应是 markdown 到 block 映射失败的明确错误。
|
|
|
|
|
|
- 修复后回读 Page Aggregate,断言第二段实际变为 `测试123`,同时 AI 回复能正常说明已读取到第一段。
|
|
|
|
|
|
|
2026-05-18 17:01:35 +08:00
|
|
|
|
## 8. 修复
|
2026-05-17 23:16:37 +08:00
|
|
|
|
|
2026-05-18 17:01:35 +08:00
|
|
|
|
已修复:
|
2026-05-17 23:16:37 +08:00
|
|
|
|
|
2026-05-18 17:01:35 +08:00
|
|
|
|
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:862) 在 `mnote.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](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:928) 抽出 `search_replace_exact_or_normalized`,让 markdown 层与 block 映射层共享“精确 / 忽略空白 / 全半角归一化”规则。
|
|
|
|
|
|
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:1038) 在线 block ops 构建不再用 `block.text.contains(search)`,改为对单块文本执行同一套安全匹配;fuzzy 仍只保留在 markdown 预处理层,避免写错块。
|
|
|
|
|
|
- [hermes_tools.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_tools.rs:1670) 增加回归测试,覆盖空映射语义错误与忽略空白后正确映射到 `p_2`。
|
|
|
|
|
|
|
|
|
|
|
|
## 9. 验证
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_markdown_edit -- --nocapture
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
结果:4 个相关测试通过。
|
|
|
|
|
|
|
|
|
|
|
|
## 10. 剩余风险
|
|
|
|
|
|
|
|
|
|
|
|
本修复解决单块 search/replace 映射和空 ops 下沉;跨块 Markdown 重写、完整最终 Markdown 作为写回真源仍归属于 `7-24`。
|