Skip to content

EPUB 导航精度 B5 + B6 —— 交接稿(as-built)

物理仓库位置~/reading-browser 2026-08-08 收口。范围 = backlog §🔴 EPUB 长期阅读 第 5、6 条。零 migration,schema.sql 未动。 前置:epub-usability-sprint1-handoff.md(B1-B4)· epub-longform-acceptance-handoff.md(验收 E5)


1. 实测归因(推翻了 backlog 里的原假设,以本节为准)

原 backlog 写 B5「未细分是高亮注入回流还是字体 settle,实施时用『关掉 CEFR 高亮再走一遍』判别」。 实际做法是往 content-script 加分相埋点console.logdebug_log_console/debug/logs), 一次往返就把位移拆开了,不需要切开关。

回程 h-8 → h-0#FNanchor_25_25 实测:

时刻锚点距视口顶文档总高已重排版
t0 DOMContentLoaded745635,243
t1 排版落地后510765,223
t2 高亮注入完513965,255
t3 +1.5s513965,255

归因:排版 2,349px(98.7%)/CEFR·短语高亮 32px(1.3%)/此后无任何变化。

真凶 = content-script/features/settings.js::loadSettingsrb-cache:// 页做的整页重排版 (body 塞进 max-width:680px + Georgia + line-height:1.8 + fontSize + padding:48px 40px 80px), 把文档高度撑了 +85%。原生锚点滚动发生在这之前(t1 时 scrollY=7452 ≈ t0 时锚点旧偏移 7456), 排版一落地,锚点上方全部长高 → 锚点下移、视口原地不动 → 停在目标上方

同文件锚点跳转不受影响的原因也在这里:那条路(MainApp::handleTocNavigateevalInWebview(scrollIntoView) 分支)执行时页面早已排版完毕。E5 记的"去程/回程不对称"就是这么来的。

