PrimeBot 启元 Q1 & T1 功能包工作台
Developer Playground · 先连接,再获取,最后操作

用 DSH 或 WorkBuddy,从 0 到 1 做出你的启元机器人功能包

先安装并连接一个 Agent,再打开本工作区获取模板,接着用自然语言让 Agent 帮你改出第一个技能。
这里同时提供 DSH / WorkBuddy 官方入口、可直接复制的 Agent 提示词、Q1/T1 示例和从模板到真机验收的完整路径。

启元机器人 Developer Playground

下载源码工作区,带到自己的电脑上实践

这是给 DSH / WorkBuddy 使用的最小源码包。里面有功能包模板、构建/测试/部署工具和两份 Agent 工作流; 不包含 HTML 教程、旧虚拟环境、Token 和已生成的 `.rpk` 成品。

下载 workspace.zip ↓ 解压后运行
python Tools/scripts/setup_workspace.py
START HERE

先学会连接、获取和操作,再让 Agent 帮你做功能包

给第一次接触 DSH、WorkBuddy 和机器人技能的人

你只需要先完成下面四步:安装一个 Agent、连接模型、把本工作区交给 Agent、用自然语言描述想让机器人做什么。DSH 和 WorkBuddy 都能完成这条路径;技术细节和现场命令放在后面的进阶区。

02

连接模型

在 Agent 的设置里选择模型并保存凭证。先用一个小问题测试“能回答、能读文件”。

查看连接提示词 →
03

获取工作区

把本目录作为工作区打开,确认 Agent 能看到 Example、MustRead 和 Tools。

查看资料地图 →
04

开始操作

先让 Agent 阅读规则,再让它改台词或流程,最后由它运行构建和离线测试。

复制第一条任务 →
推荐顺序 第一次建议从最简单的 社交能量补给站 开始:先改玩法、生成包、完成电脑测试,再考虑联网和复杂互动。真机连接、上传和两轮验收要在本机测试通过后进行。
以后连接真机时再看:同一局域网和机器人 IP

连接真机前必须先做:电脑和机器人加入同一个局域网连接前提

Agent 负责帮你读文件、改技能和运行命令,但它不能隔着互联网直接找到机器人。电脑执行命令时,必须能通过局域网里的 IP 地址找到机器人。最简单的理解是:电脑和机器人要连到同一个现场 Wi-Fi 或同一个路由器,才能互相“看见”。

1. 让两台设备进同一网络网络

电脑连接现场 Wi-Fi;机器人也连接这个 Wi-Fi。不要让电脑连手机热点、机器人连另一个热点,也不要使用访客网络隔离。

2. 找到机器人的 IP地址

从现场 App、机器人网络设置或导师提供的信息中找到类似 192.168.112.222 的 IPv4 地址。每次换网络后 IP 可能变化。

3. 先测试能不能找到它验证

在终端执行 ping 机器人IP。能收到回复,说明基础网络通常已经连通;完全没有回复时,先处理网络,不要急着改代码。

怎么看是不是同一网段? 例如电脑 IP 是 192.168.112.35,机器人 IP 是 192.168.112.222,前面三段相同,通常表示它们在同一个局域网。若一个是 192.168.1.x、另一个是 192.168.112.x,通常不是同一网段。最终以现场网络配置和实际连通测试为准。
Windows 终端:先测试 IP,再探测并运行技能
# 1) 把下面的 IP 换成现场机器人 IP
ping 192.168.112.222

# 2) 先连接、读取真机动作表并上传技能(不运行)
python Tools/scripts/robot_run.py --model q1 --ip 192.168.112.222 --skill energy_station

# 3) 确认周围 1 米安全、动作表正确后,再运行技能
python Tools/scripts/robot_run.py --model q1 --ip 192.168.112.222 --skill energy_station --run

连不上时按这个顺序排查: ① 两台设备是否连的是同一个 Wi-Fi; ② IP 是否是当前网络下的最新地址; ③ Windows 防火墙或现场网络是否禁止设备互访; ④ 机器人是否开机并完成联网; ⑤ 先让现场导师确认网络,再重复探测命令。不要把公网 IP、手机热点地址或电脑自己的 IP 误填成机器人 IP。

第二步 · 让 Agent 帮你制作

你要制作的是机器人功能包(.rpk),不是 Agent 的 Skill

你说清楚想让机器人做什么,Agent 帮你生成可测试的安装包
先分清三个角色: 你提出玩法并确认安全;Agent读取工作区、修改模板、运行本机工具;机器人安装并执行最后生成的 .rpk 功能包。

DSH:把工作区交给 Agent

按官方说明完成安装并打开 DSH。第一次使用时只做三件事:启动 DSH、选择当前 workspace、确认它能看到项目资料。

  • 1. 打开官方入口,按页面提示安装。
  • 2. 启动 DSH,选择本 workspace。
  • 3. 让 Agent 先介绍它看到的目录。
查看 DSH 官方入口 ↗

WorkBuddy:在桌面工作台中操作

