一、从一个字段名说起

AudioFile.msg 里有这么一行注释:

# 可选项,(1~100) priority + priority_weight% is final priority
uint32 priority_weight

而 PlayTtsRequest.msg 里同样叫 priority_weight 的字段,注释是这样的:

# 可选,非产品要求勿用,优先级加权参数(0-99),最小单位为 1
uint32 priority_weight

两个字段同名、同类型(都是 uint32),含义却差得很远。前者的取值范围是 1–100,是个百分比;后者是 0–99,是个绝对偏移。更关键的是,PlayTts 的最终权重算法在《接口说明.md》里写得很明确:

最终权重 = 主级 * 100 + 子权

也就是说,TTS 的 priority_weight 直接加到百位以上;而音频文件那边,它是对 priority 做一个百分比调整。同样是「微调优先级」,一处加在百位,一处加在个位,还带着百分号。

我最初以为这是文档写错了。把三套机制的定义都取出来放在一起之后才看清楚:不是写错,是三套独立设计的优先级机制,恰好共享了两个字段名。

这篇文章拆的就是这三套机制。它们分别属于运控输入源、TTS 播报、音频文件播放,各有各的取值空间、比较方式和失败行为。搞混任何一处,代码都会在没有任何报错的情况下做出错误行为——这是我在实测里反复撞到的同一类问题。

二、三套机制并排看

先把三套机制放在一张表里。

机制

接口

字段

取值空间

主级形态

比较方式

低优先级方的结果

运控输入源

SetMcInputSource

priority

0–100 连续整数

连续

排他锁,先到先得

帧被静默丢弃,1000 ms 看门狗脱锁

TTS 播报

PlayTts

priority_level + priority_weight

主级 6 档 × 子权 0–99

跳段

复合权重比大小

同步返回 is_success=false;高权重可中途掐断

音频文件

PlayAudioFile

priority + priority_weight + media_role

priority 1–10,weight 1–100(%)

连续

百分比加权

不终止,靠音量闪避(duck)让位

三套机制连「失败」的定义都不同:运控那边,输的一方什么都不会收到,只能靠自己周期性查询才发现控制权没了;TTS 那边,输的一方会收到一个明确的 false;音频那边,输的一方既没有错误也没有终止,只是音量被压低。

把它们画成刻度,形状的差异更直观。

图 1 中三条刻度各自独立归一化,横向长度不代表数值大小,只用来看结构:运控是均质的连续刻度,TTS 是跳段的离散块(跳过 3、5、9),音频是连续刻度上再叠一套四档角色。图中 L1–L10 的权重区间、priority 的 1–10 取值、media_role 的 40/60/80/100 四档,全部取自对应 .msg 文件与《接口说明.md》§2.1.1.4、§2.2.1.2、§2.2.1.3 三节。

有一处值得单独指出:音频文件播放那一行,priority 是 1–10,而 TTS 的主级恰好也是 1、2、4、6、8、10 这几个数字。数值看起来一样,含义毫无关系。AudioFile.priority 是连续的 1–10(默认 6),相邻值只差一档;TtsPriorityLevel 的主级是枚举语义层,跳过的那些值是为将来插入新档位预留的位置。把 priority_level = 6 的经验搬到 AudioFile.priority = 6,读起来像,实际语义完全不同。

三、TTS 的权重空间:99 与 100 的那 1 之隔

TTS 这套最值得细看,因为它的算术规则写得最明确,而边界恰好踩在一个很微妙的位置上。

TtsPriorityLevel.msg 在接口里实际只定义了 6 个有效档位:

uint8 value

uint8  UNKNOWN = 0x00

# 后台服务层
uint8  BACKGROUND_L1 = 0x01

# 主动服务层
uint8  SERVICE_L2 = 0x02

# 任务执行层
uint8  MISSION_L4 = 0x04

# 交互相应层
uint8  INTERACTION_L6 = 0x06

 # 危险预警层
uint8  WARNING_L8 = 0x08

# 生命安全层
uint8  SAFETY_L10 = 0x0a

主级取 1、2、4、6、8、10,步长 2,跳过 3、5、7、9。配合「最终权重 = 主级 × 100 + 子权」,就得到下面这张取值表。

常量

value

最终权重区间

来源

BACKGROUND_L1

0x01

100 – 199

接口 + 文档

SERVICE_L2

0x02

200 – 299

接口 + 文档

MISSION_L4

0x04

