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

3.0 KiB
Raw Permalink Blame History

7-16 [done][bug] mnote.doc.markdown_edit 中文归一化替换 byte index 误用 v1

发现时间:2026-05-17

状态:[done]

关联主线:07-ai

关联代码:

  • rust/crates/mnote-web/src/hermes_tools/doc.rs:895-956search_replace
  • rust/crates/mnote-web/src/hermes_tools/doc.rs:920-936 — 归一化匹配后的切片计算

1. 问题定义

mnote.doc.markdown_editsearch_replace 在 Level 2 “忽略首尾空白和全角 / 半角差异”分支中,先对 norm_line 调用 find(&norm_search),得到的是 UTF-8 byte offset

let start = norm_line.find(&norm_search).unwrap();
let end = start + norm_search.len();

随后代码把 start / end 当成字符序号传给 line.char_indices().nth(...)

&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:

原文:第一段内容
search 一段
replace:二段

find() 得到的是 byte offset;当前代码按字符序号切片后,替换边界会落到错误字符位置。

4. 根因

str::find 返回 byte indexchar_indices().nth(n)n 是第几个字符。当前实现把两种索引体系混用。

5. 建议修复

  • 在归一化时保留原文字符到归一化字符的 offset map。
  • 或者只在同一字符串上使用 byte index,并确保切片边界来自同一个原始字符串的 char_indices 映射。
  • 增加中文、多字节标点、全角英文 / 数字混排的 search_replace 单测。

6. 修复

已修复:

  • doc.rs 抽出 search_replace_exact_or_normalized,归一化匹配时不再把 str::find 的 byte offset 当作字符序号使用。
  • doc.rs 新增 normalize_line_with_byte_map,在归一化字符串与原始行之间保留 byte 边界映射。
  • doc.rs 增加中文归一化替换回归测试,覆盖 一 段 命中 一段 时的多字节切片边界。

7. 验证

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

结果:1 个测试通过。