Skip to content

aiui-action

Verified

aiui-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(本机 enp4s0192.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-slotsinstruction_order / gesture_action / gesture_times 与云端后处理模板 docs/templates/robot_action_postprocess.j2 的 META 表等价且互相校验
4 造序列 dist/actions.js buildSequenceaiui.robot.actions/1(含 intent/slots 原文) 底盘动作自动补 stoptimes 原样带给执行端
5 派发 dispatchSequencePOST http://127.0.0.1:8266/robot/actions 上位机超时 5s 是硬编码 → 桥先回执秒回、后台执行
6 执行 g1/g1_bridge.pyExecuteAction(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.jsonlsource 字段:cloud-slots=云端意图插件命中,local-rules=本地兜底(两路通向同一动作);/statuswakeStats 看两种唤醒各自的乱码率

真机验证过的稳定口令(两种唤醒下都稳,识别句中包含即触发):握手 / 拍拍手 / 比个心 / 挥挥手 / 拥抱。「握个手」这类说法常被听歪(播歌手/我个手),虽有多层兜底, 演示场景请优先用上面的稳定口令。

背包 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 / 重启后直接跑就行。 常驻服务在两条路径上都会自己跟上:

  1. 发现优先:每次(重)连前经 USB 读背包实际 IP,同网段才用——配置里的 host 只是回落值。 实测把 AIUI_HOST 故意写成错的 192.168.1.199,host 仍连到 192.168.1.100via=usb-discover)。
  2. 首连重试:以前首次拨号失败只打一行日志就闲置了(AiuiClient 的自动重连只覆盖"连上后掉线"), 于是背包还没联上网时启动 host 会永远连不上,看起来"已就绪"其实是死的。 现在失败会按 3s→4.8s→…→15s 封顶重试,背包一上线自动连上,并打印当前目标与发现情况。

排查用 curl http://127.0.0.1:8267/statusresolvevia / 发现的 IP)。 若日志出现「与本机不在同一网段」,说明本机和背包不在一个网——把本机网段调到与背包一致即可 (本次实测就是本机跑在手机热点 192.168.43.x、背包在 192.168.0.x,怎么重试都连不上,这不是 bug)。

唤醒应答(默认从电脑出声):通道被我们占着时背包自己没有任何反馈,人不知道该何时开口。

场景 出声位置 说明
说唤醒词 电脑播「我在 每次都响(不只第一次);回复播放中会说唤醒词则打断播放并复位到「待唤醒」
检测到人脸 电脑电子哔声(880Hz/0.18s,不带文字) 带 15s 抑制窗口(人在镜头前不动时不会哔个没完)
云端回复(插件命中 / 闲聊) 电脑播(设备侧从来不出声,音频是推给 19199 上位机的) 可再转发给机器人音箱,见下
语音背包 完全静音 设备喇叭通路被 HAL 关着,见 §9.6

dist/wake-prompts.jsattachWakePrompts() 统一挂接 —— 常驻服务、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_aichainanswerSource / intent / slots / answer)、 以及本插件转成的 aiui.robot.actions/1 动作序列(与交给机器人的是同一份)。 同时追加写入 cloud-json.jsonlkind = 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.jsnode 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=truecae_mode="mmsp"min_face_w/h=100face_out_ms=800(即人脸唤醒由 mmsp 引擎负责,与厂商演示程序无关)。

判别唤醒来源:下发给 19199 的 wakeup 报文不带类型字段(人脸与语音的报文一模一样), 唯一可用的差异是语音唤醒会配对出现 sub=keywords 事件。上位机据此推断并直接报出来:

[host] 唤醒词命中(语音唤醒)
[host] 唤醒(语音):电脑播「我在」——听到就立刻说指令
[host] 唤醒(人脸/视觉(未伴随唤醒词)):电脑播「人脸·noticed_you」——直接说指令即可,不需要唤醒词

curl .../statuslastWakeSource 会给出 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 + adbutilspip 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