从官方入口下载安装 WorkBuddy。第一次使用时依次登录、连接模型、打开当前 workspace,然后让它确认目录和资料是否可见。

  • 1. 下载与你的电脑系统对应的版本。
  • 2. 安装后登录并完成模型连接。
  • 3. 打开本 workspace,再开始制作功能包。
查看 WorkBuddy 官方入口 ↗
你要告诉 Agent 什么机器人型号、想实现的功能、台词或互动流程,以及是否需要联网。
Agent 要交付什么一个基于现成模板修改的机器人功能包,以及电脑上的构建和测试结果。
你最后确认什么功能包名称、电脑测试结果、现场网络、动作安全和真机运行结果。

把下面提示词复制给 DSH 或 WorkBuddy,开始制作第一个机器人功能包:

请帮我制作一个启元机器人功能包。
我的创意:【填写,例如:生日祝福、倒计时合影、每日天气播报】
目标机器人:【Q1 / T1 / 还没确定】
是否需要联网:【不需要 / 需要】

请按这个顺序工作:
1. 先阅读当前 workspace 的 README.md、MustRead/01_从零开发Q1_T1技能_SOP.md 和 MustRead/02_机器人运行时契约与避坑必读.md;
2. 用一句话告诉我准备使用哪个现成模板,以及最终会生成什么 .rpk 功能包;
3. 优先从 Example/energy_station 开始,只有确实需要更多能力时再换更复杂的模板;
4. 先完成电脑上的修改、构建和测试,再告诉我结果;
5. 不要把电脑测试通过说成真机已经通过;
6. 最后列出:我改了什么、生成了什么、下一步如何连接机器人并验证。

实践环节:现在由你提出需求,Agent 负责实现动手做

不要照抄固定答案。请参与者自己想一个机器人功能,再把需求交给 Agent。 Agent 会根据对应的仓库 Skill 询问细节、选择模板、生成 `.rpk` 并完成电脑测试。

1. 选 Agent

使用 DSH 或 WorkBuddy,并打开当前 workspace。

DSH 功能包 Skill →
WorkBuddy 功能包 Skill →

2. 提出想法

例如:迎宾、生日祝福、抽签、拍照倒计时、天气播报或互动问答。

3. 回答追问

告诉 Agent 目标机型、台词、动作顺序、是否联网和完成标准。

4. 检查结果

确认生成了 `.rpk`,电脑测试通过,再决定是否连接真机。

参与者现在可以这样说: “我想做一个给来宾送祝福的机器人功能包。请先问我需要补充的信息,不要直接改文件。”
第一次成功的标准: Agent 能看懂工作区;你能说清楚想让机器人做什么;电脑里生成了对应的 .rpk 功能包;离线测试通过。到这一步之后,才进入“连接局域网、使用 IP、真机运行”的章节。
进入进阶内容:理解功能包、模板、构建、真机和排障

如果你只是第一次制作功能包,可以先完成上面的 Agent 任务和电脑测试,再回来阅读这里。 这一部分解释 .rpk 安装包的组成、Q1/T1 的运行规则、真机连接方式、现场验收和常见问题。

准备进入真机实践:选择机型、功能包和 IP 本区域给已经完成电脑测试的人使用。选择现场分配的 Q1/T1、准备安装的功能包,再填入机器人 IP,系统会生成对应的连接和运行命令。
01 · ANATOMY

先理解结果:你最后会得到一个 .rpk 机器人功能包

先看懂最终产物,再决定是否需要阅读后面的技术资料

1. 核心必读阁 保命必读

大白话闯关攻略、防炸机铁律与现场抢分清单

2. 技能剧本库 3套现成剧本

抄作业不丢人!改几句台词就能变身你的原创技能

3. 军火工具箱 自动化神器

影分身打包、0.4秒替身试毒与全浏览器顺 Token

4. 规则与报告室 铁证档案

飞书官方评分圣旨、交接指南与实测证据陈列馆

🤫 课代表悄悄话:神神秘秘的 .rpk 技能包到底是个啥? —— 其实就是把 .zip 压缩包改了个后缀名!扒开看里面就躺着这 3 样核心宝贝:

① 技能包户口本 身份登记

一共就 6 行字!写明唯一英文小名 package_name、版本号,以及这包是喂给谁吃的(robot_model: q1 或 t1,狗粮猫粮严禁混吃!)。

② 演出台词剧本 核心代码

机器人肚子里的老管家 lumina_server 会在拉起代码前,偷偷往全局变量里塞一个万能遥控器 robot,直接按遥控器就能说话跳舞!

③ 相亲简历说明 玩法攻略

夸夸你的技能有多好玩,顺便提醒围观群众退后 1 米(如果要放内置音效,可额外配备媒体库存放 .wav 音频)。

02 · CONTRACT

需要时再看:机器人运行规则与 Q1 / T1 安全动作

更详细的 API 签名与喂 AI 咒语见 机器人运行时契约与避坑必读 →

铁律 1 · 打死别写 import robot 或 rclpy 写必暴毙

