归档本轮 P0/P1 bug 修复、设计审查迁移、AI selection scope 收口与 stream contract 调整,并保留当前 05 主线迁移起点。
92 lines
3.7 KiB
Markdown
92 lines
3.7 KiB
Markdown
# 7-17 [done][bug] mnote.doc.markdown_edit 同块多操作写回会覆盖前序修改 v1
|
||
|
||
> 发现时间:2026-05-17
|
||
>
|
||
> 状态:`[done]`
|
||
>
|
||
> 关联主线:`07-ai`
|
||
>
|
||
> 关联代码:
|
||
> - `rust/crates/mnote-web/src/hermes_tools/doc.rs:779-824` — markdown 字符串层顺序执行 operations
|
||
> - `rust/crates/mnote-web/src/hermes_tools/doc.rs:854-878` — Convex 写回重新转换成 block ops
|
||
> - `rust/crates/mnote-web/src/hermes_tools/doc.rs:976-1008` — `build_block_ops_from_markdown_edit`
|
||
|
||
## 1. 问题定义
|
||
|
||
`doc_markdown_edit` 会先把所有 search/replace 顺序应用到 `md`:
|
||
|
||
```rust
|
||
match search_replace(&md, &search, &replace) {
|
||
Ok(new_md) => {
|
||
md = new_md;
|
||
applied += 1;
|
||
}
|
||
...
|
||
}
|
||
```
|
||
|
||
但在线 Convex 文档写回时没有使用这个最终 `md`。当前实现重新读取 Page Aggregate blocks,然后对每条 operation 基于原始 block 文本生成一个 replace block op:
|
||
|
||
```rust
|
||
let block_text = block.get("text").and_then(Value::as_str).unwrap_or("");
|
||
let new_text = block_text.replacen(search, replace, 1);
|
||
```
|
||
|
||
如果两条 operation 命中同一个 block,第二条 operation 的 `new_text` 仍从原始 `block_text` 计算,会覆盖第一条 operation 的结果。
|
||
|
||
## 2. 影响
|
||
|
||
用户在页面 AI 中一次提出多个同段修改时,可能只保留最后一个修改。例如:
|
||
|
||
```json
|
||
[
|
||
{ "search": "A", "replace": "B" },
|
||
{ "search": "C", "replace": "D" }
|
||
]
|
||
```
|
||
|
||
若 `A` 和 `C` 都在同一个 block 中,markdown 层预期最终是 `B ... D`,但写回层可能生成两个 replace block op:
|
||
|
||
```text
|
||
op1 content = 原始文本中 A -> B
|
||
op2 content = 原始文本中 C -> D
|
||
```
|
||
|
||
最终第二次 replace 会把 block 写成只包含 `C -> D` 的版本,前一次 `A -> B` 被丢失。
|
||
|
||
## 3. 根因
|
||
|
||
`doc_markdown_edit` 有两套编辑结果:
|
||
|
||
- `md`:顺序应用 operations 后的真实 markdown 结果。
|
||
- `block_ops`:从原始 blocks 和原始 operations 重新推导出来的写回操作。
|
||
|
||
Convex 写回使用的是 `block_ops`,而不是 `md`。这让“markdown 编辑主路径”在在线文档里退化成了不完整的 block ops 转换层。
|
||
|
||
## 4. 建议修复
|
||
|
||
- 最小修复:`build_block_ops_from_markdown_edit` 先按 block 聚合 operations,并基于当前已更新文本连续计算同块最终 content。
|
||
- 更稳妥修复:以最终 `md` 作为单一结果,走 markdown -> EditorBlockDocument / page.body.save 的正式转换链,不从原始 operations 二次推导写回结果。
|
||
- 增加同一 block 多 operation 的单测和真实 Page Aggregate 回读 smoke。
|
||
- 明确部分失败是否允许写入;默认建议任一 operation 失败时不写入,除非调用方显式允许 partial apply。
|
||
|
||
## 5. 修复
|
||
|
||
已修复:
|
||
|
||
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:1026) `build_block_ops_from_markdown_edit` 现在维护每个块的当前文本,按 operations 顺序累积同块修改。
|
||
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:1061) 同一块多次命中时只输出一次最终 `replace` block op,避免后续 op 基于原始文本覆盖前序修改。
|
||
- [hermes_tools.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_tools.rs:1772) 增加 dry-run 路由级回归测试,覆盖第二个 search 依赖第一个替换结果的同块多操作场景。
|
||
|
||
## 6. 验证
|
||
|
||
```bash
|
||
cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_markdown_edit_merges_same_block_operations -- --nocapture
|
||
```
|
||
|
||
结果:1 个测试通过。
|
||
|
||
## 7. 剩余风险
|
||
|
||
本修复解决“同块多操作覆盖前序修改”的最小 P0;跨块 Markdown 重写、full_content 与最终 Markdown 作为唯一写回真源仍归属于 `7-24`。
|