400 – 499

接口 + 文档

INTERACTION_L6

0x06

600 – 699

接口 + 文档

WARNING_L8

0x08

800 – 899

接口 + 文档

SAFETY_L10

0x0a

1000 – 1099

接口 + 文档

SYSTEM_L7

7

700 – 799

仅文档,接口无此常量

表中最后一行的 SYSTEM_L7 是这篇文章的一个关键发现,后面第七节会给出实测证据。先看一个更基础的性质。

子权上限是 99,主级步长是 100。相差 1,这个 1 不是巧合。

图 2 的横轴是最终权重,1 单位对应 1 像素。可以直观看到:L4 用满子权封顶 499,L6 一个子权都不给也有 600,两者相差 101。中间那 100 个整数位(500–599)没有任何档位覆盖。

这意味着子权的作用半径被精确限制在同档内部。任何一个「低一档但更紧急」的播报,都没有任何可用手段能顶到上一档去——把 priority_weight 调到 99,也只是让它在本档里排到最前面。

这是设计上的精确,也是设计上的代价。它换来的是排序的绝对可预测性:主级一旦确定,跨档关系就锁死了,不会因为某个调用方擅自把子权拉满而产生档位穿透。代价是分层变得刚性——如果产品上确实需要「低一档的紧急播报压过高档的例行播报」,这套算术给不出解法,只能改主级。

再看那些空档。接口里的主级是 1、2、4、6、8、10,跳过 3、5、7、9,对应 300–399、500–599、700–799、900–999 四段。其中 700–799 被文档的 SYSTEM_L7 填上了,另外三段仍然空着。这不是浪费,是预留:往这些区间插入新档位,不会扰动任何已有排序。SYSTEM_L7 的位置完全合理——它就在 L6 与 L8 之间那个天然的空位里;问题只在于接口没给它常量。

四、仲裁矩阵:两格与字面意思相反

权重算出来之后,谁先播、谁被打断,由《接口说明.md》§2.2.1.2 的四格矩阵决定。文档原文是这样的:

权重对比

is_interrupted: true

is_interrupted: false

新任务权重 ≥ 当前任务

当前播报被立即掐断(且不可恢复),新语音开始播放

仲裁优先级相同时存入缓冲区;仲裁优先级大于当前任务时强制打断

新任务权重 < 当前任务

请求丢弃,is_success: false

请求丢弃,is_success: false

这张表里藏着两个与字段名字面意思相反的结论,我在实测中逐一确认过。

图 3 把四格矩阵与实测样本画在一起。格内样本编号对应第七节 8 个场景的序号,服务端为按该矩阵实现的 mock,非真机行为。

第一个反直觉点:is_interrupted = true 不是强制插队开关。

按字面理解,「打断=true」应该意味着「我有话要说,现在就说」。但矩阵写得清楚:这一列只在「新权重 ≥ 当前」那一行才起作用;权重不足时,填 true 一样被丢弃。实测中第 5 条请求 L6+2 = 602,当前播报是 700,即使 is_interrupted=true,返回的仍是 is_success=false,error_message 写着「权重 602 < 当前 700」。

第二个反直觉点:is_interrupted = false 不保证不被打断。

同样按字面理解,「不打断」应该意味着「别掐我」。但矩阵在 false 这一列写的是「大于则强制打断」。实测第 3 条请求 L6+60 = 660,当前是 600,is_interrupted=false,结果仍是 PREEMPT_NOW——非抢占模式下权重更高,照样掐断。

两点合起来,这个字段的真实语义是:「权重足够时,同档要不要让」。它管的是同级仲裁,不是插队权。

而文档对这个字段有三处描述,只有一处与矩阵一致:

出处

原文

与矩阵是否一致

PlayTtsRequest.msg 注释

是否打断同等优先级播报(true:打断;false:不打断,进入队列播报)

一致

§2.2.1.2 字段表

是否打断现有任务(true: 立即抢占;false: 自动排队)

不一致

§2.2.1.2 仲裁矩阵

见上表

—

.msg 注释说的是「同等优先级」,与矩阵吻合;字段表说的是「现有任务」,把范围放大了。差别就在这一行:照字段表理解,会写出一个自己以为安全的调用——以为什么都不填就万事大吉,实际只要权重够高就会被掐。

五、音频:一处未闭合的歧义

AudioFile.msg 那一行的原文是:

# 可选项,(1~100) priority + priority_weight% is final priority
uint32 priority_weight