万能遥控器 robot 是管家自动塞进全局变量的!你要是手欠写一行 import robot,Python 找不到模块当场给你表演 ImportError 原地去世!且只准导 Python 3.10 标准库(time、json、random、urllib.request)。

铁律 2 · 说话必须盯够 0.7 秒“真闭嘴” 治好吞字

robot.play_tts(text) 是个不管售后的急性子,调完 0.001 秒就返回!不等说完就发下一句会直接把上一句掐脖子憋回去。更调皮的是长句换气时状态会在 1↔2 假空闲抖动,必须盯到连续 0.7 秒稳定空闲(2)才算真说完!

铁律 3 · 动作传 interrupt=False 并乖乖等扭完 不抢麦克风

黄金节奏:先等嘴巴彻底闭上 ➔ 再调 robot.play_animation(id, False) ➔ 再 time.sleep(秒数) 等它扭完。要是传了 interrupt=True,正在播的声音会被一刀切掉!

铁律 4 & 5 · 机型动作别串门 + print 必加 flush 拒绝盲调

Q1(3000系)和 T1(1~72系)动作 ID 完全不通用!而且所有日志必须写 print(..., flush=True),否则全憋在内存缓冲区里,调试时屏幕比脸还干净。

🤖 Q1 保命口诀(专治傲娇拒绝码 reason 24):在普通用户室内模式(motion_mode=0)下,Q1 的腿被安全结界封印了——你非让它做全身动作(whole_body),底层硬件会直接甩给你一个冷酷的 reason 24!上架技能只准用上半身动作(upper_body,3000 系列),动动手照样帅翻全场:
3001 👋 招手(迎宾首选) 3002 🤝 握手 3003 🤜🤛 碰拳 3004 🙋 挥手 3005 😄 开心 3006 😅 擦汗 3007 🫶 双手比心(求赞必杀) 3008 💪 加油(打气首选) 3011 🙏 抱拳 3021 🫡 敬礼 3046 💃 舞蹈 A 3047 🕺 舞蹈 B
🐾 T1 保命口诀(专治四条腿没法比心 reason 19):T1 一会儿是两条腿站立的酷盖(biped),一会儿是四条腿趴着的修勾(quadruped)。要是它正四条腿趴着呢,你非逼它直立比心握手,它当场报 reason 19 罢工(四条腿怎么比心嘛,臣妾做不到啊!)!必须先调 robot.get_t1_robot_form() 问清形态,且四足下只认 1 / 7 / 26 / 43 四个动作!
✅ 双足站着 + 四足趴着通吃王炸动作(闭眼用,永不报 reason 19):
26 🎉 喜悦 (~5s,本项目主力) 43 🎵 律动 (~9s,摇头晃脑极有节奏) 1 👋 打招呼 (~15s,霸体慢动作不可打断) 7 💃 跳舞 (~33s,超长才艺秀)
🔹 仅限双足站立(biped)才准调用的动作:
2 🤝 握手 6 🫶 比心 10 / 11 🕺 舞蹈 20 / 21 / 22 📸 摆拍姿势 31 🙌 欢呼 52 🌙 太空步
03 · EXAMPLES

选择一个现成的机器人功能包模板

点击卡片链接即可查看带课代表逐段批注的源码精讲 · 查看剧本库总览 →

1. 启元百变互动官 👑 C位全能王

导览:魔改攻略 | 核心源码精讲 →

  • ✅ 英文动作别名(greet / cheer / heart 不用背数字)
  • ✅ 随机好运盲盒池(LUCKY_POOL 改词即变算命摊)
  • ✅ 联网金句 API + 拔网线 100% 自动切回离线保底词
  • ✅ 呼吸灯带气氛组联动 + 0.7s 防抖不吞字底座

2. 社交能量补给站 🛡️ 离线保底王

导览:项目说明 | 核心源码精讲 →

  • ✅ 全篇仅 78 行极简代码,30 秒从头看懂到尾
  • ✅ 勇气 / 灵感 / 快乐三格充电暖心小剧场
  • ✅ 零外网与外部音频依赖,现场首轮保底锁分首选

3. 官方点歌台 🎵 高阶炫技王

导览:项目说明 | 核心源码精讲 →

  • 🎵 联网搜歌断点续传下载 + ffmpeg 转 .wav
  • 🎵 纯标准库算音频包络与自相关 BPM 节拍
  • 🎵 适合想研究内置播音与卡点蹦迪的高手取经
《启元百变互动官》核心源码 · 👉 打开带课代表逐段拆解与行号的源码精讲页
"""启元百变互动官(Q1 / T1 通用全能技能模板,纯 Python 3.10 标准库)。

设计目标:
1. 开箱即用:可直接打包部署到 Q1 或 T1 真机运行,完成「迎宾 → 每日金句(联网+断网兜底) → 随机抽签互动 → 谢幕」完整闭环。
2. 极易改编:只需修改顶部的配置区(台词、动作别名、抽签池、是否开启联网/灯光),即可快速改成迎宾、算命、播报、小剧场等任意技能。
3. 严守契约:不导入 robot / ROS2,内蔵 TTS 0.7 秒防抖轮询与超时保护,自动适配 Q1 upper_body 与 T1 双足/四足动作白名单。
"""

