需 macOS 与支持头部追踪的 AirPods。戴着 AirPods 用点头/摇头回答 DSH 的权限许可与提问——点头=允许或确认,摇头=拒绝或切换选项;会话里一张卡片就能装好配套的 macOS 菜单栏 App。
安装
# npm 包(预构建)
dsh plugin --profile web add dsh-airpod-control
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:fishcg/dsh-airpod-control
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
用 AirPods 的头部动作回答 DeepSeek Harness 里的两件事,同一套手势:
| 场景 | 点头 | 摇头 |
|---|---|---|
| 权限许可(approval) | 允许(allowed-once) |
拒绝 |
模型提问(ask_user_question,3 个以上选项) |
确认当前项 | 切到下一项 |
手势由 HeadOrbit(AirPods 头部追踪的 macOS 菜单栏 App)识别, 经本机回环 HTTP 送到这个插件。插件在这里做了四件事:
- 自动装好 HeadOrbit:插件自带安装器,页面右下角一张卡片就能下载、安装、写配置、启动、等心跳;
装到
~/Applications/HeadOrbit.app,端口和 token 全自动,用户不用手填。 - 当审批回答者:只有真有请求在等、且 HeadOrbit 在线时才认领,否则一行都不碰。
- 当提问回答者:点头/摇头答题;3 个以上选项时摇头切换、点头确认。
- 手势答完撤掉网页卡片:插件带一个浏览器半区,把已经被手势结算的那张卡片收掉, 不会出现「点头同意了、卡片还挂在页面上」。
安装
dsh plugin --profile desktop add dsh-airpod-control
--profile换成你实际在用的那个(桌面版默认desktop,dsh web是web)。 npm 包:https://www.npmjs.com/package/dsh-airpod-control,当前版本0.3.0。
想改代码就从源码装:
git clone https://github.com/fishcg/dsh-airpod-control.git ~/dsh-airpod-control
dsh plugin --profile desktop add link:~/dsh-airpod-control
装完重启 DSH,打开任意会话,右下角会出现「用点头 / 摇头回答 DSH」的卡片,点一下即可:
- 下载 HeadOrbit(MIT,源码在 fishcg/HeadOrbit-dsh)并装到
~/Applications - 把 DSH 的端口和共享 token 写进它的偏好,启动它
- 等它开始轮询(说明链路通了)
- 引导去「系统设置 → 隐私与安全性 → 动作与健身」给 HeadOrbit 打勾(只能由系统弹窗授予,所以这一步需要你点一下)
不想点卡片也可以一条命令:
curl -s -X POST "http://127.0.0.1:<port>/dsh-airpod-control/setup/run" | jq
共享 token 也不需要手填:配置里留空时会自动生成一份,落在 ~/.dsh-airpod-control/token
(0600),安装器会把同一个值写进 HeadOrbit。
首次使用前请戴好 AirPods(Pro / 3 / 4 / Beats 等支持头部追踪的型号),并在 HeadOrbit 设置里确认「已连接」。
怎么用
- 戴好耳机,HeadOrbit 菜单栏图标显示已连接。
- 会话里出现审批卡片 / 提问卡片:
点头会按下的那个按钮(审批里的「允许」)会被描边点亮, 一眼看得出点头会选什么。 - 点头 = 允许(审批)或确认当前选项(提问);摇头 = 拒绝或切到下一个选项。
- 手势一落定,卡片就收起(宿主结算后就收回),所以插件会补一句结果:
已允许 <工具>/已拒绝 <工具>/已选择 <选项>。这句话优先说在小鲸鱼头顶的气泡里 (装了dsh-whale-widget时), 没装鲸鱼就退化成屏幕顶部的提示条——不然卡片消失得太快,人会不知道自己刚才选了什么。
手势和网页按钮同时有效,谁先到算谁的:插件认领请求时也调用 next(),卡片照常渲染,
你在页面上点「允许」和做一个手势是在赛跑。没有回答者时 DSH 本来就是 fail closed,本插件不改变这个性质。
顺手:按住 ← 或 → 说话
官方的语音输入插件(dsh-experimental-voice-input-bundle)没有快捷键,只能点输入框旁边那个麦克风。
本插件的浏览器半区补了一个:同时按住 ← 和 → 开始录音,松开任意一个就结束并转录,文字照常落进输入框草稿。
- 没装官方语音插件时完全不生效,
⌃Space该干什么还是什么(只找得到按钮时才会拦截按键)。 - 单个方向键完全不拦截(光标照常移动);只有左右两个方向键同时按住时才录音。
- 带 ⌘/⌃/⌥/⇧ 的组合一律放行。
- 也保留 ⌃Space(按下即开始、松开即停止)作为备用。
- 它会真的"点一下"那个麦克风按钮,所以屏幕上的录音状态、波形、取消按钮都是官方组件自己的, 失败提示(没权限、没模型、转录失败)也照常显示。
设置页
设置 → 左侧栏「AirPods 控制」(右侧栏里和皮肤插件那几个并列)里能改手感,点保存即时生效,不用重启 DSH:
| 项 | 作用 |
|---|---|
| 点头 / 摇头幅度 | 转多少度算一次手势;旁边两条进度条是实时幅度,边说边调最直观 |
| 语音输入按键 | 按住 ← + →、⌃Space、或关掉 |
| 高亮当前选项 | 摇头切选项时在卡片上圈出当前项 |
| 界面语言 | 跟随系统 / 中文 / English |
| 连续手势去重、只在 HeadOrbit 在线时接管 | 高级项,一般不用动 |
界面语言默认问系统要,而不是看浏览器的 navigator.language——Electron 里那个值经常是
en-US(这台中文 Mac 就是),照它判断会让中文用户看到英文菜单。
演示:点头 = 允许
下面是一段 5 秒的真机录屏——审批卡片弹出后点一次头,卡片自动收起、命令随即执行(摇头就是「拒绝」):

