aiui-action
Verifiedaiui-action · v1.4.0 · MIT
讯飞语音背包(iFlytek AIUI / ZC-H358S)语音 + 机器人动作插件:19199 协议客户端、唤醒/识别/语义、官方 TTS 通道、动作序列下发 + 宇树 G1 执行桥、设备运维、USB 自动发现背包 IP。
Install
dsh plugin add aiui-action Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Tags
Readme
aiui-action
讯飞语音背包(iFlytek AIUI 语音背包 / ZC-H358S)的语音 + 机器人动作插件。
把背包的唤醒、语音识别、云端语义结果与机器人动作序列封成 agent 工具(MCP),
并自带一个常驻上位机服务 mcp/robot-host.mjs,做「语音 → 云端语义 → JSON 动作序列 → 机器人」的桥。
- 协议:TCP 19199,帧格式/命令字逐字节对齐厂商
TcpDemo_1.3.0的 UARTKit 与四篇官方协议文档, 并经真机联调逐条验证 - 环境:Node ≥ 20;设备侧需 root(
adb root—— 提示音与流式播放要直写 ALSA) - 形态:Linux / Windows / macOS 都能跑;背包与上位机在同一局域网(网线或 WiFi)
安装
npm i aiui-action # 装进项目
node node_modules/aiui-action/mcp/robot-host.mjs --help # 常驻上位机(推荐形态)
作为 ZCode 插件:把本包目录放进本地市场目录(marketplace.json 指向的 zcode-plugins/),
然后在 ZCode 里启用 aiui-action@<marketplace>;插件会带来 11 个 aiui_* 工具。
依赖:MCP 服务与命令行(
mcp/*.mjs)零依赖,只用 Node 内置模块;只有 ZCode 插件入口 (dist/index.js)需要@deepseek-ai/schemastery(公共 npm 包,npm i会自动装)。
一条命令启动(连好线就能用)
./tools/start-g1.sh -d # 前置自检 → 起 G1 桥 → 起上位机 → 打印就绪横幅
./tools/start-g1.sh --status # 看桥/上位机/端口状态
./tools/start-g1.sh --stop # 全停
前提:① 背包 USB 线插着(自动发现 IP);② 网线接 G1(本机 enp4s0 配 192.168.123.x);
③ 机器人已开机站立。启动后说「小飞小飞」→(机器人「嘟」一声)→「握个手 / 挥挥手 / 鼓个掌 / 抱一下 / 比个心 / 飞个吻 / 奥特曼…」。
30 秒上手(推荐形态:背包当耳朵,电脑当嘴)
# ① 背包侧:停掉厂商演示程序(它周期性地抢回 19199,会让常驻服务被反复踢)
adb -s <ip:5555> shell pm disable-user --user 0 com.iflytek.aiint.app.speechassistant
# 想恢复:换成 pm enable(aiui_device enable_demo 同款)
# ② 上位机常驻:语音唤醒/人脸唤醒都只「嘟」一声(电脑出声),云端回复也从电脑出
node mcp/robot-host.mjs --robot <机器人IP> # --robot 一次配齐:声音 →:19177,动作 →:8266
node mcp/robot-host.mjs # 只在本机跑(动作 POST 到本机收据端,便于验证)
# ③ 看它到底听见了什么、设备上报了什么
curl -s http://127.0.0.1:8267/status
curl -s 'http://127.0.0.1:8267/events?kind=keyword,transcript&limit=20'
curl -X POST http://127.0.0.1:8267/control/stop # 手动打断
架构:为什么「声音出电脑、背包只负责听」
真机实测(2026-09-19):背包喇叭一响,它自己的麦克风就聋了 —— 播报期间设备 0 条识别、0 条唤醒事件
(连"每 2 秒插 600ms 静音"的变体也一样)。原因是我们的播放走 tinyplay 直写 ALSA,绕过了引擎的回声消除,
引擎拿不到参考信号、也消不掉自己的声音。于是「播放中说唤醒词打断」在"声音从背包出"的架构下不可能成立。
把回复与提示音都挪到电脑后:背包全程静音 → 麦克风干净 → 唤醒词随时听得见 → 打断成立。 提示音改用哔声(不是语音):电脑的声音被麦克风拾回去也转不成句子,既保留"听见就说话"的确认感, 又断掉一个回声源。云端回复(SSE 流式文本 → 设备音频帧)仍在背包侧识别、在电脑侧播放。
实现细节与完整实测记录见工作区文档 LOCAL-SETUP.md §16/§17 与 CHANGELOG.md。
接入宇树 G1:语音口令 → 真机动作
g1/g1_bridge.py 是动作执行端:上位机把 aiui.robot.actions/1 序列 POST 过来,桥按
g1/g1_action_map.json 翻译成 G1 原语,走 DDS 下发(PC 与 G1 网线直连/同网段,domain 0)。
# 本机网口配到机器人网段(G1 默认 192.168.123.161,PC 配 192.168.123.x)
ip -br addr show enp4s0 # carrier=1 才算链路通
ping -c2 192.168.123.161
./tools/start-g1.sh -d # 桥(真机)+ 上位机(动作指到桥)
grep -a "机器人动作表" tools/g1-bridge.log # 桥会把机器人**实际**支持的动作表打出来
BRIDGE_DRY_RUN=1 ./tools/start-g1.sh -d # 不碰真机,全链路联调(推荐先跑这个)
python3 g1/g1_bridge.py --self-test # 只校验映射表 + SDK 可导入
当前支持的动作(16 个手势 + 5 个底盘动作,全部真机验证过其中 6 个)
| 类别 | action |
|---|---|
| 手势 | shake_hand wave clap high_five hug heart right_hand_up both_hands_up refuse fly_kiss ultraman_ray(+ nod shake_head bow salute dance 机器人无对应动作,回执标 unmapped) |
| 底盘 | forward backward left right stop(受运动模式门控,见下) |
- 一个动作对应多个 G1 动作时随机选一个(整轮挑一次,日志可追溯):
wave→ 25 头下挥手 / 26 头上挥手;heart→ 20 双手比心 / 21 右手比心;fly_kiss→ 11/12/13 飞吻 - 每个动作都带识别抖动变体(如
鼓个长→鼓掌、播歌手/我个傻→握手),并有模糊兜底兜住没登记过的新变体 - 云端要产出这些 id,需要同步改插件槽位 enum 与后处理模板:见
docs/cloud-plugin-update-1.1.md
映射(g1/g1_action_map.json,改文件不用改代码)
| 云端 action | G1 原语 | 说明 |
|---|---|---|
shake_hand |
arm.ExecuteAction(27) → 等 3.5s → ExecuteAction(99) 收臂 |
真机确认 id 27 = shake_hand |
wave |
arm.ExecuteAction(25) → 收臂(可改 26 = 头上挥手) |
真机 25 = wave_under_head、26 = wave_above_head |
forward/backward/left/right |
loco.Start()(首次)→ SetVelocity(vx,vy,vyaw,duration=秒) → StopMove() |
速度 0.3/0.2 m/s、±0.6 rad/s,可用 --no-loco 关闭 |
stop |
loco.StopMove() |
|
nod/shake_head/bow/salute/dance |
—— | G1 SDK 无现成动作 → 回执 status:'unmapped',不执行不报错 |
params.times会循环执行(每次等完整演示时长 + 1s 间隔),默认上限 5(--times-cap可调,防云端误识别连挥几十次)- arm 调用返回码非 0 时回落
LocoClient.WaveHand/ShakeHand;动作结束一定收臂,不留悬空姿态 - 回执契约与
mcp/robot-agent.mjs一致(先回执、后台执行:上位机 5s 超时是硬编码,收据必须秒回)
握手 / 挥手:一次语音的完整实现链路
真机已跑通(2026-09-20):「小飞小飞」→ 机器人「嘟」一声 →「握个手」→ G1 真握手;「头下挥手」「头上挥手」同理。
| # | 环节 | 实现 | 关键设计 |
|---|---|---|---|
| 1 | 听见 | 背包 4 麦阵列 → 唤醒词(ivw) → 19199 推 keyword/wakeup |
dist/wake-prompts.js:每次唤醒都「嘟」一声(放机器人喇叭,28ms);播放中说唤醒词 → 打断 |
| 2 | 转写+语义 | 云端 AiChain → iat_aichain(文本)+ nlp_aichain(意图插件) |
事件按 cid 认回合(sid 是整个会话的,会误伤) |
| 3 | 抽动作 | dist/planner.js extractCloudActions → 真机命中形态 cloud-slots:instruction_order / gesture_action / gesture_times |
与云端后处理模板 docs/templates/robot_action_postprocess.j2 的 META 表等价且互相校验 |
| 4 | 造序列 | dist/actions.js buildSequence → aiui.robot.actions/1(含 intent/slots 原文) |
底盘动作自动补 stop;times 原样带给执行端 |
| 5 | 派发 | dispatchSequence → POST http://127.0.0.1:8266/robot/actions |
上位机超时 5s 是硬编码 → 桥先回执秒回、后台执行 |
| 6 | 执行 | g1/g1_bridge.py:ExecuteAction(27)(握手)/ExecuteAction(25)(头下挥手)→ 等 3.5s → ExecuteAction(99) 收臂 |
params.times 循环(每次完整动作 + 1s 间隔,上限 5);arm 返回码非 0 回落 LocoClient.ShakeHand/WaveHand;动作结束必收臂,不留悬空姿态 |
| 7 | 兜底 | 云端没给结构化结果 → 本地规则 planFromText |
精确变体表(真机实录「播歌手/我个傻/我的照」→ 握个手)+ 模糊兜底(只对手势!短句 ≤8 字、相似度 ≥0.6,命中在 warnings 标低置信) |
真机日志(节选)
[host] 唤醒词命中(语音唤醒,设备状态=工作中):电脑播/机器人播「哔声」
[host] 识别:握 个 手 。
[host] 动作序列 act-20260920T205551-0004(cloud-slots):挥手
线上证据:✅ 已确认走云端 appid=… 插件=robot_action 形态=cloud-slots
[host] 已转发到机器人 http://127.0.0.1:8266/robot/actions
[g1] 收到序列 …(1 个动作,utterance='握 个 手 。')
[g1] 序列 … 执行完(7401ms):shake_hand=executed
提示音放机器人喇叭(默认,AIUI_PROMPT_TARGET=robot):唤醒/人脸时由 G1 自己「嘟」一声 ——
POST http://127.0.0.1:8266/robot/cue?name=beep(DDS AudioClient.PlayStream 直推 16bit PCM,
实测 28ms;电脑侧每条提示音要新起一个 ffplay,几百毫秒,且和用户第一个字叠一起会毁掉口令)。
桥不在时会自动回落到电脑出声。机器人音量由桥 --cue-volume(默认 80)设定。
真机实测到的动作表(GetActionList(),比 SDK 源码注释更权威):
握手 27、头下挥手 25、头上挥手 26、转身挥手 1、鼓掌 17、击掌 18、拥抱 19、比心 20/21、
摆手拒绝 22、右手举高 23、奥特曼光线 24、双手举高 15、飞吻 11/12/13、前推 36、
以及 4 段自定义轨迹(Waist_Drum_Dance 9.5s、Scratch_head 8.1s、Spin_discs 6.9s)。
想加新口令 → 在映射表里加一条即可(nod 这类没有的动作将来可用 rt/arm_sdk 自定义轨迹补)。
环境坑:
cyclonedds 0.10.2的 C 扩展在 Python 3.14 上是坏的(undefined symbol: _Py_IsFinalizing)。tools/start-g1.sh默认用 conda 环境unitree_g1_vibe(Python 3.10,实测 SDK 可导入),G1_PYTHON可覆盖。
经验总结:真机踩过的坑(一句话版)
| 现象 | 结论 |
|---|---|
| 唤醒提示音只在第一次响 | 连续交互模式下设备只发 keywords(不发新 wakeup),提示音要挂在关键字事件上 |
| 「意图插件突然不命中」 | 多半是我们播的声音被拾回去污染了识别;用"我们播过的话"文本比对丢回声 |
| 「唤醒词打断失效」 | 「还在响」不能看播放窗(长回答分片之间会闪断),要看播放器进程是否还活着 |
| 「两条语音同时说」 | 开新的声音前必须先停旧的;按 cid 认回合(每轮唯一;sid 是整个会话的,会误伤) |
| 播放没声音但退出码 0 | tinyplay 只认 ≥2 声道,单声道静默丢弃;另需 tinymix 打开喇叭模拟通路 |
| 官方 TTS 文本通道没反应 | 本批固件 mmsp.close_aiui=true(经典 AIUI 关着)→ 只回 10120,一个音频帧都不回 |
| 官方音量包没效果 | {"type":"voice"} 管不到 tinyplay(直写 ALSA);音量得用 tinymix 'Output 2 Playback Volume' |
| 设备界面音量条没效果 | 同上:那条只影响设备自身的播放体系 |
| 「用电脑麦克风收音」 | 不支持:CMD_WRITE + raw_audio 在官方文档里只用于保存数据,拾音由 mmsp 独占(产品定义如此) |
| 常驻服务会整个退出 | 控制面一个坏参数曾把进程打挂;现在解析失败透传 + 全局异常兜底 |
| 唤醒了但说什么都没反应 | AiChain 会话状态机偶发卡死(引擎按「未唤醒」丢音频)→ 120s 内 3 次唤醒无识别自动「休眠→唤醒」自愈 |
| 说「握个手」机器人鼓掌 | 听歪成「播个手」后云端归错类 → 窄幅纠偏:文本精确命中另一命令时以文本为准 |
| 回答越聊语言越乱(韩/西/英) | 自己播的回答被麦克风拾回去形成自激 → 播报窗内识别不采信 + 回声回合不播回答 |
| 有时识别到了却不动 | 云端 answer 丢失 → VAD 兜底:说完 2.5s 无回答按识别文本走本地兜底 |
| 语音唤醒后识别乱/外语乱码 | 唤醒词自己的声音混进了识别流(抢说被按「未唤醒」丢、残留污染转写、解码脱轨成外语)→ 哔声后等约 1 秒再开口 + 前缀剥离 + 连续乱码自动复位会话 |
| 人脸唤醒识别明显更准 | 会话在你开口之前就由视觉打开、音频里没有唤醒词——结构性优势,推荐用法;一次唤醒后 ~10s 内可连续说指令 |
| 想知道动作是谁触发的 | robot-host.jsonl 的 source 字段:cloud-slots=云端意图插件命中,local-rules=本地兜底(两路通向同一动作);/status 的 wakeStats 看两种唤醒各自的乱码率 |
真机验证过的稳定口令(两种唤醒下都稳,识别句中包含即触发):握手 / 拍拍手 /
比个心 / 挥挥手 / 拥抱。「握个手」这类说法常被听歪(播歌手/我个手),虽有多层兜底,
演示场景请优先用上面的稳定口令。
背包 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.1.199"; node mcp\server.mjs --self-test
# → [aiui] host 192.168.1.199 → 192.168.1.100,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.1.100: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,导致常驻服务被反复踢
adb.exe -s <USB序列号> \
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.1.199,host 仍连到192.168.1.100(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)。
唤醒应答(默认从电脑出声):通道被我们占着时背包自己没有任何反馈,人不知道该何时开口。
| 场景 | 出声位置 | 说明 |
|---|---|---|
| 说唤醒词 | 电脑播「我在」 | 每次都响(不只第一次);回复播放中会说唤醒词则打断播放并复位到「待唤醒」 |
| 检测到人脸 | 电脑响电子哔声(880Hz/0.18s,不带文字) | 带 15s 抑制窗口(人在镜头前不动时不会哔个没完) |
| 云端回复(插件命中 / 闲聊) | 电脑播(设备侧从来不出声,音频是推给 19199 上位机的) | 可再转发给机器人音箱,见下 |
| 语音背包 | 完全静音 | 设备喇叭通路被 HAL 关着,见 §9.6 |
由 dist/wake-prompts.js 的 attachWakePrompts() 统一挂接 —— 常驻服务、ZCode MCP 会话、
看 JSON 的工具三条路共用同一套逻辑(避免三份实现漂移)。它自己解析 ffplay 播 wav / 合成哔声,
ffplay 不可用时回落本地单音(提示音是「该说话了」的唯一信号,不能没有)。
提示音回声会被过滤掉。 我们播的声音会被背包麦克风拾回去、被云端当成用户说的话 (真机实录:播「我在」→ 云端识别「我 在 」并回答;播「我注意你好久了」→「可 注 意 你 就 了 」)。 判断以文本比对为主(相似度 ≥0.5 或互相包含),所以打断后立刻说的正常指令(「握手」等) 不会被误伤;被打断回合的报文按 sid 抑制。
- 换词:
--wake-prompt zaide(在的)/--face-prompt face_awake(换回人声 wav);关掉:--no-wake-cue - MCP 侧用环境变量控制:
AIUI_WAKE_CUE=off/AIUI_WAKE_PROMPT/AIUI_FACE_PROMPT - 唤醒词永远听得到:设备端唤醒引擎常开,不受任何过滤影响 —— 它就是打断的入口
- 为什么不从背包喇叭出声:实测是哑的(
tinyplay退出码 0 但听不见),原因是设备 card1 的 模拟输出通路被 Android HAL 关着。--prompt-target device|both仍保留,但要先做混音器路由才行, 详见本工作区LOCAL-SETUP.md§9.6
也可以 curl -X POST http://127.0.0.1:8267/control/wake 远程唤醒并试听。
对背包说「小飞小飞」→「握个手吧」,终端会打出:
══ 19:52:03 收到语音「握 个 手 吧 。」
线上证据:✅ 已确认走云端 appid=<你的APPID> 插件=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(处理一轮就退出,脚本化验证用)、
--wake-prompt / --face-prompt(换唤醒应答的词)、--prompt-target host|device|both、
--face-prompt-cooldown <秒>、--no-wake-cue、--no-device-audio。
把两样信息传给机器人(声音 + 动作 JSON)
用户要传给机器人的是两样:① 云端回复的声音数据(机器人播出来)② 动作 JSON(机器人照着做动作)。 机器人侧一个文件全收(零依赖,可直接 scp):
# 机器人侧(谁出声、谁做动作就装谁身上;机器人没到可先在本机跑一份验证链路)
node mcp/robot-agent.mjs # 声音 19177 + 动作 8266
node mcp/robot-agent.mjs --test-tone # 先自检本机音箱通路
node mcp/robot-agent.mjs --exec-base 'http://127.0.0.1:{port}/do?action={action}' # 接真实硬件
# 上位机侧:一条命令配齐两样信息的去向
./tools/start-host.sh -d --robot <机器人IP>
--robot <ip> = --robot-audio <ip>:19177 + --robot-url http://<ip>:8266/robot/actions。
- 声音:一条 TCP 连接 = 一轮话,首行 JSON 声明格式,之后裸 PCM,FIN 收尾。
上位机侧实现在
dist/audio-forward.js,转发与本地播放互不依赖(--no-tts只关本机播放)。 MCP 会话里用$AIUI_ROBOT_AUDIO=host:port即可。 - 动作:
POST /robot/actions收整份序列,遍历actions[]按action/port/params/seconds执行。 回执立刻返回、动作在后台按顺序跑(握手 3s 若等待会把回执拖过上位的 5s 超时)。 默认只记日志(模拟),--exec-base给了就转发给机器人自己的硬件接口;回执格式与dist/robot-stub.js一致,上位机侧不用改。 - 网线直连的网络准备见
./tools/direct-link.sh(status / plan / apply / test)。
看云端返回的 JSON
要拿云端结果做二次开发(或看它到底返回了什么),用这个——只打结构化帧,音频静音:
npm run cloud-json # 或本机包装脚本 ./tools/watch-cloud.sh
npm run cloud-json -- --no-actions # 只看云端原始 JSON,不看本插件转的动作序列
它打三种帧:iat_aichain(云端 STT)、nlp_aichain(answerSource / intent / slots / answer)、
以及本插件转成的 aiui.robot.actions/1 动作序列(与交给机器人的是同一份)。
同时追加写入 cloud-json.jsonl(kind = keyword|asr|nlp|sequence,每条带 raw 原样报文)。
{ "answer": "好的,前进。", "answerSource": "plugin", "intent": "robot_action",
"text": "向 前 走 。", "nlpTimeConsuming": 461, "sid": "<你的APPID>@...",
"slots": { "instruction_order": "forward", "move_action": "forward", "gesture_times": 1, "tool": "robot_action" } }
报文解析在 dist/cloud-format.js;node tools/verify-cloud-format.mjs 用真机报文夹具守着它(5/5)。
会话内触发
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 aiui-action # ≥0.7.0
# 常驻上位机:连背包 → 语音 → JSON 动作序列 → 转发给机器人适配器
node node_modules/aiui-action/mcp/robot-host.mjs --robot-url http://127.0.0.1:8266/robot/actions
方式二:拷贝源码(要跑测试/改代码时用这个)
把 Windows 上的 aiui-action/ 整个目录拷到 Ubuntu(如 /opt/aiui-action),然后:
cd /opt/aiui-action && 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] 唤醒(人脸/视觉(未伴随唤醒词)):电脑播「人脸·noticed_you」——直接说指令即可,不需要唤醒词
curl .../status 的 lastWakeSource 会给出 voice / face。
2026-09-18 复核:当天日志里 face wake up : true 出现 1017 次、face count :1(检出人脸)972 帧,
即人脸唤醒确实一直在触发(不需要额外去"打开"它)。
实测注意(10:36 那次):人脸唤醒后云端会话可能很快被丢弃 (日志
Drop active cid for sleep,建起来约 0.4 秒就结束)。 所以听到提示音后要立刻说指令;人脸唤醒不是"一直待命"。 另外:厂商演示程序那个「图像理解迎宾」会主动打招呼,我们现在禁用了它, 所以提示音就是唯一的开口信号 —— 别把它关掉。
「有时能触发、有时不触发」是怎么回事(真机实测)
先看日志里云端听成了什么 —— 上位机现在会把每轮的识别文本打出来:
[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.1.100 | 背包 IP(自动发现关闭或失败时的回落值) |
port |
19199 | AIUI 控制通道(单会话,后被连者踢前者) |
adbSerial |
192.168.1.100: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 截屏/录屏保存目录;留空用系统临时目录 aiui-action/ |
autoReconnect |
true | 断线自动重连(含 5s 心跳、12s 静默重连) |
底盘接入
把底盘控制做成一个 HTTP 服务,配置 motionWebhookUrl 即可:
// 示例(Node):接收 aiui-action 的运动指令 → 转成你的底盘协议
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_aiui-action_aiui__<tool>(8 个)。差异与装卸步骤见
ZCODE.md。
License
MIT