Readest 校对替换规则失效排查:Selection 作用域规则的 Section 身份锚定修复(Issue 6148 深度解析)
桌面应用跨平台前端【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址https://gitcode.com/gh_mirrors/re/readest点击查看免费下载本文围绕 Readest 阅读器校对Proofread替换功能中一个棘手的回归问题展开当用户为一段选中的文本创建selection作用域的替换规则后一旦切换开关、编辑规则或重新打开书籍规则便不再生效。文章以 apps/readest-app/.claude/memory/proofread-selection-section-anchor-6148.md 记录的问题诊断为主线深入源码剖析根因TOC href 与 spine item href 两套节身份标识的错位、修复方案基于 CFI 的节匹配以及配套的验证与审查过程。读完本文你将理解 Readest 校对替换管线的完整链路掌握规则创建时生效、重放时失效这一类问题的通用排查方法并能复现文档中给出的 vitest 浏览器级验证流程。一、问题表象规则只生效一次的假象Issue #6148 描述的现象非常典型用户选中一段文本创建校对替换规则点击Apply后替换立刻生效但一旦切换规则的启用开关、编辑规则内容或者关闭书籍后重新打开这条规则就再也无法复现替换效果。从 ProofreadPopup.tsx 的源码可以看到规则创建后先做了一次实时 DOM 编辑再通过addRule持久化规则// handleApply 中scope selection 分支 const textNode range.startContainer as Text; const text textNode.textContent ?? ; textNode.textContent text.slice(0, range.startOffset) replacement text.slice(range.endOffset);也就是说用户点击 Apply 后看到的效果其实是ProofreadPopup直接修改了当前已加载的活文档与后续渲染所依赖的转换器transformer没有任何关系。而书籍每一次重新加载、章节每一次重新渲染时内容都会经过 proofreadTransformer 重新处理。问题诊断结论该记忆文档原话规则在创建时一直生效但原因是错误的——selection 作用域规则在 transformer 路径上从来就没有真正工作过只是被实时 DOM 编辑掩盖了。二、根因剖析两套相互冲突的节身份标识记忆文档给出了清晰的根因定位ProofreadPopup与proofreadTransformer对当前节采用了两种不同的身份标识。2.1 存储端TOC href修复前ProofreadPopup将progress?.sectionHref存入规则的sectionHref字段。而progress.sectionHref来自 foliate 的TOCProgress.getProgress语义是当前阅读位置之前最近的一条目录TOC导航条目——它是一个tocItem.href。2.2 匹配端spine item hrefproofreadTransformer拿rule.sectionHref与转换上下文的ctx.sectionHref比较。在 FoliateViewer.tsx 中这个值被赋为 foliate 的detail.name来自Loader.createURL即spine/manifest 清单中的章节条目 hrefbookDoc.sections[i].id item.href。2.3 两者何时不一致两种 href 只有在每个 spine 条目恰好都有独立 TOC 条目时才相等。而现实中的 EPUB 往往不满足这一假设。记忆文档记录了作者用仓库自带 epub 测试夹具实测的数据sample-table-layout.epub12 个节中有 10 个不匹配sample-alice.epub16 个节中有 2 个不匹配索引 15 是OPS/feedbooks.xml其 TOC 条目却是OPS/main11.xml大多数书籍的第一个 spine 条目不匹配因为封面cover通常没有 TOC 条目此时toc为undefined。这解释了为何规则偶尔生效、大多数情况下静默失效——匹配成功纯属巧合。2.4 佐证TransformContext 的注释与字段演化修复后的 TransformContext 类型定义 用注释直接记录了这段历史sectionHref?: string; // Spine CFI of the section being transformed (sections[i].cfi), when the // format has one. Selection-scoped proofread rules match against this rather // than sectionHref, which is the TOC href at the reading position and so // names a different file whenever a spine item has no TOC entry (#6148). sectionCfi?: string;三、修复方案用规则自带的 CFI 锚定节身份修复落地于 PR #61522026-09-09 以 squash 方式合并提交1b681939dCI 全 15 项检查通过。方案分两步核心思路是放弃用 href 判断当前节改为用规则自身携带的 CFI 判断。3.1 第一步给 TransformContext 增加 sectionCfiTransformContext新增sectionCfi字段在FoliateViewer装配上下文时从书文档的 sections 索引中按 spine id 查找sectionHref: detail.name, sectionCfi: bookData.bookDoc?.sections?.find((s) s.id detail.name)?.cfi,bookDoc.sections中每一项都带有自己的 spine CFI形如epubcfi(/6/14)这正是节身份的权威表达。3.2 第二步inThisSection 改用 CFI 的 spine step 比较proofread.ts 中新增了cfiSpineStep辅助函数与inThisSection判定// Spine step of a CFI: everything before the first indirection (!), or the // whole path for a section CFI, which has none. sections[i].cfi is // epubcfi(/6/14) and a selection made in it is epubcfi(/6/14!/4/2,...). const cfiSpineStep (cfi?: string): string | undefined cfi?.match(/^epubcfi\((.*?)(?:!|\)$)/)?.[1]; const inThisSection (rule: ProofreadRule, ctx: TransformContext): boolean { const ruleSpine cfiSpineStep(rule.cfi); const ctxSpine cfiSpineStep(ctx.sectionCfi); if (ruleSpine ctxSpine) return ruleSpine ctxSpine; return ctx.sectionHref?.split(#)[0] rule.sectionHref?.split(#)[0]; };这里的技巧是提取 CFI 中第一个!间接寻址指示符之前的spine step一条在章节内做出的选择其 CFI 是epubcfi(/6/14!/4/2,...)而章节本身的 CFI 是epubcfi(/6/14)二者去掉!后的 spine step 完全一致从而可以可靠判断这条规则属于当前正在转换的章节。无需迁移旧规则虽然存的是 TOC href但其cfi字段天然携带 spine 信息新逻辑自动治愈heal已存储的旧规则优雅降级没有 spine CFI 的格式如 markdown 书籍见 utils/md.ts其section.id是String(index)回退到旧的 href 比较行为不变。3.3 修正存储端ProofreadPopup 存 spine hrefProofreadPopup.tsx 现在优先从视图的书对象中取 spine 条目 id让sectionHref字段名副其实// Anchor to the spine item href, which is what every section load hands // the proofread transformer (foliates detail.name). progress carries // the TOC href ... const sectionHref getView(bookKey)?.book?.sections?.[selection.index]?.id ?? progress?.sectionHref;3.4 规则数据模型ProofreadRule的完整结构见 types/book.ts其中与本次修复直接相关的字段是cfi选择锚点与sectionHref节身份order字段决定规则应用顺序数值小者先应用enabled/deletedAt/updatedAt则支撑规则的启用状态与 CRDT 同步book/selection 作用域规则随书配置同步library 作用域规则走设置副本的整体字段 LWW 合并。四、排查过的死胡同非根因项记忆文档明确记录了一组被验证过、但并非本 bug 成因的候选路径对后续排查有极高的参考价值text/html与 XHTML 解析差异transformContent调用时未传docType选项所以转换器内docType恒为text/html——无害foliate 跨recreateViewer的 blob-URL 缓存不是成因cfi-inert无障碍跳过链接注入foliate 的 CFI 索引会过滤这些节点不是成因enabled/onlyForTTS过滤逻辑见 proofread.ts规则需enabled !deletedAt pattern.trim()且通过onlyForTTS分流后才参与处理——非成因viewSettings.proofreadRules持久化数据能正常落盘非成因。五、验证配方只有浏览器级测试才是忠实复现记忆文档强调jsdom 之外的唯一可靠复现方式是 vitest 浏览器测试。给出的验证步骤值得完整保留用DocumentLoader加载sample-alice.epub测试夹具创建真实的foliate-view与paginator.js把transformContent挂到book.transformTarget的data事件上逐个捕获detail.name→ 原始字符串的映射执行view.goTo(15)跳转到目标节用getContents().find((c) c.index INDEX)选取目标文档——注意不能直接取getContents()[0]那通常是相邻章节会静默得到错误文档的 CFI计算view.getCFI(index, range)再对捕获的原始字符串重跑transformContent验证替换是否命中。技术要点jsdom 能复现除sanitizer之外的全部行为因为 DOMPurify 在 jsdom 环境下会输出重复的xmlns属性——在 jsdom 中验证时应把sanitizer从转换器列表中剔除。六、CodeRabbit 审查7 项发现与三项关键修正PR 的 CodeRabbit 审查共提出 7 项发现作者全部认可其有效性其中 3 项以不同于建议的方式解决提交3b5b086d3、cc59920a56.1 真实 bugtrim 不一致导致的同会话漂移实时拼接live splice使用的是原始replacementText而规则持久化的是replacementText.trim()。结果是同一替换本会话显示一个样子后续每次重放又是另一个样子——这正是本次 PR 要消除的漂移。修复方式在handleApply顶部统一 trim 一次两处复用见 ProofreadPopup.tsx 的注释与const replacement replacementText.trim();。6.2 拒绝提示语与实际约束不符审查指出规则的实际约束是单个文本节点——一个em或链接就会在一个段落内打破这个前提因此请在同一段落内选择是可照做却被拒绝的建议。作者没有采纳建议中的single text node对读小说的人来说是 DOM 术语而是把提示改写为面向用户的自然语言This selection spans formatting or paragraph boundaries. Select a smaller piece of text, or choose another replacement scope.并重新翻译了全部 34 个语言文件。6.3 术语一致性翻译必须复用既有词条两条术语审查sv、bo都先对照语言文件核实sv用Scope → Omfattningbo用Paragraph → དོན་ཚན།/Chapter → ལེའུ།而新造的ཚིག་ལེའུ混淆了段落与章节。由此确立一条规则新翻译必须复用该语言文件已有的术语Scope、Paragraph等不得自造新词。该规则推广到全部 34 个语言文件随后又揪出德语生造词Ersetzungs-Geltungsbereich最终采用与作用域选择器标签一致的Geltungsbereich für die Ersetzung。七、同轮落地的两个关联修复7.1 跳转到选择位置的可见入口#6109 遗留Selection作用域徽章曾被直接做成跳转目标但它在视觉上与旁边的 Regex、Case sensitive 徽章完全一样用户根本发现不了。修复后徽章恢复为普通RuleChip跳转变成编辑/删除按钮旁独立的btn btn-ghost btn-sm h-8 w-8 p-0按钮使用MdOutlineArrowOutward图标与已有翻译串_(Jump to Location)FootnotePopup 已在用无需新增 key见 ProofreadRules.tsx行内为绝对定位的操作簇预留宽度所以额外按钮需要把pe-28改为pe-36——这个数值是在apps/readest-app/src/__tests__/components/proofread-rule-row-clearance.browser.test.tsx中实测出来的pe-28加第三个按钮确实重叠不是拍脑袋。7.2 deleteContents 的标记丢失ProofreadPopup原先用deleteContents()insertNode()做实时替换这会剥离选择触达的标记并把文本节点拆成三段——后续该章节内所有 CFI 计算依赖的文本节点索引因此偏移。修复后改为在单个文本节点内原地拼接跨元素的选择会被 toast 拒绝因为applyReplacementSingle要求startContainer endContainer此类规则根本无法重放过去只是靠实时 DOM 编辑假装生效。顺带说明这一改动使ProofreadPopup.test.tsx中共享的defaultProps.selection.range在测试间泄漏旧夹具是带deleteContents: vi.fn()的假对象现在改为在beforeEach中重建。八、小结与排查经验Issue #6148 的完整闭环可以浓缩为几条可迁移的经验创建时生效不能证明重放路径正确——任何在创建时直接改动活 DOM 的功能都必须额外验证持久化后的重放路径节身份锚定要选权威表达EPUB 中 spine item及其 CFI是每章的权威身份TOC href 只是最近前驱导航条目两者在封面、合并章节等场景必然错位CFI 的 spine step!之前的部分是判断规则属于哪个节的可靠依据修复要让旧数据自动愈合基于规则已携带的cfi做匹配无需迁移存量规则同时为无 spine CFI 的格式保留 href 回退提示语面向用户而非 DOM把单一文本节点翻译成选择更小文本或换一种替换作用域并保持 34 个语言文件的术语一致性验证必须有浏览器级测试jsdom 只差sanitizer一个转换器即可完整复现选文档时用getContents().find((c) c.index INDEX)而非getContents()[0]避免拿到相邻章节的 CFI。如需继续深入可进一步阅读关联的记忆文档 proofread-panel-design-pass-6109.mdselection 徽章与面板设计演进与 proofread-gate-reflowable-formats.md重排格式的校对功能门控以及 proofreadStore.ts 中规则的增删改查与排序持久化实现。赞分享桌面应用跨平台前端【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址https://gitcode.com/gh_mirrors/re/readest点击查看免费下载相关推荐commitlint核心规则解析深度理解30校验规则commitlint核心规则解析深度理解30校验规则 commitlint是一个强大的Git提交信息校验工具通过30多种核心规则确保团队代码提交的规范性和开发工具Lint代码质量eslint-plugin-unicorn prefer-string-slice 规则深度解析用 Stringslice() 替换 substr 与 substring 的自动修复机制eslint plugin unicorn prefer string slice 规则深度解析用 String slice 替换 substr 与 subsLint代码质量Complete-Python-3-Bootcamp变量作用域LEGB规则深度解析Complete Python 3 Bootcamp变量作用域LEGB规则深度解析 在Python编程中变量作用域Scope是每个开发者必须掌握的核心概教程示例工程上一篇Cuckoo Sandbox开源自动化恶意软件分析系统详解下一篇NBA_API v1.9.0版本发布新增实时数据接口与构建优化创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

