Skip to content

引入阅读器三件套#466

Merged
chiimagnus merged 36 commits into
mainfrom
crh1
Jun 21, 2026
Merged

引入阅读器三件套#466
chiimagnus merged 36 commits into
mainfrom
crh1

Conversation

@chiimagnus

@chiimagnus chiimagnus commented Jun 21, 2026

Copy link
Copy Markdown
Member

背景:

  • Conversation metadata 缺少视图与 reader 特性描述,导致仓库内多处使用 URL/sourceType 的脆弱启发式判断(重复实现),会造成评论侧栏与阅读工具栏在错误的对话类型中显示或隐藏。
  • 阅读偏好由 legacy markdown profile 间接表示,未归一成结构化、带限幅的数值,可能写入无效 CSS 值或丢失迁移路径。
  • 朗读能力缺乏框架无关且可注入的引擎实现,过去的设计会把 Web APIs 与 React/DOM 直接耦合,导致难以单元测试并存在音频/请求泄漏风险。

变更:

  1. 会话视图与侧栏门控
  • 改了什么:在 ConversationKind 定义中新增 view.renderer ("chat" | "article")、view.readerFeatures.{textLayout,theme,narration} 以及 view.commentsSidebar 布尔字段;移除组件内对 URL/sourceType 的重复启发式检测,使用 conversationKinds.pick(...) 获取 view 描述并据此开关行为。
  • 为什么这样改:旧实现基于脆弱的字符串/URL 检测,容易在边界场景(非 web 来源的 article / 视频)触发错误渲染且代码重复;将元数据上移到协议层能保证单一真源并让 UI 安全地依据显式契约决策。
  • 关键实现细节:assertKindDef 在协议层强校验 view.renderer 与 readerFeatures 三个布尔字段,提供缺省视图 DEFAULT_VIEW;AppShell 与 ConversationDetailPane 改为读取 conversationKinds.pick(...).view,并用 view.commentsSidebar 决定评论侧栏自动打开/关闭逻辑(不再依赖 canonicalizeArticleUrl 限定)。
  1. 结构化 Reader 偏好与 CSS 变量
  • 改了什么:新增 services/protocols/reader-prefs.ts,定义 ReaderPrefs、ReaderTtsPrefs、默认值、限幅 READER_PREFS_LIMITS、normalize/resolve/migrate 函数,以及 readerPrefsToCssVars 和存储键 READER_PREFS_STORAGE_KEY;UI 端通过该协议产生 --reader-* CSS 变量并把 theme 保留为 data-reader-theme 属性。
  • 为什么这样改:之前依赖 legacy markdown profile 导致无明确数值边界和迁移策略,可能写入超出范围的 CSS(破坏布局)或在不同存储键间丢失偏好;把常量/limits 提升到协议层能保证一致渲染与安全迁移。
  • 关键实现细节:clampNumber/resolveEnum 保证所有数值/枚举受限;readerPrefsFromLegacyProfile 提供只读迁移路径(不删除旧键);READER_TYPOGRAPHY_PRESETS 以 legacy id 为索引便于直接 lookup;readerPrefsToCssVars 仅导出排版相关变量,theme 用 data attribute 实现以避免样式侧副作用。
  1. 可注入且可测试的朗读引擎(ReaderTtsEngine)
  • 改了什么:新增 services/reader/tts/reader-tts-engine.ts,包含纯净的句子分割函数 buildSentences,以及 ReaderTtsEngine 类(支持依赖注入的 getSynth/createUtterance/fetch/createAudio/createObjectURL/revokeObjectURL),并实现 AI(fetch->blob->Audio)与 Web Speech 两条播放路径、generation 单调 token 取消策略、暂停/恢复/停止与资源释放。
  • 为什么这样改:把 Web API 与播放生命周期封装到可注入的引擎可在 node 环境下单元测试分割逻辑和错误路径,避免真实语音/网络在测试环境阻塞或泄漏资源(audio/objectURL 未释放)。
  • 关键实现细节:SENTENCE_BREAK_RE 支持 CJK 与拉丁标点;generation 自增用于使延迟回调(如 onend)可判断是否已被 superseded;AI 路径把 API key 仅放入请求头且不记录;playBlob 使用 createObjectURL + HTMLAudio 并在任一退出点 revokeObjectURL,当前播放状态与错误通过 listeners 回调上报。
  1. UI 集成与辅助手段
  • 改了什么:新增 ReaderToolbar/NarrationPanel 的集成(readerFeatures 驱动渲染)、移除组件内重复的 isArticle 判断;i18n keys 补全(en/zh),并添加 reader 三面板的样式 token (--reader-*) 与半径 token 约束。
  • 为什么这样改:确保阅读面板只出现在支持阅读功能的会话类型,并统一无障碍标签与本地化;样式变量与 token 约束避免硬编码样式导致视觉偏差。
  • 关键实现细节:ReaderToolbar 仅在 view.readerFeatures 任一为 true 时挂载;TextLayoutPanel/ThemePanel 将偏好写入 READER_PREFS_STORAGE_KEY;tokens.css 引入 --reader-* 变量和 radius token 用于样式一致性。
  1. 文档与测试
  • 改了什么:增加 docs/reader-three-piece-acceptance.md 描述验收标准并补充多项单元/集成测试(reader-tts-engine.test.ts、use-reader-narration.test.ts、reader-prefs.test.ts、会话视图相关 smoke 测试)。
  • 为什么这样改:把预期行为与测试覆盖写清以便在不同运行环境(node、浏览器、firefox build)复现与验证关键边界。
  • 关键实现细节:测试通过依赖注入 mock synth/fetch/audio 验证播放、取消与隐私安全的 snapshot(globalThis.__syncnosReaderNarration)仅包含索引/计数/错误,不包含文章文本或 API key。

