前言¶
DeepSeek Harness(DSH)采用「一切皆插件」的理念。开发者在对话与多轮问答场景中常遇到输入框切换导致的语音输入中断:内置语音插件把麦克风挂载在 conversation.input.activity 槽位,但在 ask_user_question 提问卡片弹出时会接管整个输入栏(conversation.composer 被顶替),麦克风按钮随之消失,主观作答框无法语音输入。
dsh-voice-hotkey 避开了槽位冲突。它不占用任何 DSH 槽位,直接在 window 上以捕获阶段监听快捷键:按下时开始录音,松开时触发本地识别,并将文字插入当前获得焦点的输入框。无论是对话输入栏、提问卡片的作答框,还是任何 input / textarea / contenteditable,只要光标在那里,文字就会插到那里。
是什么¶
dsh-voice-hotkey 是 DSH 的 Host + Client 双半插件。按住 F9 说话,松开后自动用本地 SenseVoice 识别,并把结果插入当前焦点输入框。插件不依赖任何槽位,也不接管页面交互,仅负责捕获快捷键、录音、识别与插入三个环节。
维护者为 Tony250-officeteam,分类为模型推理。
核心能力¶
- 快捷键触发:按住
F9开始录音,松开自动触发识别与插入。 - 本地识别:使用 DSH 内置语音 Bundle 提供的 SenseVoiceSmall INT8 模型(约 230 MB),需已下载到
$DSH_HOME/speech-to-text。 - 目标输入框:直接读取
document.activeElement,支持对话输入栏、提问作答框、以及任意input/textarea/contenteditable。 - 状态浮标:顶部胶囊显示录音中(红点+波形)、识别中(黄点脉冲)、成功(绿点+文字,约 1.5 秒后消失)、出错(红点,约 4 秒后消失且可关闭)。
- 错误提示:录音太短、识别为空、麦克风权限被拒等情况均有明确文案。
安装与启用¶
前置条件:
- DSH 内置语音 Bundle
@deepseek-ai/dsh-experimental-voice-input-bundle需在 profile 的dsh.profile.bundles中; - 识别模型 SenseVoiceSmall INT8(约 230 MB)需已下载到
$DSH_HOME/speech-to-text(首次启用语音 Bundle 时自动下载)。
安装命令:
# 从 GitHub 安装
dsh plugin --profile desktop add github:<your-user>/dsh-voice-hotkey
# 或从本地目录
dsh plugin --profile desktop add link:/path/to/dsh-voice-hotkey
安装后 Host 半侧随 profile 的 patchReload: live 即时生效;浏览器半侧需要刷新一次页面才加载。
配置¶
DSH 的 loader 只给客户端插件传 { name: id },拿不到 profile 里的 config。因此配置从 localStorage['dsh-voice-hotkey'] 读取:
localStorage.setItem('dsh-voice-hotkey', JSON.stringify({
key: 'F9', // KeyboardEvent.code
modifiers: [], // 例如 ['ctrl','shift']
holdToTalk: true, // false = 按一下开始、再按一下结束
maxSeconds: 120,
showIndicator: true
}))
缺省值即上表。若 F9 被其它软件全局占用,可改 key(使用 event.code,如 F8、KeyG)。
HTTP 路由(Host 半侧)¶
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/dsh-voice-hotkey/status |
识别器状态,诊断用 |
| POST | /api/dsh-voice-hotkey/transcribe |
body {audioBase64, providerId?, language?} → {ok, text} |
两条路由都只接受回环地址(req.socket.remoteAddress 必须是 127/8 或 ::1,含 ::ffff: 映射形式,且 Host 头也须解析为 localhost/127.x/[::1]),否则 403。
/transcribe 在把音频交给识别器之前先做结构校验:解析 RIFF/WAVE 块,要求 fmt(PCM / 16 bit / 16 kHz / 单声道)、非空 data、恰好 44 字节头的规范形态,并算时长;不符合返回明确的 400/413。上限 6 MB body / 120 秒。
识别服务用 ctx.get('speechToText') 惰性获取:语音 Bundle 被禁用时插件仍能正常加载,只是 /status 报服务不可用、/transcribe 返 503。
实现中的坑位与处理¶
-
WAV 必须是 44 字节头的规范形态。
Windows TTS 产出的 WAV 是 18 字节 fmt 块,而 provider 的validateWave要求恰好 16,会直接拒绝。插件在浏览器侧用OfflineAudioContext(1, frames, 16000)重采样成 16 kHz 单声道后,手工编码成 44 字节头 PCM16 WAV。 -
Lexical 的
execCommand是异步提交。
对话输入框是 Lexical。execCommand('insertText', false, text)会派发beforeinput,Lexical 拦下并preventDefault(),然后在微任务里自己提交 DOM 更新。于是:
-execCommand(...)返回true
-element.textContent同步看「没变」
- 一个 tick 后出现了这段文字
若按「同步检查没变 → 判定失败 → 回退手动插入」,编辑器自己的提交和回退插入会同时落地,同一句话被插两遍。正确策略是「只要编辑器动过就绝不再插」:只调用一次 execCommand → 只要 textContent 有任何变化(同步或异步都算)就立刻收手 → 给编辑器 3 个宏任务的宽限后仍无变化,才补一次纯 DOM 插入(且刻意不派发 input 事件)。判定用「有没有变化」而不是「有没有包含这段文字」—— 编辑器可能对文字做归一化(合并/拆分文本节点),用「包含」判定会误判失败而重复插入。
- 配置只能走
localStorage,因为 loader 不给客户端插件传 config。
插入目标优先级与行为¶
- 插入目标优先级:按下快捷键那一刻的
document.activeElement(若仍 connected)→ 当前document.activeElement→ 提问作答框[data-question-key] textarea→ 对话输入框[data-composer-input]。 <input>/<textarea>用原型上的原生 value setter 写入(React 受控组件才会同步),再setSelectionRange定位光标,最后派发input事件。- 输入法组合中(
isComposing或keyCode === 229)不抢键;event.repeat忽略。 - 窗口
blur、document.hidden、按 Escape 一律取消录音,避免麦克风一直开着。
诊断¶
插件每走一步都会把状态写进 localStorage['dsh-voice-hotkey-probe'],出问题时先看它:
JSON.parse(localStorage.getItem('dsh-voice-hotkey-probe'))
// { phase: 'inserted', extra: '今天天气不错', at: '...', href: '...' }
常见 phase:listeners-installed → recording-start → stop-requested → transcribing → wav-encoded → inserted。若停在 failed / getusermedia-failed,extra 里是原因。
刻意不做¶
自动发送、常开麦克风、唤醒词、流式字幕。快捷键在页面失焦时不生效(浏览器收不到键盘事件)。
适用场景与注意¶
适合在 DSH 环境中需要跨输入框、跨槽位进行语音输入的开发者与使用者。
插件以当前 DSH 进程权限运行,安装前应检查源码与许可证。
小结¶
dsh-voice-hotkey 通过快捷键 + 本地识别的方式,解决了 DSH 内置语音插件在提问卡片等场景下无法使用的问题,把语音输入能力从固定槽位解放出来,插入到任何获得焦点的输入框中。
- 目录页:https://www.skillhub.cn/plugins/Tony250-officeteam/dsh-voice-hotkey
- GitHub:https://github.com/Tony250-officeteam/dsh-voice-hotkey