3个坑搞懂土壤pH数据清洗,手写实现避坑指南

3个坑搞懂土壤pH数据清洗,手写实现避坑指南

3个坑搞懂土壤pH数据清洗,手写实现避坑指南 看了一堆教程还是不会写项目?别急,问题不在你,在于教程只讲了原理没讲工程。在水利和农业信息化领域,处理传感器回传的 土壤pH…

2026/9/21 18:47:37 阅读更多 →
改进减法优化器算法GSABO:融合黄金正弦与混沌映射

改进减法优化器算法GSABO:融合黄金正弦与混沌映射

1. 项目概述在智能优化算法领域,2023年新提出的减法优化器算法(SABO)因其独特的数学基础和优化机制引起了广泛关注。作为一名长期从事算法优化研究的工程师,我在实际应用中发现原始SABO算法在解决高维非线性问题时存在收敛速度不稳定、易陷入局部最优等问…

2026/9/21 18:46:36 阅读更多 →
SpringBoot2+Vue3农事管理系统开发实践

SpringBoot2+Vue3农事管理系统开发实践

1. 项目概述农事管理系统是现代农业信息化建设的重要组成部分。作为一名长期从事农业信息化系统开发的工程师,我深刻理解传统农事管理方式面临的挑战:手工记录效率低下、数据容易丢失、信息传递不及时等问题。这套基于SpringBoot2Vue3的农事管理系统&…