import json
import random
import time
import urllib.request

# ==============================================================================
# 【第一部分:积木配置区 —— 改这里就能变成你的新技能!】
# ==============================================================================

# 构建脚本 build.py 会在打包时自动将此值替换为 "q1" 或 "t1"
ROBOT_MODEL = "q1"

# 功能开关
ENABLE_NET_QUOTE = False         # True=尝试联网拉取每日一言(失败自动用离线兜底);False=纯离线最稳模式
ENABLE_RANDOM_DRAW = True        # True=从 LUCKY_POOL 随机抽签;False=固定使用第一支签
ENABLE_MOOD_EFFECTS = True       # True=尝试联动呼吸灯带与面部表情(自带安全容错)

# 联网 API 配置与断网兜底金句
QUOTE_API_URL = "https://v1.hitokoto.cn/?c=i&encode=json"
FALLBACK_QUOTE = "代码写得稳,真机跑得准,每一次动手都是超酷的创造!"

# 随机互动签文池:(签文台词, 动作语义名)
LUCKY_POOL = (
    ("叮!为你抽到「真机丝滑上上签」!今天你的所有动作指令全部一次通过!", "cheer"),
    ("叮!为你抽到「脑洞大开幸运签」!你的创意全场最亮眼,评委看了都点赞!", "heart"),
    ("叮!为你抽到「零Bug欧皇签」!编译不报错,网络不超时,好运直接拉满!", "greet"),
)

# 主流程固定剧本:(动作语义名, 台词文本, 备注说明)
# 支持占位符:{quote} 会自动填入联网/兜底金句,{lucky_text} 会填入抽签结果
OPENING_LINE = "哈喽大家好!我是启元百变互动官,很高兴在活动现场见到你!"
CLOSING_LINE = "今日互动圆满完成!祝你玩得开心,作品大放异彩,我们下次见!"

# 语义化动作映射表:把好记的名字映射到 Q1 / T1 的安全动作 ID 和保持秒数 (动作ID, 等待秒数)
# - Q1 严格只用室内模式允许的 upper_body 动作(3000 系)
# - T1 四足/双足均默认使用四足白名单兼容动作(26 喜悦 / 43 律动),也可在 biped 下扩展
ACTION_TABLE = {
    "q1": {
        "greet": (3001, 6.5),   # 招手
        "cheer": (3008, 6.5),   # 加油
        "heart": (3007, 6.5),   # 双手比心
    },
    "t1_quadruped": {
        "greet": (26, 16.0),    # 喜悦(四足安全白名单)
        "cheer": (43, 16.0),    # 律动(四足安全白名单)
        "heart": (26, 16.0),    # 喜悦(四足安全白名单)
    },
    "t1_biped": {
        "greet": (26, 16.0),    # 喜悦(双足同样安全可用)
        "cheer": (43, 16.0),    # 律动
        "heart": (26, 16.0),    # 喜悦
    },
}

POLL_SECONDS = 0.1
IDLE_SECONDS = 0.7


# ==============================================================================
# 【第二部分:安全硬件底座 —— 防吞字轮询、形态校验、网络兜底(一般无需修改)】
# ==============================================================================

def audio_state() -> int:
    """读取并校验音频状态:1=播放中,2=空闲。"""
    snapshot = robot.get_audio_play_state()
    if isinstance(snapshot, dict):
        state = snapshot.get("state")
        if isinstance(state, dict) and state.get("value") in (1, 2):
            return state["value"]
    raise RuntimeError("AUDIO_STATE_INVALID")


def speak(text: str) -> None:
    """说话并等待真实播完(含 4 秒起声监控 + 0.7 秒防抖滤波 + 超时保护)。"""
    print("SPEAK " + text, flush=True)
    robot.play_tts(text)
    start_deadline = time.monotonic() + 4.0
    while audio_state() != 1:
        if time.monotonic() >= start_deadline:
            raise RuntimeError("TTS_NOT_STARTED")
        time.sleep(POLL_SECONDS)

    finish_deadline = time.monotonic() + max(len(text) * 0.45, 4.0) + 8.0
    idle_since = None
    while time.monotonic() < finish_deadline:
        if audio_state() == 1:
            idle_since = None
        elif idle_since is None:
            idle_since = time.monotonic()
        elif time.monotonic() - idle_since >= IDLE_SECONDS:
            return
        time.sleep(POLL_SECONDS)
    raise RuntimeError("TTS_NOT_FINISHED")


def trigger_mood(color: str = "#FF6B2B", mode: int = 3) -> None:
    """可选的灯带氛围触发(带安全容错,不影响主流程与离线替身测试)。"""
    if not ENABLE_MOOD_EFFECTS:
        return
    play_light = getattr(robot, "play_light", None)
    if callable(play_light):
        try:
            play_light(color, mode)
        except Exception as exc:
            print("LIGHT_SKIP " + str(exc), flush=True)


def fetch_quote() -> str:
    """获取今日金句:若开启联网则请求 API,超时或断网时 100% 自动回退到离线金句。"""
    if not ENABLE_NET_QUOTE:
        return FALLBACK_QUOTE
    try:
        req = urllib.request.Request(QUOTE_API_URL, headers={"User-Agent": "PrimeBot-Skill/1.0"})
        with urllib.request.urlopen(req, timeout=2.5) as resp:
            data = json.loads(resp.read().decode("utf-8"))
            hitokoto = data.get("hitokoto")
            if isinstance(hitokoto, str) and hitokoto.strip():
                return hitokoto.strip()
    except Exception as exc:
        print("NET_FALLBACK " + str(exc), flush=True)
    return FALLBACK_QUOTE


def resolve_action_profile() -> dict:
    """根据机型(Q1 / T1)与实时形态(biped / quadruped)选择安全动作表。"""
    if ROBOT_MODEL == "q1":
        return ACTION_TABLE["q1"]
    if ROBOT_MODEL == "t1":
        form = robot.get_t1_robot_form()
        if form not in ("biped", "quadruped"):
            raise RuntimeError("T1_FORM_UNKNOWN")
        print("T1_FORM " + form, flush=True)
        return ACTION_TABLE["t1_" + form]
    raise RuntimeError("ROBOT_MODEL_UNKNOWN")


# ==============================================================================
# 【第三部分:主演出流程】
# ==============================================================================

def main() -> None:
    profile = resolve_action_profile()
    print("UNIVERSAL_SKILL_START model=" + ROBOT_MODEL, flush=True)
    trigger_mood("#FF6B2B", 3)

    # 1. 开场问候
    speak(OPENING_LINE)

    # 2. 组装三幕互动内容(迎宾问候 → 今日灵感金句 → 幸运抽签)
    quote_text = fetch_quote()
    lucky_line, lucky_action = (
        random.choice(LUCKY_POOL) if ENABLE_RANDOM_DRAW else LUCKY_POOL[0]
    )
    steps = (
        ("greet", "第一幕,元气问候。先挥个手,给你打满今日份的开发者Buff!"),
        ("cheer", "第二幕,灵感广播。送你一句话:" + quote_text),
        (lucky_action, "第三幕,好运盲盒。" + lucky_line),
    )

    # 3. 逐幕执行:先说话播完 → 再触发安全动作 → 等待动作完成
    for index, (action_key, line) in enumerate(steps, 1):
        speak(line)
        animation_id, hold = profile[action_key]
        accepted = robot.play_animation(animation_id, interrupt=False)
        print("ANIMATION step=%d key=%s id=%d accepted=%s" % (index, action_key, animation_id, accepted), flush=True)
        if not accepted:
            raise RuntimeError("ANIMATION_REJECTED id=%d" % animation_id)
        time.sleep(hold)

    # 4. 收场谢幕与完成标记
    speak(CLOSING_LINE)
    print("UNIVERSAL_SKILL_DONE steps=%d" % len(steps), flush=True)


# 机器人运行时在注入全局 robot 对象后直接执行
main()
《社交能量补给站》核心源码 · 👉 打开带课代表逐段拆解与行号的源码精讲页
"""社交能量补给站:基于官方示例的 TTS 轮询与动作编排,纯标准库。"""

import time

ROBOT_MODEL = "q1"
POLL_SECONDS = 0.1
IDLE_SECONDS = 0.7
STEPS = (
    "第一格,勇气充电。今天你已经迈出了第一步,给自己加个油!",
    "第二格,灵感充电。先让一个小点子跑起来,再慢慢把它变精彩!",
    "第三格,快乐充电。请收下这份机械式认真,和百分之百的祝福!",
)
# 等待时长为保守初值,现场按动作权威表、任务日志与实际观感调整。
MOTIONS = {
    "q1": ((3001, 7.0), (3008, 7.0), (3007, 7.0)),
    "t1": ((26, 18.0), (43, 18.0), (26, 18.0)),
}


def audio_state() -> int:
    snapshot = robot.get_audio_play_state()
    if isinstance(snapshot, dict):
        state = snapshot.get("state")
        if isinstance(state, dict) and state.get("value") in (1, 2):
            return state["value"]
    raise RuntimeError("AUDIO_STATE_INVALID")


def speak(text: str) -> None:
    """必须观测到起声及稳定空闲;超时终止,避免下一句截断当前音频。"""
    print("SPEAK " + text, flush=True)
    robot.play_tts(text)
    start_deadline = time.monotonic() + 4.0
    while audio_state() != 1:
        if time.monotonic() >= start_deadline:
            raise RuntimeError("TTS_NOT_STARTED")
        time.sleep(POLL_SECONDS)

    finish_deadline = time.monotonic() + max(len(text) * 0.45, 4.0) + 8.0
    idle_since = None
    while time.monotonic() < finish_deadline:
        if audio_state() == 1:
            idle_since = None
        elif idle_since is None:
            idle_since = time.monotonic()
        elif time.monotonic() - idle_since >= IDLE_SECONDS:
            return
        time.sleep(POLL_SECONDS)
    raise RuntimeError("TTS_NOT_FINISHED")


