fix: 修正答案生成中途打开语音输出只从当前位置播报的问题

用户反馈:答案还在流式输出时才打开"语音输出",朗读会从打开那一刻
"之后"的新增量开始,开 TTS 之前已经显示在屏幕上的那部分正文被跳过
没有播报。

根因:TtsPlayer.append() 第一行是 `if (!this.options.enabled) return`,
TTS 关闭期间每次 onDelta 调用 append() 都会被直接挡掉,内部的 rawText/
lastSpokenPlain 完全不会累积。toggleTts() 原来只处理了"答案已生成完毕
时打开 TTS"(调用 speakWithAnnouncement 整段重播)这一种情况,没有处理
"生成中途打开"的情况——这时 ttsEnabled 变 true 后,下一次 onDelta 送来
的只是新增的那一小段 delta,append() 在一个空白的 rawText 基础上开始
累积,效果就是只从当前位置往后播。

修复:toggleTts() 新增分支,检测到 store.querying 为 true 时,先播
"开始播报"提示音,再用 append(store.rawAnswer) 把已生成的全文"补喂"
一遍,相当于告诉播放器"这就是从头到现在的内容";之后 onDelta 的增量
在这个基础上正常追加,衔接不重复。

验证方式:真机通过 CDP 直连运行中的页面,用 Input.dispatchMouseEvent
模拟真实点击触发中途开启,在主进程 tts:speak IPC 处理器临时加日志
(验证后已移除),确认实际送去朗读的文本顺序是"开始播报"→"根据手册,
蓄电池的拆卸与安装步骤如下:"→"拆卸:"→"一、旋松蓄电池正极导线端子
螺母并取下端子"→...,正确从头开始、后续衔接无重复无跳字。

过程中也确认了 window.electronAPI 这类 contextBridge 暴露对象默认被
冻结,渲染进程侧没法通过 monkey-patch 拦截调用做验证(赋值会静默失败),
后续类似验证只能从主进程 IPC handler 侧加日志或用其他方式。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QeoAy5KmkHuKWHAktpEsaN
This commit is contained in:
ssurui 2026-10-05 17:40:16 +08:00
parent a8bce9f985
commit b45c7d2f27

View File

@ -503,6 +503,18 @@ async function toggleTts(): Promise<void> {
} else if (store.rawAnswer && !store.querying) { } else if (store.rawAnswer && !store.querying) {
// 答案已显示完毕时打开 TTS,立即从头播 // 答案已显示完毕时打开 TTS,立即从头播
ttsPlayer.speakWithAnnouncement(store.rawAnswer) ttsPlayer.speakWithAnnouncement(store.rawAnswer)
} else if (store.querying) {
// 提问还在流式生成中途打开 TTS:ttsPlayer 内部的 rawText 此前因为
// enabled=false 一直没有累积(append() 一开头就 return),直接等后续
// onDelta 的话只会从"此刻之后的新增量"开始朗读,把已经显示在屏幕上但
// 开 TTS 之前生成的那部分正文漏掉。这里先播"开始播报"提示音(跟正常
// 提问开始时一致),再用 append() 把已生成的全文"补喂"一遍,相当于
// 告诉播放器"这就是从头到现在的内容",让它从头开始排队朗读;之后
// onDelta 送来的增量会在此基础上正常追加,衔接不会重复。此刻还没有
// 任何输出(刚发问、第一个 delta 还没到)时 append 传空字符串会被
// append() 内部的 if (!text) return 挡掉,不会误触发。
ttsPlayer.announceStart()
ttsPlayer.append(store.rawAnswer)
} }
} }