它给了表达式,没给运算次序。至少有两种都能自圆其说的读法:

  • 读法 A(加性):final = priority + weight / 100
  • 读法 B(乘性):final = priority × (1 + weight / 100)

两种读法在权重较小时结果相近,在权重接近上限时结论相反。

输入 1

A 值

输入 2

A 值

A 判定

B 值 1

B 值 2

B 判定

是否分歧

p6 + 50%

6.50

p7 + 0%

7.00

后者

9.00

7.00

前者

分歧

p6 + 100%

7.00

p7 + 0%

7.00

平手

12.00

7.00

前者

分歧

p6 + 100%

7.00

p10 + 0%

10.00

后者

12.00

10.00

前者

分歧

p6 + 10%

6.10

p7 + 0%

7.00

后者

6.60

7.00

后者

一致

读法 A 的跨档上限是固定的:增量恒 ≤ 1.0,所以 priority=6 用满权重最多追平 7,永远超不过,相当于「一档之内微调」。读法 B 在 weight=100 时直接翻倍,priority=6 可以压过 7 甚至 10,相当于「变相增加一到两档」。

同一个字段,两种读法,跨档能力差了两档。这不是笔误级别的歧义,是会影响排序结果的歧义。

我没有替它下结论,因为这需要真机实测才能确定(用两个不同 priority 的音频文件同时播放,观察实际输出顺序)。在没有真机的情况下断言其中一种,就是在编数据。这一处我留在第九节的「未决问题」里。

顺带一提,AudioFile 还有第三个维度:media_role。

# 可选项,PulseAudio media.role,不传默认 Music
# 可选值:Alert(系统告警,优先级100) | Notification(系统提示音,优先级80) | Communication(对话语音,优先级60) | Music(音乐/媒体,优先级40,默认)
# 非 Music 角色播放时会自动降低(duck)音乐音量
string media_role

这是第四套编号:40/60/80/100,由 PulseAudio 的角色体系决定,与 priority 完全并行。所以严格说,音频这一路内部同时跑着两套排序:一套是应用层的 priority + 百分比,一套是系统音频服务层的 media_role。前者决定谁先播,后者决定谁压低谁。这两套之间如何交互,文档没有展开。

六、验证一:把规则写成代码

三套机制的规则都是纯算术,可以脱离 ROS 完整复现。我把它们写成了一个零依赖的模块,只用标准库。下面节选自 taxonomy/levels.py,完整文件在仓库里。

TTS_LEVEL_STRIDE = 100
TTS_WEIGHT_MAX = 99


def tts_final_weight(level_value: int, priority_weight: int) -> int:
    """最终权重 = 主级 * 100 + 子权。"""
    return level_value * TTS_LEVEL_STRIDE + priority_weight


def sub_weight_can_cross_level() -> bool:
    """子权能否跨越主级:子权上界 99,主级步长 100。"""
    return (TTS_WEIGHT_MAX + 1) > TTS_LEVEL_STRIDE

这个函数不做范围钳制,因为接口层同样不做——priority_weight 声明为 uint32,0–99 只是文档约定,类型系统挡不住 100 以上的输入。这一点第七节的实测会确认。

跑一遍边界场景,命令与输出都在图 4 里。从这一处起,凡是程序输出一律给真实终端截图,不再手抄——写稿时把输出贴进代码块,最容易出的错不是抄错数字,而是抄错一个自己没注意到的对齐空格。

python3 scenarios/run.py weight

图 4 是容器 pgc/primebot-humble:1.0 内的真实终端画面:Xvfb 上起一个 80×32 的 xterm,用伪终端把命令逐条敲进交互式 bash,等输出定格后用 ImageMagick 抓窗口,未做任何裁剪与修饰。画面上半部分是七个档位的取值区间,下半部分是相邻两级的临界值——L1 满子权 199 对上 L2 零子权 200,只差 1;其余各相邻对都差 101。这个「差 1」正是子权跨不了级的算术来源。抓图脚本在仓库 tools/shot/ 下,all_shots.sh 一次跑完本文全部五张终端图,可以原样复现。

除了验证算术,我还用穷举确认了一个文档没写、但可以从矩阵推出来的结论。问题是:is_interrupted=false 时入队的任务,按什么顺序出队?文档只写「存入缓冲区,在当前任务播放完成后再进行处理」,没说排序键。

