DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-voice-typing

l

dsh-voice-typing

Manifest valid

语音输入与对话朗读插件

UI (client)hasBundlePatch

dsh-voice-typing

DeepSeek Harness 的语音输入与对话朗读插件。

点一下麦克风按钮(或按 Alt+V)就能进入语音模式:你说话,文字自动打进输入框; 助手的回复可以自动朗读出来。全程本地离线识别,不需要 API Key、断网也能用。

本 README 面向使用者,按「从零到会用」的顺序一步步引导。每一步都告诉你 做什么、为什么、以及做完会看到什么。维护者须知与技术参考见文末。


目录

  1. 它是什么
  2. 安装(一步完成)
  3. 第一次使用(跟着走一遍)
  4. 日常操作速查
  5. 设置面板详解(按你的需求调)
  6. 模型与下载(该选哪个)
  7. 自定义词表(修同音字与专名)
  8. 故障排查
  9. 技术参考(维护者/进阶)
  10. 许可

1. 它是什么

一句话:「说话代替打字,回复自动念给你听。」

它把两件独立的事合在一起,但各自可控:

功能做什么什么时候用
语音输入你说话 → 文字自动进输入框 → (可选)自动发送不想打字、手在忙、想快点说
对话朗读助手的回复自动念出来不想盯着屏幕、听回复更方便
全局语音任意页面常驻语音浮层:说话直接进当前会话,不经过输入框;窗口内快捷键 Ctrl+Alt+V(macOS 还接受 Cmd+Option+V),窗口未聚焦时可用系统级热键(Windows 内置 helper;macOS/Linux 需可选依赖 uiohook-napi);可选唤醒词「小D」待机(先说唤醒词再说命令)在设置页、插件页、没有输入框的地方也想开口说话;想完全不动手就喊唤醒词
AI 界面操控模型通过 voice_ui 工具操作界面:语义动作(切/建会话、执行命令、发消息、停止生成、侧栏、插件页、查看状态)+ 通用 a11y 原语(先拍快照,再按元素 ref 点击/输入/按键;共 14 个动作)。默认关,危险动作要确认(也可以直接说「允许/拒绝」)「帮我把侧栏收起来」「打开插件页」「点一下设置里的那个开关」这类界面操作

关键特性:

  • 点一下就开,再点一下就关:麦克风按钮是开关,不是「按住说话」。
  • 本地离线识别:默认用 SenseVoice 模型,复用 DSH 已装好的那份,零下载、零密钥、断网可用。
  • 边说边出字:说话时文字实时回填到输入框,不用等说完。
  • 三种朗读引擎:在线 Edge(音质最好)、本地神经模型(离线、音质接近在线)、系统语音(最快)。
  • 全局语音(不挑页面):打开「全局语音浮层」后,任意页面右下角常驻一个麦克风小窗, 识别结果直接送进当前会话;窗口内快捷键默认 Ctrl+Alt+V(macOS 额外接受 Cmd+Option+V)。
  • 跨平台系统级热键(可选):窗口未聚焦也能按 —— Windows 内置 PowerShell helper 零依赖; macOS/Linux 需要可选依赖 uiohook-napi(macOS 另需「辅助功能」授权;Linux 走 X11,Wayland 通常不可用)。
  • AI 界面操控(默认关):打开后模型可以调用 voice_ui 工具操作界面(14 个动作);危险动作会先在浮层里要你确认。 需要同时打开「全局语音浮层」——动作执行器和确认条都住在浮层里。
  • 通用界面操作(v2):以后新增的界面功能不用再改动作协议 —— 模型先拍一张当前页面的可交互元素快照(a11y), 再按快照里的 ref 点击/输入/按键;ref 只在最近一次快照内有效,过期会要求重新拍。
  • 唤醒词待机(v3):打开全局语音后麦克风常开待机,先说唤醒词(默认「小D」)再说命令; 待机时识别到的环境闲聊不会发送;活跃 60 秒没说话自动回待机;唤醒词留空 = 关闭。

2. 安装(一步完成)

前置条件:你已经装好并运行过 DeepSeek Harness 桌面应用。

第 1 步 — 安装插件。在 DSH 的终端/命令行里执行:

dsh plugin --profile desktop add dsh-voice-typing

第 2 步 — 重启 DSH。装完插件必须重启才会生效。

第 3 步 — 启用组合包。打开 DSH →「设置 → 插件」,把 dsh-voice-typing 加进 dsh.profile.bundles(勾选启用)。

第 4 步 — 确认出现麦克风按钮。重启后,在聊天输入框附近应该能看到一个 麦克风图标。看到它,安装就算成功了。

💡 看不到麦克风按钮? 先确认「第 3 步」的启用组合包有没有勾上,再重启一次。 还不行就跳到故障排查。

🧩 macOS / Linux 的系统级热键:需要可选依赖 uiohook-napi(由包管理器按 optionalDependencies 安装)。 缺失/装不上不影响语音主功能,只影响「窗口未聚焦也能按热键」;更新插件后跑一次 pnpm install 并重启 DSH。


3. 第一次使用(跟着走一遍)

下面按顺序做,每一步都说明你会看到什么。

3.1 打开语音模式

  • 点一下麦克风按钮,或按全局热键 Alt+V。
  • 看到:按钮变「实心/高亮」,旁边状态行出现 「聆听中」。

语音模式是一个开关。再点一下(或右键)就关闭,不是按住说话。

3.2 开始说话

  • 对准麦克风说一句完整的话,例如「今天天气怎么样」。
  • 看到:你说话时,文字实时出现在输入框里(边说边出字)。

3.3 停顿,自动发送

  • 说完停下来。默认静音 1.5 秒 后判定「说完了一句」。
  • 看到:文字被发送给助手,助手开始回复。

想改成「说完不自动发,我自己按回车」?在设置里关掉「识别后自动发送」。 见 5.2 语音识别。

3.4 (可选)听回复

  • 勾选设置里的「朗读回复」后,助手的回复会自动念出来。
  • 朗读时麦克风旁边会出现一个 ■ 停止朗读 按钮,点它就不念了。
  • 想打断朗读并自己说话?直接开口说,朗读会自动停,你说的话会发出去。

「朗读回复」不需要开语音模式,只要勾上就生效。

3.5 退出语音模式

  • 再点一下麦克风按钮(或按 Alt+V),或右键麦克风按钮。
  • 看到:按钮恢复原样,状态行显示「已停止」。

4. 日常操作速查

你想做什么怎么操作
开/关语音模式点一下麦克风按钮,或按 Alt+V
说完一句并发送说一段话后停顿 1.5 秒(默认)
说完立刻退出语音模式设置「交互模式」→ 说完就关
停止朗读这一轮点 ■ 停止朗读 按钮
打断朗读、自己说话直接开口说(朗读自动停)
临时不自动发送关掉「识别后自动发送」,说完手动回车
只朗读、不开语音模式只勾「朗读回复」,不点麦克风
在任意页面说话(不经输入框)设置里打开「全局语音浮层」,按 Ctrl+Alt+V 或点右下角浮层麦克风
不按键、喊一声就说话打开「全局语音浮层」,保持麦克风开着;先说唤醒词(默认「小D」)再说命令。留空唤醒词 = 关闭
窗口未聚焦也能按热键「高级与诊断」打开「系统级全局热键 helper」:Windows 直接可用;macOS/Linux 需装可选依赖 uiohook-napi(macOS 还要授权辅助功能)
让 AI 操作界面打开「允许 AI 操控界面」和「全局语音浮层」,危险动作在浮层确认(也可以直接说「允许/拒绝」)
让 AI 打开插件页 / 收起侧栏直接说「打开插件页」「把侧栏收起来」(走语义动作,不需要 a11y)
让 AI 点按钮/填输入框先说「看看当前页面有什么」让模型 ui_snapshot,再按返回的元素操作;ref 过期就让它重新拍

麦克风按钮只有两个动作:

手势行为
点一下开 → 进入语音模式并开麦;关 → 只关麦,朗读继续播完
右键同「点一下」(顺带屏蔽浏览器默认菜单)

全局热键(Alt+V):按一下切换开/关,行为与点麦克风一致。 热键必须带至少一个修饰键,且主键不能是单个字母(否则打字会误触发)。

这是输入框麦克风的快捷键。打开「全局语音浮层」后输入框麦克风会隐藏,Alt+V 改为切换浮层麦克风; 浮层自己的快捷键默认 Ctrl+Alt+V,系统级热键 helper 注册的也是这个组合。关闭浮层即恢复原来的行为。


5. 设置面板详解(按你的需求调)

打开 DSH →「设置 → 语音输入」(settings.section 槽位)。 所有设置分 6 个可折叠分区(前 5 组默认展开,最后一组「高级与诊断」默认收起): 语音识别 / 语音交互 / 文本处理 / 语音合成 / 模型与下载 / 高级与诊断。 改完记得点 保存。

5.1 先知道这两个总开关

开关控制什么默认
语音模式(麦克风按钮)是否聆听你的说话关
朗读回复(设置里)是否朗读助手的回复关

两者独立。想「只听不说」就只开麦克风;想「只听回复不发言」就只勾朗读回复。


5.2 语音识别

设置作用怎么调
识别模型用哪套模型认字见 第 6 节。默认 SenseVoice,零下载。
识别语言告诉模型你说的是哪种语言(仅 SenseVoice 生效;Whisper/zipformer2 会忽略,UI 会按模型禁用/提示)自动(推荐,由模型判断)/ 中文 / 英文 / 粤语 / 日文 / 韩文
实时字方式说话时实时出字用哪种方式见下方说明。默认「整段重识别」(精度最好)。

「实时字方式」三种选法:

方式优点缺点何时用
整段重识别(默认)精度最好,与最终稿同源句子越长越慢多数情况
流式解码延迟低,长句不变慢精度略低说长句、要快
自动流式模型用流式,否则整段—不确定时

最终稿(断句后的那次识别)始终用当前模型,不受「实时字方式」影响。 「流式解码」固定依赖 zipformer2-zh(与当前所选识别模型无关);它没下载时 UI 会提示,实际会回退整段重识别。


5.3 语音交互

