ReadViewSDK/Doc/当前阅读器问题修复开发清单.md
shen 6f75b083f7 feat: EPUB 阅读器搜索、选中注释、书签 chrome 状态及大量重构优化
- 新增 RDEPUBReaderSearchCoordinator 与 RDEPUBSelectionState 管理搜索和选中状态
- 新增 BookmarkChromeStateTests、NavigationBackwardTests、SelectionAnnotateTests 等 UI 测试
- 新增多个边界测试 epub 样本(损坏结构、空归档、缺失文件、流式外链验证)
- 重构阅读器 chrome 状态管理,统一 tool bar 与 search bar 交互
- 优化大书分页缓存策略(RDEPUBChapterSummaryDiskCache、RDEPUBPageCountCache)
- 移除废弃的 RDEPUBLocationConverter 和 RDEPUBPageBreakPolicy
- 更新 epub-bridge.js 与 JS bridge 通信协议
- 全面更新现有 UI 测试以适配新的 helper 和状态管理
2026-06-13 22:48:56 +08:00

758 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 当前阅读器问题修复开发清单
> 最后更新2026-06-13
> 依据说明:本清单仅基于当前仓库代码实现整理,不以现有说明文档是否准确为前提。
---
## 1. 文档目标
本文将“当前阅读器还存在的问题”展开为可执行的逐任务开发清单,目标是把工作从“方向建议”落到“具体改哪里、怎么改、如何验收”。
本文聚焦以下四类问题:
1. 核心交互链路不稳定:选区、高亮、书签、工具栏状态分散。
2. 默认安全策略偏弱:外链、解压、日志、持久化默认行为需要收口。
3. 资源与分页稳定性不足:大资源内存峰值、离屏分页 WebView 边界控制不足。
4. SDK 契约与回归手段不足:默认 no-op 容易误用,自动化观测点不够结构化。
---
## 2. 总体实施顺序
建议按以下顺序执行,减少返工:
1. `bookmark-chrome-state-unification`
2. `selection-state-refactor`
3. `external-link-policy-hardening`
4. `epub-archive-path-validation`
5. `webview-debug-sanitization`
6. `persistence-defaults-hardening`
7. `resource-scheme-streaming-and-limits`
8. `pagination-webview-hardening`
9. `cache-hardening`
10. `sdk-contract-and-test-hardening`
原因:
- 任务 2 是已确认的重复 UI 状态写入问题,收益最高、风险相对低,应优先处理。
- 任务 1 更偏“状态模型收口优化”,问题真实存在,但严重度低于任务 2。
- 第 3-6 项收口默认安全行为,改动局部、收益高。
- 第 7-9 项涉及资源与分页底层,副作用更大,放在前面稳定后更容易验证。
- 最后一项负责把前面改动固化为稳定契约和测试基线。
---
## 3. 任务清单
## 任务 1`selection-state-refactor`
### 3.1 目标
在不破坏当前清晰分层的前提下,收口选区相关状态的冗余表达,降低 view 层与 controller 层各持有一份选区状态所带来的维护成本和时序问题。
### 3.2 主要问题代码
- `Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextSelectionController.swift`
- `Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextContentView.swift`
- `Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderAnnotationCoordinator.swift`
- `Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift`
### 3.3 修改方案
当前代码并不是“混乱散写”,而是相对清晰的分层流:
- gesture
- view
- coordinator
- context/controller
其中真正值得优化的点主要有两个:
1. view 层和 controller 层各自持有一份 `currentSelection`
2. `menuSelection` 是为 `UIMenuController` 时序保底引入的 workaround属于合理存在但可以尝试被更明确的状态模型替代
新增一个统一的选区状态模型仍然是可行方案,例如:
```swift
enum RDEPUBSelectionState: Equatable {
case idle
case selecting(anchor: Int)
case selected(RDEPUBSelection)
case committingAction(RDEPUBSelection, action: RDEPUBAnnotationMenuAction)
}
```
建议新增文件:
- `Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBSelectionState.swift`
建议在 `RDEPUBReaderAnnotationCoordinator` 内新增或抽出一个单一入口:
- `applySelectionState(_ state: RDEPUBSelectionState)`
- `currentSelectionState`
具体修改:
1. `RDEPUBTextSelectionController`
- 移除“自己就是最终状态源”的职责。
- 保留字符范围计算与 `RDEPUBSelection?` 构建。
- `isSelecting` 可保留为手势内部临时状态,但不再作为 UI 的最终依据。
2. `RDEPUBTextContentView`
- 不再直接通过 `currentSelection != nil` 作为所有 UI 行为的唯一判断条件。
- 长按、拖拽、结束只上报“开始选择 / 更新选择 / 结束选择 / 取消选择”事件。
- `menuSelection` 不建议直接删除。
- 第一版应先保留 `menuSelection`,并把它明确标注为菜单时序缓冲状态。
- 待状态模型稳定后,再评估是否能安全移除。
3. `RDEPUBReaderAnnotationCoordinator.updateCurrentSelection(_:)`
- 改造成 `applySelectionState(_:)`
-`.selected` 时统一:
- 更新 `controller.currentSelection`
- 决定是否展示工具栏
- 更新底部高亮按钮可用性
- 通知 delegate
-`.idle` 时统一清理:
- 清空 `controller.currentSelection`
- 关闭菜单
- 刷新按钮状态
4. `RDEPUBReaderController`
- `currentSelection` 继续保留为公开只读语义,但底层来源改为 `selectionState` 推导,而不是多处直接赋值。
### 3.4 实施步骤
1. 新增 `RDEPUBSelectionState.swift`
2.`RDEPUBReaderAnnotationCoordinator` 中接管选区状态
3. 重写 `RDEPUBTextContentView` 中选区相关回调
4. 先收口零散的 `currentSelection != nil` 控制逻辑
5. 保留 `menuSelection`,待验证不再需要后再移除
6. 统一 delegate 通知路径
### 3.5 风险点
- 选区菜单弹出时机可能变化。
- `menuSelection` 是现有时序补丁,若贸然删除,菜单动作可能拿不到正确 selection。
- Web 路径和 Native Text 路径都要走统一状态流,避免只修一条。
### 3.6 验收标准
- 长按后必然进入“选区中”状态。
- 松手后有有效文本时必然进入“已选中”状态。
- 复制/高亮/批注后必然回到空闲状态。
- `selection=1` 的 Demo 状态与按钮启用状态始终一致。
### 3.7 优先级修正
本任务应视为“结构优化 + 降低后续维护成本”,不是当前代码库里最严重的缺陷。建议优先级下调到与任务 2 同级,排在任务 2 之后实施。
---
## 任务 2`bookmark-chrome-state-unification`
### 3.8 目标
统一顶部书签按钮、底部书签按钮、高亮按钮、添加高亮按钮的可用状态计算,避免不同协调器各自改一部分 UI。
### 3.9 主要问题代码
- `Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderChromeCoordinator.swift`
- `Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderAnnotationCoordinator.swift`
- `Sources/RDReaderView/EPUBUI/RDEPUBReaderTopToolView.swift`
- `Sources/RDReaderView/EPUBUI/RDEPUBReaderBottomToolView.swift`
### 3.10 修改方案
新增统一 UI 状态模型:
```swift
struct RDEPUBReaderUIState {
let canToggleBookmark: Bool
let hasBookmarkAtCurrentLocation: Bool
let canShowBookmarks: Bool
let canAddHighlight: Bool
let canShowHighlights: Bool
}
```
建议新增文件:
- `Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderUIState.swift`
具体修改:
1. `RDEPUBReaderChromeCoordinator`
- 新增 `makeUIState()``applyUIState(_:)`
- `updateReaderChrome()` 内不再直接散写多个 `setXxxEnabled`
2. `RDEPUBReaderAnnotationCoordinator`
- `updateBookmarkChrome()` 不再直接写 view改为触发 `context.updateReaderChrome()`
3. `RDEPUBReaderTopToolView` / `RDEPUBReaderBottomToolView`
- 保持 dumb view只接收状态不参与规则判断
### 3.11 实施步骤
1. 新增 `RDEPUBReaderUIState.swift`
2. 改造 `updateReaderChrome()`
3. 不要简单删除 `AnnotationCoordinator` 内的按钮状态刷新
4. 区分两类触发时机:
- `ChromeCoordinator` 负责整页/整轮 chrome 同步
- `AnnotationCoordinator` 负责用户操作后的即时反馈触发
5. 将二者统一收口到同一个状态计算入口,例如 `context.updateReaderChrome()`
6. 调整书签新增/删除/跳转后的刷新入口为统一 `updateReaderChrome()`
### 3.12 风险点
- 如果简单删除 `AnnotationCoordinator` 内的调用,可能丢失“用户操作后立即反馈”的刷新时机。
- 如果某些按钮当前依赖副作用显示,需要顺便梳理显示时机。
### 3.13 验收标准
- 添加第一个书签后,底部书签列表按钮立即可用。
- 删除最后一个书签后,底部书签列表按钮立即禁用。
- 当前位置已加书签时,顶部书签按钮始终是选中态。
---
## 任务 3`external-link-policy-hardening`
### 3.14 目标
把“外链点击后直接打开系统应用”改成受控策略,避免 SDK 默认行为过于激进。
### 3.15 主要问题代码
- `Sources/RDReaderView/EPUBCore/RDEPUBWebView+JavaScriptBridge.swift`
- `Sources/RDReaderView/EPUBUI/RDEPUBReaderController+ContentDelegates.swift`
- `Sources/RDReaderView/EPUBUI/Settings/RDEPUBReaderConfiguration.swift`
### 3.16 修改方案
`RDEPUBReaderConfiguration` 新增:
- `allowedExternalURLSchemes: Set<String>`
- `requiresExternalLinkConfirmation: Bool`
- 可选:`allowedExternalURLHosts: Set<String>?`
默认值建议:
- scheme 仅允许 `https`
- 需要确认框
具体修改:
1. `RDEPUBWebView+JavaScriptBridge.swift`
- 继续识别外链,但不再表示“允许直接打开”。
- 只负责把外链事件上抛。
2. `RDEPUBReaderController+ContentDelegates.swift`
- 新增私有方法:
- `shouldAllowExternalURL(_:)`
- `presentExternalLinkConfirmation(for:)`
- `openExternalURLIfAllowed(_:)`
- `didActivateExternalLink` 改为:
- 先 delegate 回调
- 再走配置校验
- 再弹确认框
- 最后调用 `UIApplication.shared.open`
3. 可选扩展 `RDEPUBReaderDelegate`
- 增加可选拦截钩子:
- `epubReader(_:shouldOpenExternalURL:) -> Bool`
### 3.17 实施步骤
1. 改配置模型与默认值
2. 改控制器外链处理逻辑
3. 增加确认对话框
4. 增加测试用 mock URL 覆盖 `https`、`mailto`、未知 scheme`
### 3.18 风险点
- 某些 Demo 书可能依赖 `mailto``tel`,要明确它们现在默认不再直接打开。
### 3.19 验收标准
- `https` 外链在确认后打开。
- 默认配置下 `mailto``tel` 不直接打开。
- 未知 scheme 一律拒绝。
---
## 任务 4`epub-archive-path-validation`
### 3.20 目标
为 EPUB 解压路径增加显式安全校验,防止恶意压缩包通过相对路径写出解压根目录。
### 3.21 主要问题代码
- `Sources/RDReaderView/EPUBCore/RDEPUBParser+Archive.swift`
### 3.22 修改方案
新增私有方法:
- `validatedExtractionDestination(for:extractionRoot:) -> URL?`
校验规则:
1. 拒绝绝对路径。
2. 拒绝包含 `..` 的路径段。
3. `standardizedFileURL.path` 必须位于 `extractionRoot.standardizedFileURL.path` 下。
具体修改:
1. `extractArchiveIfNeeded(epubURL:)`
- 在循环内先调用 `validatedExtractionDestination`
- 无效 entry 可:
- 直接抛错终止解析,或
- 记日志并跳过
- 建议优先抛错,行为更清晰
2. 增加自定义错误:
- 若当前 `RDEPUBParserError` 里没有合适 case可新增 `invalidArchiveEntryPath(String)`
### 3.23 实施步骤
1. 增加路径校验方法
2. 替换原始 `destinationURL` 生成逻辑
3. 增加针对恶意 entry path 的单元测试
### 3.24 风险点
- 少数历史 EPUB 可能带奇怪路径分隔符,需要兼容性验证。
### 3.25 验收标准
- 正常 EPUB 仍可正常解压。
-`../` 的恶意 entry 会被拒绝。
---
## 任务 5`webview-debug-sanitization`
### 3.26 目标
减少默认调试日志泄露阅读内容的风险,并关闭不必要的 inspectable 默认值。
### 3.27 主要问题代码
- `Sources/RDReaderView/EPUBCore/RDEPUBWebViewDebug.swift`
- `Sources/RDReaderView/EPUBCore/RDEPUBPaginator.swift`
- `Sources/RDReaderView/EPUBCore/RDEPUBWebView+Configuration.swift`
### 3.28 修改方案
1. `RDEPUBWebViewDebug`
- `logMessage` 改为只打印:
- message name
- 字段名列表
- 文本长度
- URL 摘要
- 不再打印完整 message body
2. 新增更细粒度配置
- `RDEPUBWebViewDebugEnabled`
- `RDEPUBVerboseWebViewDebugEnabled`
3. `RDEPUBPaginator` 与正常阅读 WebView
- `isInspectable` 改为受配置控制
- 默认关闭
4. `RDEPUBReaderConfiguration`
- 增加:
- `allowsInspectableWebViews`
- `enablesVerboseWebViewLogging`
### 3.29 实施步骤
1. 改日志输出格式
2. 改 inspectable 的默认逻辑
3. 为 verbose 模式保留调试后门
### 3.30 风险点
- 调试某些 JS 问题时信息会变少,因此要保留显式 verbose 开关。
### 3.31 验收标准
- 默认 DEBUG 包不输出完整选中文本。
- 默认分页 WebView 不可 inspect。
- 打开 verbose 开关后仍能深度调试。
---
## 任务 6`persistence-defaults-hardening`
### 3.32 目标
降低默认 `UserDefaults` 持久化的误用风险和数据膨胀风险,同时不破坏当前 API 可用性。
### 3.33 主要问题代码
- `Sources/RDReaderView/EPUBUI/RDEPUBReaderPersistence.swift`
- `Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderAnnotationCoordinator.swift`
- `Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift`
### 3.34 修改方案
短期目标:
1. 明确 `RDEPUBUserDefaultsPersistence` 是轻量实现。
2. 高亮持久化优先依赖:
- `location`
- `rangeInfo`
- `style`
- `note`
3. `text` 字段作为冗余缓存,而不是唯一恢复依据。
中期可选目标:
新增文件持久化实现:
- `Sources/RDReaderView/EPUBUI/RDEPUBFilePersistence.swift`
### 3.35 具体修改
1. `RDEPUBReaderPersistence.swift`
- 为默认实现增加 DEBUG 警告,提示 bookmarks/settings 默认实现是 no-op
2. `RDEPUBUserDefaultsPersistence`
- 增加大小保护,例如当高亮集合序列化后超阈值时打印告警
- 可选增加版本字段,便于后续迁移
3. `RDEPUBReaderController`
- 初始化默认 persistence 时,在注释与命名上明确这是默认轻量实现
### 3.36 实施步骤
1. 增加 warning/assert 机制
2. 优化高亮持久化字段策略
3. 视时间增加 `RDEPUBFilePersistence`
### 3.37 风险点
- 如果外部代码依赖 `text` 永远存在,需要做好兼容。
### 3.38 验收标准
- 未实现书签持久化时DEBUG 下能看到明确提示。
- 高亮即使 `text` 丢失,也可基于范围信息恢复定位。
---
## 任务 7`resource-scheme-streaming-and-limits`
### 3.39 目标
降低 `WKURLSchemeHandler` 对大资源的内存冲击,避免每次都整块 `Data(contentsOf:)` 读入。
### 3.40 主要问题代码
- `Sources/RDReaderView/EPUBCore/RDEPUBResourceURLSchemeHandler.swift`
### 3.41 修改方案
分两步做:
第一步,先加大小阈值和告警:
- 读取文件属性获取大小
- 超过阈值时走分块发送
第二步,再补流式发送:
- 使用 `FileHandle``InputStream`
- 按固定 chunk size 反复 `didReceive(data)`
建议新增私有方法:
- `resourceMetadata(for:)`
- `respondWithStreaming(fileURL:requestURL:taskID:urlSchemeTask:)`
- `respondWithInMemoryData(...)`
### 3.42 实施步骤
1. 先加文件大小检测
2. 小文件保留内存直读
3. 大文件切换到分块发送
4. 增加取消任务时的中断判断
### 3.43 风险点
- `WKURLSchemeHandler` 分块发送要注意 stop 之后不继续回调。
### 3.44 验收标准
- 大图片/字体加载时内存峰值下降。
- 取消任务不会继续发送数据。
---
## 任务 8`pagination-webview-hardening`
### 3.45 目标
加强离屏分页 WebView 的边界控制,减少外部资源波动和调试副作用。
### 3.46 主要问题代码
- `Sources/RDReaderView/EPUBCore/RDEPUBPaginator.swift`
### 3.47 修改方案
1. 为分页 WebView 增加严格导航限制
- 仅允许 `file://`
- 仅允许 `ss-reader://`
- 拒绝 `http/https`
2.`isInspectable` 受配置控制
3. 增加测量稳定性日志
- 记录三轮测量结果
- 当三轮差异过大时告警
建议新增私有方法:
- `shouldAllowPaginatorNavigation(url:)`
- `recordMeasurement(pass:value:)`
### 3.48 实施步骤
1. 扩展 `WKNavigationDelegate`
2. 加入导航 allowlist
3. 记录多轮测量差异
### 3.49 风险点
- 某些 EPUB 样式若依赖外链资源,分页结果可能与当前行为不同,但这本身就是更安全的策略。
### 3.50 验收标准
- 分页 WebView 不发起外部导航。
- 三轮测量波动可观测。
---
## 任务 9`cache-hardening`
### 3.51 目标
提升章节摘要磁盘缓存的健壮性、可观测性和可清理能力。
### 3.52 主要问题代码
- `Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterSummaryDiskCache.swift`
### 3.53 修改方案
1. 原子写入
- 先写 `.tmp`
- 再 replace 到正式文件
2. 读失败分类
- 文件不存在
- 读取失败
- 解码失败
3. 精细化清理
- `removeAll()`
- `removeAll(forBookID:)`
- `removeAll(forRenderSignature:)`
4. 增加统计接口
- 缓存文件数
- 总大小
### 3.54 实施步骤
1. 改写入策略
2. 增加读失败日志
3. 增加清理接口
4. 补测试覆盖损坏文件、半写文件
### 3.55 风险点
- 清理策略接口若公开,需要评估是否作为 public API。
### 3.56 验收标准
- 缓存文件损坏不会影响整本书重新打开。
- 能按需清理缓存,而不是只能全删。
---
## 任务 10`sdk-contract-and-test-hardening`
### 3.57 目标
把前面所有修复固化为更清晰的 SDK 契约和更稳定的测试观测点。
### 3.58 主要问题代码
- `Sources/RDReaderView/EPUBUI/RDEPUBReaderPersistence.swift`
- `Sources/RDReaderView/EPUBUI/RDURLReaderController.swift`
- `ReadViewDemo/ReadViewDemoUITests/`
### 3.59 修改方案
1. 强化 persistence 契约
- 继续保留默认实现,但在 DEBUG 下对 no-op 行为给出明确信号
2. Demo 状态能力修正
- 当前仓库并非完全没有结构化状态。
- `ReadViewDemo/ReadViewDemoUITests/Helpers/DemoReaderState.swift` 已经提供了状态解析器和 `waitForDemoReaderState(...)` 结构化等待能力。
- 真正需要推进的不是再造一个 `DemoReaderSnapshot`,而是把仍然大量存在的 `waitForReaderState(containing:)` 迁移到结构化断言。
建议方向:
- 继续保留隐藏 label 输出
- 保持 `key=value` 格式
- 统一测试侧只通过 `DemoReaderState` 解析和断言
- 逐步淘汰 `containing:` 子串匹配,避免 `highlights=1` 命中 `highlights=10` 之类误判
3. 补测试
- 单元测试:
- `RDEPUBParserArchiveSafetyTests`
- `RDEPUBExternalLinkPolicyTests`
- `RDEPUBChapterSummaryDiskCacheTests`
- UI 测试:
- 选区状态流
- 书签启用/禁用
- 外链确认框
### 3.60 实施步骤
1. 改 Demo 状态串生成逻辑
2. 优先将 `waitForReaderState(containing:)` 迁移为 `waitForDemoReaderState(...)`
3. 调整 UI 测试断言
4. 增加单元测试 target 或现有测试目录内的新文件
### 3.61 风险点
- 现有 UI 测试如果强依赖旧状态串,需要同步迁移。
### 3.62 验收标准
- UI 测试失败时能明确知道是选区状态、书签状态还是导航状态失配。
- persistence 的默认 no-op 行为在开发期不再“静默”。
---
## 4. 补充清理项
以下问题已在代码中可见,但未纳入前 10 个主任务,建议作为补充任务或穿插清理项处理。
### 4.1 `pageBreakBefore/pageBreakAfter` 相关死代码审计
涉及代码:
- `Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift`
- `Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBSemanticMarkerInjector.swift`
- `Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBPageBreakPolicy.swift`
- `Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBChapterPageCounter.swift`
建议动作:
- 审计 `pageBreakBefore/pageBreakAfter` 从注入到消费的完整链路
- 判断是否有“已定义、已注入、但实际无有效触发”的死路径
- 若确认无效,删除或补测试锁定其真实行为
### 4.2 `PAGINATION-DEBUG` 输出清理
涉及代码:
- `Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBCoreTextPageFrameFactory.swift`
- `Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBChapterPageCounter.swift`
建议动作:
- 将裸 `print` 改为受控调试开关
- 默认关闭
- 避免分页细节和正文片段直接进入控制台
### 4.3 `ChapterRuntime` 旧代码与遗留路径清理
建议动作:
- 梳理 `ChapterRuntime` 下是否存在已不再被主流程使用的辅助类型或过渡实现
- 对“仍被引用但语义已经过时”的代码先补注释标记
- 对“完全无调用”的代码建立删除候选清单
---
## 5. 推荐里程碑切分
### 里程碑 A核心交互稳定
包含任务:
- `selection-state-refactor`
- `bookmark-chrome-state-unification`
完成标准:
- 选区、高亮、书签相关 UI 测试恢复稳定
### 里程碑 B默认安全行为收口
包含任务:
- `external-link-policy-hardening`
- `epub-archive-path-validation`
- `webview-debug-sanitization`
- `persistence-defaults-hardening`
完成标准:
- 默认配置下不再直接打开高风险外链
- 解压路径具备显式校验
- 默认调试日志不泄露完整阅读内容
### 里程碑 C底层稳定性优化
包含任务:
- `resource-scheme-streaming-and-limits`
- `pagination-webview-hardening`
- `cache-hardening`
完成标准:
- 大资源加载和分页更稳定
- 缓存更健壮、可清理
### 里程碑 D契约与测试固化
包含任务:
- `sdk-contract-and-test-hardening`
完成标准:
- SDK 默认行为更清晰
- 回归测试对关键状态具备稳定观测能力
---
## 6. 建议的开发节奏
若按 2 周一个小迭代,可参考:
1. 第 1 周:`bookmark-chrome-state-unification` + `selection-state-refactor`
2. 第 2 周:`external-link-policy-hardening` + `epub-archive-path-validation` + `webview-debug-sanitization`
3. 第 3 周:`persistence-defaults-hardening` + `resource-scheme-streaming-and-limits` + `pagination-webview-hardening`
4. 第 4 周:`cache-hardening` + `sdk-contract-and-test-hardening` 与回归收口
如果只想先做最值的部分,建议最小交付集合为:
1. `bookmark-chrome-state-unification`
2. `selection-state-refactor`
3. `external-link-policy-hardening`
4. `epub-archive-path-validation`
这四项完成后,阅读器的“主观可用性”和“默认安全性”会先提升一个台阶。