dsh-voice-typing
Manifest valid语音输入与对话朗读插件
dsh-voice-typing
DeepSeek Harness 的语音输入与对话朗读插件。
点一下麦克风按钮(或按 Alt+V)就能进入语音模式:你说话,文字自动打进输入框;
助手的回复可以自动朗读出来。全程本地离线识别,不需要 API Key、断网也能用。
本 README 面向使用者,按「从零到会用」的顺序一步步引导。每一步都告诉你 做什么、为什么、以及做完会看到什么。维护者须知与技术参考见文末。
目录
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 走现有播放管线。
- 在 GPT-SoVITS 目录启动 API 服务:
python api_v2.py -a 127.0.0.1 -p 9880 -c GPT_SoVITS/configs/tts_infer.yaml - 设置 → 语音引擎选「GPT-SoVITS 外部服务」,填:
- 服务地址(默认
http://127.0.0.1:9880,填根地址或/tts都行); - 服务端参考音频路径(官方 api_v2 每个请求都要求它,必须是服务端机器上可访问的 wav);
- 参考音频文本 / 语言(可选;与参考音频逐字匹配时音质更好);
- 合成语言(zh / en / ja / ko / yue)。
- 服务地址(默认
- 模型/音色/参考音频都由 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 tiny | 99MB | ★ | 最小 | 先跑通 |
| Whisper base | 153MB | ★★ | 轻量折中 | 中文可用 |
| Whisper small | 358MB | ★★★ | 精度明显优于 base | 均衡 |
| Whisper medium | 902MB | ★★★★ | 更准 | 重精度 |
| Whisper large-v3-turbo | 989MB | ★★★★☆ | 接近 large-v3、体积小一半 | 综合最优 |
| Whisper large-v3 | 1.7GB | ★★★★★ | 精度天花板 | 重精度、不怕慢 |
怎么选:默认 SenseVoice 就行。它零下载、中文强、带标点。 想要实时字更快选 Zipformer2;想要更准选 Whisper 大档(代价是体积和耗时)。
6.2 合成(TTS)模型
| 档位 | 大小 | 特点 |
|---|---|---|
| MeloTTS 中英 | 170MB | 多说话人、本地离线、音质接近在线 |
6.3 怎么下载
第 1 步 — 打开「设置 → 语音输入 → 模型管理」。
第 2 步 — 在「识别模型」或「合成模型」列表里,找到你要的档位,点 下载。
第 3 步 — 看进度。下载时显示百分比;完成后出现 ✓ 和「已就绪」。
第 4 步 — 回到「语音识别」或「语音合成」,把「识别模型 / 神经模型」切到你刚下载的档位。
下载失败? 确认网络能访问模型源(国内走
hf-mirror.com镜像)。 进度卡在 100% 不动通常是校验失败,重试一次或换档位。
7. 自定义词表(修同音字与专名)
词表里两种写法,作用完全不同:
| 写法 | 作用 | 示例 |
|---|---|---|
正确=错误 | 替换:识别出「错误」时改写成「正确」 | 深度求索=深度球搜 |
只写 正确 | 热词提示:不改文本,只给识别器做偏置 | 专业术语 |
怎么用:
- 打开「设置 → 语音输入 → 交互与后处理」。
- 在「自定义词表」里一行一条填写。
- 保存。
注意事项:
- 中文精确匹配;英文忽略大小写并保持原词形态。
- 热词提示只有 Zipformer2(transducer)档位支持。用 SenseVoice 时,只写「正确」的条目不会有任何效果(想要替换效果请用
=写法)。
8. 故障排查
按症状找对策。
8.1 看不到麦克风按钮
- 确认插件已安装:
dsh plugin --profile desktop list里有没有dsh-voice-typing。 - 确认已在「设置 → 插件」的
dsh.profile.bundles里启用。 - 重启 DSH(插件加载需要重启,不是刷新页面)。
8.2 点麦克风没反应 / 显示「麦克风被阻止」
- 检查浏览器/系统是否给了麦克风权限(地址栏权限图标)。
- 确认没有别的程序(会议软件等)占着麦克风。
- 重试;若仍不行,重启 DSH。
8.3 能说话,但文字没出来 / 显示「未听到声音」
- 确认麦克风没静音、音量够大。
- 靠近麦克风,清晰地说一句。
- 若提示「模型加载中…」,等首次加载完成。
- 检查「识别模型」是否已就绪(见 6.3)。
8.4 能识别,但不对 / 同音字错
8.5 朗读没声音
按顺序排查(最常见的是「在线引擎需联网」):
- 确认设置里「朗读回复」已勾选。
- 确认「语音引擎」:
Edge 在线需要联网(断网请换本地引擎)。本地系统语音在当前平台需支持(Windows SAPI / macOS say / Linux espeak-ng)。本地神经模型需要已下载该档位模型(见 6.3)。
- 打开浏览器控制台,输入
__DSH_VOICE_DEBUG__()看ttsFailures。 - 若显示「朗读失败」,看 诊断接口里的
onlineTts/localTts/neuralTts/sovitsTts的lastError。 - 若本地引擎失败但显示「朗读中」却没声,多半是自动回退到在线但没联网。
8.6 朗读不完整 / 句子被切碎 / 把代码念出来
这些都是有意设计的防御,不是 bug:
- 代码块(
```)一个字都不会念 —— 这是刻意的,防止把代码念出来。 - 列表编号不会当句子念 ——
1. 第一步…只念「第一步…」,不念「一」。 - 长句不会被拦腰切断 —— 尽量切在句末标点(
。!?;)。 - 按段落朗读 —— 先按空行 / 列表项 / 标题 / 引用把回复分成段落,段落内再按句末标点分句。一段话从头读到尾才进入下一段,绝不从段落中间开始念。
- 朗读完整、不跳段 —— 宿主按客户端的真实播放进度逐句补发(客户端每开始播一句回一条 ack),生成再快也不会积压、不会丢掉「接下来要播的句子」。
8.7 快捷键不生效
- 确认设置里「语音模式快捷键」的格式正确(如
Alt+V)。 - 别与浏览器/系统的快捷键冲突(
Ctrl+Space、Ctrl+Shift+V等)。 - 确认主键不是单个字母。
8.8 全局语音 / 系统热键不生效
- 确认「设置 → 语音交互 → 全局语音浮层」已打开并保存(关着时浮层根本不渲染)。
- 窗口内快捷键不响应:确认「全局语音快捷键」格式正确(默认
Ctrl+Alt+V),别和系统/浏览器快捷键冲突;设置面板正在录制快捷键时不会触发。 - 窗口未聚焦 / 最小化也要能按:打开「高级与诊断 → 系统级全局热键 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。 - 说话没反应:看
/diagnostics.voicePrompt的count/failures/lastError(客户端直发会话失败时才会走这条回退注入);浮层本身也会显示「未找到当前会话 / 发送失败」。 - 窗口隐藏/最小化时识别不稳定是已知限制:渲染器定时器会被 Chromium 节流,优先的 AudioWorklet VAD 已缓解,但它不是系统级后台服务。
- 唤醒词待机:浮层显示「待机 · 说『小D』唤醒」时,必须先说唤醒词再下命令;待机时的环境闲聊会被静默丢弃(每 30 秒最多提示一次)。唤醒词留空 = 关闭待机(说话直接发送);留空后若仍显示待机,重启一次 DSH 让新配置键生效。
- 可选依赖
uiohook-napi装不上(无预编译二进制 / 无编译环境):系统级热键在 macOS/Linux 上不可用,但应用内快捷键、语音、朗读全部不受影响;/diagnostics.hotkeyHelper.reason会写明原因。
8.9 AI 不执行界面动作(uiControl)
- 「设置 → 高级与诊断 → 允许 AI 操控界面」默认关,先打开并保存;同时要打开「全局语音浮层」 —— 动作执行器和确认条都住在浮层里,只开
uiControl时工具返回voice_overlay_disabled。 - 危险动作(切会话/新建/执行命令/发消息/停止生成,以及 a11y 的点击/输入/按键)会弹确认条;没点「允许」就不会执行,工具收到
confirm_denied。 - 语音确认:确认条挂着时,直接说「允许/确认/可以」或「拒绝/取消/不要」等同点按钮;这句只用于回答确认,不会被当成聊天消息发进会话。
- 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 只操作应用内界面。 - 改了工具 schema 不生效:v2 新增了
ref/key/collapsed参数,宿主工具定义不会热更新 —— 升级后重启一次 DSH 才生效;客户端刷新即可。 - 排查:
GET /dsh-voice-typing/diagnostics的uiActions(最近 50 条:动作、ok、error,不含参数正文);确认条与浮层状态在右下角浮层里。 - 只让模型「看看有哪些会话」不需要浮层(
list_sessions由宿主直接回答)。 - 多窗口:每个动作执行前,窗口会先向宿主认领(
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-claim | UI 动作认领(渲染器调用;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 /stream | SSE: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-action | requestId、sessionId、actions:[{action, params}]、confirm、expiresAt? | 宿主 voice_ui 工具下发的界面动作;confirm 是宿主算好的生效值(只读动作永远 false)。expiresAt(epoch ms,可选)与宿主工具超时同一个 deadline:渲染器据此拒绝迟到的确认(回执 expired,不执行)。渲染器执行后必须 POST /ui-action-result 回执 |
global-hotkey | action(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/stream | SSE: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.js | UI 动作协议(宿主纯函数):动作枚举、每动作参数白名单/长度上限、确认策略、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.js | Edge 在线朗读(共享连接 + 串行锁) |
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.js | v2 通用 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)。 - 队列为空时不要把状态设成「朗读中」。
朗读切分:按段落收集,段内再按完整句子,代码块绝不能念
层级:
- 先按段落收集(splitParagraphs,lib/paragraph.js):空行 / 列表项 / 标题 / 引用为段落边界。段落是朗读的基本单元,段内句子连贯、不混入其它段落。
- 段落内按句末标点切句(splitLongText):保证完整句子,绝不在句子中间 / 词汇中间断开。
两条硬规则:
- 不要在句子中间切断。切分优先级(
splitLongText):- 句末标点
。!?;→ 完整句子; - 句内标点
,、:→ 半句,仅在远超上限(>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;到期自动回待机。 - 唤醒词只对全局语音浮层生效;输入框麦克风完全不受影响。
- 语音确认条优先于唤醒词:确认条挂着时说「允许/拒绝」直接回答确认,不需要先喊唤醒词。
- 关麦 / 点浮层麦克风 / 快捷键关闭都会回到待机;再开麦也从待机开始(有唤醒词时)。
跨平台系统级热键:平台矩阵与可选依赖
系统级热键(窗口未聚焦也能按)分两条实现;应用内快捷键(窗口聚焦)三平台都可用:
| 平台 | 实现 | 依赖 / 授权 | 已知限制 |
|---|---|---|---|
| Windows | PowerShell 子进程 + 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/numberinput、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 不会被调用;但朗读相关设置仍可编辑/保存 |
ttsEngine | online→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 按模型禁用/提示) |
liveAsr | stream 固定依赖 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(当前轮下一步) |
uiControl | AI 界面操控总开关(默认关)。需要 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 前要做的两件事:
- 在 GitHub 仓库设置里加一个
NPM_TOKENsecret(值来自 npm 官网的 Access Token, 建议用publish权限的 token,不要用自动生成的npm_token)。 - 在 GitHub 上创建一个 Release(
vX.Y.Z),tag 就打在要发布的提交上。
发布成功与否看 workflow 的 publish job;测试失败时不会发出版本,修复后重新打 Release 即可。
版本号以
package.json的version为准,tag 只用来触发工作流。
10. 许可
MIT
Comments
Loading…
Similar plugins
by dusbin
Dsh(deepseek harness)语音输入插件 Ps: 朗读功能目前还不是很棒。
★ 0
JavaScript
Aug 27, 2026
dsh plugin --profile web add dsh-voiceby wangcd1313
DSH(DeepSeek Harness)回答语音朗读插件:把回复念出来,切窗口/切会话立即停止,设置页可调
★ 0
MIT
JavaScript
Oct 20, 2026
dsh plugin --profile web add dsh-plugin-speakby yfy12306
DSH 语音输入插件:输入框麦克风按钮,录音经同源路由转发 ASR 识别,文本插入草稿可编辑后发送
★ 0
MIT
JavaScript
Oct 30, 2026
dsh plugin --profile web add dsh-voice-inputby NewDaNew
DeepSeek Harness 语音输入插件:在 Web GUI 输入框添加麦克风按钮,基于浏览器 Web Speech API 的语音转文字输入,支持可选自动发送。
★ 0
MIT
JavaScript
Aug 15, 2026
dsh plugin --profile web add @local/dsh-plugin-voice-inputby forrestahha
Voice-to-text input plugin for the DeepSeek Harness Web UI
★ 2
↓ 63/wk
MIT
TypeScript
Aug 14, 2026
dsh plugin --profile web add dsh-voice-inputby dytom18
说话即任务:给 DSH Web GUI 加语音听写,说完自动发送给 Agent(纯前端,无需 API Key)
★ 0
MIT
JavaScript
Sep 18, 2026
dsh plugin --profile web add dsh-plugin-voice-dictation