设置作用怎么调
交互模式说完一段之后怎么办连续对话(默认):说完自动接下一轮,麦克风不关;说完就关:说完就退出语音模式。依赖自动发送:关闭自动发送时该选项会被禁用(否则说完不会发送、也不会退出)。
静音断句(毫秒)静音多久算「说完一句」默认 1500。太短会把长句切断;太长回复会慢。
识别后自动发送说完是否自动发送开(默认):停顿后自动发;关:文字只留在输入框,手动回车发送。
发送前等待(毫秒)说完后再等多久才发默认 1200。用来合并短句、避免抢话。只在「自动发送」开时显示/生效;清空会回退默认。
语音模式快捷键切换语音模式的全局热键默认 Alt+V。格式如 Alt+V,改完立即生效;非法组合会阻止保存并提示。
全局语音浮层(globalVoice)任意页面常驻的麦克风/状态小窗:说话直接进当前会话,不依赖输入框默认关。打开后输入框麦克风隐藏、Alt+V 也改为切换浮层麦克风(只剩一个语音入口,不会混);并且会自动开麦进入待机/聆听:有唤醒词时先说唤醒词才接收命令(唤醒词留空则直接聆听)。点浮层麦克风或按下面的全局快捷键可随时开始/停止。
全局语音快捷键(globalShortcut)在窗口内任意页面切换全局语音默认 Ctrl+Alt+V;macOS 还额外接受 Cmd+Option+V(默认组合的顺手别名)。浮层开启时上面的 Alt+V 也等效切换浮层;窗口未聚焦时要系统级热键 helper。
语音文本注入方式(voicePromptMode)识别出的文字怎么送进会话排到下一轮(默认:助手空闲时就是发一条新消息)/ 插入下一步(助手正在生成时用 steer 插到当前这一步)。
唤醒词(wakeWord)全局语音浮层待机时的唤醒词:先说词,再说命令默认「小D」;留空 = 关闭(回到 v2:说话直接发送)。建议 2~4 个字:2 个字(如「小D」)必须精确匹配,3 个字以上才容忍 1 个字的口音误差;只对右下角浮层生效,输入框麦克风不受影响。

全局语音的注意事项:

  • 浮层开启时输入框麦克风隐藏,Alt+V 改为切换浮层麦克风;两个引擎在运行期也互斥(任一方开麦会停掉另一方),避免同一句话被识别两遍。
  • globalShortcut 同样要求至少一个修饰键;单字母主键需搭配 Ctrl/Alt/Meta(Shift+V 非法,会吞掉全局大写输入)。
  • 系统级热键:Windows 用内置 PowerShell helper(零依赖);macOS/Linux 用可选依赖 uiohook-napi(macOS 需「辅助功能」授权,Linux 需 X11)。两者都要求「全局语音浮层」打开,否则辅助进程/监听会被自动回收。
  • 窗口隐藏/最小化时渲染器定时器会被 Chromium 节流:后台可靠性有限(已优先用 AudioWorklet VAD 缓解)。

唤醒词待机说明(v3):

  • 唤醒词 = 麦克风常开待机:系统会持续显示「录音中」,更耗电、也可能误唤醒;wakeWord 留空即可完全关闭。
  • 只对全局语音浮层生效;输入框麦克风不受影响。
  • 待机时识别到的非唤醒内容不会发送(隐私:不把环境闲聊送进会话);最多每 30 秒提示一次「待机中」。
  • 唤醒后 60 秒没有语音会自动回待机(避免误当常开录音);再说一次唤醒词即可。

快捷键注意事项:

  • 必须带至少一个修饰键(Ctrl / Alt / Shift / Meta)。
  • 主键是单个字母时,必须搭配 Ctrl / Alt / Meta(不能只配 Shift,否则打字会误触发)。
  • 别用 Ctrl+Shift+V(浏览器「粘贴为纯文本」)、Ctrl+Space(输入法占用)。

静音断句与发送前等待在面板里并排显示,方便对照调整「说完」与「发送」的节奏。


5.4 文本处理

设置作用怎么调
清理语气词去掉「嗯」「啊」「那个」等口头禅勾选即生效(只处理识别结果,不改写说话内容)。
自定义词表热词与替换规则一行一条;正确=错误 表示把「错误」替换成「正确」,单行表示热词。热词仅 zipformer2(transducer)档位生效;保存时按行 trim、去空、上限 100 字/条、200 条。空格/换行输入不会被逐键吞掉。

5.5 语音合成(朗读)

本区顶部会显示「当前合成方式」(引擎 + 音色/模型),改完设置后保存即生效。

设置作用怎么调
朗读回复总开关。关掉则下面所有朗读参数都不生效勾选即朗读,不需要开语音模式。朗读关闭时本区仍可编辑/保存(仅保存、不生效),方便先配置。
朗读追赶最新生成远快于朗读时,允许落后屏幕多少秒;超过就整段跳旧段、保留最新 3 段(从最新第 3 条开始读,不从段落中间开始念)默认 7.5 秒(0.5 秒步进);0 = 全部朗读、不跳段(长回复会越读越滞后);>120 按 120 秒生效。只在 flow(客户端 ack)模式生效。
语音引擎用哪种方式念Edge 在线 / 本地神经模型 / 本地系统语音 / GPT-SoVITS 外部服务,见下方对比。引擎值非法时保存会被拒绝(不会静默落在线)。
朗读音色用哪个发音人在线用 Edge 音色(zh-CN-XiaoxiaoNeural 等);本地用系统语音;神经模型用「说话人」(单说话人模型会自动隐藏该项)
GPT-SoVITS 服务地址外部 GPT-SoVITS 服务地址默认 http://127.0.0.1:9880;根地址或 /tts 均可
服务端参考音频路径官方 api_v2 必填GPT-SoVITS 所在机器上可访问的 wav 路径(插件只透传)
参考音频文本/语言、合成语言参考音色与多语种参考文本与音频逐字匹配效果最好;语言 zh/en/ja/ko/yue
朗读语速快慢默认 +10%;+0% 为正常,+50% 更快,-50% 更慢。四种引擎都生效(SoVITS 转 speed_factor 0.5~2)
朗读音调高低只有在线 Edge 支持(如 +0Hz、-5Hz)
朗读音量大小在线/本地支持(如 +0%),神经模型与 SoVITS 不支持
音频格式MP3 码率只有在线 Edge 支持。24kHz/48kbps(推荐)/ 24kHz/96kbps(音质更好)

四种语音引擎对比:

引擎音质联网速度备注
Edge 在线★★★需要快(预热后 ~600ms)音质最好,音色选择多
本地神经模型★★☆离线中(~1.6s)比系统语音自然;需下载约 170MB 模型
本地系统语音★☆离线最快(~800ms)机器感较强;Windows SAPI / macOS say / Linux espeak
GPT-SoVITS 外部服务取决于你的服务与模型本机 HTTP取决于服务端(GPU 快)用户自己跑 GPT-SoVITS api_v2;插件只转发文本合成,不内置模型、不做克隆

在线引擎合成失败时自动回退到本地,保证「总有声音」;x-tts-engine-fallback 会在 UI 提示一次。 GPT-SoVITS 外部服务不回退(避免把文本发给微软、也避免偷偷换音色),失败见 /diagnostics 的 sovitsTts。

配置保存会做校验:非法枚举/类型/范围(如 ttsEngine 未知、sovitsUrl 非 http(s)、readAloudLag 非数字)返回 400 并显示错误,不会被静默改成别的值。

外部 GPT-SoVITS 引擎(可选)