2026/9/21 18:46:36 阅读更多 →

最新新闻

3步搞定哆点下载卡顿 一文搞懂性能调优实战

3步搞定哆点下载卡顿 一文搞懂性能调优实战

3步搞定哆点下载卡顿 一文搞懂性能调优实战 打开 IDE,盯着屏幕上一片红色的 StackTrace,是不是血压瞬间飙升?报错日志长得像天书, OutOfMemoryError 、 SocketTimeoutException 、…

2026/9/21 20:06:17 阅读更多 →
面试被问android.process.acore已停止别慌,这份速查手册救命

面试被问android.process.acore已停止别慌,这份速查手册救命

面试被问android.process.acore已停止别慌,这份速查手册救命 面试现场,面试官盯着你问:“Android 为什么频繁崩溃?看到 android.process.acore…

2026/9/21 20:06:17 阅读更多 →
日路面试被问懵?这份保姆级教程带你通关

日路面试被问懵?这份保姆级教程带你通关

日路面试被问懵?这份保姆级教程带你通关 面试被问“日路”原理答不上来,那种大脑一片空白的窒息感,谁懂?别慌,今天这篇保姆级教程,专治各种原理盲区。不管你是刚入行的萌新,还是准备跳槽的老兵,只要想在这个领域站稳脚跟,把“日路”搞透是硬道理。很…