测试:

  • 在 AI 聊天会话页面打开,期望 ReaderToolbar 不渲染且页面未挂载 --reader-* CSS 变量。
  • 在文章(article)会话页面打开,期望 ReaderToolbar 渲染,且对话容器上应用由 readerPrefsToCssVars 生成的 --reader-font-size / --reader-content-width 等变量。
  • 在设置中将 fontSize 设置为 1000(超出范围),期望最终生效的 font-size 被 clamp 到 READER_PREFS_LIMITS.fontSize.max(34px)。
  • 在存储中写入 legacy markdown reading profile id 为 'notion',加载会话时期望最终生效的排版为 READER_TYPOGRAPHY_PRESETS['notion'] 对应的数值。
  • 使用注入的假 synth(无真实 Web Speech)和 fake fetch+audio 运行 ReaderTtsEngine.play/stop/pause/resume,期望:播放顺序按照 buildSentences 输出的句子索引推进、stop 会取消 in-flight 请求并释放 objectURL、pause 使 state 进入 paused,resume 从 paused 恢复。
  • 在朗读运行时检查 globalThis.__syncnosReaderNarration 快照,期望其字段仅含 {state,isPlaying,stateChanges,errorCount,lastError,activeIndex,updatedAt},且不包含原文文本或 aiApiKey。
  • 在某个 ConversationKind 将 view.commentsSidebar 设为 false 后,打开该对话并尝试切换评论侧栏,期望侧栏不能自动打开(canToggleCommentsSidebar 为 false)。

…els/toolbar

Add 60 reader* keys to en/zh locales; wire TextLayoutPanel, ThemePanel, NarrationPanel and ReaderToolbar to t(); deprecate legacy markdownReadingProfileLabel.
…ce doc

useReaderNarration publishes counters/state to globalThis.__syncnosReaderNarration (never article text, prefs, or API key). Add docs/reader-three-piece-acceptance.md.
@chiimagnus chiimagnus merged commit c559a00 into main Jun 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant