npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

aiui-action

v1.4.0

Published

讯飞语音背包(iFlytek AIUI / ZC-H358S)语音 + 机器人动作插件:19199 协议客户端、唤醒/识别/语义、官方 TTS 通道、动作序列下发 + 宇树 G1 执行桥、设备运维、USB 自动发现背包 IP。

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

  1. 发现优先:每次(重)连前经 USB 读背包实际 IP,同网段才用——配置里的 host 只是回落值。 实测把 AIUI_HOST 故意写成错的 192.168.1.199,host 仍连到 192.168.1.100(via=usb-discover)。
  2. 首连重试:以前首次拨号失败只打一行日志就闲置了(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