截图
首次打开时右下角会自动出现安装卡片(左);装好后审批卡片长这样(右):
| 一键安装引导 | 审批卡片 |
|---|---|
配置
在 profile 的 cordis.patch.yml 里覆盖(都有安全默认值):
| 键 | 默认 | 说明 |
|---|---|---|
enabled |
true |
关掉就什么都不注册,等于没装。默认开是因为「HeadOrbit 不在线就不碰请求」,而默认关着太脆:插件管理层重写 profile 层时会把用户手写的行抹掉,插件就静默失效了 |
token |
'' |
共享密钥。留空则自动生成到 ~/.dsh-airpod-control/token(0600);非空时以配置为准。请求需带 X-HeadOrbit-Token |
nodAllows |
true |
false 则反过来:摇头=允许、点头=拒绝 |
claimQuestions |
'auto' |
'off' 则完全不接模型的提问,只回答权限许可 |
timeoutMs |
600000 |
审批没人理时的兜底(按拒绝结算) |
questionTimeoutMs |
600000 |
提问没人理时的兜底(工具返回 pending,不是跳过) |
requireHeadOrbit |
true |
false 时不看心跳、永远抢答(只建议自测时用) |
aliveWindowMs |
6000 |
HeadOrbit 多久没轮询就当它不在线 |
debounceMs |
200 |
两次手势之间的最小间隔 |
端点
全部挂在 /dsh-airpod-control 前缀下,复用宿主已有的 webServer 路由(因此只听 127.0.0.1)。
除 Host / 同站检查之外,都需要 X-HeadOrbit-Token;标了免 token 的那几个是给浏览器半区用的,
只读且不含会话内容。
| 方法 | 路径 | 用途 |
|---|---|---|
GET |
/state |
{ waiting, kind, label, question?, deadlineAt, canAnswer, headOrbit, ... },HeadOrbit 靠它决定要不要发手势,同时它本身就是心跳 |
GET |
/health |
自检:{ ok, hasToken, hasApprovalAnswerer, hasQuestionAnswerer, headOrbitAlive, answered, ignored }(不回报 token 本身) |
GET |
/events |
最近的事件流水,排查用 |
GET |
/last-decision |
免 token:最近一次手势/按键结算了哪个请求(浏览器半区据此撤卡片) |
GET |
/cursor |
免 token:当前光标落在第几项(只回序号与那一项的文字) |
GET |
/setup |
免 token:{ app: { installed, running }, bridge, alive, tokenConfigured, last, running } |
POST |
/setup/run |
免 token:下载 + 安装 + 写配置 + 启动 + 等心跳;{ reinstall?: true },返回逐步状态 steps[] |
POST |
/setup/motion |
免 token:打开系统设置的「动作与健身」隐私页 |
POST |
/gesture |
{ "gesture": "nod" | "shake", "ttlMs": 2000, "requestId"?: "q1" } |
POST |
/answer |
{ "decision": "allow" | "reject" | "select:N", "requestId"?: "q1" },没有耳机时的等价入口 |
/state 里 kind 是 "approval" 或 "question";等提问时 question 会带上题目与选项
({ requestId, label, count, questions, deadlineAt, cursor })。浏览器半区拿 question.cursor
在网页卡片上把「现在点头会选中哪一项」圈出来。带 requestId 的调用是网页卡片回的答案,
id 对不上会得到 409 { stale: true }。
$ curl -s -H "X-HeadOrbit-Token: $TOKEN" http://127.0.0.1:$DSH_WEB_PORT/dsh-airpod-control/health
{"ok":true,"plugin":"dsh-airpod-control","version":"0.3.0","hasToken":true,"hasApprovalAnswerer":true,"hasQuestionAnswerer":true,"headOrbitAlive":true,"headOrbitWindowMs":6000,"waiting":false,"waitingKind":null,"answered":3,"ignored":0}
端口就是 DSH 自己的 GUI 端口(dsh web 启动时打印的那个);插件不开新端口。
安全性(值得说清楚)
- 不自己开端口:复用宿主已有的 webServer 路由,因此天然只听 127.0.0.1。
- 必须有共享密钥:token 为空时自动生成一份(0600),并要求请求带
X-HeadOrbit-Token, 所以本机别的进程即使能访问这个端口也伪造不了答案。 /setup*是免 token 的(浏览器拿不到 token),因此只接受回环 Host + 同站请求; 它做的都是固定参数的本机动作(defaults/ditto/open),请求内容不会进 shell。 装上之后任何本机进程仍然可以调它重装 —— 这在能读你用户目录的进程面前不算新增权限。- 只回答「正在等待中的那一次请求」:过期的、没有请求在等的、手势与映射不符的,一律忽略; 一次请求只会被结算一次。
- 没有回答者时 DSH 本来就会 fail closed,本插件不改变这一点。
测试
cd dsh-airpod-control
node --test test/plugin.test.js # 52 个用例,手写假宿主,不需要真 DSH
覆盖:审批允许/拒绝/超时/取消、提问(两选项与多选项光标)、请求竞态、token 与 Host/同站校验、
自动生成 token、/setup 状态、卡片撤除的依据(/last-decision)、手势去抖、/motion 实时幅度、
/settings 读写与夹取范围、网页半区在 zh/en 下都能加载且两份文案键一致。
排障
插件像没装(网页上没卡片、App 日志里 无法连接服务器):先看它在不在。
$ curl -s -o /dev/null -w '%{http_code}\n' "http://127.0.0.1:$DSH_WEB_PORT/dsh-airpod-control/setup"
200 # 挂载了
404 # 没挂载:看 profile 的 cordis.patch.yml 里 dsh-airpod-control 那行是不是被关掉/抹掉了
按钮没高亮 / 结果气泡没出现:先问浏览器那一半活着没有。
$ curl -s "http://127.0.0.1:$DSH_WEB_PORT/dsh-airpod-control/client-state"
{"version":"0.3.0","at":1792575600000,"bootAt":1792575590000,"boots":1,"notes":2,"lang":"zh","kind":"note","text":"已允许 Bash","channel":"whale"}
boots: 0 说明前端那一半根本没加载(改过前端代码要 ⌘R,改过宿主代码要重启);
notes 和 text/channel 是它最近一次说话的内容和走的通道(whale = 小鲸鱼气泡,toast = 顶部提示条),
这样能分清「没触发」和「触发了但被鲸鱼的渲染盖掉」。
端口变了:~/.dsh-airpod-control/endpoint.json 里的 port 必须等于当前 GUI 端口。
插件启动后 10 分钟内会一直重试发布,之后任何一次请求也会顺手刷新;App 每轮轮询都会重读这个文件,
所以 DSH 换端口不需要改 App 配置。
日志:App 侧 ~/Library/Logs/HeadOrbit-DSHBridge.log;宿主启动崩溃
~/Library/Logs/DeepSeek Harness/crash-*-web-boot.log(1 entry did not activate 说明某个插件
半区在 import 阶段抛了异常,整个条目会被停用)。
已知边界
- 只支持 macOS(HeadOrbit 是 macOS 菜单栏 App,用 CoreMotion 读 AirPods)。
- 需要支持头部追踪的耳机;没有耳机时插件不会认领任何请求,行为与未安装一致。
- 手势识别由 HeadOrbit 负责,阈值可在 App 里按自己的幅度校准(点头约 12°、摇头约 19°)。
- 插件和网页卡片是竞态关系:两边同时给答案时,先到的那个生效。
- 安装器下载的是
fishcg/HeadOrbit-dsh的 latest release(ad-hoc 签名、未公证), 安装后会自动去掉 quarantine 属性;签名/公证版本后续再补。
链接
同类插件
PolinniZhong/dsh-omi-voice★ 76
DeepSeek Harness 对话内朗读:点一下即可朗读、暂停、继续 AI 回复,豆包 TTS 自然音色(BYOK),只读最终回答并过滤代码、表格与图形,本地引擎,插件零 Key。
beiyege-01/dsh-voice-ai-girlfriend-plugin★ 40
Web UI 语音 AI 女友:FunASR 麦克风语音输入、Qwen3-TTS 语音回复、数字人动画窗、QQ 双向聊天(文本/语音/图片推送,经 NapCat)。
PensiveFei/dsh-voice-scribe★ 35
面向 Web UI 的语音输入插件:点按 Alt(或 Alt+空格)开始/停止听写,支持浏览器内置 Web Speech(零配置)或 OpenAI 兼容云端 ASR,可选经 DSH 已配置模型润色,带设置页。
1624318455/dsh-plugin-tts★ 29
用免费 Edge TTS 或你自己的 RVC 音色朗读 AI 回复:消息朗读与自动朗读、长文自适应分块渐进播放(无缝衔接)、音色包仓库一键安装、便携 RVC 运行时。
ppy-web/dsh-plugin-xiaomi-mimo-tts★ 28
为 DSH 提供小米 MiMo、豆包和浏览器本地语音朗读,支持自动播报、自定义音色与可选 UI 音效。
WizisCool/dsh-ears★ 24
面向 DeepSeek Harness (dsh) 的语音输入插件:输入框的麦克风按钮把语音转成草稿文本,支持多种语音识别后端,可选经 dsh 自有 LLM 路由润色,并带原生设置页。
社区评论
评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。