2026/9/21 20:06:17 阅读更多 →
Win7摄像头软件一文搞懂:老系统视频调试避坑全指南

Win7摄像头软件一文搞懂:老系统视频调试避坑全指南

Win7摄像头软件一文搞懂:老系统视频调试避坑全指南 官方文档太长抓不住重点,Win7摄像头软件调试时是不是常对着报错发呆?别急,这篇文章带你一文搞懂,从底层原理到实战代码,彻底解决老系统视频采集的难题。…

2026/9/21 20:06:17 阅读更多 →
3行代码搞懂雾霾指数速查手册,面试原理不再卡壳

3行代码搞懂雾霾指数速查手册,面试原理不再卡壳

3行代码搞懂雾霾指数速查手册,面试原理不再卡壳 面试被问原理答不上来,那种尴尬谁懂? 很多人背了一堆八股文,问到 AQI 计算逻辑就懵圈。 别慌,今天把【雾霾指数】的底层逻辑扒开揉碎,给你一份【速查手册】。 入口定位:AQI 到底是个啥…

2026/9/21 20:06:17 阅读更多 →
猴子摘鲜果源码解析:新手避坑与多语言选型实战指南

猴子摘鲜果源码解析:新手避坑与多语言选型实战指南

猴子摘鲜果源码解析:新手避坑与多语言选型实战指南 配置环境就卡半天,是不是你的常态?很多刚入行的应届生朋友,面对经典的“猴子摘鲜果”算法题,还没开始写逻辑,就在 Python 和 Java 的环境切换中耗尽了耐心。这种 新手避坑…

2026/9/21 20:05:17 阅读更多 →

日新闻

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程 【免费下载链接】agentic-awesome-skills AAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and …

2026/9/21 0:00:01 阅读更多 →
gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析

gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析

gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析 【免费下载链接】gin-vue-admin 🚀ViteVue3Gin拥有AI辅助的基础开发平台,企业级业务AI开发解决方案,内置mcp辅助服务,内置skills管理,…

2026/9/21 0:00:01 阅读更多 →
Wox 全功能插件开发实战指南:基于 Python / Node.js 宿主与 WebSocket 的持久化插件体系

Wox 全功能插件开发实战指南:基于 Python / Node.js 宿主与 WebSocket 的持久化插件体系

桌面应用AI 应用插件系统 【免费下载链接】Wox A cross-platform launcher that simply works 项目地址: https://gitcode.com/gh_mirrors/wo/Wox 点击查看 免费下载 全功能插件(Full-featured Plugin)是 Wox 三类插件实现方式中能力最完整的…

2026/9/21 0:00:01 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/21 3:13:20 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/21 2:19:36 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/21 4:51:05 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/21 15:36:51 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/21 15:36:51 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/19 23:35:34 阅读更多 →