# 节选自 scenarios/exhaustive_queue.py 的 compute()
def compute():
    """返回 (入队总数, 差值->次数, 主级->入队数)。纯函数,供 run.py 复用。"""
    queued_diffs: dict = {}
    per_level: dict = {lv: 0 for lv in TTS_VALID_LEVEL_VALUES}
    total_queued = 0

    for level in TTS_VALID_LEVEL_VALUES:                  # 6 个合法主级
        for w in range(0, TTS_WEIGHT_MAX + 1):            # 子权 0-99
            req = TtsRequest("probe", level, w, False)
            for current in range(CURRENT_MIN, CURRENT_MAX + 1):   # 100-1099
                if arbitrate(req, current) is Verdict.QUEUED:
                    total_queued += 1
                    per_level[level] += 1
                    diff = req.weight - current
                    queued_diffs[diff] = queued_diffs.get(diff, 0) + 1
    return total_queued, queued_diffs, per_level

图 5 与图 4 出自同一个容器、同一次会话、同一套抓图流程。画面里先确认解释器版本为 Python 3.10.12,再执行 scenarios/exhaustive_queue.py:600 个合法请求 × 1000 个当前权重共 600000 种组合,判为 QUEUED 的只有 600 组,「新权重 − 当前权重」的取值集合只有一个元素 [0]。画面下半部分按主级拆成六行,每行 100 组,正好等于该主级下合法的子权个数——每个请求只在「与自己权重相等」的那一个当前权重上入队。

也就是说,能被入队的任务,权重必然与当前播报权重严格相等——更高会被立即抢占,更低会被丢弃,只有相等才进缓冲区。

于是缓冲区里所有任务的权重全都相同,FIFO 与「按权重排序」给出完全一样的出队顺序。文档没写排序键,但在这个矩阵下,写不写都不影响结果。这类推断比「文档没写,所以不知道」有用得多。

七、验证二:用官方接口跑一次真实回环

纯算术只能验证规则,验证不了契约。真正要确认的是三件事:接口里到底有哪些常量、越界值能不能发出去、矩阵的实际裁决是什么样。

先在容器里核对枚举的可用性。这段代码不需要真机、不需要启动节点,只要 import 消息类型。

# 节选自 ros2_ws/src/priority_lab/priority_lab/enum_probe.py
# 为便于展示,省略了「与文档值是否相等」的比较分支
from aimdk_msgs.msg import TtsPriorityLevel

# 依据《接口说明.md》2.2.1.2「TtsPriorityLevel (推荐主级常量列表)」逐条核对
DOC_TABLE = [("BACKGROUND_L1", 1), ("SERVICE_L2", 2), ("MISSION_L4", 4),
             ("INTERACTION_L6", 6), ("SYSTEM_L7", 7), ("WARNING_L8", 8),
             ("SAFETY_L10", 10)]

for name, doc_value in DOC_TABLE:
    try:
        actual = int(getattr(TtsPriorityLevel, name))
        print(f"{name:<16}{doc_value:<8}0x{actual:02x}")
    except AttributeError:
        print(f"{name:<16}{doc_value:<8}接口无此常量")

图 6 是 ros2 run priority_lab enum_probe 的原样输出。上半段逐条核对文档 §2.2.1.2 的七个常量:六个能取到,SYSTEM_L7 取不到;分隔线以下的两段是同一个脚本紧接着做的越界试验,下面会用到。

SYSTEM_L7 在文档 §2.2.1.2 的推荐常量表里带着完整语义——「系统重要提示(如'电量低'、'网络断开')」,但它不在 ros2 interface show 里,也不在 aimdk_msgs/interaction/msg/、install/、build/ 三处 .msg 副本的任何一处。写 TtsPriorityLevel.SYSTEM_L7 会直接抛 AttributeError。

那能不能硬填数字?可以。类型层完全不校验——图 6 下半段就是这件事:value 填 7、5、9、11 全部写入成功并原样读回;priority_weight 填 100、250 也被接受,按公式算出的最终权重分别是 700 与 850。

weight = 250 这一条值得停一下:850 直接落进 WARNING_L8 的 800–899 区间——一个本该「微调」的参数,越界之后能顶掉整档。接口不拦,只能靠调用方自觉。

接下来是端到端。我按第四节那张矩阵实现了一个 PlayTts 服务端,挂到官方 aimdk_msgs/srv/PlayTts 上,然后依次发 8 个请求。

