主题
roadmap 3-5 · EPUB 长期阅读验收 —— 交接稿
物理位置:
~/reading-browser/docs/plans/epub-longform-acceptance-handoff.md(RB 仓,Tauri/Rust/TS) roadmap 条目:docs/plans/product-iteration-roadmap-2026h2.md§三 3-5(第三梯队最后一条) 创建:2026-08-04(由 3-2 收尾会话代写,未动任何代码) 性质:纯 RB 验收任务。零 Supabase、零同步矩阵、零 RVH 交接、零 LLM、预期零 migration。
0. 这条任务是什么,以及它不是什么
roadmap 原文(§三 3-5):
把报告 §六 P2 清单转为测试 checklist:全书搜索 / 书签划线 / 排版设置 / 滚动分页 / 脚注图表兼容 / 书库元数据 / 位置恢复 / 复习回跳稳定性。只验收,缺什么再立项。
- ✅ 交付物 = 一份 checklist + 每项的实测结论(本文件收口时回填 §4 表格)。
- ❌ 不在本次范围:修任何捞出来的缺陷。发现即记
docs/plans/backlog.md, 按影响面各自立项。唯一例外是「一行文案 / 一个明显笔误」级别的顺手修。 - 为什么这条值得做:产品已对外发版,EPUB 是两个内容入口之一,却从未被系统性验证过 长文阅读。且这一类缺陷(位置恢复错、划线丢失、脚注错乱)Sentry 抓不到——不崩溃, 只是体验坏。5-6 明确把「EPUB 验收清单(3-5)」列为大版本人工 checklist 的组成部分。
来源报告(P2 清单的出处,需要原文时再读,别整篇灌进上下文): /Users/larry/Documents/Codex/2026-07-21/qin/ReadingBrowser_桌面端产品与竞品分析报告_2026-07-22.md §六
1. 🔴 先读这一节:EPUB 的实现形态与你的预期很可能不同
动手前必须知道的一条:本仓的 EPUB 不是 epubjs 渲染的分页阅读器。
| 事实 | 证据 |
|---|---|
epubjs ^0.3.93 在 package.json:38,但全仓零 import | rg 'epubjs' src/ → 0 命中(只有 package.json 那一行)。它是未使用依赖 |
唯一的 Rust 命令是 prepare_epub | commands/epub.rs 里只有这一个 #[tauri::command](约 :357),lib.rs:545 注册 |
| 渲染 = 解压后的章节 XHTML 直接进普通 content webview | useTabsStore.ts:317 addEpubTab → url = cacheUrl(firstChapter.href)(rb-cache://),BrowserPage.tsx 对 type==='epub' 与 'web' 走同一条 webview 生命周期 |
commands/webview.rs 对 epub 零特化 | rg epub src-tauri/src/commands/webview.rs → 0 命中 |
由此可直接推断(但仍需实测确认,别当结论写进 checklist):
- 「滚动分页」:一章 = 一个网页,只有滚动,没有分页概念。这一项的验收口径应改为 「长章节滚动是否流畅 / 章末能否自然接下一章」,而不是「翻页是否正确」。
- 「全书搜索」:
find.js是 content-script 特性,作用域 = 当前 webview = 当前章节。全书搜索大概率不存在。 - 「位置恢复」:
MainApp.tsx:255-330/:461-510是 epub 协调段,跟踪的是 章节级epubCurrentIdx+computeNextTocAnchor。章内滚动偏移是否持久化 —— 未确认,必查。 - 「书签划线」:走的是通用
annotations.js+page_annotations表(按 URL 归属), 而 epub 章节 URL 是rb-cache://localhost/epub/<pathHash>/OEBPS/...。pathHash=sha256(file_path)[..8](epub.rs::path_hash)——文件被移动/改名后 hash 变, 这是划线丢失的一个具体可测假设。 - 「排版设置」:epub 章节是
rb-cache://网页 → 走的是lib/fontScale.ts/useReadingPrefsStore(reader 字号)还是domain_zoom(整页缩放)?domain_zoom按域名存,rb-cache://localhost所有 epub 共享同一条记录—— 若走这条,则「给某本书单独设字号」不成立。必查。
这些不是缺陷判定,是验收前的假设清单。每条都要用实机 + 窄探针证实或推翻, 再写进 §4。别把推断写成结论——3-2 那轮的教训是推断错三次、白验三个假设。
2. 代码/数据锚点(省掉重新摸索)
前端
src/MainApp.tsx:255-330(TOC 注入 + 章节跟踪 +nextTocAnchor)、:461-510(TOC 点击导航)src/stores/useTabsStore.ts:70(TabType = 'web' | 'epub' | 'review-source')、:85-86(epubInfo/epubCurrentIdx)、:317 addEpubTabsrc/hooks/useNavigation.ts:103-141(开文件对话框 →prepareEpub→addEpubTab→logResourceOpen('epub', …))src/components/TocPanel.tsx(纯展示,onNavigate(href)回调给 MainApp)src/components/AddressBarReviewSegment.tsx:146(第二个 epub 打开入口:复习回跳到 epub 来源) ← 「复习回跳稳定性」那一项的主战场src/pages/BrowserPage.tsx:91/116-145/173(epub 与 web 共用的 webview 显隐/resize)
Rust
src-tauri/src/commands/epub.rs:prepare_epub(解压到 cache + 解析 OPF/NCX →EpubInfo { title, author, chapters, cache_base, toc })、path_hash(:33,sha256[..8])src-tauri/src/lib.rs:105/139-140(cache-index HTML 里的 "epub" 徽标)、:545(命令注册)commands/{reading,storage}.rs(阅读日志 / 缓存文件落盘)
DB 探针字段(src-tauri/assets/sql/schema.sql:230-254)
reading_pages:source_type('web'|'epub')、cached_file_path、storage_path、last_opened_at、deleted_at;索引idx_pages_last_opened(user_id, last_opened_at DESC)page_annotations(划线)、resource_history(书库元数据/最近打开)、domain_zoom(缩放)
content-script 特性(src-tauri/src/content-script/features/) find.js(搜索)· annotations.js(划线)· page-snapshot.js · review.js · highlight.js — 全部按 webview 作用域跑,对 epub 无特化。改任何一个 → 必跑 pnpm build:cs。
3. 工作方式(分工铁律,别自己造轮子)
🔴 用户管「到达现场」,MCP 管「检查现场」。
- 需要「打开某本书 / 进到某一章 / 划一条线 / 按某个键」→ 写成一次做完的编号步骤交给用户, 等一句「好了」,再一次性批量探测。
- 禁止合成按键/点击去驱动:
osascript keystroke在本机对 dev app 根本不抵达 (2026-08-04 授了 Accessibility 仍无效;System Events click at {x,y}报 -25204);rb_click//debug/click只触发 JS 事件、不改变原生焦点。 硬试的真实代价不是 token 而是误诊(上一轮据此构造了三个错误假设,全部白验)。 - 需要一本真实 EPUB。开工第一句就问用户要文件路径(或让他直接打开), 别假设
~/Downloads里有。长书(≥10 万词、带脚注/插图/表格)才验得出东西。
省 token 的探测配方(比 MCP 工具便宜一个量级):
bash
P=$(cat "$TMPDIR/rb-debug-port") # 注意是 $TMPDIR,不是 /tmp
curl -s --get "http://127.0.0.1:$P/debug/dom" \
--data-urlencode 'selector=.rb-reader-content > h1' --data-urlencode 'webview=content-4' \
| jq -c '{found:.result.found, n:.result.total_matches}'路由:/debug/{snapshot,snapshot/tab,logs,state,dom} (GET)、/debug/{click,type,scroll,db/query} (POST)。
- 探测前先确认进程活着:
pgrep -f 'target/debug/app' >/dev/null && echo alive || echo DEAD。 app 不在时先问一句,别推断成「自行退出」(exit 0 + 无 panic 是手动关闭的特征)。 - selector 要精确到目标小元素;用
:root:has(...) title做便宜的存在性探针, 别 dumpbody/main(一次整页 dump 几千 token)。 rb_state必须带{store:"tabs"}之类的窄参数;rb_state读不出Set内容。rb_db_query起手COUNT(*)/LIMIT 5。- 截图(
rb_snapshot)最贵,只在需要人眼判断视觉观感且用户当下不在看时用; 用户在场的视觉判断交给用户。
4. Checklist 骨架(收口时回填「实测结论」列)
每项写清:验收口径(这条到底在验什么)→ 驱动步骤(🙋用户 / 🤖探针)→ 客观断言(DOM/state/DB,不是截图印象)→ 结论(✅ / ❌+已记 backlog / ⚪ 不适用)。
实测会话:2026-08-06。测试书 =
~/Downloads/The City of God, Volume I by Saint of Hippo Augustine (4505).epub(Gutenberg 45304,13 个章节 xhtml,632 处class="footnote",3 张表,cache hash4aa9d2707417279a)。 选它的理由:脚注密集 → 唯一能真正压到 #5 的书。dev 期解压缓存落<repo>/temp/library/epub/<hash>/。
| # | 项 | 建议口径(按 §1 的形态修正过) | 结论 |
|---|---|---|---|
| 1 | 全书搜索 | ⌘F 能搜当前章?跨章?无跨章能力时用户有无可见提示 | ❌ 见 E1 |
| 2 | 书签划线 | 划线落 page_annotations;关书重开仍在;书文件改名/移动后是否还在(path_hash 假设) | 🔶 落库/重开 ✅,改名 ❌ 见 E2 |
| 3 | 排版设置 | ⌘+/-/0 生效于 epub?走字号还是 domain_zoom?是否所有 epub 共享一条 rb-cache://localhost 记录 | ✅ 见 E3(假设推翻) |
| 4 | 滚动分页 | 长章滚动流畅度;章末能否自然接下一章(computeNextTocAnchor);章间切换有无跳位/白屏 | ❌ 见 E4 → 🔶 Sprint 1 后:章末有去处了(EPUB tab 的 ‹ › = 上一章/下一章)→ ✅ 2026-08-08 后:跳位已修(B5)→ ✅ 2026-08-12 压测完:最长章 h-6(232KB / 47 屏 / 2792 高亮)用户实测无掉帧、无空白块、无跳动;探针:滚动往返 4–5ms 且管线内外无差、快滚 20 次后目录联动仍精确、高亮落地位移仅 14 逻辑像素。详 backlog 第 11 条 |
| 5 | 脚注图表兼容 | 脚注锚点点击能否跳转+返回;图片/表格是否溢出面板宽度;rb-cache:// 相对资源是否 404 | ❌ 见 E5 → ✅ 2026-08-08 后:往返定位(B5)+ TOC 高亮丢失(B6 附带缺陷①)+ 目录不自动滚(②)三条全修 → ✅ 2026-08-12 表格压测完:3 张表(含 6 列×87 行)默认与「200% 字号 + 560px 窄栏」极端下均不溢出、不裁切(scrollX 恒 0)。⚠️ 图片仍未压到 —— 本书除封面 SVG 外 0 张 <img>,需另找带插图的书 |
| 6 | 书库元数据 | 标题/作者取到(prepare_epub);resource_history 有行;HomePage「Continue Reading」露不露 epub | 🔶 见 E6 → ✅ Sprint 1 后:「继续阅读」的 epub 条目可点了(此前整条通路死,= B3);条目粒度问题未动(一章一行,= B9) |
| 7 | 位置恢复 | 关标签/重启 app 后回到哪一层(书?章?章内偏移?);reading_pages.last_opened_at 是否 touch | ❌ 见 E7 → 🔶 Sprint 1 后:书 ✅ 章 ✅ 章内偏移 ❌。章级恢复靠 source_ref 里的锚点(零 migration);章内偏移需新列 → 独立立项。last_opened_at 仍不由章节导航 touch(刻意,见 backlog B4) |
| 8 | 复习回跳稳定性 | 从 epub 里存的词去复习 → 回跳(AddressBarReviewSegment.tsx:146)能否落回原章原句;书已移动/删除时的降级 | 🔶 见 E8 → ✅ Sprint 1 后:「打开原书」两种情况(书已开着 / 没开)都实测通过,且会落到那条词的来源章锚点;落到原句仍未做(= B5 定位精度) |
另有 6 条不在原 8 项内、但本轮捞到的缺陷 → 见 §4.1「计划外发现」。 其中 N1(开书即死路)与 N4(fragment 未剥,一因三处)的影响面大于表内多数条目。
实测证据(E1-E8,写成可复现形式)
E1 · 全书搜索 ❌
- 驱动:内容区有焦点时按 ⌘F → 无反应;点主界面(地址栏)后按 ⌘F → 面板开,搜
Rome显示「第 1 / 共 56 处」。 - 断言:当前章节文件
h-0内Rome出现 56 次,全书 12 个章节文件合计 188 次 (grep -o -i Rome <每个 xhtml> | wc -l)→ 作用域 = 当前章节,全书搜索不存在。 - 代码点:
find.js:120createTreeWalker(document.body …)按单 webview 跑;events.js:110-138content-script 只桥接 ⌘+/-/0 与 ⌘⌥R,无 ⌘F 桥(对照useKeyboardShortcuts.ts:54的注释「Content-webview-focus path comes via the 'zoom-shortcut' event instead」——find 没有对偶)。 - 两个独立缺陷:① 内容区焦点下 ⌘F 不响应;② 计数「共 56 处」不含作用域说明,读起来像全书。
E2 · 书签划线 🔶(落库 ✅ / 关书重开 ✅ / 改名 ❌)
- 落库 ✅:划线写
page_annotations(annotation_type='highlight'、source_text对得上正文、user_id= 当前登录用户),存词写word_page_links+word_cloze_contexts,全部正确。 - 关书重开 ✅:关掉书 → 从「最近打开」重开(新 tab、新 webview)→ 导航回 EDITOR'S PREFACE,
.rb-annotation计数 = 2,高亮完整恢复。 - 改名 ❌(
path_hash假设证实):把…(4505).epub改名为…(4505)-RENAMED.epub后重新打开, 同一章.rb-annotation= 0。同一时刻的对照组(仍指向旧 hash 的另一个标签页,导航到同一章) = 2。→ 同页、同探针、同时刻的 A/B,排除探针误差。 机制:epub.rs:33 path_hash = sha256(file_path)[..8]→ 改名后4aa9d2707417279a→cdcc6a7211a9d56f→ 缓存目录与rb-cache://URL 全变 → 按来源页归属的page_annotations失联。 移动文件(换目录)同理,因为 hash 吃的是完整路径。 - 附带:磁盘泄漏。改一次名就留下一份完整的解压副本(本书 2.1MB),旧目录永不回收。 实测后
temp/library/<uid>/epub/下同时躺着4aa9d2707417279a(2.1M) +cdcc6a7211a9d56f(2.1M)。 - 未测:改名后若在新副本里存词,「继续阅读」是否出现同一本书的重复条目 (来源页只在存词/划线时才建,本轮没在副本里存词,故无数据 —— 别写成结论)。
- ⚠️ 本轮实测中途发生一次账号切换,
clear_learning_data_if_user_changed(auth.rs:196,由save_session调用、每次 token 刷新都跑)按user_id IS NULL OR user_id != <当前>清空了 12 张表。 这是设计内行为(红线 #5i),不是缺陷(known_words剩下的 211 行 user_id 全部是当前用户 = 清扫器工作正常;reading_log/word_encounters按 v27/v31 设计不在清理清单里,完好)。 切回原账号后数据经 sync 回灌。下轮复验须在同一账号内一气做完,否则中途的 token 刷新会吃掉证据。
E3 · 排版设置 ✅(§1 的两个担忧都不成立,但代价是另一个)
- 假设推翻:epub 不走
domain_zoom。⌘+ 之后domain_zoom表仍为空(0 行)。 代码点:sizeActions.ts:105-112 applySizeShortcut对type==='epub'路由到applyFontStep;applyZoomStep:41硬性要求tab.type === 'web'。→ §1 里「所有 epub 共享一条rb-cache://localhost记录」的担忧不成立。 - 显示态与持久态一致 ✅:药丸显示 110% ↔
settings.font_size = 18(fontScale.ts阶梯 13/14/16/18/20/24/32 ↔ 80/90/100/110/125/150/200)。 过程中一度出现「显示 125% 而落库 18」的读数,复验未复现(疑为跨账号切换期间的读数错位), 故不作为缺陷记录。 - 真正的代价:字号是全局的(
useReadingPrefsStore.fontPx→settings.font_size, reader 与所有 EPUB 共用一个值)。「给某本书单独设字号」同样不成立 —— 只是坏在字号维度而非缩放维度。
E4 · 滚动分页 ❌(口径已按 §1 改为「章末衔接」,结论是根本没有衔接)
- 章末没有「下一章 / Next」按钮或任何续读入口(实测)。一章 = 一个网页,滚到底就是尽头, 用户必须回目录才能继续 —— 而目录默认不开(见 N1)。
- 全仓无任何翻章 UI:
EpubNavBar已删除,computeNextTocAnchor唯一消费者是reader.js:504(reader 模式下的正文切片),不驱动界面。 - 章间切换本身正常(TOC 点击跳转即时、无白屏);跨文件锚点定位失准的问题归在 E5。
- 未压测:超长章的滚动流畅度(本书最大单章 xhtml 约 2MB)。下轮补时建议直接用
h-1.htm.xhtml(全书最长、Rome出现 104 次那一章)连续滚到底计时。
E5 · 脚注图表兼容 ❌(往返定位坏了) → 2026-08-08 已修(B5+B6),本节三处结论按下方 ⚠️ 修正
⚠️ 复测(2026-08-08)推翻了本节的三条判断,as-built 见
epub-anchor-precision-handoff.md:
- 「正文点
[1]→ ✅ 正确落到 h-8」不成立 —— 去程同样偏(实测top=1072≈ 1 屏)。 偏移量正比于锚点在文件里的深度,去程恰好浅所以当时没察觉。两个方向都坏。- 归因错:位移里整页重排版占 98.7%(
loadSettings给rb-cache://页套 680px 窄栏 + Georgia +line-height:1.8+ 字号 + padding,文档高度 +85%), CEFR/短语高亮只占 1.3%(32px)。原文并列的「高亮注入回流 / 字体 settle」两个候选 都不是主因,那个判别步骤不必做。- 附带缺陷①的机制描述准确,但「URL hash 是
Footnote_1_1」这点值得强调——MainApp当时的注释写「cross-file 导航 URL 通常不带锚点(webview 滚完就剥)」是错的, 实测 fragment 一路带到navigation-state-changed。B6 从来不是"没 hash",是"hash 不是目录锚点"。
- 脚注结构(源数据):正文
[1]=<a class="fnanchor" href="…h-8.htm.xhtml#Footnote_1_1">, 跨章节文件;h-8 的条目里有回链<a href="…h-0.htm.xhtml#FNanchor_1_1">。源支持完整往返。 - 实测:正文点
[1]→ ✅ 正确落到 h-8 的第 1 条脚注; 点脚注回链 → ❌ 落到 h-0 的 CONTENTS 表中部(BOOK VI-VIII 一带),距目标约 2 屏,需手动下滚。 - 机制(已定位到对称性差异,未再细分):同文件锚点跳转走
MainApp.tsx:475-481的evalInWebview(… scrollIntoView …)——页面早已 settle,定位准;跨文件跳转(脚注链接 与MainApp.tsx:505 navigateTo)走原生导航 + 浏览器原生锚点滚动,发生在 content-script 注入 CEFR 下划线 span / 字体 settle 之前,锚点上方内容随后变高 → 视口停在目标上方。 未细分是「高亮注入回流」还是「字体 settle」——立项时用「关掉 CEFR 高亮再走一遍」判别。 - 附带缺陷 ①:落到 h-8 后左侧 TOC 高亮整个消失。
MainApp.tsx:317-321用base === spineBase && hash === urlHash找 TOC 条目;URL hash 是Footnote_1_1, 而 TOC 里 FOOTNOTES 条目的 hash 是pgepubid00575→ 匹配失败 → 回落spineHref(不带#) →TocPanel.tsx:45的item.href === currentHref全不命中 → 无任何高亮。 - 附带缺陷 ②:
TocPanel.tsx全文无scrollIntoView——即使高亮正确,长目录(本书 35 条) 里当前条目在折叠线以下时用户也看不到。 - 图/表:本书正文无内嵌图(仅 1 张封面,
wrap0000.xhtml用<svg><image>包裹,渲染正常); 未压到表格溢出场景。rb-cache://相对资源未见 404。 - ⚪ 非缺陷:TOC 标签里的
BOOK FOURTH.[155]/BOOK FIFTH.[183]尾巴是 Gutenberg 源 NCX 自带 (grep '<text>BOOK F' toc.ncx原样如此),不是我们解析引入的。 另注:本书toc.ncx共 579 个<text>,其中 526 个是[Pg NNN]翻页锚点垃圾, 我们的解析器只吐 35 条,说明过滤是有效的。
E6 · 书库元数据 🔶
prepare_epub解析 ✅:title="The City of God, Volume I"、author="Saint of Hippo Augustine"、 12 个 spine chapters、35 条 TOC。- HomePage「继续阅读」会露出 epub ✅,但两个问题: ① 粒度是章不是书——同一本书出现多行(实测同时有
… - EDITOR'S PREFACE.与… - BOOK FIRST.),getContinueReading(5)只取 5 条(HomePage.tsx:242),长书会把其它来源挤出榜单; ② 点击静默 no-op(见 E7)。 resource_history.title存的是文件名(useNavigation.ts:126传filePath.split('/').pop()) 而非已解析出的epubInfo.title—— 列表里显示带.epub后缀的文件名。
E7 · 位置恢复 ❌(一层都不恢复)
- 「继续阅读」点 epub 条目 = 静默 no-op。根因:
HomePage.tsx:482把source_ref原样传给onOpenFile,而source_ref带锚点(/Users/…/xxx.epub#pgepubid00010);useNavigation.ts:110的守卫!filePath.toLowerCase().endsWith('.epub')→ 直接return(只留一条logger.warn)。对照:Rust 侧classify_source_type明确写了「Strip fragment first so/book.epub#ch4still matches」,TS 侧漏了这一步。 - 日志直证(决定性):点击瞬间控制台留下
[warn] [RB] Only EPUB files are supported: /Users/…/(4505).epub#pgepubid00010—— 带锚点,只可能来自HomePage.tsx:482(「最近打开」传的是resource_history.uri,干净路径)。 全程没有Failed to open file,说明prepareEpub从未被调用,不是解压/路径问题。 - 断言(关键,避免被假象骗):点击前后
tabsstate 完全不变——activeTabId仍是原 tab、lastActiveAt一秒未动、没有新 tab 产生。若真的打开了,openFileByPath会先关掉当前 tab、 再建一个新 id 的 tab。⚠️ 视觉上会"像是跳转成功了",因为MainApp.tsx:825-828的onOpenFile先setActiveModule('read')再调openFileByPath—— 模块切过去了、书没开,屏幕上显示的是 本来就开着的那个标签页。点「EDITOR'S PREFACE」和点「BOOK FIRST」画面完全相同 = no-op 指纹。 - 对照:「最近打开」那条(
entry.uri= 干净路径)能正常打开 ✅ —— 同一个openFileByPath,差别只在有没有锚点。两条列表项外观极像,区别只在标题文案 (继续阅读带章节名… - EDITOR'S PREFACE.;最近打开是带.epub后缀的文件名)。 - 即使走通的路径也不恢复位置:
useTabsStore.ts:325 addEpubTab永远取epubInfo.chapters[0]= Gutenberg 的wrap0000.xhtml= 纯封面(实测落封面)。 - 章内滚动偏移从未持久化(全仓无相关写入)。
MainApp.tsx:278的if (url && tab.type !== 'review-source' && tab.type !== 'epub')让 epub 跳过logResourceOpen——即章节导航不刷新resource_history/ 不 touchreading_pages.last_opened_at; 这两处的行只在存词/划线(ensure_source_for_url)时才产生。- 结论:书 / 章 / 章内偏移三层全不恢复,且唯一露出「续读」承诺的入口本身是断的。
E8 · 复习回跳 🔶(右页 ✅ / 打开原书 ❌)
- ✅ 落回原章原句:复习卡右页正确渲染 EDITOR'S PREFACE 并高亮目标语境。 但它走的不是 epub 缓存 ——
reading_pages.cached_file_path = web/28ce7a0843385148.html, 是存词时打的页面快照(epub 章节被当普通页快照了一份)。 - ✅ 书移动/删除时的降级:因为复习右页只依赖快照,完全不依赖原
.epub文件是否还在 —— 交接稿问的这个子问题答案是「降级良好」。 - ❌ 「打开原书」📂 按钮完全无反应,连错误日志都不留。根因是
source_url=rs.source_ref(srs.rs:312-319的 SELECT 直取),带#pgepubid00010锚点,于是AddressBarReviewSegment.tsx两条路一起失手::139store.tabs.find(t => t.filePath === activeUrl)——tab.filePath是干净路径 → 永不匹配, 即使那本书正开着也切不过去;:145activeUrl.toLowerCase().endsWith('.epub')→ false → 整个try块跳过 →logger.error无从触发(这就是"静默"的来源)。
4.1 计划外发现(不在原 8 项口径内,但同轮捞到)
N1 · 开书即死路 🔴(影响面大于表内多数条目) —— 三条独立事实叠加:
addEpubTab永远落chapters[0],Gutenberg 的 spine[0] 是wrap0000.xhtml= 纯封面 (<div class="x-ebookmaker-cover"><svg><image></svg></div>,无正文、页高=视口高、滚都滚不动);- 全仓唯一能打开目录的入口是
AddressBar.tsx:381-386那个列表图标,开书时不会自动打开; - 没有任何「下一章」UI ——
EpubNavBar组件已删(MainApp.tsx:250注释尚存),computeNextTocAnchor现在唯一消费者是reader.js:504,用途是从多章合一的 xhtml 里切正文,不驱动 UI。
→ 新用户打开一本书:看到一张封面图,滚动无反应,没有下一章,没有目录, 唯一出路是发现地址栏里那个刚刚才出现的第 4 个图标。 修法候选:① 开书时 openLeftPanel('toc');② addEpubTab 跳过纯封面 spine 项;③ 章末补续读入口。 倾向 ①+③(② 要小心:不是所有书的 spine[0] 都是封面)。
N2 · TOC 间歇性空白且不自愈 🔴 —— 实测出现过一次:tab 里 epubInfo.toc 有 35 条、 chapters 12 条,而面板显示「暂无目录」。判别实验:新建一个空白标签页再切回来 → 35 条立刻出现。 根因在 MainApp.tsx:270-279 的 effect 只依赖 [activeTabId]:
js
const tab = useTabsStore.getState().getActiveTab();
if (tab?.type === 'review-source') return; // ← 早退不写 tocItems而 openFileByPath 在 closeTab() 与 addEpubTab() 之间隔着 await prepareEpub(filePath) (真实异步边界,React 会在中间提交渲染)。中间那次提交若让 getActiveTab() 落在 review tab 上 走了早退分支,tocItems 就停在空数组,此后不会自愈(它不认 tab 内容变化,只认 activeTabId 变化)。 配合 N1(目录是 EPUB 唯一导航入口)→ 这本书彻底打不开正文,只能关掉重开碰运气。
N3 · TOC 被右面板驱逐后不恢复 —— usePanelStore.ts:148/170 的 openRightPanel/openTools 显式写 leftPanel: null(「单辅助面板互斥(3-3)」),而 closeRightPanel:149 只清右侧、不恢复左侧。 于是每次 ⌘F 都要手动重开目录。 判断:TOC 与其它左面板不是一类东西 —— Sites/History/RSS 是瞬时导航(点完就走),自动弹回是噪音; TOC 是文档结构框架,读书时半常驻。Kindle / Apple Books / 浏览器 PDF 阅读器都不会让查找框吃掉目录侧栏。 修法(窄):加 preemptedLeftPanel,只在被右面板抢占时记录、只对 toc 生效, closeRightPanel 时恢复,期间用户手动动过任何左面板就清空。 (store 里已有 lastLeftPanel/openLastLeftPanel 可参照形状,但那个服务 ⌘B,语义不同,别直接复用。)
N4 · source_ref 的 fragment 从不剥离 —— 一个根因,三处断路 🔴reading_pages.source_ref 对 EPUB 存的是 /Users/…/book.epub#pgepubid00010(带锚点)。 Rust 侧 classify_source_type 明确写了「Strip fragment first so /book.epub#ch4 still matches .epub」, TS 侧三个消费点都漏了这一步:
| 位置 | 后果 |
|---|---|
useNavigation.ts:110(经 HomePage.tsx:482 传入) | 「继续阅读」→ EPUB 整条通路死,且被自动模块切换伪装成成功 |
AddressBarReviewSegment.tsx:139 | 复习「打开原书」找不到已开着的同一本书 |
AddressBarReviewSegment.tsx:145 | 复习「打开原书」也开不了新的,且静默无日志 |
统一修法:抽一个 stripFragment(path)(或复用 Rust 侧同名语义)在这三处入口先剥再判。 |
✅ 2026-08-07 已修(Sprint 1,as-built 见
epub-usability-sprint1-handoff.md§6)。 落地时多出一处:三处剥完 fragment 后,复习 📂 仍然点了没反应 ——AddressBarReviewSegment.handleOpen从不切 workspace 模块,tab 在 store 里 active 了、 画面还停在复习卡上(实测activeTabId:"2"/activeModule:"review")。已补setActiveModule('read')。 上表第一行「被自动模块切换伪装成成功」只对「继续阅读」那处成立(MainApp.tsx的onOpenFile包了setActiveModule('read'));复习那处恰恰相反 —— 是根本不切。两者方向相反,写成同一句会误导。
N5 · 改名/移动 → 划线全丢 + 孤儿缓存不回收(证据见 E2)。 两半可以分开修:归属可改为按「文件内容指纹或稳定书标识」而非路径 hash;缓存回收可加 LRU/启动清理。
N6 · 「最近打开」把裸 URL 当标题显示 —— 列表里出现 rb-cache://localhost/text/b9bf3198-25f2-45a1-b57b-e73a7c9ac1e8.html(text 来源,非 epub)。 同一区块里 EPUB 条目显示的是带 .epub 后缀的文件名而非已解析出的书名(见 E6)。 两者同属「resource_history 的 title 取值不讲究」。
5. 顺手可以在同一次实机里收掉的东西
roadmap 5-5 尾巴(docs/plans/backlog.md §🟡测试线收尾): docs/smoke-test-runbook.md 的断言层 2026-07-22 已实跑校准,待固化两件事—— ① 复习入口选择器、② 写驱动步骤(双击存词 / 评分)。 runbook 的 6 条核心链路里本来就有一条 「EPUB→位置恢复」,与本任务 #7 完全重合。 既然 dev app 已经开着、用户已经在手动驱动,顺手把这两处回填掉,5-5 就能收口 (之后挂 /release Step 1 前)。这是本次会话的可选第二交付物,不要因它挤掉 3-5 本体。
6. 收口动作
- 回填本文件 §4 全部 8 行 + 每项的断言证据(写成可复现的形式,下个版本能照着再跑一遍)。
- 捞出的缺陷 →
docs/plans/backlog.md各自立项(写清现象 + 已定位到的代码点 + 触发条件, 别只写「XX 不好用」)。 docs/plans/product-iteration-roadmap-2026h2.md3-5 标 ✅ + 链本文件; §进度行补一句(第三梯队至此全绿)。- 若同时收了 5-5:更新
docs/smoke-test-runbook.md+ backlog 那条 + roadmap 5-5 行。 /doc-sync-check→ commit(git add只给具体文件路径,禁止-A、禁止裸目录)。
7. 环境/纪律速查
- 本仓 =
~/reading-browser(Tauri + React + Rust)。RVH(~/reading_vocab_helper)本任务零涉及; 真需要碰 → 新会话。 - 红线 #11:
schema.sql/ v1 已冻结,新表新列只进新编号迁移,下一个从 v32 开始。 本任务预期不产生 migration——若发现「位置恢复需要新列」,那是立项,不是本次动手。 - 红线 #2 无
LOWER()(用COLLATE NOCASE)· #3 content-script 用__TAURI_INTERNALS__.invoke()· #9 写 word 字段前过lemmatizer::normalize()。 - content-script 是打包产物:改完
pnpm build:cs;grep 该 bundle 要grep -a(否则被当二进制、静默 0 命中)。 - 禁止无 pathspec 的
git reset --hard/git checkout ./git stash push(会抹掉整棵树的未提交改动,不看文件域)。 - 动手前
git status+git worktree list;检查有无上一轮遗留的pnpm tauri dev孤儿进程 (两个 watcher 抢同一个target/会互相打断)。worktree 删后target/仍缓存其绝对路径 → 构建报Info.plist找不到时,touch src-tauri/build.rs强制 build script 重跑即可,不必cargo clean。 - 工具链完整配方见
docs/plans/archive/read-tier-followup-handoff.md§5(那份的 §5 是通用资产)。