def choreography() -> tuple:
    if ROBOT_MODEL == "t1":
        form = robot.get_t1_robot_form()
        if form not in ("biped", "quadruped"):
            raise RuntimeError("T1_FORM_UNKNOWN")
        # 两种形态均使用 26/43,属于手册所列的四足允许集合。
        print("T1_FORM " + form, flush=True)
    return MOTIONS[ROBOT_MODEL]


def main() -> None:
    motions = choreography()
    print("ENERGY_STATION_START model=" + ROBOT_MODEL, flush=True)
    speak("欢迎来到社交能量补给站!不用扫码,送你三格好心情。")
    for index, (text, (animation_id, hold)) in enumerate(zip(STEPS, motions), 1):
        speak(text)
        accepted = robot.play_animation(animation_id, interrupt=False)
        print("ANIMATION step=%d id=%d accepted=%s" % (index, animation_id, accepted), flush=True)
        if not accepted:
            raise RuntimeError("ANIMATION_REJECTED id=%d" % animation_id)
        time.sleep(hold)
    speak("充电完成!愿你带着自己的点子出发。掌声送给每一个敢动手的人!")
    print("ENERGY_STATION_DONE steps=%d" % len(STEPS), flush=True)


# 与官方示例一样,在运行时 exec 注入 robot 后直接执行。
main()
一源双包影分身构建器源码 · 👉 打开带课代表逐段拆解与行号的源码精讲页
"""从 Example/ 下的技能源码构建 Q1/T1 双机型 .rpk 包及可审阅的发布元数据。"""

import importlib.util
import json
import shutil
import tempfile
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
WORKSPACE = ROOT.parent
DEV_KIT = ROOT / "primehub_dev_kit"
EXAMPLE_ROOT = WORKSPACE / "Example"
OUTPUT = ROOT / "dist"

SKILL_SPECS = (
    {
        "folder": "energy_station",
        "pkg_prefix": "cans518_energy_station",
        "publish_prefix": "publish",
        "title": "社交能量补给站",
        "short_desc": "三格好心情,给每个敢动手的人充个电",
        "long_desc_tpl": (
            "离线小剧场:开场、勇气/灵感/快乐三格充电、谢幕。"
            "每格短台词配动作,无需联网或语音输入。"
            "仅适配%s,使用预设动作,运行时请留出周围1米空间。"
            "基于官方示例改编;台词和编排可在源码中修改。"
        ),
        "icon": "skill-game.svg",
    },
    {
        "folder": "universal_skill",
        "pkg_prefix": "cans518_universal_skill",
        "publish_prefix": "publish_universal",
        "title": "启元百变互动官",
        "short_desc": "迎宾问候+灵感广播+好运盲盒抽签的通用互动剧场",
        "long_desc_tpl": (
            "Q1/T1通用互动技能模板:包含元气迎宾、每日灵感金句(支持联网API与断网自动兜底)、"
            "随机好运抽签及谢幕。仅适配%s,内置形态检测与TTS防抖轮询,运行时请留出周围1米空间。"
        ),
        "icon": "skill-verify.svg",
    },
)