两条原稿错误(已在下游文档修正)

  1. 「去程 ✅」不成立——去程(正文 → 脚注页 #Footnote_25_25)同样偏,实测 top=1072(≈1 屏)。 偏移量正比于锚点在文件里的深度,去程恰好浅,验收时没察觉。两个方向都坏,都由本次修复覆盖。
  2. MainApp 老注释「cross-file 导航 URL 通常不带锚点(webview 滚完就剥)」是错的。 实测 url 一路带着 #FNanchor_25_25navigation-state-changed。 B6 从来不是"没 hash",是"hash 不是目录锚点"。注释已删。

2. B6 的落点(为什么"退一步按 base 文件匹配"不够)

这本书 h-0.xhtml 一个文件里住着 7 条目录条目:

#pgepubid00001  THE WORKS OF AURELIUS AUGUSTINE,
#pgepubid00002  REV. MARCUS DODS, M.A.
#pgepubid00007  CONTENTS.
#pgepubid00010  EDITOR'S PREFACE.
#pgepubid00021  BOOK FIRST.          ← FNanchor_25_25 上方最近的一条
#pgepubid00067  BOOK SECOND.
#pgepubid00068  ARGUMENT.

只按文件回落 → 恒取第 0 条 → 「下一章」= 第 1 条 = 往回跳。 避开了"回到全书开头",但换了个小一号的错。所以做成两级:精确匹配 + 同文件兜底, 再由页内脚本把 fragment 校正成上方最近的目录锚点,让精确分支真正命中。


3. 改了什么

文件改动
src/lib/epubToc.ts新文件:目录锚点推导的唯一决策处。resolveTocEntry(两级)/ sameFileTocEntries / tocAnchorsForFile / tocBase / tocAnchor,并把 computeNextTocAnchorMainApp 挪进来
src/lib/epubToc.test.ts新文件:13 条单测,数据取自实测书真实 TOC 形状(含"兜底结果必须是目录里真实存在的 href"这条——它才是「‹ › 跳回开头」的真正判据)
src-tauri/src/content-script/features/epub-anchor.js新文件:B5 的重定位(initEpubAnchor / restoreEpubAnchor)+ B6 的 fragment 校正(syncEpubTocAnchor
src-tauri/src/content-script/index.jsonPageReady 起点 initEpubAnchor()loadSettings().thenrestoreEpubAnchor() + syncEpubTocAnchor() + 500ms 重试;full-pipeline 末尾各补一次
src/MainApp.tsx两级推导替换原 tocMatch__RB_EPUB_META 两处注入点都加 tocAnchors;删除本地 computeNextTocAnchor
src/components/TocPanel.tsx当前条目 activeRef + scrollIntoView({block:'nearest'})ListItem 本就 forwardRef<HTMLDivElement>,无需包 div)

三个关键设计决定

① B5 的两道让位闸(都在 restoreEpubAnchor 内部): _userMoved(wheel/touchstart/keydown 任一触发即永久让位)+ _appliedY(当前 scrollY 不等于 我们上次放的位置 → 说明别人动过 → 让位)。必需:full-pipeline 在这本书上要跑好几秒 (短语 LLM judge),那时把正在阅读的人拽回锚点,比定位不准更糟。

② B6 走 replaceState 改写 fragment,而不是新开一条 page→main 上报通道。 fragment 本来就该表示"你在哪"。改写后 MainApp 既有的精确匹配分支自然命中, 零 Rust 改动、零新事件。回路是刻意复用的:tracking.jshistory.replaceState 已被 patch, 会捎带一次 reportNavigationState已实测 replaceStaterb-cache:// 上允许 (这是设计成立与否的唯一未知数,已落实;失败也有 try/catch 退回同文件兜底)。

③ 只在"已有 fragment 且它不是目录锚点"时才改写。 无 fragment 的情况(从文件头读起)不动——那时 scrollY=0,同文件第一条本来就是对的, 不必平白给 source_ref 添一个锚点。

顺带的正面副作用

effectiveSourceUrl() 用的是 __RB_EPUB_META.tocHref。B6 之前,落在脚注锚点的页面 tocHref 是不带 #spineHref → 在那儿存的词 source_ref 丢章节信息。 现在拿到精确章节锚点 → B4 的「恢复到章」在脚注往返之后也准了


4. 实测结论(全部经 /debug 探针断言,非看屏幕)

ground truth 取自 h-0.xhtml 字节偏移:pgepubid00010@11920 / pgepubid00021@35974 / FNanchor_25_25@36751 / pgepubid00067@154154 → 正确答案 = pgepubid00021(BOOK FIRST.)。

条目证据
fragment 改写tab url = …h-0.htm.xhtml#pgepubid00021(从 FNanchor_25_25 改写而来)
replaceState 可用性✅ 允许(否则会看到同文件兜底的 pgepubid00001
目录高亮[class*="bg-primary/10"]total_matches: 1,命中 BOOK FIRST.(精确,非兜底)
‹ › 状态[aria-label="上一章"] / [aria-label="下一章"] 均 found,disabled 文案均 not-found → tocIdx=4 有效
B5 落点(间接)校正结果按 scrollY 实算得 pgepubid00021;若仍偏 5107px,落点会在 35974 之前 → 必然算出 pgepubid00010。得到 21 ⇒ 落点已在 BOOK FIRST 区间内,目标在其后约 780 字节
B5 落点(直接,用户肉眼回程一跳直接落到含 [25] 的那一句。修复前同一操作偏目标上方 ≈5 屏 → 像素级到位,无需再往后挪定位时机
错误日志0 条

pnpm test 133 passed(新增 13)· cargo check ✅ · pnpm build ✅ · /arch-check H1-H6 零命中,S17=7 / S24=7 / S25=0 / S27=3 / S19·S22·S23=0 全持平基线 (MainApp 909→895 行,content-script.js 7751→7828 行)· /ui-check U1=2(永久豁免)U2=U3=0、U4=1。 改了 content-script → 已 pnpm build:cs,并 grep -a 确认诊断埋点已从 bundle 清干净(0 命中)。


5. 留下的东西

  • B6 的高亮只在导航那一拍校正一次_tocSynced 一次性闸)。在 h-0 这种一文件 7 条的书里 滚动时,高亮不跟着走 → 滚动联动(scroll-spy)见 epub-toc-quality-handoff.md §2 项 D(含三个已知坑)。
  • 同文件兜底仍是近似(取同文件第一条)。它现在只在 fragment 校正落地前那几百毫秒、 以及校正失败时生效。刻意保留AddressBartocIdx < 0 → 下一章 = toc[0] 那条兜底 服务"刚开书、锚点还没回传",不能删。
  • 本轮挖到但未修epub.rs 没有 EPUB 3 nav.xhtml 回落(只认 application/x-dtbncx+xml)→ 纯 EPUB 3 无 ncx 的书目录整个空、‹ › 全灰 = 又一个"开书即死路"。 Gutenberg 全系带 ncx 故六轮实测未暴露。详见 epub-toc-quality-handoff.md §2 项 B'(最高优先)。
  • 同轮证伪:旧文档「解析器过滤掉 526 个 [Pg NNN]」结论对、归因错——那 542 条是 <pageTarget>,我们只扫 <navPoint>仓里不存在任何 [Pg 过滤
  • 未做:B4 章内偏移(需新列 → 迁移从 v32 起)、B7-B9。