勘误系统三连翻车:从内存回放到直写源文件

AI 工作日志 作者头像野生码农 阅读 11 min
返回板块 GEO 源码全文 ✓
勘误系统三连翻车:从内存回放到直写源文件

2026-07-26 勘误系统三连翻车:从内存回放到直写源文件

背景与目标

给系统分析师备考学习系统(study-app,纯静态 HTML + marked)做勘误功能:在知识点/题目页面底部点"报告勘误"→ 选中错误元素 → 替换文字或图片。需求一句话:改就要改得永久,刷新不能丢。

技术栈:原生 JS(无框架)、localStorage、IndexedDB、File System Access API、marked + KaTeX、git。

过程回顾

尝试 1:修错了地方

最初以为勘误在 Chrome 插件(sys-analyst-ai-assistant)里,把插件的 fix.js 改成修复结果写 IndexedDB + 页面加载重放。用户一句话打回:"勘误逻辑已经变了,不在插件里,在页面底部直接报告"。教训:动手前先确认活代码在哪,别凭第一印象。

尝试 2:修 study-app,发现三个叠加 bug

真正的勘误在 study-app/js/corrections.js + store.js:

  • 坑 1(主因):Store.addCorrection() 无条件把 status 写成 "pending",把修复流程传入的 "resolved" 覆盖了。刷新恢复函数只捞 resolved 的记录 → 一条都恢复不了。用户看到的"数据库里面没换"就是这个。
  • 坑 2:elementInfo(刷新后用来找回元素的定位信息)是在 DOM 替换之后才抓的,存的是"新文字/新图 dataURL"。刷新后页面显示原始内容,拿新内容去找,永远找不到。必须先捕获原文/原 src 再动手。
  • 坑 3:patchDataSource 引用 KNOW/CHOICE,但这俩只是 app.js 闭包里的局部变量,corrections.js 里是 undefined → 数据源补丁从没生效过。真身是 window.KNOWLEDGE / window.CHOICE_QUESTIONS。
  • 顺带:弹窗 close 按钮调的是删 #corr-modal 的函数,而弹窗 id 是 #fix-modal,关不掉。

尝试 3:去防抖治"闪一下错图",直接搞出页面卡死

错误图片会先显示一下再被替换,原因是恢复逻辑挂在 MutationObserver 上有 300ms 防抖。去掉防抖、改成回调里同步恢复——页面直接卡死。

根因:在 MutationObserver 回调里同步改 DOM,改动又触发回调,只要某条修复还能匹配到元素(元素带 id 策略1永远命中、替换文本以原文开头策略2继续命中),就是无限微任务循环。

尝试 4:用户点破本质——"要永久性修改,不是内存"

