dsh-voice-input-plugin
给 DeepSeek Harness Web GUI 加一个语音输入按钮:输入框工具行里、发送按钮一侧出现一个麦克风按钮,点一下开始录音,识别结果实时逐字写入当前会话的待发送框;再点一下(图标变成停止方块)结束录音。发送仍然用原来的发送按钮。
- 仓库:https://github.com/zRa1ny/dsh-voice-input-plugin · 许可:MIT
- 要求:dsh
>= 0.1.0-rc.5;浏览器 Chrome / Edge - 零依赖、无构建步骤:
index.js与lib/client.js就是发布产物,npm / git / tarball 三种安装方式都不需要 pnpm 构建授权
浏览器半边独立完成全部工作:使用 Web Speech API(SpeechRecognition / webkitSpeechRecognition),不占用 Host 能力、不上传音频到本地后端、不注册任何模型工具或服务。Host 半边是空插件,只用来让 Web 客户端的模块登记表能扫描到这个包的 dsh.client 声明。
安装
这是一个 dsh 组合包(bundle):声明了 dsh.bundle(贡献一个配置层)和 dsh.client(贡献浏览器半边)。用 dsh plugin 装进 web profile 即可,它会同时把包加入依赖和 dsh.profile.bundles。
从 GitHub 安装(当前可用)
dsh plugin --profile web add https://github.com/zRa1ny/dsh-voice-input-plugin
# 等价简写
dsh plugin --profile web add github:zRa1ny/dsh-voice-input-plugin
从 dsh 创意工坊安装
dsh-market.com(创意工坊)收录后,可在 Web GUI 的「创意工坊 → 插件」卡片里一键安装;market/listing.json 是本包准备好的目录条目。
从 tarball 安装
pnpm pack
dsh plugin --profile web add ./dsh-voice-input-plugin-0.1.0.tgz
安装后必须重启
客户端模块登记表按包名缓存元数据,插件集合的变化在重启后生效:
dsh --profile web --dump-config # 应能看到 "# == dsh-voice-input-plugin" 层
dsh --profile web
卸载
dsh plugin --profile web remove dsh-voice-input-plugin
关于 npm 包名
npm registry 上的 dsh-voice-input-plugin 不是本包:那个包由另一位作者于 2026-08-15 发布,与本包同名的另一个实现。因此本包不分发到 npm,安装请用上面的 GitHub 地址。
如果你要发布到 npm,请使用你自己的 scope(例如 @<你的用户名>/dsh-voice-input-plugin),scoped 名称在创意工坊的一键安装里同样有效,安装命令形如:
dsh plugin --profile web add @<你的用户名>/dsh-voice-input-plugin
从源码运行本仓库
git clone https://github.com/zRa1ny/dsh-voice-input-plugin
dsh plugin --profile web add ./dsh-voice-input-plugin
在 dsh 仓库 checkout 内调试时,dsh 是 pnpm dsh,路径按当前工作目录解析:
cd source-code
pnpm dsh plugin --profile web add ../dsh-voice-input-plugin
使用
- 点麦克风按钮 → 按钮变红并出现脉冲光圈,浏览器首次会弹麦克风授权。
- 说话时文字实时出现在待发送框(含未定稿的中间结果),可以继续用键盘编辑。
- 再点一次按钮 → 结束录音,已识别的文字留在输入框。
- 点原来的发送按钮发送。
状态与失败提示都在按钮上:录音中为红色停止方块,出错时图标转红、鼠标悬停(title)显示具体原因,再点一次即可重试。
行为契约
| 事项 | 行为 |
|---|---|
| 写入方式 | 通过会话标准工具包的 inputActions.setDraft(text) 写整段草稿,不是键盘模拟 |
| 起始草稿 | 开始录音时输入框里已有的文字会被保留,识别结果拼在其后(前一个字符是 ASCII 字母/数字时补一个空格,中文直接相接) |
| 识别语言 | 跟随浏览器语言(navigator.language),取不到时用 zh-CN |
| 连续录音 | continuous = true;Chrome 在静音后会自行结束,插件在 onend 里自动续录,直到用户点停止 |
| 去重 | 以结果下标记录已定稿的片段,事件重复投递不会重复追加文字 |
| 静音 / 主动停止 | no-speech 与 aborted 视为正常,不进入错误态 |
| 卸载 | 切换会话或插件卸载时中止识别并释放麦克风(abort()),随插件 fiber 一起撤销 |
| 样式 | 自注入一个 <style data-plugin="dsh-voice-input-plugin" data-plugin-css="…"> 标签,使用主题 CSS 变量;卸载时由模块登记表按 data-plugin 归属清除 |
实现原理
唯一的产品接触点是插槽座位 conversation.input.right——输入框工具行里、发送按钮一侧的追加座位(kind: 'list',replaceRisk: none)。组件从座位提供的标准会话工具包读取数据:
props.useInput(selector):订阅本会话输入状态(用来取起始草稿)。props.inputActions.setDraft(text):唯一的草稿写入路径。props.input:座位 owner(InputZone)提供的实时输入状态快照,事件处理中可直接读。
本包不替换任何既有座位:注册用的 id: 'voice-input' 是自己的键,不会顶掉别人的格子。
浏览器半边按 Web 客户端的模块协议注册:
window.__ModuleLoader__.load({ id: 'dsh-voice-input-plugin', factory: (require) => { /* … */ return module.exports } })
执行脚本只登记工厂;样式注入等副作用都在工厂闭包里,在首次 import 物化时运行。工厂只 require('react')(冻结平台模块表里的种子条目),返回 { inject, name, apply }——没有 default 导出(default 会让 Loader 的 unwrapExports 丢弃命名空间与 inject)。
项目结构
dsh-voice-input-plugin/
├── package.json # dsh.bundle + dsh.client + dsh.engines 声明
├── cordis.patch.yml # 被 profile 列入 bundles 时应用的层:插入一行空插件
├── index.js # Host 半边:空 apply,仅为让 client 扫描有 entry 可读
├── lib/client.js # 浏览器半边:发布产物本身(闭包工厂 bundle)
├── market/listing.json # 创意工坊目录条目(提交用),不参与运行
├── test/ # 契约测试,不随 npm 包发布(见 files 字段)
├── LICENSE
└── .gitignore
校验
node test/contract.smoke.mjs
契约测试加载真实 lib/client.js,断言注册协议(handoff id、工厂返回值、inject、注册到的座位与 id);若能从同仓库的 dsh checkout 解析到 react 与 react-dom/server,还会把按钮真正渲染一次,断言支持/不支持两条分支的 aria-label 与禁用态。
已知限制
- 只在 Chrome / Edge 可用:Firefox 与 Safari 没有
SpeechRecognition,按钮会置灰。Chromium 的实现把音频送到它的在线识别服务,因此需要联网,离线或受限网络会报network错误。 - 依赖麦克风权限:权限被拒绝时功能不可用,需要用户在地址栏侧重新允许。
- 一次录音只属于一个会话:切换会话会中止本次录音(旧组件的清理函数释放麦克风),已识别的文字保留在该会话的草稿里。
- 不解析口述标点命令:直接采用浏览器返回的文本,未做「逗号 / 句号」等口令后处理。
- 文案为中文写死:没有引入
locale服务注册字典;需要多语言时应改为inject: ['slots', 'locale']并用props.t。 - 入口 id 与动态插件互斥:如果同时运行注册到
conversation.input.right、id也是voice-input的进程内动态 Cordis 插件,两者会占同一个格子;动态插件不跨进程存活,重启后只有本包生效。
维护者:发布与收录
目录条目
创意工坊的线上清单是 https://dsh-market.com/manifest/plugins.json,条目字段为 id / name / nameEn / rank / author / description / descriptionEn / repo / npm / category / subcategory。现有条目的 category 取值为 tools / ui / knowledge / utility / integration / security / agent;本包是输入框控件,取 ui + chat。
market/listing.json 已按该字段集写好,并且故意不写 npm 字段:工坊的一键安装取 ${npm ?? repo ?? id},一旦 npm 指向 npm 上那个同名他人包,安装就会装错东西。将来若发布了 scoped npm 包,再把 npm 加回来即可。
收录渠道是该生态的公开仓库与社区:zhu1090093659/dsh-web 的 GitHub Issues / Discord 社区;另有独立社区商店 deepseek-plugin-store。
安装端的准入规则(已实测)
- 一键安装把条目 spec 交给
pluginManager.install(spec),等价于dsh plugin --profile web add <spec>。 - spec 只接受 npm 包名(可带
@版本,scoped 亦可)或https://开头的 git 地址;^1.0.0这类范围、ssh://、file://、http://、相对路径、裸仓库名都会被拒绝。 - 不带
.git的https://github.com/<owner>/<repo>与带.git的写法都能被 pnpm 正常安装(两种均已实测)。 - 安装端会校验依赖真实落盘、拒绝重复入口 id 认领与引用不可解析包的 insert 行,并用
--dump-config做组合预检;失败会经官方 remove 路径自动回滚新包。因此入口 idvoice-input不能与既有入口 id 冲突。 dsh.engines.dsh是更新前的兼容性门禁(也兼容读顶层engines.dsh):本包声明>=0.1.0-rc.5。运行版本低于声明时更新按钮会被禁用。- 发布到 npm 时确认
files覆盖index.js、lib/、cordis.patch.yml、market/、README.md,并且exports保留"./package.json"(Host 侧client-modules通过require.resolve('<包名>/package.json')读dsh.client声明,exports未导出的子路径会被 Node 拒绝)与"./client"(浏览器半边的产物路径)。
Model Experience
None:本包不注册提示词段落、工具、模型消息或提供方请求。识别出的文字只写入本地草稿,只有在用户点发送、由普通 composer 提交流程发出时才会进入模型请求。
KV Cache effect
None;本包不组装、不发送提供方请求。
No comments yet. Be the first to write one.