原始文档:飞书《PrimeHub 机器人技能:开发到上架通用手册》
https://bcn3gscsq9m8.feishu.cn/wiki/Iv94wYbm4iEazmk3yNjc8tYNn8c
空间:N1--机器人小组 | 最新修改:10月21日 | 抓取:2026-10-23
权威全量版本:02_开发工具包/primehub_dev_kit/SKILL_PUBLISH_GUIDE.md(飞书文档自称「本文的超集」,命令/失败码以它为准)。
本文件 = 飞书文档的骨架 + 与全量手册对齐的要点,方便一页速查。
飞书原文目录(结构即下面章节顺序):
心智模型:一个技能是什么
开发顺序总览
第 0 步:环境与资产盘点(5 分钟,可提前在家做)
第 1 步:注册 PrimeHub(提前在家做)
第 2 步:给 agent 喂机器人契约(成败的分水岭)
2.1 robot 对象 API(运行时自动注入的全局对象)
2.2 动作 ID 常用表(权威全量表在机器人上,不在任何 SDK 文档)
2.3 平台约束 6 条(实测物理事实,写代码前必看)
2.4 和 agent 协作的方式(模板只是起点,不是天花板)
第 3 步:vibe coding(复制示例改起)
第 4 步:打包 .rpk(1 分钟)
第 5 步:真机调试环(开发主循环,每轮 3 条命令)
第 6 步:验收检查表(全绿才发布)
第 7 步:提取登录账号的浏览器 token(一条命令)
第 8 步:上传 PrimeHub(一条命令,几十秒后全场可搜)
附录 A:常见坑速查
附录 B:示例能力对照心智模型:一个技能是什么
技能包 = zip 改名 .rpk,固定结构:
你的技能/
├── cfg/pkg.yaml # 包名、版本、作者、机型(q1/t1)
├── src/main.py # 技能逻辑(纯 Python stdlib)
├── readme.md # 说明(可选)
└── media/ # mp3/mp4 资源(可选,原样落盘)执行时机器人上的 lumina 运行时拉起 src/main.py 并注入全局对象 robot——说话、动作、表情、灯带、音频全用它。 没有 rclpy、没有 ROS2 节点、没有相机抓帧。(ROS2 SDK 是另一条路,见 03_官方工作台与资料包)
开发工具包 dev kit(7 个文件):
| 文件 | 作用 |
|---|---|
example-jukebox/ | 点歌台完整源码(技能基底,复制改起) |
SKILL_PUBLISH_GUIDE.md | 全流程手册(飞书文档的超集,细节字典) |
pack.py | 打包器:技能目录 → .rpk |
deploy_test.py | 真机调试 harness |
extract_token.py | 从 Firefox 登录态提取发布凭证 |
primehub_publish.py + primehub_client.py | 发布/搜索/删除客户端 |
第 0 步:环境与资产盘点(5 分钟)
git clone https://github.com/miracle-techlink/primehub_dev_kit
cd primehub_dev_kit
pip install gmssl
python3 primehub_publish.py search "你好" # 自检:能返回结果 = 网络+传输层 OK
sshpass -p '1' ssh -o StrictHostKeyChecking=no run@<IP> 'echo OK' # 机器人连通
sshpass -p '1' ssh run@<IP> 'curl -sI --max-time 8 https://www.baidu.com | head -1' # 联网类技能才需要机器人 SSH 用户 run、密码 1(现场分配 IP)。
第 1 步:注册 PrimeHub(提前在家做)
用本机 Firefox 打开 https://primehub.primebot.com → 手机号 + 验证码注册/登录 → 随便点一个页面(确保 token 写入 localStorage)。
为什么必须 Firefox:发布 token 只能由验证码登录换取,Firefox 把它落盘到 localStorage,第 7 步的 extract_token.py 只认 Firefox 的落盘格式。token 7 天有效。
第 2 步:给 agent 喂机器人契约(成败的分水岭)
AI agent 默认按 ROS2/rclpy 常识写代码——装上必炸(ImportError)。先喂契约再让它写。
2.1 robot 对象 API
robot.play_tts(text) # 说话;异步无完成回调 → 必须轮询等播完
robot.play_animation(anim_id, interrupt) # 预设动作;返回 bool(True=已受理≠已执行)
robot.play_audio_file(path) # 播音频;板载只吃 wav(mp3 无声)
robot.play_light(color, mode) # 灯带(mode 3 律动)
robot.play_emotion(emotion_ids, ...) # 屏幕表情
robot.get_audio_play_state() # → {'pkg_name':..,'state':{'value':1播放中/2空闲}}
robot.get_t1_robot_form() # T1 专属:'biped'/'quadruped'
robot.wait("time", seconds) # 等待2.2 动作 ID 常用表(权威全量表在机器人上)
# Q1:58 个动作(3000 系),上架技能只用 upper_body
sshpass -p '1' ssh run@<Q1_IP> 'cat /robot/software/task_engine/bin/cfg/motions/q1/upper_body.yaml'
# T1:1-72
sshpass -p '1' ssh run@<T1_IP> 'cat /robot/software/task_engine/bin/cfg/motions/t1/*.yaml'- Q1(室内模式只用 upper_body):3001 招手 / 3002 握手 / 3003 碰拳 / 3004 挥手 / 3005 开心 / 3007 比心 / 3008 加油 / 3010 伸懒腰 / 3012-14 讲话 / 3046-48 舞蹈 A/B/C / 3052-57
- T1:1 打招呼 / 2 握手 / 6 比心 / 7、10、11 跳舞 / 20-22 拍照姿势 / 26 喜悦 / 31 欢呼 / 43 律动 / 52 太空步
2.3 平台约束 6 条
- Q1 室内模式(
motion_mode=0)禁全身动作(reason 24)→ 上架技能只用 upper_body ID - T1 四足形态只能用 1/7/26/43(其余 reason 19)→ 用
get_t1_robot_form()分支 - 音频互斥:
play_tts停掉当前音频且不自动恢复;play_animation(interrupt=True)也停音频 → 台词和音乐别同时发;要音频持续时一律interrupt=False - 音频状态对长音频 1↔2 抖动 → 判播完别信单次采样,轮询稳定空闲或按已知时长计时
- 时长感:TTS 起声 ≈0.5s、4-6 字 ≈1.1s;anim 受理→真正起跳 ≈0.3s
- stdout 必须
flush=True,否则调试看不到 print
2.4 和 agent 协作(两种开场 + 推荐循环)
两种典型开场白(直接把 dev kit 丢给 agent):
读 SKILL_PUBLISH_GUIDE.md 的 §2 契约。基于 example-jukebox/ 复制改造,改成:〈你的玩法一句话〉。
约束:只用 upper_body 动作、纯 stdlib、遵守 §2.4 平台约束。我要做一个〈玩法描述〉的机器人技能,请你自己读 dev kit 里的 SKILL_PUBLISH_GUIDE.md §2,
按契约设计实现;有多个方案就先给我方案再写代码。推荐循环:① 先让 agent 复述它理解的契约(答不出 rclpy 之外的写法就会写废代码)→ ② 出初版跑调试环,把 final/stdout/error 原样贴回 → ③ 观感问题用自然语言提(「台词太长」比手改代码快)。
第 3 步:vibe coding(复制示例改起)
cp -r example-jukebox my-skill保底骨架(speak() 轮询 + flush=True 两个坑必须保留):
import time
STEPS = [(3001, "台词一", 3), (3007, "台词二", 3)] # (动作ID, 台词, 保持秒)
def speak(text):
robot.play_tts(text)
deadline = time.time() + max(len(text) * 0.35, 3.0) + 8.0
time.sleep(0.3)
while time.time() < deadline:
if robot.get_audio_play_state().get("state", {}).get("value") == 1: break
time.sleep(0.12)
while time.time() < deadline:
if robot.get_audio_play_state().get("state", {}).get("value") != 1: break
time.sleep(0.12)
speak("开场白")
for anim, line, hold in STEPS:
speak(line)
robot.play_animation(anim, interrupt=True) # 无音乐并行时允许打断
time.sleep(hold)
speak("收场白")
print("MY_SKILL_DONE steps=%d" % len(STEPS), flush=True)技能形态不止「台词+动作」:状态机(合影导演流程控制)、事件循环(点歌台节拍网格)、API 拉数据(资讯播报)、随机池(随机舞步)示例里都有模块可抄。
agent 三条硬约束(当 lint 用):纯 stdlib + robot 全局对象(不 import);只用查到的动作 ID;台词必须走 speak() 轮询、音频并行按约束 3 处理。
第 4 步:打包 .rpk(1 分钟)
python3 pack.py my-skill
python3 -m zipfile -l my-skill.rpk # 校验:必须含 cfg/pkg.yaml 与 src/main.pymedia/ 可选;包体控制在 200MB 内(上传限制)。
第 5 步:真机调试环(开发主循环)
sshpass -p '1' scp -o StrictHostKeyChecking=no deploy_test.py run@<IP>:/tmp/ # 一次
# 每轮 3 条
python3 pack.py my-skill
sshpass -p '1' scp my-skill.rpk run@<IP>:/tmp/
sshpass -p '1' ssh run@<IP> 'python3 /tmp/deploy_test.py /tmp/my-skill.rpk 300' # timeout ≥ 技能总时长输出判读:
| 现象 | 含义 |
|---|---|
final: success + stdout 到 DONE | 脚本逻辑通 |
final: failed + error: ImportError... | 契约写错,回 §2 |
final: timeout | harness timeout 没给够(默认 120 常常不够) |
| stdout 空白 | print 忘了 flush=True |
动作到底触没触发(stdout 只证明「受理」):
sshpass -p '1' ssh run@<IP> 'grep "Animation task成功" /robot/persist/log/task_engine/task_engine.log | tail -8'失败 reason 表:24 室内模式禁全身 / 19 形态不符 / 42 上一动作占队列 / 28 动作不可打断 / 17 动作 ID 不存在。
清理:curl -s -X DELETE 127.0.0.1:58080/packages/my_skill(其它:GET /packages、GET /tasks/running、DELETE /tasks/{id})。
第 6 步:验收检查表(全绿才发布)
deploy_test.py一轮final: success- stdout 到达 DONE 标记(有 flush)
- 动作日志里每个预期动作 ID 都「创建成功」,无 24/19/42/28 拒绝
- 台词全部实际播出(无吞句)
- 音频类技能:音频从头活到尾
- 连跑两轮不炸
- 安全:机器人周围 1 米清空测试过
第 7 步:提取 token(一条命令)
python3 extract_token.py # → /tmp/ph_token.txt(JWT,7 天有效)原理:找 Firefox profile 的 storage/default/https+++primehub.primebot.com/ls/data.sqlite → 拷贝快照(运行中会锁库)→ 查 skillhub_token → 纯 stdlib 解 snappy → 写 /tmp/ph_token.txt。
失败判读:没找到 Firefox 的 primehub localStorage → 没用 Firefox 登录;没找到 skillhub_token → 登录态过期,重登录再跑。
第 8 步:上传 PrimeHub(一条命令)
python3 primehub_publish.py publish my-skill.rpk '{
"skillName": "我的技能名",
"version": "1.0.0",
"skillDesc": "一句话简介(30 字内最好)",
"long_description": "详细说明(详情页显示)",
"categoryIds": [104], "categoryCodeList": ["action"],
"tagsIds": [127], "tagsCodeList": ["companion"],
"robotModelIds": [100], "robotModelCodeList": ["robot_q"],
"iconUrl": "skill-music.svg"
}'
# 成功回 {'code': 1000, 'msg': '成功'}字典 ID 速查(双字段必填,缺了报 1001):
| 项 | ID → 值 | codeList |
|---|---|---|
| 分类 | 102 视觉 / 103 语言 / 104 动作 / 105 听力 / 106 规划 / 107 表情 | vision / language / action / hearing / plan / expression |
| 标签 | 115 音乐 / 124 拍照 / 127 陪伴 / 20 亲子 | music / take_photo / companion / parent_child |
| 机型 | 100 机器人Q / 101 机器人T | robot_q / robot_t |
| 图标 | skill-music / skill-video / skill-radio / skill-game / skill-verify | 同名 .svg |
发布后验证:primehub_publish.py search "我的技能名"、primehub_publish.py my;终极验证=官方 App 技能市场搜到 → 下载部署 → 点运行 → 机器人真演一遍。
管理:primehub_publish.py my / delete <id>;同名再 publish 不覆盖,会新建一条 → 换版先 delete 再 publish。
API 坑:本机 urllib 打 primehub 会被掐 SSL → 用 curl 子进程;真实前缀 /app-api/content-server/(发布)、/app-api/search-server/(搜索);搜索参数是 keywords/page/rows;传输层 SM4-CBC(primehub_client.py 已实现)。
附录 A:常见坑速查
| 症状 | 根因 | 修法 |
|---|---|---|
| 装了技能没反应 | 用了 rclpy/ROS2 契约 | 改用 robot 对象契约 |
| 动作全部不执行 | whole_body + 室内模式 | 换 upper_body ID |
| 部分动作没触发 | 编排表索引越界 | 检查索引 |
| 台词被吞/截断 | 没等上一句播完 | 用 speak() 轮询 |
| 音效放到一半消失 | 后发 TTS/动画 interrupt=True | 音频期间 interrupt=False |
| mp3 放了没声 | 板载只吃 wav | ffmpeg 转 wav |
| 看不到 print | 缓冲 | flush=True |
| deploy 显示 timeout | harness 超时 < 技能总时长 | 给足第二个参数 |
| publish 报 1001 | 字段名拼错/漏 codeList | 对照 §8 模板 |
| querySkillList 报 9999 | 参数名 pageNum/pageSize | 改 keywords/page/rows |
| 同名技能出现两条 | 平台不去重 | 先 delete 再 publish |
附录 B:示例能力对照(example-jukebox/)
| 想学什么 | 看哪里 |
|---|---|
| 台词轮询播完(所有技能的地基) | speak() |
| 联网取资源 | find_song() / download_song() |
| 音频播放与状态处理 | play_audio_file 调用处 + music_really_over() |
| 音频持续时动作怎么发 | 主循环 play_animation(anim, interrupt=False) |
| 持续随机动作池 | MOVE_POOL + next_move() |
| 换自己要的东西 | 默认热歌榜随机;固定曲目用 media/songlist.txt |