2026-05-18 17:01:35 +08:00
|
|
|
|
# 7-16 [done][bug] mnote.doc.markdown_edit 中文归一化替换 byte index 误用 v1
|
2026-05-17 23:09:56 +08:00
|
|
|
|
|
|
|
|
|
|
> 发现时间:2026-05-17
|
|
|
|
|
|
>
|
2026-05-18 17:01:35 +08:00
|
|
|
|
> 状态:`[done]`
|
2026-05-17 23:09:56 +08:00
|
|
|
|
>
|
|
|
|
|
|
> 关联主线:`07-ai`
|
|
|
|
|
|
>
|
|
|
|
|
|
> 关联代码:
|
|
|
|
|
|
> - `rust/crates/mnote-web/src/hermes_tools/doc.rs:895-956` — `search_replace`
|
|
|
|
|
|
> - `rust/crates/mnote-web/src/hermes_tools/doc.rs:920-936` — 归一化匹配后的切片计算
|
|
|
|
|
|
|
|
|
|
|
|
## 1. 问题定义
|
|
|
|
|
|
|
|
|
|
|
|
`mnote.doc.markdown_edit` 的 `search_replace` 在 Level 2 “忽略首尾空白和全角 / 半角差异”分支中,先对 `norm_line` 调用 `find(&norm_search)`,得到的是 UTF-8 byte offset:
|
|
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
|
let start = norm_line.find(&norm_search).unwrap();
|
|
|
|
|
|
let end = start + norm_search.len();
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
随后代码把 `start / end` 当成字符序号传给 `line.char_indices().nth(...)`:
|
|
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
|
&line[..line.char_indices().nth(start).map(|(i, _)| i).unwrap_or(0)]
|
|
|
|
|
|
&line[line.char_indices().nth(end).map(|(i, _)| i).unwrap_or(line.len())..]
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
这在 ASCII 文本里不明显,但中文、中文标点、全角字符都是多字节。byte offset 不等于字符序号,最终替换范围会偏移。
|
|
|
|
|
|
|
|
|
|
|
|
## 2. 影响
|
|
|
|
|
|
|
|
|
|
|
|
- 页面 AI 对中文正文执行 `mnote.doc.markdown_edit` 时,归一化匹配可能替换错位置。
|
|
|
|
|
|
- 本地 `.md` 文件和在线 Convex 文档共用该工具,因此两条 AI 编辑路径都会受影响。
|
|
|
|
|
|
- 如果替换结果继续写回,用户看到的正文可能被局部破坏,而不是简单失败。
|
|
|
|
|
|
|
|
|
|
|
|
## 3. 复现思路
|
|
|
|
|
|
|
|
|
|
|
|
构造一行中文正文,让精确匹配失败但归一化匹配命中,例如带首尾空白或全角 / 半角差异的 search:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
原文:第一段内容
|
|
|
|
|
|
search: 一段
|
|
|
|
|
|
replace:二段
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`find()` 得到的是 byte offset;当前代码按字符序号切片后,替换边界会落到错误字符位置。
|
|
|
|
|
|
|
|
|
|
|
|
## 4. 根因
|
|
|
|
|
|
|
|
|
|
|
|
`str::find` 返回 byte index;`char_indices().nth(n)` 的 `n` 是第几个字符。当前实现把两种索引体系混用。
|
|
|
|
|
|
|
|
|
|
|
|
## 5. 建议修复
|
|
|
|
|
|
|
|
|
|
|
|
- 在归一化时保留原文字符到归一化字符的 offset map。
|
|
|
|
|
|
- 或者只在同一字符串上使用 byte index,并确保切片边界来自同一个原始字符串的 `char_indices` 映射。
|
|
|
|
|
|
- 增加中文、多字节标点、全角英文 / 数字混排的 `search_replace` 单测。
|
|
|
|
|
|
|
2026-05-18 17:01:35 +08:00
|
|
|
|
## 6. 修复
|
2026-05-17 23:09:56 +08:00
|
|
|
|
|
2026-05-18 17:01:35 +08:00
|
|
|
|
已修复:
|
|
|
|
|
|
|
|
|
|
|
|
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:928) 抽出 `search_replace_exact_or_normalized`,归一化匹配时不再把 `str::find` 的 byte offset 当作字符序号使用。
|
|
|
|
|
|
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:959) 新增 `normalize_line_with_byte_map`,在归一化字符串与原始行之间保留 byte 边界映射。
|
|
|
|
|
|
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:670) 增加中文归一化替换回归测试,覆盖 `一 段` 命中 `一段` 时的多字节切片边界。
|
|
|
|
|
|
|
|
|
|
|
|
## 7. 验证
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
cargo test --manifest-path rust/Cargo.toml -p mnote-web test_search_replace_normalized_chinese_byte_boundaries -- --nocapture
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
结果:1 个测试通过。
|