Persist PageTree expand state via control-plane view-state and align chevron/DOM with restored expansion; keep Sidex-style shallow page-tree scan and drop the unused recursive scanner that only added cargo noise. Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi into a module package, and retire Hermes/ACP/OpenHub recycle + root harness evidence from the index while gitignoring recycle and local diag dumps. Archive superseded design/bugs docs under old/, point architecture at ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor regressions so the working tree can stay clean.
109 lines
6.0 KiB
Markdown
109 lines
6.0 KiB
Markdown
# [recycle] 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`,输入:
|
||
|
||
```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 写回层仍失败。
|
||
|
||
## 6. 建议修复:建议以长期,彻底的修复优先,建议参考/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/cli-main中对全局替换和单块替换的判断逻辑。
|
||
|
||
短期:
|
||
|
||
- `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 回复能正常说明已读取到第一段。
|
||
|
||
## 8. 修复
|
||
|
||
已修复:
|
||
|
||
- [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`。
|