def build_skill(pack_module, spec_cfg: dict) -> None:
    source_dir = EXAMPLE_ROOT / spec_cfg["folder"]
    for model, model_id, model_code in (("q1", 100, "robot_q"), ("t1", 101, "robot_t")):
        with tempfile.TemporaryDirectory(prefix="qiyuan_build_") as staging:
            skill_dir = Path(staging) / (spec_cfg["folder"] + "_" + model)
            shutil.copytree(source_dir, skill_dir, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
            script = skill_dir / "src/main.py"
            source = script.read_text(encoding="utf-8")
            assignment = 'ROBOT_MODEL = "q1"'
            if source.count(assignment) != 1:
                raise ValueError("期望恰好一个 ROBOT_MODEL 声明: " + str(script))
            script.write_text(source.replace(assignment, 'ROBOT_MODEL = "%s"' % model), encoding="utf-8")
            config = skill_dir / "cfg/pkg.yaml"
            package_name = "%s_%s_20261024" % (spec_cfg["pkg_prefix"], model)
            lines = []
            for line in config.read_text(encoding="utf-8").splitlines():
                if line.startswith("package_name:"):
                    line = "package_name: " + package_name
                elif line.startswith("robot_model:"):
                    line = "robot_model: " + model
                lines.append(line)
            config.write_text("\n".join(lines) + "\n", encoding="utf-8")
            pack_module.pack(skill_dir, OUTPUT)
        metadata = {
            "skillName": spec_cfg["title"] + " " + model.upper(),
            "version": "0.1.0",
            "skillDesc": spec_cfg["short_desc"],
            "long_description": spec_cfg["long_desc_tpl"] % model.upper(),
            "categoryIds": [104],
            "categoryCodeList": ["action"],
            "tagsIds": [127],
            "tagsCodeList": ["companion"],
            "robotModelIds": [model_id],
            "robotModelCodeList": [model_code],
            "iconUrl": spec_cfg["icon"],
        }
        publish_file = "%s_%s.json" % (spec_cfg["publish_prefix"], model)
        (OUTPUT / publish_file).write_text(
            json.dumps(metadata, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
        )


def build() -> None:
    spec = importlib.util.spec_from_file_location("primehub_pack", DEV_KIT / "pack.py")
    pack_module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(pack_module)
    OUTPUT.mkdir(exist_ok=True)
    for spec_cfg in SKILL_SPECS:
        build_skill(pack_module, spec_cfg)


if __name__ == "__main__":
    build()
04 · WORKFLOW

从模板到真机:完整实践流程

图文完整版见 从零开发 Q1/T1 技能 8 步 SOP →
STEP 0

30 秒环境大体检

查看体检仪原理 →

派体检官检查你的电脑上 Python 温室环境、国密加密库 gmssl、ssh/scp/curl 三件套以及 PrimeHub 市场匿名搜索通不通。

✓ 过关暗号:终端欢快吐出 PREFLIGHT_OK 并搜出官方「51 点歌台」
终端命令
python Tools/scripts/preflight.py
STEP 1

全浏览器顺手牵羊提 Token

查看提Token原理 →

再也不用专门下火狐了!随便用 Chrome / Edge / Brave / Firefox 登录 primehub.primebot.com 后,连浏览器都不用关,一键把最新登录凭证顺进保险箱。

✓ 过关暗号:跑 my 子命令不报错,说明发版通行证 100% 绿灯
自动扫描全浏览器 / 也可传 --browser edge 或 --token
# 1. 自动扫描 Chrome / Edge / Brave / Firefox 顺走最新 Token
python Tools/primehub_dev_kit/extract_token.py

# 2. 验明正身(查查当前账号下的技能列表)
python Tools/primehub_dev_kit/primehub_publish.py my
STEP 2 - 3

花 3 分钟改几句台词与抽签词

查看源码精讲 →

打开《启元百变互动官》源码最顶部的【积木配置区】,改改开场白、往 LUCKY_POOL 塞几条搞笑运势签,或者拨动联网金句开关(拔网线也能自动切回离线保底词!)。

✓ 过关暗号:只动顶部配置字典,严守纯标准库与全局 robot 铁律
《启元百变互动官》顶部积木配置区
ENABLE_NET_QUOTE = False     # True=联网拉每日金句(没网自动用 FALLBACK_QUOTE 兜底!)
ENABLE_RANDOM_DRAW = True    # True=从 LUCKY_POOL 随机抽签
LUCKY_POOL = (
    ("叮!为你抽到「真机丝滑上上签」!动作一次通过!", "cheer"),
    ("叮!为你抽到「脑洞大开幸运签」!全场为你点赞!", "heart"),
)
STEP 4

启动“一源双包”影分身构建器

查看构建器精讲 →

不用苦哈哈维护两份代码!构建器会自动把源码复印两份并分别注入 q1 / t1 参数,在 成品弹药库 里吐出 4 个 .rpk 安装包和 4 份上架小抄。

✓ 过关暗号:终端连打 4 行 OK .../*.rpk
一键双机型打包
python Tools/scripts/build.py
STEP 5

0.4 秒“赛博替身”试毒

查看替身演武场 →

还没连真机心里没底?用“虚拟快进时钟 + 假机器人替身”在电脑上 0.4 秒把 Q1、T1 双足、T1 四足、0.3 秒大喘气抖动、拔网线降级与花式报错熔断全测一遍!

✓ 过关暗号:全部测试 OK(⚠️ 敲黑板:电脑替身彩排通过 ≠ 真机跑通!)
运行全套替身仿真单元测试
python -m unittest discover -s Tools/tests -v
STEP 6

现场连真机:先派侦察兵抓动作表,再加 --run 点火!

查看真机点火器 →

🚨 上机前保命三结界:① 周围 1 米内清走人类与水杯;② 先低位姿态试跑再站立测试;③ 运行期间谁也别拿手机 App 抢方向盘遥控!
SSH 提示密码时敲 1。第一遍先不加 --run 把机上真实 YAML 动作表抓回 验证与探测报告厅,核对无误后再加 --run 起跳!

✓ 过关暗号:final: success + 硬件日志印着 创建Animation task成功
真机侦察与实弹点火(已与上方参数台实时联动)
# 1. 第一步:先侦察真机环境、抓回机上 YAML 动作表并上传 .rpk 包(先不跑!)
python Tools/scripts/robot_run.py --model q1 --ip 192.168.112.222 --skill universal_skill

# 2. 第二步:确认周围 1 米安全净空后,追加 --run 正式点火起跳!
python Tools/scripts/robot_run.py --model q1 --ip 192.168.112.222 --skill universal_skill --run
STEP 7 - 8

真机“双响炮”连续跑两轮 + 发射火箭上架 PrimeHub

查看发版火箭筒 →

Step 7(双响炮验收):原样再跑一遍 --run,确保连续两轮独立稳过且亲耳听完 5 段台词不吞字。
Step 8(上架封神):敲右侧命令上架 PrimeHub(注意同名重复发布会生出双胞胎条目,换版先 delete),最后掏出手机官方 App 搜到下载并当众跑一遍!

✓ 过关暗号:返回 {'code': 1000, 'msg': '成功'} + 手机 App 下载演出成功!
一键加密上架命令(已与上方参数台实时联动)
# 1. 一键国密加密上传并发布对应机型到 PrimeHub 技能市场
python Tools/primehub_dev_kit/primehub_publish.py publish Tools/dist/universal_skill_q1.rpk --metadata-file Tools/dist/publish_universal_q1.json

# 2. 查查自家在架列表与全网公开搜索验货
python Tools/primehub_dev_kit/primehub_publish.py my
python Tools/primehub_dev_kit/primehub_publish.py search "启元百变互动官 Q1"
05 · CHECKLIST

活动现场 2.5 小时安全结界与满贯打勾表

点击即可打勾(自动保存在浏览器) · 完整时间轴见 现场 2.5 小时行动与安全清单 →
🚨 现场真机保命三结界(全程焊死在脑子里,一票否决!):
1. ⭕ 半径 1 米净空结界:机器人挥起手来六亲不认,周围 1 米内清走所有人类、水杯与笔记本电脑。
2. 🐢 先蹲着再站着(先低位后站立):初次上机调试先在安全低位姿态下试跑,确认温顺不抽风后再站立演示。
3. 📵 别抢方向盘(严禁同时用 App 遥控):技能运行期间严禁任何人同时用手机 App 遥控机器人(会触发底层运控精神分裂保护停机),一切听现场导师指挥。
06 · TROUBLESHOOT

机器人闹脾气怎么办?拒绝码(reason)10 秒哄好字典

对照硬件动作日志与状态输出快速排障
日志报错 / 翻车现场 机器人在抱怨啥?(大白话翻译) 10 秒钟怎么哄好它?
reason 24 Q1 在普通用户室内模式下,下盘被安全结界封印了,拒做全身动作(whole_body) 乖乖换成 Q1 upper_body 上半身动作 ID(如 3001 招手、3007 比心、3008 加油)
reason 19 T1 正四条腿趴着当修勾呢,你非逼它做双足站立专属动作(四条腿怎么比心嘛!) 换成四足通吃王炸动作(26 喜悦、43 律动、1、7)或用 get_t1_robot_form() 分支
reason 42 上一个动作还在扭呢,动作队列堵车了,新动作挤不进去 把上一个动作后面的 time.sleep(hold) 等待秒数调大一点
reason 28 上一个动作自带“霸体”不可打断(比如 T1 动作 1 打招呼要慢悠悠扭 15s) 老老实实等够它的完整时长(如 16~18 秒),或者换用短平快的动作 26 / 43
reason 17 机器人翻遍了固件字典,也没找到你瞎编的这个动作 ID 打开 验证与探测报告厅 里抓回来的机上真实 YAML 动作表核对
TTS_NOT_FINISHED 单句台词写成老太太的裹脚布太长了,超出了语音轮询等待上限 把单句台词精简到 30 字以内,或者调大 speak() 里的超时余量
final: timeout 整场演出磨磨唧唧超过了 deploy_test.py 默认 120 秒监工窗口 适当缩减动作等待秒数,把整场演出控制在黄金 60~90 秒内
07 · VISUAL ATLAS

启元官网配色 · 6 张高清全彩作战图库

点击标签切换图解,点击图片可全屏放大看清每一个细节
工作区全景架构图
图 1:启元 Q1 & T1 技能整活工作台四大专区与 .rpk 内部三件套扒皮图(点击图片全屏放大)
八步通关流程图
图 2:从 Step 0 开工体检到 Step 8 市场封神的完整命令与过关暗号(点击图片全屏放大)
运行时与动作红线
图 3:lumina_server 老管家塞遥控器机制、0.7 秒闭嘴防抖与 Q1/T1 安全动作白名单(点击图片全屏放大)
一源双包与替身仿真
图 4:一源双包影分身流水线与 0.4 秒赛博替身演武场架构(点击图片全屏放大)
全浏览器提Token与发布
图 5:Chrome / Edge / Brave / Firefox 全浏览器顺走 Token 与国密 SM4 上架闭环(点击图片全屏放大)
现场时间轴与排障速查
图 6:官方活动视觉、2.5 小时现场抢分时间表、安全三结界与拒绝码急救表(点击图片全屏放大)
08 · SITEMAP

全站专题导航地图(攻略 · 剧本 · 工具 · 报告一网打尽)

点击任一标题即可直达对应的专题指南、源码精讲或核验报告