voice-plugin
Dsh(deepseek harness)语音输入插件 Ps: 朗读功能目前还不是很棒。
- Stars
- 0
- Language
- JavaScript
- Created
- Aug 27, 2026
- Updated
- Aug 27, 2026
Introduction
dsh-voice — DeepSeek Harness 语音输入输出插件
给 DeepSeek Harness Web 界面(dsh web)添加语音能力,纯浏览器实现,零后端、零 API key。
当前版本:v0.1.12(变更记录见 CHANGELOG.md)
功能一览
- 语音输入(模式切换) — 输入框工具行右侧(发送按钮前)有麦克风按钮 🎤:
- 点击进入「语音模式」(图标变深色并出现绿色边框闪动)→ 长按空格开始听写(Web Speech Recognition)、松开结束,识别文本自动填入输入框(可配置
autoSend直接发送); - 再点按钮退回「文本输入」模式(图标恢复浅色弱化态),键盘完全不受干预。
- 点击进入「语音模式」(图标变深色并出现绿色边框闪动)→ 长按空格开始听写(Web Speech Recognition)、松开结束,识别文本自动填入输入框(可配置
- 语音模式下回车直接发送 — 回车不屏蔽:直接发送输入框内容;录音中按回车 = 结束录音并立即发送已识别内容;焦点在输入框时仍走原生发送,不重复提交。
- 文章朗读 — 每条助手消息的操作条上多一个「🔊 朗读」按钮,用浏览器 speechSynthesis 朗读该消息正文(跳过思考过程与工具输出),音色自动优选,符号自动清洗。
本目录是插件的唯一可信源(source of truth),已用 git 管理。
web profile 通过 cordis.patch.yml 直接加载这里的包,改动被 commit 后,
下一次启动(或 patch 热更新)即生效。
文件结构
voice-plugin/ # ← git 仓库根
├── package.json # dsh.client 声明(platform: web → 浏览器端 bundle)
├── lib/
│ ├── index.js # Node 半身:空插件(让 loader 能创建 fiber)
│ └── client.js # 浏览器端 bundle(全部功能所在,手写、零构建)
├── test/
│ └── smoke.test.mjs # 冒烟测试:模拟模块表 + 模式切换/听写/回车/朗读全流程
├── README.md
└── CHANGELOG.md
安装(一次性接线)
本机已完成后两步,无需重复执行;重装或迁移时按此操作。
-
把插件软链进 web profile 的
node_modules(使dsh-voice可被 Node 与 client-modules 从 profile 目录解析):mkdir -p ~/.dsh/profiles/web/node_modules ln -sfn /Users/robinddu/Desktop/workspace/robinddu/voice-plugin \ ~/.dsh/profiles/web/node_modules/dsh-voice -
在
~/.dsh/profiles/web/cordis.patch.yml追加:# dsh-voice: 语音输入输出插件(浏览器端) - insert: - id: voice name: 'dsh-voice' config: holdMs: 250 # 语音模式下长按空格判定阈值(毫秒) sttLang: 'zh-CN' # 语音识别语言 ttsLang: 'zh-CN' # 朗读语言 ttsRate: 1 # 朗读语速 autoSend: false # true = 识别结束直接发送;false = 只填入输入框 -
重启
dsh web(或依赖 patch 热更新自动加载),浏览器硬刷新(Cmd+Shift+R)。 首次使用语音输入时浏览器会请求麦克风权限,请允许。
为什么是
dsh-voice(包名)而不是绝对路径?client-modules 会用条目名 解析<name>/package.json并据此扫描dsh.client声明、服务/plugins/<id>/client.js; 而 loader 的 Node 侧 import 也从 profile 目录解析包名。软链把这两条解析 路径都指向本仓库,一次接线、两处生效。与 scratch-plugin 不同,这里不需要 node_modules 软链到 harness: 浏览器端 bundle 只
require('react')(平台种子模块),Node 半身零依赖。
使用
语音输入(模式切换)
- 点击输入框工具行右侧的 🎤 按钮进入「语音模式」:图标变为深色并出现 绿色边框闪动(文本输入时为浅色弱化态);识别器立即常驻启动并预热, 因此按下空格的同时开口说话也不会漏字(浏览器识别预热在进入语音模式时已完成)。
- 按住空格:按住超过
holdMs(默认 250ms)确认长按,提示「请开始说话:松开空格结束」。 - 说话,胶囊实时显示本次片段的转写(之前的输入内容不会残留显示);
松开空格结束:只提交按下之后说出的内容(按压前的讲话不会混入),
填入当前会话输入框(
autoSend: true则直接发送)。 多次听写会追加到已有内容(空格分隔),不会清掉前一次输入或手动输入的文字。 - 再点 🎤 退出语音模式(识别器停止),恢复正常文本输入。
细节:
- 短按空格(未超过
holdMs)会放弃本次片段,不提交、不影响输入框。 - 语音模式下短按空格仍是普通空格,正常打字不受影响。
- 未开启语音模式时,插件完全不干预键盘输入。
回车直接发送(语音模式下)
- 非录音时按 回车 → 直接发送输入框内容(语音刚填入的文字不用再点发送按钮)。
- 录音中按回车 → 结束录音并立即发送已识别内容。
- 焦点在输入框内时回车走输入框原生发送,不会重复提交。
- 带修饰键的回车(Cmd/Ctrl/Alt+Enter)不拦截。
文章朗读
- 在任意助手消息的操作条(复制/分支那一行)点 🔊 朗读;再点一次 ⏹ 停止。
- 切换朗读另一条消息会自动停止上一条;Chrome 15 秒停顿 bug 已用 keep-alive 规避。
- 朗读只读正文 text 块:跳过思考过程、工具输出;朗读前会清洗 Markdown 与装饰符号。
配置项
| 配置 | 默认 | 说明 |
|---|---|---|
holdMs | 250 | 语音模式下,按住空格多久判定为语音触发(短按仍是普通空格) |
sttLang | zh-CN | 语音识别语言(BCP-47,如 en-US) |
ttsLang | zh-CN | 朗读语言(BCP-47) |
ttsRate | 1 | 朗读语速(0.1–10) |
ttsVoice | '' | 手动指定朗读音色名(留空 = 自动挑选自然音色) |
autoSend | false | 识别结束后是否直接发送 |
修改 cordis.patch.yml 的 voice.config 后保存即热生效(刷新页面生效)。
朗读音色与清洗
- 自动挑选音色:按「语言匹配 > 名称含自然/神经音色线索(Xiaoxiao/Yunxi/Neural/Online/Natural…)> 云端合成(
localService:false)」评分,优先用最自然的音色(如 Edge 的Microsoft Xiaoxiao Online (Natural)、Chrome 的 Google 普通话),不再用生硬的本地默认音色。 - 手动指定音色:
voice.config加ttsVoice: '音色全名'。查看可用音色: 控制台执行speechSynthesis.getVoices().map(v => v.name + ' [' + v.lang + ']')。 - 朗读前清洗(只读正文,不念符号):
- Markdown:
**加粗**、*斜体*、# 标题、[链接](…)(读链接文字)、行内代码、引用、列表标记、表格线;代码块替换为「代码略」。 - 装饰符号:对勾/叉号/复选框(✓ ✅ ✗ ❌ ☑)、箭头(→ ← ↑ ↓ ⇒ ➜)、emoji(🎉 ✅ ⚠️)、项目符号(• · ▪)、HTML 实体(
&等)全部清除,只保留正文文字。
- Markdown:
- 语音列表异步加载时(
getVoices未就绪)会自动等待voiceschanged后再播,避免回落到默认生硬音色;插件激活时预热语音列表。
工作原理
- 加载:
package.json声明dsh.client.platform: "web"与exports["./client"]→ 被 client-modules 扫描进window.__DSH_BOOT__图, 浏览器端以/plugins/dsh-voice/client.js?rev=<hash>加载;lib/client.js是手写的 factory bundle(window.__ModuleLoader__.load({id, factory})), 与 tsdown 产物格式一致,改动后 rev 自动变化(HMR 无需手动?v=)。 - 语音输入:
conversation.input.right插槽(输入框工具行右侧)注册麦克风按钮, 点击切换voiceMode(ReactuseSyncExternalStore订阅驱动按钮高亮)。 进入语音模式即常驻启动SpeechRecognition(continuous + interimResults) 并保持预热(意外结束自动重启);语音模式下window捕获阶段监听键盘:Space:keydown 记录按压起点(当前转写进度),hold 定时器(holdMs) 超时确认长按并显示指示胶囊;keyup 只提交起点之后的转写(按压前讲话不混入)。Enter:非按住时提交当前会话输入框草稿(焦点在输入框则放行原生发送, 避免重复提交);按住空格中按回车 = 提交本次片段并立即发送。- 提交经
ctx.sessions(当前会话)→ctx.conversation.input.for(scope)的setDraft()写入输入框(追加、不清前文),autoSend时再submit()。
- 朗读:
ctx.slots.inject('conversation.chat.assistant-actions')注册dsh-voice-speak条目(order 20,排在反馈之后);组件经标准套件useSession从会话快照按messageId取该消息的 text 块拼接成文 (真实节点形状kind='assistant-step',正文在data.finalNode.blocks),speechSynthesis.speak()朗读;含音色评分挑选与 Markdown/符号清洗。 - 无状态、无后端:全部为浏览器原生 Web Speech API;Firefox 无
webkitSpeechRecognition时语音输入优雅降级(提示不支持),朗读仍可用。
已知限制
- 语音识别依赖 Chrome / Edge / Safari 的
webkitSpeechRecognition(需 HTTPS 或 localhost,且需麦克风权限);Firefox 暂不支持该 API。 - 可选音色取决于浏览器/系统:Windows + Edge 的中文神经音色最自然;macOS 上
若无云端音色,效果受限于系统本地音色(可用
ttsVoice手动指定,或换浏览器)。 - 语音输入写入当前会话的输入框;未打开会话时长按空格会提示「请先打开一个会话」。
- 语音模式下若焦点在输入框且长按空格,判定前的 1 个空格可能被输入(极短窗口, 通常无感);长按触发后会自动屏蔽后续重复空格。
- 识别文本追加到输入框(与已有内容以空格分隔,不清空前文),可人工编辑后再回车。
调试
- 激活成功:控制台可见
[dsh-voice] 插件已激活 v0.1.12(…)及slots / sessions / conversation / stt / tts五个能力开关。 - 每次朗读会打印
[dsh-voice] 朗读音色: <音色名> | 语言: <语言>。 - 若刷新后按钮/功能缺失,把控制台报错发回,或在
cordis.patch.yml检查voice条目与软链(ls -la ~/.dsh/profiles/web/node_modules/dsh-voice)。
测试
node test/smoke.test.mjs # 40 项断言:模块表加载、插槽注册、模式切换、
# 长按空格听写、回车发送、音色挑选、符号清洗