dsh-aiui
已验证dsh-aiui · v0.7.0 · MIT
讯飞语音背包 AIUI 控制插件:TCP 19199 协议客户端、语音监听/播报、机器人运动控制、设备运维、USB 自动发现背包 IP
安装
dsh plugin add dsh-aiui 用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。
源码
发布到 npm 但没有公开仓库。安装前请检查包内容。
标签
说明文档
dsh-aiui
讯飞语音背包(iFlytek AIUI 语音背包 / ZC-H358S)DSH 插件:把背包的语音识别、语义理解与机器人运动控制封装为 8 个 agent 工具。
协议实现依据 TcpDemo_1.3.0 内 UARTKit 源码逐字节还原,并经真机联调验证(完整过程见工作区《分析报告-语音控制机器人.md》)。
背包 IP 自动发现(默认开启)
背包重启后 DHCP 可能换 IP(实测 .153 → .172)。插件默认在每次(重)连之前经 USB 线 + adb
读背包当前网卡地址,与本机网卡同网段才采用,否则回落配置的 host:
USB 线 + adb(python/adbutils)→ ip addr show → wlan0/eth0 的 IPv4
├─ 与本机同一网段 → 用发现的 IP 连 19199;adb 工具用 <发现的IP>:5555
└─ 否则 / 发现失败 → 回落配置 host(旧行为),日志说明原因
- 需要USB 线连接背包;关掉它(
discover: false)则完全按配置的host/adbSerial走,不依赖 python 5555未监听时(设备重启后adb tcpip状态丢失)会经 USB 自动重发adb tcpip 5555自愈- 分辨率 30s 缓存;发现失败同样缓存,避免重连风暴里反复拉 python
aiui_status会显示来源:usb-discover/fallback/config
# 手动验证(配置 host 故意写错也能连上)
$env:AIUI_HOST="192.168.0.199"; node mcp\server.mjs --self-test
# → [aiui] host 192.168.0.199 → 192.168.0.172,via=usb-discover
接口一览(6 层 11 工具)
| 层 | 工具 | 作用 |
|---|---|---|
| 连接层 | aiui_status |
连接背包,返回设备信息、speech/mmsp 参数、通道状态、动作词典、健康度 |
| 连接层 | aiui_config |
查询/下发 speech 运行参数(固件可能强制覆盖,工具内已说明) |
| 连接层 | aiui_channel |
19199 通道管理:status 看争抢情况、release 让给常驻服务/演示程序、acquire 抢回(仅 ZCode 侧) |
| 诊断层 | aiui_diagnose |
一次跑完现场诊断:通道争抢、线上引擎、演示程序、引擎与桥服务、外网、WiFi 省电、TTS 播放器、唤醒后秒睡,并给下一步建议 |
| 语音层 | aiui_listen |
监听 N 秒:唤醒、识别文本、AiChain 回答;可 playTts 出声、可 raw 取原始报文 |
| 语音层 | aiui_say |
TTS 播报一句话(经典云 TTS 通道) |
| 语音层 | aiui_control |
远程唤醒 / 休眠 / 停止播报 / 查询状态机 |
| 控制层 | aiui_actions |
听一轮 → 线上抽槽 → 返回 JSON 动作序列;也可 actions 直接下发、text 离线规划 |
| 控制层 | aiui_motion |
单条机器人运动:forward/backward/left/right/stop |
| 运维层 | aiui_device |
info / check_net / engine_log / wifi_powersave(查/关/开 WiFi 省电,需 root) / disable_demo / enable_demo |
| 运维层 | aiui_screen |
背包屏幕:截屏 PNG、scrcpy 投屏、录屏 mp4 |
典型用法
用户:你听一下背包说什么
agent:→ aiui_listen { seconds: 20, prompt: "请说指令" }
← 识别:「向前走」 / 语义答「2 times 6 equals 12.」
agent:→ aiui_motion { action: "forward", seconds: 2 }
唤醒词 「小飞小飞」,休眠词 「休眠一下」。唤醒后背包进入"工作中"才接受指令。
连不上 / 没反应?先跑诊断
agent:→ aiui_diagnose {}
← 诊断:发现 2 项异常:通道争抢、背包外网
✅ 19199 连接:192.168.0.172:19199 ready
❌ 通道争抢:已让出通道(连续被踢 2 次)
✅ 线上引擎:intent_engine_type=cloud
✅ 经典 AIUI 已关:close_aiui=true,work_mode=rec_only
➖ 厂商演示程序:(未取到,需网络 ADB)
...
建议:
· 19199 是单会话。同一时刻只能有一个客户端…
aiui_diagnose 就是把 2026-09-17 那次人工排查的每一步(netstat 看谁占通道、adb 看演示程序、
看引擎服务、看外网丢包、看是否秒睡)固化成一张检查表。
让 agent 听得见、说得出
19199 单会话:我们的客户端占着通道时,背包自己不出声(TTS 音频块是推给上位机的)。 所以:
aiui_listen { playTts: true }/aiui_actions { playTts: true }—— 把设备推来的 PCM 播出来(需 ffplay), 也可以把userConfig.play_tts设成 true 作为默认aiui_channel { action: "release" }—— 干脆把通道还给厂商演示程序,让背包自己亮屏出声; 下次调用任一aiui_*工具会自动抢回
语音 → JSON 动作序列 → 机器人(0.4.0)
人说话,线上模型理解,产出动作序列驱动机器人。链路:
人:「小飞小飞」→「握个手吧」
↓ 背包本地唤醒(mic4)
↓ AiChain 云管线(线上) 云端 STT → 大模型命中意图插件 robot_action → 抽取槽位
↓ TCP 19199:iat_aichain(转写)→ nlp_aichain(data.slots 带上插件槽位)→ tts
↓ 上位机:校验线上证据 → 槽位展开成动作序列 → 整份 POST 给机器人
↓ 机器人:按 action → 动作端口,逐个执行
前置条件:先在 AIUI 控制台配好意图插件。照
docs/aiui-plugin-robot-action.md 逐步填(插件别名 robot_action、
5 个参数、28 条示例说法、粘一份 Jinja2 后处理模板),发布插件 → 关联到背包所用的应用 → 发布应用。
常驻上位机服务(推荐)
跑之前先做两件事,否则会看到每秒「重连中」(19199 单会话,多方互抢):
# ① 停掉厂商演示程序:它会周期性地抢回 19199,导致常驻服务被反复踢
G:\讯飞语音背包\scrcpy-win64-v1.20\scrcpy-win64-v1.20\adb.exe -s f3d87c45528dc7e8 \
shell pm disable-user --user 0 com.iflytek.aiint.app.speechassistant
# 撤销:把 disable-user 换成 enable
# ② 本会话/别的终端里不要再调 aiui_* 工具(MCP 客户端也会抢这条单会话通道)
npm run host # 或 node mcp/robot-host.mjs
# 机器人收据端:http://127.0.0.1:8266/robot/actions (PC 扮演机器人,按动作端口模拟执行)
# 控制面: http://127.0.0.1:8267 GET /status /last /sequences
# POST /control/release /acquire
停掉演示程序不影响语音链路:唤醒、云端识别、意图插件都在引擎与云端,退掉的只是背包那块屏幕界面。
测量:停掉后 32 秒窗口内 已连接 1 次、重连 0 次(停之前是 30 秒内重连 2 次)。
2026-09-17 复核:曾一度怀疑「演示程序是音频来源、停掉就没声音」——不成立。 20:08 那次完整跑通(握手 → 端口 9101 → 回执)就是在演示程序已禁用状态下发生的。
背包换 IP / 重启后直接跑就行。 常驻服务在两条路径上都会自己跟上:
- 发现优先:每次(重)连前经 USB 读背包实际 IP,同网段才用——配置里的
host只是回落值。 实测把AIUI_HOST故意写成错的192.168.0.199,host 仍连到192.168.0.172(via=usb-discover)。 - 首连重试:以前首次拨号失败只打一行日志就闲置了(
AiuiClient的自动重连只覆盖"连上后掉线"), 于是背包还没联上网时启动 host 会永远连不上,看起来"已就绪"其实是死的。 现在失败会按 3s→4.8s→…→15s 封顶重试,背包一上线自动连上,并打印当前目标与发现情况。
排查用 curl http://127.0.0.1:8267/status 看 resolve(via / 发现的 IP)。
若日志出现「与本机不在同一网段」,说明本机和背包不在一个网——把本机网段调到与背包一致即可
(本次实测就是本机跑在手机热点 192.168.43.x、背包在 192.168.0.x,怎么重试都连不上,这不是 bug)。
唤醒提示音(默认开):通道被我们占着时背包自己没有任何反馈,人不知道该何时开口
(实测过一次「唤醒了但没说话」,云端只收到 713ms 静音)。所以主机在收到唤醒事件的瞬间
用 ffplay 本地合成一声短音(sine via lavfi,不走云端,弱网也稳):听到就立刻说指令。
关掉用 --no-wake-cue。也可以 curl -X POST http://127.0.0.1:8267/control/wake 远程唤醒并试听这一声。
对背包说「小飞小飞」→「握个手吧」,终端会打出:
══ 19:52:03 收到语音「握 个 手 吧 。」
线上证据:✅ 已确认走云端 appid=jh7BMBRi 插件=robot_action 形态=cloud-slots
动作序列:握手→端口9101
JSON: { "schema": "aiui.robot.actions/1", … }
机器人回执 rcpt-…:端口 9101 ← 握手 {"times":1} 预计 3000ms [simulated]
常用开关:--robot-url <url>(指到真机底盘;给了就不起本机收据端)、--require-cloud(证据不满足就拒绝出序列)、
--release-after 60(空闲 60s 把 19199 还给演示程序)、--no-tts、--once(处理一轮就退出,脚本化验证用)。
会话内触发
agent:→ aiui_actions { seconds: 30, prompt: "请说,握个手吧" } ← 听一轮,返回动作序列 JSON
agent:→ aiui_actions { text: "握个手吧" } ← 离线自测:不连设备,只跑规划
agent:→ aiui_actions { forward: true } ← 顺便转发到 robotUrl
⚠️ 19199 是单会话:常驻服务与
aiui_*工具不要同时用,否则每秒互相踢一次 (现象是响应变慢、背包更没反应)。二选一即可。
Ubuntu 安装与运行(换系统后从这里开始)
系统依赖
sudo apt update
sudo apt install -y nodejs npm ffmpeg android-tools-adb git # nodejs 需 ≥20,旧源请用 NodeSource
pip3 install adbutils jinja2 # adbutils=USB 发现/设备运维;jinja2=模板自检
方式一:npm 安装(推荐,最省事)
mkdir -p ~/voice-robot && cd ~/voice-robot
npm init -y
npm install dsh-aiui # ≥0.7.0
# 常驻上位机:连背包 → 语音 → JSON 动作序列 → 转发给机器人适配器
node node_modules/dsh-aiui/mcp/robot-host.mjs --robot-url http://127.0.0.1:8266/robot/actions
方式二:拷贝源码(要跑测试/改代码时用这个)
把 Windows 上的 dsh-aiui/ 整个目录拷到 Ubuntu(如 /opt/dsh-aiui),然后:
cd /opt/dsh-aiui && npm install
npm test # 全部自检(需要 pip3 install jinja2)
npm run host -- --robot-url http://127.0.0.1:8266/robot/actions
两种方式的差异
| 方式一(npm 包) | 方式二(源码) | |
|---|---|---|
| 运行 host / 连背包 / 转发动作 | ✅ | ✅ |
aiui_diagnose 等 11 个 MCP 工具 |
✅(配 ZCode MCP 时) | ✅ |
npm test 自检 |
❌(包里不含 test/) | ✅ |
| 改代码 | ❌ | ✅ |
Windows 特有项在 Ubuntu 上不存在
D:\Anaconda3\python.exe这类兜底不需要——设PYTHON=python3或什么都不设(会自动用python3)- ffplay 直接
apt install ffmpeg即可(声道参数会自动探测-ch_layout mono/-ac 1) - USB 自动发现需要当前用户有 USB 权限(Ubuntu 通常已在
plugdev组;adb devices能看到设备即可)
人脸唤醒(真机实测:已开启,无需唤醒词)
背包的人脸/视觉唤醒是开着的,引擎日志可证:
MMSPProcess onWakeUpChange wakeUp=true, wakeUpType=FACE
AiChain_CustomHandler enterWakeUpState: type=FACE → 连 AiChain
AIUIService onAIUIEvent EVENT_WAKEUP
aiui.cfg 里的相关配置:mmsp.wakeup_mode="auto"、identity_enable=true、cae_mode="mmsp"、
min_face_w/h=100、face_out_ms=800(即人脸唤醒由 mmsp 引擎负责,与厂商演示程序无关)。
判别唤醒来源:下发给 19199 的 wakeup 报文不带类型字段(人脸与语音的报文一模一样),
唯一可用的差异是语音唤醒会配对出现 sub=keywords 事件。上位机据此推断并直接报出来:
[host] 唤醒词命中(语音唤醒)
[host] 唤醒(语音):已响提示音——听到就立刻说指令
[host] 唤醒(人脸/视觉(未伴随唤醒词)):已响提示音——直接说指令即可,不需要唤醒词
curl .../status 的 lastWakeSource 会给出 voice / face。
实测注意(10:36 那次):人脸唤醒后云端会话可能很快被丢弃 (日志
Drop active cid for sleep,建起来约 0.4 秒就结束)。 所以听到提示音后要立刻说指令;人脸唤醒不是"一直待命"。 另外:厂商演示程序那个「图像理解迎宾」会主动打招呼,我们现在禁用了它, 所以提示音就是唯一的开口信号——这也是为什么要开着--no-wake-cue之外不要动它的原因。
「有时能触发、有时不触发」是怎么回事(真机实测)
先看日志里云端听成了什么 —— 上位机现在会把每轮的识别文本打出来:
[host] 识别:握 个 手 吧 。
[host] 动作序列 act-…-0001(cloud-slots):握手 ← 命中
[host] 云端走闲聊,未命中意图插件(识别为「播 个 守 班 。」) ← 没听懂
[host] 云端回答:好的,小飞为您播放一首欢快的歌。
实测:说「握个手吧」,云端有时转写成**「播个守班」/「播个歌曲」**——握/播、手/守、吧/班 全是近音, 于是走闲聊、答"小飞为您播放一首欢快的歌",当然没有动作。这是识别准确率问题,不是链路故障。
两类失败要分清(日志里的 判定 字段直接给出):
| 判定 | 含义 | 怎么办 |
|---|---|---|
chat |
云端走闲聊(没命中插件) | 把该说法与近音变体补进插件的示例说法(手册 3.1.1),或改用歧义更小的说法(「握手」) |
ignore |
无原话且无云端结构(超时帧/尾帧) | 那一次没说出话或开口太晚;听到提示音后立刻说 |
empty |
命中插件但没抽到动作 | 补示例说法覆盖该表述 |
dispatch |
正常下发 | — |
aiui_diagnose 的「最近识别记录」会列最近几轮 「识别文本」 → 命中插件 / 走闲聊,
不用翻设备日志就能判断。详见 docs/aiui-plugin-robot-action.md 第 7.5 节。
「背包没反应」是怎么回事(真机实测)
对背包说唤醒词,背包屏幕不亮、也不出声——这不是识别失败。抓包证明唤醒 6 次、云端转写、 插件命中、answer 全部到位。真正原因是 19199 单会话:
- 背包自己的屏幕界面 + 本地播报由厂商演示程序
com.iflytek.aiint.app.speechassistant负责 - 上位机一连上 19199,演示程序就被踢 → 背包既不亮屏也不出声
- 同时设备把 TTS 音频块推给上位机,等上位机播放 —— 所以由本机音箱出声(
dist/tts.js走 ffplay)
想让背包自己亮屏出声:curl -X POST http://127.0.0.1:8267/control/release 把通道还回去,
或启动时用 --release-after <秒> 空闲自动还;/control/acquire 抢回。
屏幕查看(aiui_screen)
用户:看看背包屏幕上现在显示什么
agent:→ aiui_screen { action: "screenshot" } ← 存 PNG 并返回路径/分辨率,agent 读图即可"看到"屏幕
用户:把背包画面投到电脑上
agent:→ aiui_screen { action: "mirror" } ← 本机桌面弹出 scrcpy 窗口(自动 -s 指定网络 ADB serial)
agent:→ aiui_screen { action: "stop" } ← 关闭投屏
mirror需要 本机有 scrcpy:配置项scrcpyPath指向 scrcpy.exe,或已加入 PATH;投屏进程以 detached 方式启动,独立于 DSH 存活- 背包同时插着 USB 线时 adb 会出现两条记录(scrcpy 不带参数会报 Multiple devices connected),本工具始终用
-s <adbSerial>指定,不受影响 record走设备端 screenrecord(无需 scrcpy),5~180 秒,保存 mp4
配置项
| 键 | 默认 | 说明 |
|---|---|---|
host |
192.168.0.172 | 背包 IP(自动发现关闭或失败时的回落值) |
port |
19199 | AIUI 控制通道(单会话,后被连者踢前者) |
adbSerial |
192.168.0.172:5555 | 网络 ADB(aiui_device 用;discover 关闭时使用) |
discover |
true | USB 自动发现背包 IP(换 IP 自动跟上;需 USB 线 + python/adbutils) |
usbSerial |
空 | 指定 USB 设备序列号;留空取第一台 USB 设备 |
adbPath |
空 | adb.exe 路径(5555 端口自愈用);留空自动探测插件目录 / 工作区 scrcpy / PATH |
motionWebhookUrl |
空 | 运动 HTTP 转发(POST JSON {action,seconds,vx,wz,…});留空=模拟 |
robotUrl |
空 | 动作序列端点(POST 整份 aiui.robot.actions/1);aiui_actions.forward 用它;留空=只模拟 |
requireCloud |
false | 要求线上证据全部通过才出动作序列;默认只警告并在 warnings 里逐条标注 |
playTts |
false | aiui_listen/aiui_actions 默认是否把设备推来的 TTS 播出来(需 ffplay) |
ffplayPath |
空 | ffplay 绝对路径;留空自动探测 PATH 与 ffmpeg 同目录 |
yieldOnKicks |
true | 工具客户端连上即被踢 2 次后让出 19199(避免与常驻服务互踢);下次工具调用自动重试 |
autoMotion |
true | 语音说"向前走/停"等自动转发运动指令 |
maxListenSeconds |
120 | aiui_listen 上限 |
adbTimeoutMs |
30000 | aiui_device 单次 ADB 操作超时(超时杀整棵进程树) |
scrcpyPath |
空 | scrcpy.exe 路径(aiui_screen mirror 用);留空自动探测 PATH 与插件 tools 目录 |
screenshotDir |
空 | aiui_screen 截屏/录屏保存目录;留空用系统临时目录 dsh-aiui/ |
autoReconnect |
true | 断线自动重连(含 5s 心跳、12s 静默重连) |
底盘接入
把底盘控制做成一个 HTTP 服务,配置 motionWebhookUrl 即可:
// 示例(Node):接收 dsh-aiui 的运动指令 → 转成你的底盘协议
http.createServer((req, res) => {
let body = ''
req.on('data', c => body += c)
req.on('end', () => {
const { action, seconds, vx, wz } = JSON.parse(body)
// ROS: 发布 geometry_msgs/Twist(linear.x=vx, angular.z=wz) 到 /cmd_vel
// 串口底盘: 发运动帧;持续 seconds 后自动停
res.end('ok')
})
}).listen(8266)
已知固件行为(真机实测)
- 本批固件走 AiChain 全双工云管线(云端 STT + 大模型 NLU + TTS),
work_mode被引擎强制rec_only,经典iat/nlp事件基本不再出现;上位机收到的是iat_aichain/nlp_aichain/tts事件 - 唤醒后"秒睡" = 背包外网不通/弱网(云端 WebSocket 建不起来),可用
aiui_device check_net诊断;建议给背包插网线 - 19199 单会话互斥:厂商演示程序会抢占连接并回滚配置,可
aiui_device disable_demo禁用(enable_demo恢复) - 引擎
com.iflytek.aiuiservice与协议桥com.iflytek.aiui.devboard.uartservice勿停
依赖
- 宿主:
@deepseek-ai/dsh-tools、@deepseek-ai/schemastery(peer,随宿主/profile 安装) aiui_device:本机python+adbutils(pip install adbutils),无需 adb 可执行文件
离线/断连时的行为
背包未连接(关机、换网段、19199 被演示程序占用)时,语音类工具都会快速返回 ok:false 与可读提示,不会挂起:
aiui_status/aiui_listen/aiui_say/aiui_control/aiui_config:连接或握手失败立即返回,提示检查 IP、网段与会话占用aiui_device:先做 ADB 端口预检(1.5s 超时),不可达直接返回,不拉起 python/adb 服务- 断线自动重连为退避重试:1.5s → 2.4s → 3.8s → … → 15s 封顶,连上即复位
aiui_motion与背包无关:未配置motionWebhookUrl时只做模拟执行- IP 自动发现失败(没插 USB 线 / 没装 adbutils / 背包不在本机网段)不影响使用:回落配置
host,日志与aiui_status会写明原因
自检与测试
npm test # 协议单测 + IP 发现单测 + 宿主契约/离线测试(不需要背包)
npm run test:protocol # 帧编解码与事件解析(以真机抓包字节为基准向量)
npm run test:discover # 网段比对、候选挑选、adb 探测、resolver 分支
npm run test:contract # 用宿主真实 defineTool 加载插件 + 离线降级 + webhook 转发
npm run smoke # 真机烟雾测试(需背包在线,可传 IP 参数)
已知环境坑(本机 Windows 实测)
- Node
fs.cpSync与中文路径:目标路径含中文时静默不复制;源路径含中文时直接崩进程(exit 127)。测试脚手架因此改用手写copyTree,插件运行时不受影响 - webhook 用
node:http而非全局fetch:避免 undici 保活 socket 在进程退出时触发 libuv 断言崩溃 - 本机 CLI 直接
dsh web起不来(应用内置 node shim 在应用外无法执行),插件加载验证请以 DSH Desktop 实际启动为准
ZCode 接入
本插件同时可挂到 ZCode:清单在 .zcode-plugin/plugin.json,工具由 mcp/server.mjs 以 MCP 协议发布
(复用 dist/ 的同一批模块),工具名为 mcp__plugin_dsh-aiui_aiui__<tool>(8 个)。差异与装卸步骤见
ZCODE.md。
License
MIT