用户原话:"不是哪天想,是我一直认为要永久性修改。数据库、本地错误的源文件直接替换掉,不要搞什么内存。" 这话把架构彻底掰正:

  • 用 File System Access API 让用户授权「系统分析师」根目录一次(句柄存 IndexedDB);
  • 勘误 = 直接改写 study-app/data/*.js(页面加载的)+ md/*.md(OCR 源文件,重新生成数据的依据);
  • 新图片存 md/assets/corrections/,两种引用写法(../md/assets/... vs assets/...)分别替换;
  • 写前滚动备份到同级 backup/;
  • 回放机制整个删除——卡死的那条路从根上消失。这也印证了用户的怀疑:"卡死 bug 就是内存临时存储搞出来的"。

尝试 5:又是"未找到匹配内容"

文字修复失败:页面渲染后的纯文本和 markdown 源文本对不上(**加粗**、\~ 转义、链接、空白折叠)。解法:模糊匹配——剥离 markdown 符号和空白建立「归一化字符 → 原文下标」映射,定位后映射回原文替换,并吃掉残留的半个星号对。

尝试 6:图片修复还是失败

"就替换个图片这么困难吗?"——查下来两个原因:

  1. 用户修的是上次修过的同一张图:上次"仅页面生效"后 img src 已是 dataURL,拿 dataURL 去源文件找当然两处都没有。解法:会话内记 dataURL → 原始src 映射;找不回就明说"请刷新后重试"。
  2. 全路径匹配太脆(三种写法)。解法:按文件名匹配——哈希文件名全局唯一,找到后把整个路径引用换掉。

用真实数据跑 6 组用例(含真实 md 文件、内联 HTML 形式)全过,这次才算真修完。

关键踩坑

坑 现象 根因 解法
状态覆盖 修复后刷新丢失 addCorrection 强制 pending 尊重调用方传入的状态
定位信息存错 刷新恢复找不到元素 替换后才抓 elementInfo 先捕获原文再改 DOM
作用域假象 数据源补丁空操作 闭包局部变量跨文件不可见 用 window 上的真全局
同步回放 页面卡死 Observer 回调里改 DOM 无限循环 删除回放,改直写源文件
纯文本≠源文本 "未找到匹配内容" markdown 符号差异 归一化模糊匹配 + 下标映射
dataURL 重修 图片修复反复失败 上次临时替换改了 src 会话映射 + 按文件名匹配
onclick 拼内容 题库页按钮没反应 题干换行/引号拼进属性 删掉没用的第三参数

最终方案(核心代码)

模糊匹配定位(文字修复的关键):

// 归一化:去掉 markdown 控制符号和空白,返回归一化串 + 每个字符在原文中的下标
function buildNormMap(text) {
  var map = [], chars = [];
  for (var i = 0; i < text.length; i++) {
    var ch = text[i];
    if (ch === "]" && text[i + 1] === "(") {          // 跳过 ](url) 部分
      var close = text.indexOf(")", i + 2);
      if (close !== -1) { i = close; continue; }
    }
    if (ch === "*" || ch === "`" || ch === "\\") continue; // 强调/代码/转义
    if (/\s/.test(ch)) continue;                      // 空白全部忽略
    map.push(i); chars.push(ch);
  }
  return { norm: chars.join(""), map: map };
}

图片引用按文件名替换(容忍 assets/ 和 ../md/assets/ 写法差异):

function replaceImageRef(source, originalSrc, newRef) {
  var fname = (originalSrc || "").split("/").pop();
  var re = new RegExp("[^\\s()\"'\\\\]*" + escapeRegExp(fname), "g");
  if (!re.test(source)) return { text: source, hit: false };
  return { text: source.replace(re, newRef), hit: true };
}

顺带完成的事

  • git 建仓:Trae 里"提交"实际只 add 了 989 个文件(含 600MB PDF、564 张图片,.git 直接 798M)。方案:PDF/图片资产移出版控 + .gitignore,只提交 md 文本和代码,gc 后 .git 瘦到 7.4M。还拦下了一个差点入库的 tools/mineru_token.txt。
  • .gitignore 陷阱:md/assets/ 整目录忽略会让 !md/assets/corrections/ 例外失效(git 不进入被排除的目录),要写成 md/assets/* + 例外。
  • KaTeX 公式渲染:网页上 $...$ 公式原样显示。vendor 离线 KaTeX,md() 管线改为「先抽公式段 → marked 渲染 → KaTeX 填回」,防止 marked 把公式里的 _/* 当 markdown 吃掉。选项按钮的公式单独走 mdOpt()。

做得好的地方

  • 架构抉择听用户的:用户坚持"直写源文件",当时看着激进,实际上一步到位消掉了回放机制的整类 bug(闪烁、卡死、quota 风险)。需求本质抓得比 AI 准。
  • 每个修复都有真实数据验证:模糊匹配、图片引用替换都在真实 knowledge.js / md 文件上跑过用例才交付(铁律一:只认实测)。
  • 写文件前滚动备份:data/backup、md/backup,写坏了能回滚。

复盘与改进

  • 最大的浪费:第一次修错了项目(插件 vs study-app),白做一轮。以后用户报 bug,第一句话先问/先找"活代码在哪",别凭功能名猜。
  • 中间方案不该存在:localStorage + 回放是个半吊子架构,它引入的 bug(卡死)比它解决的问题还多。一开始就该问清楚"修复的生命周期要多长"。
  • File System Access API 的认知更新:纯静态 HTML 页面在 Chrome 里可以直接读写本地文件(file:// 也是安全上下文),"浏览器改不了本地文件"是过时的假设。
  • 遗留:题目勘误写 md 源文件时靠全文搜索定位(题库 source 字段和 md 文件名对不上),能工作但不够优雅;案例题(questions_case)还没接勘误入口。
野生码农头像

关于作者 · 野生码农

把 AI 真放进生产环境的独立开发者。自研本地知识库、数字员工团队、软考备考系统、量化交易四套系统,全程公开复盘。

看他的平台账号 →
相关

继续阅读