# 节选自 ros2_ws/src/priority_lab/priority_lab/tts_client.py
def build_request(level: int, weight: int, interrupt: bool, text: str) -> PlayTts.Request:
    req = PlayTts.Request()
    req.header.header.stamp.sec = 0
    req.tts_req.text = text
    req.tts_req.priority_level.value = level
    req.tts_req.priority_weight = weight
    req.tts_req.domain = "priority_lab"
    req.tts_req.trace_id = f"lab-{level}-{weight}-{int(interrupt)}"
    req.tts_req.is_interrupted = interrupt
    return req

客户端看到的回执:

图 7 是 ros2 run priority_lab tts_client 的原样输出。code 与 state 两列取自 aimdk_msgs 通用响应头,成功是 0/1,失败是 1/2;时长 列对应 estimated_duration,八条全是 0。第 5、6 条被丢弃时,客户端多打一行 └─ error_message,把服务端给的原因原样带回来。

服务端侧的裁决日志(真实时序,[#n] 是请求序号):

图 8 是同一个容器里 cat /tmp/mock.log 的结果,客户端刚跑完,日志就是那一刻写下的。默认的日志前缀(级别、纳秒时间戳、节点名)被 RCUTILS_CONSOLE_OUTPUT_FORMAT='{message}' 去掉——带上它每行超过 100 列,终端里必然折行;消息体一个字没改。

这份日志确认了四件事:

  1. 第 4 条把 value=7 填进去,接口照收,且按 700 参与了排序,成功抢占了 660。文档档位在类型层可用,在枚举层不可见——两种状态同时成立。
  2. 第 3 条 is_interrupted=false 仍被掐断,与矩阵一致,与字段名字面相反。
  3. 第 5 条 is_interrupted=true 仍被丢弃,同上。
  4. 排队任务最终自行出队(mock 里把单次播报占用设为 3 秒,这是我设定的占位时长,不是官方数值),但客户端全程收不到任何通知。这是个结构性的缺口,下面单独说。

关于最后一点的成因,文档写得很直白:

时长字段局限:由于 TTS 采用云端/本地流式合成,当前版本下 estimated_duration 固定返回 0。

任务闭环建议:系统暂未开放播报结束的话题通知。若业务逻辑需在语音播报后执行(如先说话后走动),建议通过字数预估(约 200ms/字)进行延时处理。

回执里的 estimated_duration 字段实测确实是 0(图 7 的「时长」列八行全为 0)。也就是说,TTS 是一条开环链路:调用方知道「请求进队了」,但永远不知道「说完了没有」,只能拿字数乘以 200 ms 去估。相比之下,运控的 CommonTaskResponse 带 task_id,可以追踪任务状态。同一个 SDK 里,一个异步接口有任务 ID,另一个没有——这属于接口层的空白,不是使用者的疏忽。

需要说明的是:这个 PlayTts 服务端是我按文档矩阵实现的,不是启元的实现。它的作用是让请求走真实的 ROS 2 序列化通道,验证契约能通、字段能填、回执能解析。真机上「第 3 条请求到底会不会被掐断」「value=7 会不会被接受」,仍需真机确认。凡是 mock 得出的结论,我在上文都标了来源。

八、为什么会分成三套

把三处定义的文件路径摆出来,原因就清楚了:

  • aimdk_msgs/interaction/msg/PlayTtsRequest.msg —— TTS,属于交互控制模块
  • aimdk_msgs/hal/audio/msg/AudioFile.msg —— 音频文件,属于硬件抽象模块的 audio 子包
  • aimdk_msgs/mc/msg/McInputSource.msg —— 输入源,属于运动控制模块

三套机制分属三个包、三个模块、三拨人。它们各自解决各自领域里的排序问题,各自设计了字段,最终在字段名上撞了车。这不是谁抄错了谁,而是分层架构下同一概念在不同层各自演化的必然结果。

从这个角度看,三套设计其实各自都很合理:

  • 运控要的是安全兜底。控制权必须唯一,否则两路指令同时作用于关节,后果是物理的。所以它用排他锁,还用 1000 ms 超时做看门狗——发流的一方一旦挂掉,控制权必须自动释放,不能让机器人保持最后一条指令继续动。这里「优先级」回答的是「谁在控制」。
  • TTS 要的是顺序与可打断。语音是流式的,可以排队、可以中途掐断,掐断了也不会有物理后果。所以它用复合权重,主级管档、子权管同档内的先后,还单独给了 is_interrupted 控制同级是否让位。这里「优先级」回答的是「谁先说」。
  • 音频要的是混音。提示音和背景音乐可以同时响,不需要一方静音。所以它用百分比微调加音量闪避——警报声响起时把音乐压低,而不是掐掉音乐。这里「优先级」回答的是「谁盖过谁」。

三种「优先级」,对应三种完全不同的资源模型:独占资源、队列资源、混音资源。资源模型不同,仲裁方式自然不能通用。硬要统一成一套字段,反而会把三处都做坏。

真正的问题不在设计,在命名。三个模块都用了 priority 和 priority_weight 这两个词,而它们的量纲、算法、边界各不相同。跨模块开发时,前一处的直觉会直接带到后一处,出问题的地方往往连报错都没有——运控那边静默丢帧,音频那边悄悄 duck,只有 TTS 会给一个 false。

九、还没解决的问题

按重要程度排,有五处:

  1. AudioFile.priority_weight 的运算次序未定。 第五节那两种读法跨档能力差两档,需要真机同时播两个不同 priority 的文件、观察实际输出顺序才能确定。无真机时断言任何一种都是编数据。
  2. SYSTEM_L7 是预留还是遗漏,无法判断。 它在文档里有完整语义,在接口里没有常量,在权重空间里有位置。三种可能都存在:正在实现中、文档先写了、或者实现时被砍了但文档没同步。没有官方说明,只能标注现状。
  3. 缓冲区容量与溢出策略未定义。 第六节能推出「缓冲区内的任务权重全都相同」,但推不出缓冲区能装几个、装满了丢新还是丢旧。
  4. TTS 与音频文件两路之间如何交互,文档未展开。 一个 TTS 播报(INTERACTION_L6,权重 600+)与一个 media_role=Alert 的告警音(角色优先级 100)同时触发时,谁让谁?两者在不同模块、不同层,各自的排序键无法直接比较。
  5. media_role 与 priority 的优先级关系未说明。 文档只说「非 Music 角色播放时会自动降低(duck)音乐音量」,没说 priority 高的 Music 会不会拒绝被 duck。

这五处我都留在原样,没有替它补。补出来的东西看着完整,实际是错的——尤其第 1 条和第 4 条,随便写一个「应该是这样」,读的人照着做,问题会出现在几个月后的现场调试里。

十、接手这套接口时,先做四件事

如果要在启元 Q1 上写播报相关的业务,按这个顺序过一遍,能避开大部分坑:

第一,先确定你要用的是哪一套。 是让机器人「说一句话」(PlayTts),还是「放一个音频文件」(PlayAudioFile)?前者走交互模块,后者走硬件抽象模块,两套的 priority_weight 含义不同。选错了接口,后面调参数全是白调。

第二,priority_level 只用接口里那 6 个常量。 不要照着文档写 SYSTEM_L7——编译期就会失败。如果确实需要「系统重要提示」这一档,用 INTERACTION_L6 或 WARNING_L8,或者硬填 value=7 并接受它是一个接口不承认的档位。

第三,不要把 is_interrupted 当成插队开关。 它只决定同级是否让位。想让一句话压过正在播的内容,唯一可靠的手段是提高 priority_level——但记住子权跨不了级,L4+99 仍然低于 L6+0。

第四,给播报留出时间余量。PlayTts 返回 is_success=true 只代表进队了,estimated_duration 恒为 0,没有播报结束话题。要在播报后执行动作(比如先说话再走动),只能按字数估——文档给的经验值是约 200 ms/字,单次播报建议控制在 100 字以内。

这四条不需要真机就能验证前三条,第四条在真机上用秒表对一次字数与时长,就能把估算公式校准成自己的系数。

三套优先级并存的局面,短期不会改变——它们分属三个模块,各自服务不同的资源模型,统一命名的收益远小于改动的风险。对开发者来说,能做的事只有一件:把它们的差异记清楚,别用一套的直觉去写另一套的代码。

本文用到的规则复现、枚举核对、矩阵验证脚本都放在 AtomGit 上,无需真机即可复现:

https://atomgit.com/2301_76341691/primebot-priority-taxonomy-lab

如果你也在启元机器人上做二次开发,欢迎到启元机器人开发者社区 https://dev.primebot.com/ 交流,那里有官方文档、SDK 仓库入口和开发者讨论区。文中若有不准确的地方,也欢迎在仓库提 Issue 指出来。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