插件不内置 GPT-SoVITS、也不做声音克隆:需要你自己运行官方 api_v2 服务, 把服务端可访问的参考音频/参考文本/语言填到设置里;插件只把每句文本 POST 到 /tts, 拿回 wav 走现有播放管线。

  1. 在 GPT-SoVITS 目录启动 API 服务: python api_v2.py -a 127.0.0.1 -p 9880 -c GPT_SoVITS/configs/tts_infer.yaml
  2. 设置 → 语音引擎选「GPT-SoVITS 外部服务」,填:
    • 服务地址(默认 http://127.0.0.1:9880,填根地址或 /tts 都行);
    • 服务端参考音频路径(官方 api_v2 每个请求都要求它,必须是服务端机器上可访问的 wav);
    • 参考音频文本 / 语言(可选;与参考音频逐字匹配时音质更好);
    • 合成语言(zh / en / ja / ko / yue)。
  3. 模型/音色/参考音频都由 GPT-SoVITS 侧管理,插件不下载模型、不克隆声音。

服务未启动或参考音频路径不对时,/tts 会返回可读错误(含官方 api_v2 的 message 与提示), 计数进 /diagnostics.sovitsTts;插件不会替你拉起 Python 服务。


5.6 模型与下载

设置作用怎么调
启用后预加载模型选了模型就后台先加载好开(推荐)。第一次说话就不用干等(Zipformer2 首次要 40 秒)。只覆盖当前识别/神经 TTS 模型,不覆盖实时字用的 zipformer2。
空闲释放(分钟)模型闲置多久后释放内存10 分钟(默认)/ 永不释放 / 5 / 30 / 60 分钟。不释放当前正在使用的模型。
运行环境自检检查平台、显卡、各引擎可用性点「重新检查」,看绿色 ✓ / 红色 ✗(含四种 TTS 引擎与 GPT-SoVITS 配置项)。

模型常驻内存(Zipformer2 约 160MB,神经 TTS 约 170MB)。闲置释放能把内存还给系统。 下方的模型列表负责下载/删除识别与合成模型;已下载的档位才会出现在识别模型下拉里。


5.7 高级与诊断

设置面板最后一组默认收起:放三个「高级能力」开关(AI 操控、危险动作确认、系统级热键 helper)、 一行只读的系统级热键状态,以及排查入口。

设置作用怎么调
允许 AI 操控界面(uiControl)允许模型调用 voice_ui 工具操作界面(共 14 个动作):语义动作(切/建会话、执行命令、发消息、停止生成、设置侧栏方向、打开插件页、查看状态)+ 通用 a11y 原语(ui_snapshot 拍快照后按 ref 点击/输入/按键)默认关。必须同时打开「全局语音浮层」——动作执行器与确认条都住在浮层里;只开 uiControl 时工具会返回 voice_overlay_disabled。例外:list_sessions 由宿主直接查会话列表,不需要浮层。提示词 section 只在 uiControl 打开时注册(关闭立即释放、再开重新注册),避免白占 token 又宣传一个不可用的工具。
危险 UI 动作需要确认(uiConfirm)切会话 / 新建会话 / 执行命令 / 发消息 / 停止生成 / a11y 点击、输入、按键前,先在浮层弹确认条默认开。关闭后这些危险动作直接执行;只读动作(查看状态、设置侧栏、打开插件页、拍快照)永远不弹确认。点「拒绝」或**直接说「拒绝」**时回执 confirm_denied;说「允许/确认/可以」等同点「允许」。
系统级全局热键 helper(globalHotkeyHelper)窗口未聚焦/最小化也能唤起语音浮层。Windows:拉起 PowerShell 子进程注册热键(无额外依赖);macOS/Linux:在宿主进程内加载可选依赖 uiohook-napi 监听系统按键默认关;必须同时打开「全局语音浮层」(没有浮层消费事件时不会启动/会被回收)。Windows 失败常见于快捷键被占用、安全软件拦 PowerShell;macOS 需在「系统设置 → 隐私与安全性 → 辅助功能」授权;Linux 走 X11,Wayland 通常不可用。状态看 /diagnostics.hotkeyHelper 的 enabled/status/reason(不含 token):Windows 启动时先 starting,脚本确认 RegisterHotKey 成功(READY)才变 enabled;快捷键被占用(REGISTER_FAILED)或进程提前退出 → failed,并会删掉含 token 的临时 .ps1。

系统级热键状态(只读,不是配置项):三个开关下方新增一行,显示 未开启 / 等待注册… / 已启用(组合键) / 失败(原因) 以及识别到的平台;打开设置面板时拉一次 /diagnostics,保存配置后自动再拉一次,也可以点「刷新状态」手动刷新。 诊断接口不可用时显示「未知(诊断不可用)」(不重试、不报错);helper 开着但「全局语音浮层」没开时会额外提示「需要同时开启浮层才会生效」。 这一行不参与配置保存,设置面板仍然只有 6 个折叠分区、35 个配置键、3 个开关。

升级提示:v2 改了 voice_ui 工具 schema(新增 ref / key / collapsed 参数),宿主不会热更新工具定义 —— 升级后需要重启一次 DSH;客户端刷新一次即可。

排查入口:

  • GET /dsh-voice-typing/diagnostics:朗读调度(lagMs/trimmed/configWarnings 等)、各 TTS 引擎失败、模型下载状态、环境自检结果;全局语音相关的 voicePrompt(回退注入计数与最近错误)、uiActions(最近 50 条界面动作:动作/结果/错误,不含参数正文)、hotkeyHelper(enabled/status/reason/platform,不含 token)—— 设置面板底部的只读状态行读的就是这里;
  • 浏览器调试快照 __DSH_VOICE_DEBUG__():客户端播放/队列/回退/高亮等运行时状态。

6. 模型与下载(该选哪个)

6.1 识别模型

档位大小中文精度特点推荐场景
SenseVoice(推荐)0(内置)/ 230MB★★★带标点与数字规范化;零下载大多数人
Zipformer2 中文160MB★★流式,延迟最低;追求实时字速度、说长句
Whisper tiny99MB★最小先跑通
Whisper base153MB★★轻量折中中文可用
Whisper small358MB★★★精度明显优于 base均衡
Whisper medium902MB★★★★更准重精度
Whisper large-v3-turbo989MB★★★★☆接近 large-v3、体积小一半综合最优
Whisper large-v31.7GB★★★★★精度天花板重精度、不怕慢

怎么选:默认 SenseVoice 就行。它零下载、中文强、带标点。 想要实时字更快选 Zipformer2;想要更准选 Whisper 大档(代价是体积和耗时)。

6.2 合成(TTS)模型

档位大小特点
MeloTTS 中英170MB多说话人、本地离线、音质接近在线

6.3 怎么下载

第 1 步 — 打开「设置 → 语音输入 → 模型管理」。

第 2 步 — 在「识别模型」或「合成模型」列表里,找到你要的档位,点 下载。

第 3 步 — 看进度。下载时显示百分比;完成后出现 ✓ 和「已就绪」。

第 4 步 — 回到「语音识别」或「语音合成」,把「识别模型 / 神经模型」切到你刚下载的档位。

下载失败? 确认网络能访问模型源(国内走 hf-mirror.com 镜像)。 进度卡在 100% 不动通常是校验失败,重试一次或换档位。


7. 自定义词表(修同音字与专名)

词表里两种写法,作用完全不同:

写法作用示例
正确=错误替换:识别出「错误」时改写成「正确」深度求索=深度球搜
只写 正确热词提示:不改文本,只给识别器做偏置专业术语

怎么用:

  1. 打开「设置 → 语音输入 → 交互与后处理」。
  2. 在「自定义词表」里一行一条填写。
  3. 保存。

注意事项:

  • 中文精确匹配;英文忽略大小写并保持原词形态。
  • 热词提示只有 Zipformer2(transducer)档位支持。用 SenseVoice 时,只写「正确」的条目不会有任何效果(想要替换效果请用 = 写法)。

8. 故障排查

按症状找对策。

8.1 看不到麦克风按钮

  1. 确认插件已安装:dsh plugin --profile desktop list 里有没有 dsh-voice-typing。
  2. 确认已在「设置 → 插件」的 dsh.profile.bundles 里启用。
  3. 重启 DSH(插件加载需要重启,不是刷新页面)。

8.2 点麦克风没反应 / 显示「麦克风被阻止」

  1. 检查浏览器/系统是否给了麦克风权限(地址栏权限图标)。
  2. 确认没有别的程序(会议软件等)占着麦克风。
  3. 重试;若仍不行,重启 DSH。

8.3 能说话,但文字没出来 / 显示「未听到声音」

  1. 确认麦克风没静音、音量够大。
  2. 靠近麦克风,清晰地说一句。
  3. 若提示「模型加载中…」,等首次加载完成。
  4. 检查「识别模型」是否已就绪(见 6.3)。

8.4 能识别,但不对 / 同音字错

  1. 用自定义词表修正:正确=错误。
  2. 若错得离谱,换更准的识别模型(见 6.1)。
  3. 确认「识别语言」设为 自动 或你的语言。

8.5 朗读没声音

按顺序排查(最常见的是「在线引擎需联网」):

  1. 确认设置里「朗读回复」已勾选。
  2. 确认「语音引擎」:
    • Edge 在线 需要联网(断网请换本地引擎)。
    • 本地系统语音 在当前平台需支持(Windows SAPI / macOS say / Linux espeak-ng)。
    • 本地神经模型 需要已下载该档位模型(见 6.3)。
  3. 打开浏览器控制台,输入 __DSH_VOICE_DEBUG__() 看 ttsFailures。
  4. 若显示「朗读失败」,看 诊断接口里的 onlineTts / localTts / neuralTts / sovitsTts 的 lastError。
  5. 若本地引擎失败但显示「朗读中」却没声,多半是自动回退到在线但没联网。

8.6 朗读不完整 / 句子被切碎 / 把代码念出来

这些都是有意设计的防御,不是 bug:

  • 代码块(```)一个字都不会念 —— 这是刻意的,防止把代码念出来。
  • 列表编号不会当句子念 —— 1. 第一步… 只念「第一步…」,不念「一」。
  • 长句不会被拦腰切断 —— 尽量切在句末标点(。!?;)。
  • 按段落朗读 —— 先按空行 / 列表项 / 标题 / 引用把回复分成段落,段落内再按句末标点分句。一段话从头读到尾才进入下一段,绝不从段落中间开始念。
  • 朗读完整、不跳段 —— 宿主按客户端的真实播放进度逐句补发(客户端每开始播一句回一条 ack),生成再快也不会积压、不会丢掉「接下来要播的句子」。

8.7 快捷键不生效

  1. 确认设置里「语音模式快捷键」的格式正确(如 Alt+V)。
  2. 别与浏览器/系统的快捷键冲突(Ctrl+Space、Ctrl+Shift+V 等)。
  3. 确认主键不是单个字母。

8.8 全局语音 / 系统热键不生效

  1. 确认「设置 → 语音交互 → 全局语音浮层」已打开并保存(关着时浮层根本不渲染)。
  2. 窗口内快捷键不响应:确认「全局语音快捷键」格式正确(默认 Ctrl+Alt+V),别和系统/浏览器快捷键冲突;设置面板正在录制快捷键时不会触发。
  3. 窗口未聚焦 / 最小化也要能按:打开「高级与诊断 → 系统级全局热键 helper」,并确认「全局语音浮层」也开着(两个开关缺一不可)。Windows 用内置 PowerShell helper(零依赖);macOS/Linux 需要可选依赖 uiohook-napi —— 插件更新后跑一次 pnpm install(或重装插件);缺失时不会报错崩掉,只是安静降级。设置面板「高级与诊断」底部的「系统级热键状态」行会直接显示状态/平台/失败原因(保存后自动刷新,也可点「刷新状态」);也可以到 GET /dsh-voice-typing/diagnostics 看 hotkeyHelper.enabled/status/reason/platform:Windows 会先 starting,脚本输出 READY 才变 enabled;REGISTER_FAILED/提前退出 → failed 并清理临时脚本。常见原因:快捷键被占用、macOS 未授权「辅助功能」、Linux 在 Wayland 会话(需 X11)、安全软件拦截 PowerShell。
  4. 说话没反应:看 /diagnostics.voicePrompt 的 count / failures / lastError(客户端直发会话失败时才会走这条回退注入);浮层本身也会显示「未找到当前会话 / 发送失败」。
  5. 窗口隐藏/最小化时识别不稳定是已知限制:渲染器定时器会被 Chromium 节流,优先的 AudioWorklet VAD 已缓解,但它不是系统级后台服务。
  6. 唤醒词待机:浮层显示「待机 · 说『小D』唤醒」时,必须先说唤醒词再下命令;待机时的环境闲聊会被静默丢弃(每 30 秒最多提示一次)。唤醒词留空 = 关闭待机(说话直接发送);留空后若仍显示待机,重启一次 DSH 让新配置键生效。
  7. 可选依赖 uiohook-napi 装不上(无预编译二进制 / 无编译环境):系统级热键在 macOS/Linux 上不可用,但应用内快捷键、语音、朗读全部不受影响;/diagnostics.hotkeyHelper.reason 会写明原因。

8.9 AI 不执行界面动作(uiControl)

  1. 「设置 → 高级与诊断 → 允许 AI 操控界面」默认关,先打开并保存;同时要打开「全局语音浮层」 —— 动作执行器和确认条都住在浮层里,只开 uiControl 时工具返回 voice_overlay_disabled。
  2. 危险动作(切会话/新建/执行命令/发消息/停止生成,以及 a11y 的点击/输入/按键)会弹确认条;没点「允许」就不会执行,工具收到 confirm_denied。
  3. 语音确认:确认条挂着时,直接说「允许/确认/可以」或「拒绝/取消/不要」等同点按钮;这句只用于回答确认,不会被当成聊天消息发进会话。
  4. a11y 通用操作(v2):模型会先 ui_snapshot 拍快照再按 ref 操作。若返回 stale_ref 说明快照已过期(页面变了/隔了太久),让它重新拍一张;disabled = 元素被禁用;forbidden = 密码框不允许代填;unsupported = 当前界面/DSH 版本没有对应能力(例如 open_plugins 需要 pluginNavigation.openList)。限制:ui_type 只能写文本类输入(文本/搜索/邮箱/URL/电话/数字 input、textarea、contenteditable;checkbox/radio/file/range/日期等一律 unsupported,readonly 也拒绝);ui_click 会拒绝外链(http(s)/mailto/file/…、target=_blank)与 file input(unsupported)——a11y 只操作应用内界面。
  5. 改了工具 schema 不生效:v2 新增了 ref/key/collapsed 参数,宿主工具定义不会热更新 —— 升级后重启一次 DSH 才生效;客户端刷新即可。
  6. 排查:GET /dsh-voice-typing/diagnostics 的 uiActions(最近 50 条:动作、ok、error,不含参数正文);确认条与浮层状态在右下角浮层里。
  7. 只让模型「看看有哪些会话」不需要浮层(list_sessions 由宿主直接回答)。
  8. 多窗口:每个动作执行前,窗口会先向宿主认领(POST /ui-action-claim)。只有认领成功的窗口才弹确认/执行,其它窗口静默跳过 —— 两个窗口不会各发一条消息;认领失败/404/网络错误时也是保守跳过(宁可这次不执行,也不双执行)。

9. 技术参考(维护者/进阶)

9.1 宿主接口

全部路由带 /dsh-voice-typing 前缀。

路由说明
POST /transcribe请求体为 16 kHz 单声道 f32 PCM,返回 {text, model, language, hotwords, speech}(最终稿;speech 表示是否检测到人声)
POST /asr-stream?sessionId=…送入增量 PCM,返回当前实时文字(流式 zipformer2;模型不可用时返回 {unavailable:true, reason})
POST /asr-stream/reset?sessionId=…结束该会话的流式解码并丢弃状态
GET /asr-ready识别模型是否已下载 + 已加载(客户端开麦前先问,首次加载要 20~40 秒)
POST /asr-warmup确保识别模型真的加载好(没就绪就等到就绪)
GET /tts?text=…文字 → 音频(在线出 MP3,本地系统语音与本地神经模型出 WAV)。在线失败自动回退到本地系统语音,响应头 x-tts-engine-fallback: local 标出
GET /voices?engine=online|local音色列表(在线 Edge 列表 / 本地系统语音)
GET /tts/env合成运行环境自检(引擎可用性、平台能力)
GET /tts/warmup预热在线 TTS 连接(本地引擎没有连接概念,直接返回)
GET /latest-message?sessionId=…本轮 assistant 回复全文
GET /busy助手是否正在生成。降级用 —— 首选是 SSE 的 assistant-busy 事件(零延迟),SSE 断了才回退到轮询
GET /diagnostics诊断数据(统计、忙状态、各引擎合成失败计数、下载进度、朗读调度器状态 readAloud[]:mode/outstanding/acks/emitted/stalls/lagMs/playingMs/backlogMs/budgetMs/streaming/skim/trimmed/trimmedParagraphs/trimmedSentences/truncatedParagraphs/awaitingFlush,以及 readAloudScheduler 的 rejectedAcks/lastRejectedAck),全局语音的 voicePrompt(回退注入 count/failures/lastAt/lastError)、uiActions(最近 50 条界面动作 {at,requestId,action,ok,error},不含参数正文)、hotkeyHelper(enabled/status/reason/platform/tokenReady,不含 token)。不在 /config 里 —— 混进去会被客户端当配置回传
POST /voice-session客户端心跳上报(诊断用)
POST /voice-prompt全局语音的回退注入(客户端调用):客户端优先用 session.prompt() 直发,失败时才把 {sessionId, text, mode, requestId?} 交给宿主。宿主只在 sessionController 不可用时才回退 agents.followup/steer;sessionController.prompt 抛错 → 直接 502(不再二次注入,避免同一条语音入队两遍)。text trim 后 ≤4000 字;requestId ≤200 字符,用于幂等去重(客户端每条识别文本生成一个,直发与回退共用)。参数非法 → 400
POST /ui-action-result渲染器执行完 ui-action 后的回执(客户端调用):{requestId, ok, value?, error?}。未知/已过期 requestId → 404(不静默丢弃)。多窗口时只有认领成功的窗口会回执,其它窗口静默跳过
POST /ui-action-claimUI 动作认领(渲染器调用;JSON-only,缺 content-type 或非 JSON → 415):{requestId, clientId} → 200 `{claimed:true
POST /hotkey系统级热键回调(仅 Windows 路径):唯一调用方是 lib/global-hotkey-helper.js 拉起的 PowerShell 额外进程(客户端不直接调);请求头 x-dsh-voice-token 鉴权,成功广播 SSE global-hotkey;token 不对或 helper 未启用 → 403。macOS/Linux 的 native 热键在宿主进程内直接广播,不走 HTTP
GET/POST /config读写生效配置
GET /models模型档位清单与本地状态(识别 + 合成)
POST /model下载/准备指定档位
GET /streamSSE:turn-begin / turn-end / call-end / sentence / assistant-busy / model-progress / model-ready / model-error / ui-action / global-hotkey
POST /reading-ack客户端朗读进度回报(流控)。请求体 {sessionId, turnId, count, durationMs?}(durationMs = 这一句真实音频时长,仅 count=1 时用,宿主据此精确计「正在播放的剩余时长」):每开始播放一句回一条,宿主随即补发下一句。收不到 ack 时宿主自动降级为旧的 400ms 时间窗口节奏(老客户端可用)

9.2 SSE 事件信封

所有 SSE 事件的 data 都是同一个信封,业务字段平铺在里面:

event: sentence
data: {"v":3,"seq":12,"ts":1735000000000,"sessionId":"s1","text":"你好","turnId":"turn-x","index":0,"para":0,"paraStart":true}
字段含义
v协议版本(当前 3;v3 之后只做只增不改的字段扩展,同包同版本一起加载)
seq全局单调递增。跳号 = 丢了事件
ts事件产生时刻(epoch 毫秒)
sessionId归属会话;null 表示全局事件(如模型下载进度)

call-end 只表示「这一次 llm/stream 调用结束」,不表示该次调用的句子已经全部发出: flow(流控)模式下剩余句子仍会按客户端 ack 逐句补发,所以 call-end 之后仍可能出现 sentence。 真正的「一轮结束」是 turn-end(下一轮开始时广播上一轮)。

sentence 事件另有业务字段:text(要念的句子)、turnId(属于哪一轮)、 index(本轮第几句,客户端据此去重)、para(自然段序号,一轮内从 0 递增)、 paraStart(是否该段第一个发出的片段 —— 客户端永不丢弃它)。

客户端据此可以:按 sessionId 过滤(多会话不串音)、按 seq 发现丢事件、 按 turnId 精确到某一轮做静音。

全局语音 / AI 界面操控新增事件(业务字段同样平铺在信封里):

事件业务字段含义
ui-actionrequestId、sessionId、actions:[{action, params}]、confirm、expiresAt?宿主 voice_ui 工具下发的界面动作;confirm 是宿主算好的生效值(只读动作永远 false)。expiresAt(epoch ms,可选)与宿主工具超时同一个 deadline:渲染器据此拒绝迟到的确认(回执 expired,不执行)。渲染器执行后必须 POST /ui-action-result 回执
global-hotkeyaction(toggle)系统级热键 helper 进程 POST /hotkey 后广播;sessionId 为 null(全局事件)。渲染器据此切换浮层麦克风

v2 之后 ui-action 里的 action 可能是 14 种之一(8 个语义动作 + 6 个 a11y 原语);渲染器执行前仍用同一份 UI 动作协议校验,ref 只对最近一次快照有效。

多窗口:执行前必须先 POST /ui-action-claim 认领({requestId, clientId});认领失败/404/网络错误一律静默跳过(不回执、不执行),由认领成功的那个窗口完成确认/执行/回执。

9.3 识别链路

浏览器采集 → AudioContext 原始 float32 → 线性重采样到 16 kHz → 上传宿主 → 工作线程里用 sherpa-onnx 识别(SenseVoice / Whisper / Zipformer2,按设置选择)→ Silero VAD 切段(灵敏度 0.5 / 最短语音 0.25 s / 静音断句 0.5 s / 单段最长 20 s, 音点前置补偿 0.3 s)→ 自定义词表与语气词后处理 → 写入草稿并发送。

识别跑在独立工作线程(lib/asr-worker.js)里,主线程不会被 0.5~3 秒的同步推理卡住。 本地神经合成同理,放在 lib/tts-worker.js 里跑。

⚠️ Zipformer2 是流式模型,必须用 OnlineRecognizer + isReady/inputFinished 的解码方式。 用 OfflineRecognizer 配 transducer 会让 sherpa 的原生代码 直接终止进程(整个桌面应用退出)。

9.4 诊断接口

接口用途
GET /dsh-voice-typing/config当前配置(诊断/统计走 /diagnostics)
GET /dsh-voice-typing/voices?engine=online|local音色列表
GET /dsh-voice-typing/tts?text=…合成测试
GET /dsh-voice-typing/tts/warmup预热连接
GET /dsh-voice-typing/busy助手是否在生成
GET /dsh-voice-typing/streamSSE:sentence / turn-begin / turn-end / call-end / assistant-busy / model-* 事件

浏览器控制台里 __DSH_VOICE_DEBUG__() 能看到 ttsFailures、playSkipped 等运行时状态。

9.5 配置文件位置

底层配置落盘在 ~/.dsh/voice-typing.json,设置面板是它的编辑器(单一数据源)。

9.6 代码结构

文件职责
lib/index.js宿主侧:HTTP 路由、配置读写、模型管理、朗读调度
lib/client.js浏览器侧:麦克风按钮、VAD 断句、草稿管理、播放队列、设置面板、全局语音浮层 + UI 动作执行器(含 v2 a11y 快照/点击/输入/按键)(构建产物,勿手改)
lib/ui-action-protocol.jsUI 动作协议(宿主纯函数):动作枚举、每动作参数白名单/长度上限、确认策略、ui-action 载荷与回执校验;客户端 src/client/pure/ui-action-protocol.js 是逐条同构镜像
lib/global-hotkey-helper.js跨平台系统级热键 helper:Windows 生成 PowerShell RegisterHotKey 脚本并拉起/回收子进程(/hotkey 的 token 由它生成);macOS/Linux 动态加载可选依赖 uiohook-napi、在宿主进程内监听 keydown 并直接广播 SSE(不走 HTTP)。只在 globalVoice && globalHotkeyHelper 时启动。Windows 子进程 stdout 输出 READY 才把状态从 starting 置为 enabled(REGISTER_FAILED/提前退出 → failed);退出/失败/dispose 都会删除含 token 的临时 .ps1
lib/asr-worker.js识别工作线程(SenseVoice / Whisper / Zipformer2)
lib/tts-worker.js本地神经合成工作线程(sherpa-onnx VITS)
lib/edge-tts.jsEdge 在线朗读(共享连接 + 串行锁)
lib/local-tts.js跨平台系统语音(Windows SAPI / macOS say / Linux espeak-ng)
lib/neural-tts.js本地神经 TTS 的宿主侧封装(线程管理、预加载、释放)
lib/sovits-tts.js外部 GPT-SoVITS 引擎客户端:把每句文本 POST 到用户自己的 api_v2 服务;不内置模型、不做克隆、失败不回退
lib/model-catalog.js模型档位定义、并行分片下载、缓存、SHA256 校验
lib/text-rules.js词表解析、替换、语气词清理、输出规范化
lib/read-aloud-policy.js朗读发射规则(纯函数):时间窗口、单句上限、splitLongText(切成完整句子,绝不断成半句)、packWindowSentences(一批取几句,不合并成长文本)
lib/paragraph.js自然段落切分(纯函数):空行 / 列表项 / 标题 / 引用为段落边界,splitParagraphs 支持流式累积(末尾未完成段进 rest)
lib/speech-clean.js文本清洗与分句(纯函数):stripForSpeech(Markdown → 纯文本 + SSML 转义)、splitSentenceParts
lib/read-aloud-scheduler.js朗读调度器:按自然段落收集、按客户端 ack 的播放进度发句(流控),无 ack 时降级为时间窗口;事件带 para / paraStart
lib/fence-filter.js流式代码围栏过滤器(纯函数):跨 chunk 识别 ``` / ~~~ 围栏(围栏标记本身可能被 chunk 边界切开),围栏内内容一个字都不交给朗读
lib/tts-env.js运行环境自检
src/client/pure/*.js浏览器侧的纯函数源码(门限状态机、草稿合并、事件信封、定时器登记表、全局语音 VAD、UI 动作协议、a11y 原语)。不依赖浏览器 API,测试可直接原生 import
src/client/pure/global-voice.js全局语音纯逻辑:VAD 分段状态机(AudioWorklet 内联同一份源码)、识别文本归一化/限长、注入模式归一化、唤醒词匹配(matchWakeWord:开头锚定 / 语气词标点容错 / ≥3 字时 1 字容错,2 字必须精确)、快捷键平台别名(effectiveShortcut:macOS 默认组合额外接受 Cmd+Option+V)
src/client/pure/ui-action-protocol.js客户端 UI 动作协议镜像(与宿主逐条同构,测试用同一张用例表跑两端)
src/client/pure/a11y.jsv2 通用 a11y 原语(纯函数):可交互元素描述与 ref、按键字符串解析(parseKeySpec)、语音「允许/拒绝」识别(normalizeConfirmSpeech);DOM 胶水在 main.js
scripts/bundle-client.mjs把 src/client/pure/ 内联进 lib/client.js(浏览器模块的 require 只能解析包名,不能 import 本地相对路径,所以纯函数只能有一份源码)

9.7 维护者须知

这一节记录这个插件反复出问题的地方,以及对应的硬规则。 每一条都对应一个真实修过的 bug,改动这块代码前请先读一遍。

作用域:三个地方拿不到东西

宿主侧有三层作用域,引用错了会抛 ReferenceError, 而表现常常是「某个功能安静地不工作」,不报错。

位置能拿到拿不到
模块级函数只有模块级变量settings、scope、t
ctx.on('llm/stream', ...)(注册在 inject 之外)模块级变量scope、settings
ctx.inject([...], cb) 回调内全部—

规则

  • llm/stream 钩子里要用配置,走模块级的 currentSettings,不要写 settings。
  • 翻译函数 t 不能在模块级函数里用;模块级 label: () => ... 只能写死文案(写 () => t('x') 会让设置面板白屏)。

组件必须在模块级定义

Label / Section / Toggle 这类小组件如果定义在组件体内, 每次渲染它们都是新的函数 → React 认为组件类型变了 → 卸载并重建 → 设置面板里的输入框每敲一个字符就失焦。

规则

  • 任何被 React.createElement 当作类型用的组件,都定义在模块级。
  • 组件体内只放「每次渲染都该新建」的东西(style 对象、回调)。

引用稳定性:effect 的依赖必须稳定

useCallback 写成裸箭头函数时,每次渲染都是新引用, 依赖它的 effect 会反复重跑:连接刚建立就被关掉、无限重渲染、 点一下按钮就滚到顶部。

规则

  • 所有传给 useEffect 依赖数组的值都要 useCallback 包住,且依赖完整 (漏依赖 = 读到过期值)。
  • 翻译函数 t 必须 useCallback 包装。

静默失败:空 catch 会掩盖故障

catch {} 到处都是。改动时至少要区分「可忽略」和「要上报」。

规则

  • 面向用户的失败必须有可见反馈(提示条 / 状态文字),不能只写日志。
  • catch {} 里至少留一行注释说明为什么可以忽略。

状态复位:关掉什么就要清什么

规则

  • 任何「停止」类动作,都要把该动作涉及的所有状态一次清干净: 定时器、队列、标志位、草稿基准。
  • 定时器必须跟着状态一起清。漏清的后果是「关了麦草稿还自己发出去」。
  • 清状态的职责要单一:stopListening(聆听侧)、stopPlayback(朗读侧)、 stopAll(两者,仅卸载时用)。一个动作不要隐式包含多个不相关职责。

共享状态:过期快照不能写回

异步请求拿到的数据是发起请求那一刻的快照。

规则

  • 拿到配置后不要无条件写回共享存储(configStore.set(next))。 用户在这期间改过设置的话,旧快照会把他的修改覆盖掉。
  • 共享存储的权威来源只有一个(设置面板:加载时和保存时)。
  • 读配置时要合并,不要整份取舍: configRef = { ...宿主返回的, ...共享存储 } —— 存储里有值的键以存储为准(它是用户刚保存的),其余用宿主这份补齐。
  • 拉配置要能重试:只拉一次的话,宿主刚启动时那一次失败就再也补不回来。

回合边界:不要用启发式

「这是不是新的一轮对话」曾经用「会话里的用户消息条数变化」判断 —— 但本轮用户消息写入 session 的时机和 LLM 开始输出不一致。

规则

  • 判定新回合必须容忍「消息延迟写入」:同一回合内多次 llm/stream (工具调用链)不能重复触发打断。
  • 用 llm/stream 请求里的 options.messages 数用户消息条数, 不要读 session 里的。只认「条数增加」为新回合。
  • 这套回合判定抽在独立模块 lib/turn-model.js(createTurnModel 工厂)。
  • 每一轮对话有唯一的 id(turnId,递增序号),句子里带着它。 ⚠️ 轮次 id 不能用「本轮最后一条 user 消息的内容锚点」生成。
  • 回合事件只有 turn-begin / turn-end 两个,不要再加别名事件。
  • turn-end(协议 v3)表示真正的一轮结束;call-end 表示一次 llm/stream 调用结束。
  • 回合状态表不会无限增长:用空闲超时兜底 —— turnModel.prune() 每 60 秒 清掉超过 10 分钟没活动的会话的回合状态。

外部 GPT-SoVITS:只做转发,不内置、不克隆、不回退

GPT-SoVITS 是 PyTorch 推理体系(官方只提供 api_v2.py HTTP 服务),没有能在 Electron 里直接跑的 ONNX 管线。 所以插件把它当成外部 TTS 引擎:

  • 插件不管理模型/参考音频、不做声音克隆,只按官方 /tts 契约转发 text/text_lang/prompt_lang (以及用户配置的 ref_audio_path/prompt_text),拿回 wav;
  • 失败绝不回退 Edge/系统语音:用户选它就是要自己的声音与隐私,不能偷偷把文本发给微软、也不能换音色;
  • 宿主超时(SOVITS_TIMEOUT_MS=20s)必须小于客户端单句超时(25s),否则客户端先放弃、错误只剩「没声音」;
  • 失败计入 /diagnostics.sovitsTts(failures/lastError/lastAt)。

回归:test/sovits-tts.test.mjs(契约/URL 规范化/失败/超时/不回退 + 宿主接线 + 构建产物 UI)。

朗读失败不静默(可观测性)

「朗读没声」最怕无从排查。现在三种情况都留痕:

  • 宿主:/diagnostics 里 localTts / onlineTts / neuralTts 各带 failures / lastError / lastAt。
  • 客户端:synth 失败时 ttsFailuresRef 计数 + 节流(>4 秒)弹一次短词 「朗读失败」,完整说明走悬停。
  • 调试:浏览器控制台 __DSH_VOICE_DEBUG__() 能看到 ttsFailures。

朗读无声:共享连接坏掉后必须丢弃(最坑的一条)

在线(Edge)合成复用同一个 WebSocket 实例,换连接单句要贵 4~5 秒。 复用本身没错,错在什么时候该丢掉它。真实故障是「合成结束但没有收到音频数据」 —— 这也是连接坏了。这条原来不丢连接,坏连接被一直复用,之后每一句都返回空音频。

规则

  • 判断「该不该丢连接」必须走同一个判据 isBrokenConnection(),不要四处各写正则。
  • 它的正则必须包含「没有收到音频数据 / 超时」。只判 WebSocket 字面错误必然漏。
  • chunks.length === 0 那条分支要自己调 disposeTts() 再抛错,别指望外层 catch。
  • 回归测试 test/edge-tts.test.mjs(合并了原 tts-pending/reconnect/lock-hardtimeout):用假合成器模拟「坏连接返回空音频」, 断言第二次调用会重建实例并成功。
  • 改完要重启 DSH(lib/edge-tts.js 是宿主侧模块)。

朗读无声:播放上下文必须等恢复

浏览器自动播放策略会把新创建的 AudioContext 留在 suspended。 若在上下文恢复完之前就 source.start(0),音频会一直排队不响。

修复:play() 在 start 前 await resume(),并在首次用户手势(点击/按键)时 预热并恢复播放上下文。

等多久、超时怎么办:

  • 不能只等 1 秒就跳过。resume() 是异步的,实测要 1~3 秒。
  • 现在的做法:先 await playCtx.resume(),再轮询到 state === 'running', 最多等 8 秒。
  • 8 秒还挂起才算彻底失败,这时跳过这一句并 playSkippedRef += 1, 调试快照 __DSH_VOICE_DEBUG__().playSkipped 能查到。
  • 回归测试:test/client-behavior.test.mjs 用真实构建产物 lib/client.js(vm + 假 DOM/AudioContext) 驱动真实播放循环,覆盖 resume/gen 复查、ack 恰好一次、停止/换轮后不出声等行为; 仓库内原 read-aloud-resume.test.mjs 测的是复制出来的逻辑(已与真实实现漂移),已删除。

朗读队列:代次机制

speakGenRef 递增即作废所有旧音频。

规则

  • 停止朗读要三件事一起做:清队列 + 代次自增 + 停掉当前播放。 少一件就会出现「停了还在播」。
  • 停当前播放不能只靠「当前那一个取消句柄」:要登记所有还活着的音频源,停止时逐个 stop()。
  • SSE 是常驻的,所以「本轮不再朗读」必须有单独的标记(mutedTurnIdRef)。
  • 队列为空时不要把状态设成「朗读中」。

朗读切分:按段落收集,段内再按完整句子,代码块绝不能念

层级:

  1. 先按段落收集(splitParagraphs,lib/paragraph.js):空行 / 列表项 / 标题 / 引用为段落边界。段落是朗读的基本单元,段内句子连贯、不混入其它段落。
  2. 段落内按句末标点切句(splitLongText):保证完整句子,绝不在句子中间 / 词汇中间断开。

两条硬规则:

  1. 不要在句子中间切断。切分优先级(splitLongText):
    • 句末标点 。!?; → 完整句子;
    • 句内标点 ,、: → 半句,仅在远超上限(>2 倍)时才用;
    • 硬切(无任何标点)→ 停顿怪,但总比无限长好。
    • 略超上限但不到 2 倍 → 整句不切。
  2. 代码块一个字都不能念。围栏识别必须在流层面做,而且必须跨 chunk(与旧行为一致:任意位置;`~~~` 围栏按 Markdown 规范要求**行首**,避免正文里 3 个连续 `~` 把后文静默吞掉)(`lib/fence-filter.js`):模型按 token 流式输出,围栏标记 `` 可能正好被 chunk 边界切成两半("" + "js")。旧实现逐块 split('```')` 会在这种切法下状态错位:代码被念出来、围栏后的正文反而被当代码丢掉。

回归:test/read-aloud-fence.test.mjs、test/read-aloud-list.test.mjs。

当前策略速查(2026-11-01 定稿)

项值/行为
发句节奏客户端每开始播放一句回 /reading-ack,宿主补发;未确认上限 READ_ALOUD_LEAD=3
追赶预算readAloudLag,默认 7.5 秒(0 = 关闭追赶,全部朗读)
追上去哪跳过未开始的整段旧段落,保留最新 3 段(从最新第 3 段开始读)
生成很快时skim:正在读的段只读首句就跳走(判据 backlogMs 超预算且仍在生成)
计时口径lagMs = 正在播放的剩余时长 + 已发未确认 + 队列未发 + 当前段尾巴(含流水线提前量)
收尾pending(无 ack)+ 超预算:等 1.2s grace 再排空;flow:按 ack 逐句补发
兜底watchdog 10s 放行一条;SSE 重连清零旧未确认;客户端安全网按整段软上限裁剪
诊断/diagnostics 的 readAloud[]:lagMs/playingMs/backlogMs/budgetMs/skim/trimmed*/keepParagraphs/awaitingFlush

朗读发送节奏:客户端播放进度流控(不是固定时间窗口)

助手一次可能吐出一大段文本,而浏览器真正「念出来」受音频播放时长限制 (23 秒/句)。如果宿主只管按固定节奏猛发(历史上是 400ms/句 = 2.5 句/秒), 生产速度是消费速度的 58 倍 —— 客户端必然积压,最后只能丢内容, 表现为「朗读不按自然段落、总挑到段落中间」。所以节奏必须由播放进度驱动。

规则(唯一实现在 lib/read-aloud-scheduler.js):

  • 客户端播放进度驱动:宿主维护「已发未确认」句数,上限 READ_ALOUD_LEAD(3)。 客户端每开始播放一句就 POST /reading-ack 回一条,宿主才补发下一句。 于是朗读节奏 = 真实播放节奏:不积压、不跳段,和屏幕内容的差距恒定在几句以内。
  • 首句不排队:第一句无条件立刻发(实测 0ms),用户尽早听到声音。
  • 段落是朗读单元:一段的句子全部发出后才轮到下一段;短段落(≤60 字)整段作为一个朗读片段 (仍然是完整句子),超过单句上限才按句末标点切句。正常情况下绝不从段落中间开始念 (事件带 para / paraStart,客户端据此按整段裁剪);例外是下面的 「生成很快时只读首句」——此时正在读的段允许在首句后截断(用户明确要求)。
  • 老客户端兼容:收不到 ack 时自动降级为旧的 400ms 时间窗口节奏; pending(旧)模式下调用结束时同步发完剩余句子(不丢尾巴)。
  • 追赶最新(可调,默认 7.5 秒):估算「待朗读音频时长」= 正在播放的剩余时长 + 已发未确认 + 队列里没发的 + 当前段落剩余文本(按约 210ms/字估算,客户端会回传真实音频时长所以一般更准);超过设置项 readAloudLag 就跳到最新几段:保留「已开读的段」+「最新 3 段」,中间未开始发的完整段落整段跳过(当前段读完后,下一段是最新第 3 段,再顺序读到最新段 —— 既追上最新内容,又保留一点上下文)。
  • 生成很快时每段只读首句就跳:仍在生成中、且「还没发出去的内容」(backlogMs,不含正在播放/已发未播的流水线提前量)超过预算时,正在读的段落不再强制读完 —— 发出首句后就把该段剩余句子放弃,直接跳到下一段/最新段(任务要求:跳走时只需要读首句)。判断用 backlogMs 而不是 lagMs:生成慢时流水线提前量本身就可能超过预算,用 lagMs 会把正常朗读也截断。生成结束后(或 backlogMs 回到预算内)恢复正常完整朗读,剩余段落不会被读残。 跳过时只整段丢「还没开始发」的段;例外:生成很快、仍在流式输出时,正在读的段可以 只读首句就跳走(下方的 skim),不再强制整段读完;生成结束后的剩余段落完整读完。 0 = 关闭追赶(全部朗读,长回复会越读越滞后)。
  • watchdog 只在真发句 / 收到 ack 时重新计时:持续收到文本不算进度 —— 否则客户端不 ack 时 watchdog 永远不会触发(评审实测 2.2s 放行 0 次)。
  • 收尾 grace:pending(老客户端 / 首条 ack 未到)且积压超预算时,一次调用结束**先等 FLUSH_GRACE_MS(1.2s)**再同步排空;期间 ack 到了就切 flow 走追赶(否则"结束即排空"会让追赶在这条调用上完全失效)。老客户端没有 ack,grace 到点后照旧发完,不丢尾巴。
  • SSE 重连释放配额:页面刷新 / 断线重连时,旧连接遗留的「已发未确认」永远不会再被 ack,新连接建立即清零(否则新页面首句要等 watchdog 10 秒)。
  • watchdog:flow 模式下某句发出去 ACK_STALL_MS(10 秒)还没 ack (客户端掉线 / 播放被跳过;长句播放本身也可能十几秒,所以阈值不能太短), 放行一条,保证朗读不会永久卡死。

回归:test/read-aloud-scheduler.test.mjs(调度器纯逻辑:流控/追赶/保留 3 段/skim/watchdog/重连)、 test/read-aloud-catchup.test.mjs(真实钩子 + 真实 ack 的追赶端到端)、 test/reading-ack-contract.test.mjs(ack 契约/边界)、 test/read-aloud-catchup.test.mjs 的 e2(pending 收尾时限)、端到端 test/read-aloud-chain.test.mjs。

客户端安全网:按整段裁剪(不承担「追最新」职责)

「追赶最新」由宿主侧负责(readAloudLag,见上一节):积压超预算就整段跳旧段、 保留最新 3 段,生成很快时每段只读首句。 下面客户端的安全网只兜底「老宿主 / ack 丢失 / 重连」这类异常,正常流控下不会触发。

生成通常比朗读快。客户端待播队列积压太多时,听到的内容会离屏幕越来越远; 但绝不能丢「接下来马上要播的那几句」 —— 那会让播放位置向前跳、落点在段落 中间(用户听到的就是「不是按自然段落朗读,总挑到非首句」)。

规则

  • 只读当前这一轮的句子:句子的 turnId 与当前轮次不一致就丢掉。
  • 安全网裁剪(MAX_PENDING)按整段进行,且是软上限:超限时从最旧开始整段丢弃 (同一 para 的整组一起丢),并保护 ①正在播放的段落 ②本轮第一段 ③最新一段。 可丢的组都在受保护组之后时宁可不丢(允许暂时超过上限),绝不为了压上限丢掉最新内容。 于是播放永远是「完整段落」的向前推进,不会从段落中间开始、也不会丢「正在读那段的剩余句子」。
  • 不重复播放:客户端按 (turnId, index) 去重;宿主段落补全时按已发字符数 减前缀,绝不把已发句子再发一遍(旧实现用字符串 startsWith 且无兜底, 一旦不匹配就把整段重发 —— 那是「读完后面又回去读前面」的来源)。
  • 正常情况下流控已把队列压到几句,这条安全网只在异常(客户端卡顿 / ack 丢失)时生效。

朗读高亮覆盖层:绝不能画在弹框/输入框上面

朗读时会在页面上叠一层半透明色块,圈出「正在念的那一句」。它是 position: fixed 的覆盖层,z-index 不能取整型最大值:DSH 的弹框/遮罩是 z-index:1000 级别的浮层, 覆盖层取 2147483647 就会在设置弹框打开时画到弹框上面(用户反馈)。

规则

  • 覆盖层 z-index: 900:高于消息正文(正文没有 z-index),低于弹框/遮罩(1000+);
  • 第二道保险:检测到 [aria-modal="true"] / [role="dialog"] / [role="alertdialog"] 时直接隐藏覆盖层(data-voice-reading-hidden),并用 MutationObserver 监听 弹框挂载/卸载 —— 弹框关闭后自动恢复并重画;
  • 旧样式迁移:已注入的旧 <style>(z-index 2147483647)会在下次 set() 时被覆写, 页面不刷新也能生效。
  • 裁剪到会话滚动视口:消息滚出视口后,Range 的矩形仍然会报「原位置」 (父级 overflow 只是视觉裁掉),覆盖层会把高亮画到输入框/顶栏上面 (用户反馈:滚动到输入框下方时高亮显示在输入框上)。所以每个色块都与 「滚动视口 ∩ 窗口 ∩(底部输入区以上)」求交集,完全在视口外的不画; 底部输入区只认「下半屏、够宽」的 [data-slot*="composer"] / textarea / contenteditable, 避免把顶栏搜索框当底栏;

回归:test/highlight-overlay.test.mjs(对构建产物 lib/client.js 断言 z-index/隐藏/ 旧样式迁移/启动清理/视口裁剪)。

朗读管道不能永久卡死

pumpQueue 的消费循环 await 每句的合成 promise。若某句挂住,await 永不 resolve。

规则

  • 每句合成都要有超时兜底:await Promise.race([promise, sleep(...)])。
  • 播放上下文要在 try 内取。
  • object URL 必须在 finally 里释放。
  • 宿主 llm/stream 里 next() 同步抛错时必须复位 busy。

状态行只放短词

规则

  • 状态行只显示短词(「加载模型…」「朗读中」「已停止」)。
  • 完整说明放到 …Detail 文案里,鼠标悬停显示。
  • 状态行给一个 minWidth:换状态时宽度不变,界面不跳。

草稿:我们记的和用户看到的不一定一致

规则

  • 写新内容之前先调 syncUserEdit() 检测用户是否改过。
  • 「我们有没有写过」不能只看内容是否为空(首次写入会被误判)。

流式识别:门限不能一刀切

规则

  • 底噪只在静音时更新。
  • 进入说话态后,单帧偏低不能中断;连续若干帧都低才退出。

定时器:必须登记到登记表

规则

  • 源码里不允许出现裸的 window.setTimeout / window.setInterval。
  • 定时器按 group 登记(mic / send / ui / app / request),状态复位时按 group 一次清干净。
  • 短时等待统一用 sleep(ms)。

同一判据只能有一处实现

规则

  • 能共享就共享(宿主侧无限制,可以自由拆文件)。
  • 因浏览器模块不能 import 本地文件而必须写两份时, 两份要由同一份用例表(test/fixtures/)驱动,断言两端行为一致。
  • 配置清单的唯一真源在宿主:GET /config 返回生效配置。

下载:数据必须是真的

规则

  • 档位里声明的文件大小必须是实际大小,不能用估计值。
  • 文件名要对着仓库实际的名字写。
  • 进度用绝对字节语义,不要用增量语义。

原生库:类别用错会杀进程

规则

  • 流式 transducer 必须用 OnlineRecognizer + isReady/decode/inputFinished。
  • 离线模型才用 OfflineRecognizer。

唤醒词待机:常开麦的边界

v3 的唤醒词让全局语音可以「不操作」,代价是麦克风常开,边界必须写死:

规则

  • 待机(默认)时只认开头附近的唤醒词:matchWakeWord 先剥语气词/标点再做开头锚定, 不允许句中任意位置匹配。≥3 字才允许 1 个字容错(同音/口音);2 字(默认「小D」)必须精确匹配 —— 否则「小区门口」会被误唤醒成命令「门口」,还会吞掉命令首字(评审 P1-1);单字同样不容错。
  • 待机时非唤醒内容一律丢弃(不发送;最多每 WAKE_HINT_MS = 30s 提示一次),这是隐私边界。
  • 唤醒后进入活跃态,每句话直接发送并顺延 WAKE_ACTIVE_MS = 60s;到期自动回待机。
  • 唤醒词只对全局语音浮层生效;输入框麦克风完全不受影响。
  • 语音确认条优先于唤醒词:确认条挂着时说「允许/拒绝」直接回答确认,不需要先喊唤醒词。
  • 关麦 / 点浮层麦克风 / 快捷键关闭都会回到待机;再开麦也从待机开始(有唤醒词时)。

跨平台系统级热键:平台矩阵与可选依赖

系统级热键(窗口未聚焦也能按)分两条实现;应用内快捷键(窗口聚焦)三平台都可用:

平台实现依赖 / 授权已知限制
WindowsPowerShell 子进程 + C# RegisterHotKey → POST /hotkey(token)→ 宿主广播 SSE无额外依赖快捷键被占用 / 安全软件拦 PowerShell 时失败,原因进 /diagnostics.hotkeyHelper
macOS宿主进程内 uiohook-napi 监听 keydown → 直接广播 SSE可选依赖 uiohook-napi + 辅助功能授权(系统设置 → 隐私与安全性 → 辅助功能)未授权 → failed 原因可查;native 监听按配置值(默认 Control+Option+V),窗口内才额外接受 Cmd+Option+V 别名
Linux同上(uiohook-napi)可选依赖 uiohook-napi;需 X11 会话Wayland 下 libuiohook 通常拿不到全局按键,会降级并把原因写进 reason

规则

  • helper 只在 globalVoice === true && globalHotkeyHelper === true 时启动;关掉任一开关都会回收子进程/注销 hook。
  • uiohook-napi 是 optionalDependency:装不上不报错、不影响语音主链路,只在 /diagnostics.hotkeyHelper 记 failed/reason。
  • 快捷键解析:单字母/数字主键必须搭配 Ctrl/Alt/Meta(Shift+V 非法,会吞掉全局大写输入)。
  • macOS 的 Cmd+Option+V 只是窗口内默认组合的顺手别名;系统级 native 热键按配置值注册。
  • Windows 启动状态机:spawn 后先 starting;脚本在 RegisterHotKey 成功后向 stdout 打印 READY → enabled;stderr 输出 REGISTER_FAILED(快捷键被占用)、或在 READY 前退出 → failed + 原因。含 token 的临时 .ps1 在退出/失败/dispose 时删除(不再依赖下次 sync)。
  • 诊断里的 helper 状态形状:{enabled, status, reason, platform, tokenReady};status 会经历 idle/loading/starting/enabled/exited/failed/disabled,enabled=false 不等于没配置,先看 reason。

幽灵动作:expiresAt 截止时间

宿主 voice_ui 工具等待回执有超时(默认 20s)。ui-action 载荷带 expiresAt(与宿主超时同一个 deadline),客户端必须:

  • 收到时已过期 → 直接回执 {ok:false,error:'expired'},不弹确认、不执行(提示「动作已超时,未执行」)。
  • 确认条计时器 = min(30s, expiresAt-now-1s);用户点「允许」后、执行前再检查一次 deadline,过期同样回 expired。
  • 杜绝 ghost action:宿主已判失败(模型收到 ui_timeout)后,用户迟到的「允许」不能再真的发出消息/执行命令。
  • 卸载/热重载时宿主会立刻收尾所有在途动作(ui_timeout)并清定时器。

多窗口:单一执行者与请求去重

SSE ui-action 是全量广播:两个 DSH 窗口(或同应用两个标签页)都会收到同一个动作。不处理的话 send_prompt 会被执行两次。

规则

  • 执行前必须先 POST /ui-action-claim 认领:{requestId, clientId};第一个 client 得 claimed:true,其它窗口 claimed:false。
  • 认领失败 / 404 / 网络错误一律静默跳过(不回执、不执行):宁可这次不执行,也不能双执行;另一个窗口会回执,都不在则由宿主超时收尾。
  • clientId 存 sessionStorage(同标签页稳定、多标签页不同;无痕/被禁用时回落 localStorage/内存),只用于认领,不含敏感信息。
  • 请求去重:客户端每条识别文本生成一个 requestId,直发 session.prompt(parts, mode, undefined, requestId) 与回退 POST /voice-prompt 共用它;宿主据此幂等(重试/回退不会把同一条语音注入两遍)。
  • /voice-prompt 兜底只在 sessionController 缺失时启用;controller 抛错直接 502,不再 followup(防止主路径已入队后重复注入)。

通用 a11y 操作:快照 ref 是唯一入口

v2 的 ui_click / ui_type / ui_key 能操作任意可见元素,等价于用户亲手操作,安全边界必须写死:

规则

  • 只认快照下发的 ref:ref 只能来自最近一次 ui_snapshot 维护的 Map(ref → element); 过期/找不到一律回 stale_ref,让模型重新拍快照 —— 绝不接受 CSS 选择器、DOM 路径或坐标。
  • 新快照整体覆盖旧表并清掉旧 DOM 标记:不会出现「上一页的同名 ref 点到这一页」。
  • 三个原语默认危险(uiConfirm 开时弹确认);密码框(type=password)永远禁止代填(forbidden)。
  • ui_type 只写文本类目标:text/search/email/url/tel/number input、textarea、contenteditable(含 plaintext-only);checkbox/radio/file/range/color/date/hidden/... → unsupported(避免原生 setter 被吞掉却报成功),readonly → unsupported。
  • ui_click 拒绝外链导航与文件选择器:http(s)/mailto/file/tel/ftp/data/javascript/blob、协议相对 //、target=_blank、input[type=file] → unsupported;应用内锚点(#…)与相对路由允许 —— a11y 只操作应用内界面,点出去浮层会随页面消失而会话还在跑。
  • set_sidebar 读不到 [data-sidebar-collapsed] 时回 unknown_state,不盲切方向。
  • 语音确认只认整句「允许/拒绝」(去首尾标点);不是确认词就照常当聊天内容发送。

改动之后

  • npm test 必须全绿(48 个文件;先跑 bundle-sync 校验产物同步,再跑 client-behavior 行为回归)。
  • 需要连活插件的集成测试单独跑:npm run test:silence -- http://127.0.0.1:<端口>。
  • 需要跑「真加载模型」的慢集成(SenseVoice 预加载等):$env:DSH_VT_SLOW_TESTS=1; npm test。 默认套件走快速路径并显式打印 ⏭ 跳过,不会悄悄漏测。
  • 客户端行为回归在 test/client-behavior.test.mjs:直接加载 lib/client.js 产物, 用假 React/DOM/AudioContext 驱动真实播放循环,断言 ack 恰好一次 / 顺序 / 段落完整 / 停止与换轮后不出声。改完 src/client/ 并重建产物后一定要跑它(形状断言测不出时序缺陷)。
  • 改完 lib/index.js(宿主侧)要重启 DSH; 改完 lib/client.js(浏览器侧)要浏览器强制刷新。
  • 改完 src/client/pure/ 要跑 npm run build:client 重新生成 lib/client.js。 npm test 里的 bundle-sync.test.mjs 会检查产物是否同步。
  • 改了可选依赖(如 uiohook-napi)要跑 pnpm install 同步 pnpm-lock.yaml;node_modules 不入库,CI 按 lockfile 恢复;装好后重启一次 DSH 才会加载新的原生模块。
  • package.json 声明了 engines.dsh: ">=0.2.1-rc.5" 与 optional peer @deepseek-ai/dsh-llm(仅 agents.followup 兜底用,由 DSH 安装环境提供;peerDependenciesMeta.optional=true)。
  • 本仓库 pnpm-lock.yaml 用默认 autoInstallPeers=true 生成:锁文件会记录 dsh-llm 的依赖图,但本地 node_modules 不一定有它(--lockfile-only 更新后只改锁文件;测试用 setCreateUserMessageLoaderForTest 注入,不 require 真模块)。
  • 暂不添加 .npmrc auto-install-peers=false:那会改变现有 peer(如 @deepseek-ai/cordis)的自动安装行为并要求重算锁文件,属于独立改动;当前状态对运行时/测试无影响。如需收紧另开改动并在 CI(pnpm install --frozen-lockfile)验证。
  • 更新插件依赖/新增可选 peer 后跑 pnpm install --lockfile-only(或 pnpm install)保持锁文件同步。

配置项的配合关系(生效条件矩阵)

配置统一走 GET/POST /config:GET 返回已归一化的完整配置;POST 是 patch(只接受已声明键)。 加载时非法/越界值回退默认并记入 /diagnostics.configWarnings;保存时非法值返回 400 {ok:false, errors},不会静默改值。

配置生效条件 / 配合
readAloud朗读总开关。关闭时 /tts 不会被调用;但朗读相关设置仍可编辑/保存
ttsEngineonline→voice/pitch/volume/ttsFormat;neural→neuralModel/neuralSpeaker;local→localVoice;sovits→sovits*(rate 转 speed_factor)。非法值保存被拒,运行时不静默落 online
rate四种引擎都生效(online/local 百分比、neural 倍率、sovits speed_factor 0.5~2)
pitch / volume / ttsFormat仅特定引擎有效:pitch/format 仅 online,volume 仅 online/local
readAloudLag只在 flow(客户端 ack)模式生效;0=关闭追赶;非有限值回退默认(7.5)
language只对 SenseVoice 生效;Whisper/zipformer2 忽略(UI 按模型禁用/提示)
liveAsrstream 固定依赖 zipformer2-zh(与 model 无关);不可用时提示并回退整段
model必须是已声明档位;非法值保存被拒,运行时不再静默回退 SenseVoice
autoSend关闭时 autoSendExtraMs 隐藏;mode=once 的「说完退出」只在自动发送后发生,因此会禁用并说明
preloadModels × idleReleaseMinutes预加载只覆盖 model/neural(不覆盖 liveAsr 的 zipformer2);idle 释放不释放当前选中模型;0=永不释放
sovits*只对 ttsEngine=sovits 生效;sovitsRefAudio 是服务端机器可访问路径(官方 api_v2 每请求必填),留空则不带该字段由服务端决定
words热词仅 zipformer2(transducer)生效;按行保存,保存时 trim/去空
globalVoice全局语音浮层总开关。关 = 浮层不渲染、不建 SSE 连接、界面动作执行器不存在
globalShortcut窗口内全局语音快捷键(helper 也用它注册系统级热键);格式规则同 shortcut(至少一个修饰键;单字母需 Ctrl/Alt/Meta)
voicePromptMode语音文本注入方式:queue(下一轮)/ steer(当前轮下一步)
uiControlAI 界面操控总开关(默认关)。需要 globalVoice=true 才有执行器;关闭时 voice_ui 返回 ui_control_disabled。v2 共 14 个动作(8 个语义 + 6 个 a11y 原语),新增界面功能不再需要改协议
uiConfirm危险动作确认(默认开):切会话/新建/命令/发消息/停止生成 + a11y 点击/输入/按键。只读动作不受影响;用户点「拒绝」或说「拒绝」回执 confirm_denied(说「允许」等同点允许)
globalHotkeyHelper拉起跨平台系统级热键(默认关):Windows 内置 PowerShell helper;macOS/Linux 需可选依赖 uiohook-napi(macOS 授权辅助功能、Linux 需 X11)。还要求 globalVoice=true(没有浮层消费事件时不启动)。失败不影响窗口内快捷键,原因进 /diagnostics.hotkeyHelper(enabled/status/reason)
wakeWord全局语音待机唤醒词(默认「小D」;空串 = 关闭)。只对浮层生效;待机时非唤醒内容不发送;活跃 60s 无语音回待机(客户端常量 WAKE_ACTIVE_MS / WAKE_HINT_MS)

CI 与自动发布(GitHub Actions)

仓库自带两个 workflow(.github/workflows/),都走 pnpm:

  • ci.yml:每次 push / PR 到 main 自动跑 check:client + npm test。 改完代码提交后,GitHub 上的红绿状态就是最直接的回归信号。
  • npm-publish.yml:GitHub 上发布一个 Release(release 事件 published)时, 先跑一遍同样的测试,全绿后自动 npm publish --access public。

发布到 npm 前要做的两件事:

  1. 在 GitHub 仓库设置里加一个 NPM_TOKEN secret(值来自 npm 官网的 Access Token, 建议用 publish 权限的 token,不要用自动生成的 npm_token)。
  2. 在 GitHub 上创建一个 Release(vX.Y.Z),tag 就打在要发布的提交上。

发布成功与否看 workflow 的 publish job;测试失败时不会发出版本,修复后重新打 Release 即可。

版本号以 package.json 的 version 为准,tag 只用来触发工作流。


10. 许可

MIT

Comments

Loading…

Similar plugins

voice-plugin

by dusbin

Dsh(deepseek harness)语音输入插件 Ps: 朗读功能目前还不是很棒。

Manifest valid

★ 0

JavaScript

Aug 27, 2026

dsh plugin --profile web add dsh-voice

by wangcd1313

DSH(DeepSeek Harness)回答语音朗读插件:把回复念出来,切窗口/切会话立即停止,设置页可调

Manifest valid

★ 0

MIT

JavaScript

Oct 20, 2026

dsh plugin --profile web add dsh-plugin-speak

by yfy12306

DSH 语音输入插件:输入框麦克风按钮,录音经同源路由转发 ASR 识别,文本插入草稿可编辑后发送

Manifest valid

★ 0

MIT

JavaScript

Oct 30, 2026

dsh plugin --profile web add dsh-voice-input

by NewDaNew

DeepSeek Harness 语音输入插件:在 Web GUI 输入框添加麦克风按钮,基于浏览器 Web Speech API 的语音转文字输入,支持可选自动发送。

Tools & CapabilitiesVision & MultimodalManifest valid

★ 0

MIT

JavaScript

Aug 15, 2026

dsh plugin --profile web add @local/dsh-plugin-voice-input

by forrestahha

Voice-to-text input plugin for the DeepSeek Harness Web UI

Development & InfrastructureTerminal & ClientsManifest valid

★ 2

↓ 63/wk

MIT

TypeScript

Aug 14, 2026

dsh plugin --profile web add dsh-voice-input

by dytom18

说话即任务:给 DSH Web GUI 加语音听写,说完自动发送给 Agent(纯前端,无需 API Key)

Development & InfrastructureTerminal & ClientsManifest valid

★ 0

MIT

JavaScript

Sep 18, 2026

dsh plugin --profile web add dsh-plugin-voice-dictation