01出发前只看这一页
十条结论,读完你就知道现场怎么下手。
核心一句:现场不需要你写 TBEL
规则链 53 个节点已经跑通,状态机、看门狗、四维告警都在线上工作。你的任务是用它、调它,不是重写它。
- 改阈值、目标温度、输送量 → 改属性,和 TBEL 毫无关系,下一帧即生效。
- 改节点参数(延迟秒数、RPC 超时、读哪些属性)→ 点表单,拖拽和填数字而已。
- 只有"判断条件"才需要脚本,而脚本往往只是 1~5 行 if。
- 你有 C 基础就够了。TBEL 是 Java 风格,for/while/switch/函数/复合赋值都能用。只需记住 4 处不一样。
- 危险的是它比 C 宽松。写错常常不报错,只静默返回 false。所以改完必须实测,别靠肉眼。
- 本地自带 TBEL 引擎,不用外网。
try-tbel.cjs能在不碰设备、不改平台的前提下试跑脚本。 - 三个数字是命门:遥测 5 秒新鲜度、每次启动换新 runId、STOPPING ≠ 已停机。
- 最稳的退路是手动模式。面板上的泵/加热器开关不依赖状态机,任何时候能用。
- 数据和告警与自动控制独立。自动控制关掉,遥测曲线和告警照常工作。
- 没把握就别让自动任务控制加热器。过温干烧的硬保护应该在 STM32 本地。
本页所有 TBEL 结论都在本机引擎上实测过(34 条),点「TBEL」标签可逐条查看。命令、参数、故障原因都来自实际代码与验证记录,不是推测。
02命令卡
在 C:\thingsboard-4.3.1.2 下执行。前 5 条会真动设备,其余只读,随便跑。
| 用途 | 命令 |
|---|---|
| 看模式/状态/累计量 | node competition/iot_race/automation/control.cjs status |
| 只存参数,不启动设备 | node competition/iot_race/automation/control.cjs configure --target 38 --hysteresis 1.5 |
| 启动自动温控(先手动开泵) | node competition/iot_race/automation/control.cjs temperature --sensor temp2 --target 35 --hysteresis 1 |
| 启动定量输送(先关泵和加热器) | node competition/iot_race/automation/control.cjs dose --liters 1 --duty 50 |
| 请求停止 | node competition/iot_race/automation/control.cjs stop |
| 列出可试场景 | node competition/iot_race/automation/try-tbel.cjs --list |
| 体检规则链所有脚本语法 | node competition/iot_race/automation/try-tbel.cjs --check |
| 实测 34 条 TBEL 规则 | node competition/iot_race/automation/try-tbel.cjs --selftest |
| 试跑一段脚本 | node competition/iot_race/automation/try-tbel.cjs "msg.x=1; return {msg:msg,metadata:metadata,msgType:msgType};" |
| 只读核对线上与文件是否一致 | node competition/iot_race/automation/check-live.cjs |
| 状态机 25 项验证,不碰设备 | node competition/iot_race/automation/verify.cjs |
现场操作顺序:先 status 看状态 → 确认设备当前输出 → 再启动或停止。启动失败时不要反复点,先读面板或 status 给的 reason。
可试的输入场景
配合 try-tbel.cjs -f 你的片段 --scenario 名字 使用,用来观察脚本在某一种输入下返回什么。
--scenario normal普通遥测、自动模式关闭,用于看“什么都不做”的那条路
--scenario temperature温控请求启动:温度低于目标−回差,应产生开加热器指令
--scenario temperature-arrive温控运行中且已到目标:应产生关加热器指令
--scenario temperature-noflow泵没开或流量不足:应报 NO_CIRCULATION
--scenario dosing定量输送启动:泵与加热器都已关,应产生 setPump 指令
--scenario dosing-arrive定量输送已到量:应报 TARGET_REACHED 并进入 STOPPING
--scenario alarm温度越界 + 传感器缺失,用于测试告警分支的判断脚本
--scenario watchdog看门狗消息:设备超时未上报,应报 TELEMETRY_TIMEOUT
--scenario attribute-command面板写入扩展命令(controlCommand),用于测试扩展命令入口的判断
--scenario manual-rpc面板手动开泵的 RPC(Pump),用于测试手动接管判断
03TBEL:会这 10 条就够
每一条都有"错→对"对照和出错时会看到的报错。全部实测。
01每个脚本最后必须 return,返回三件套
msg.tempAvg = (msg.temp1 + msg.temp2) / 2;msg.tempAvg = (msg.temp1 + msg.temp2) / 2;
return {msg:msg, metadata:metadata, msgType:msgType};转换节点只写 msg.x=1 不 return,引擎直接拒绝。这是头号坑。
出错时会看到:Wrong result type: Double
02服务端属性在 metadata 里叫 ss_<属性名>
var limit = metadata.ss_tempHighLimit;节点读属性时自动加 ss_ 前缀,漏了会拿到 null。
出错时会看到:null pointer
03属性读出来是字符串,比大小前必须 parseDouble
if (metadata.ss_tempHighLimit > 40) { }var limit = parseDouble(metadata.ss_tempHighLimit);
if (limit > 40) { }属性的值都是字符串,"45" 跟数字比会得出字符串比较的怪结果。
出错时会看到:不报错,但判断结果错——最危险
04判断“有没有值”用 == null,判断布尔用 == true
if (msg.pump) { }if (msg.pump == true) { }
if (msg.temp1 == null) { }泵状态可能是 1 / true / "true",统一用 == true,与现有代码一致。
出错时会看到:不报错,但漏判
05数字是 Number,字符串是 String,别混着用
if (msg.temp1 == "25.4") { }var t = msg.temp1;
if (t != null && t > 25) { }设备发字符串数字会被当成“缺失”,还会触发 SENSOR_DATA_MISSING。
出错时会看到:不报错,但静默走错分支
06数组用 add / size,不是 push / length
var out = [];
out.push('A');
if (out.length > 0) { }var out = [];
out.add('A');
if (out.size() > 0) { }TBEL 是 Java 风格,写 push/length 直接报错。
出错时会看到:编译报错
07JSON 字符串用 JSON.parse 变对象再取字段
var n = metadata.autoPlan.commands.size();var p = JSON.parse(metadata.autoPlan);
var n = p.commands.size();显式 parse 更稳妥,意图也更清楚。
出错时会看到:null pointer 或取不到值
08过滤器节点只 return true 或 false
return {msg:msg, metadata:metadata, msgType:msgType};return msg.pump == true;过滤器只有两个出口,返回对象会被拒绝。
出错时会看到:Wrong result type
09分流节点要 return 一组出口名称
return msg.temperatureAlarm == true;var out = [];
if (msg.temperatureAlarm == true) out.add('TEMP_ALARM');
return out;出口名称必须与连线的关系类型逐字一致,否则消息走不出去,看起来像“没反应”。
出错时会看到:消息无输出
10字符串拼接和包含判断
if (metadata.extensionRoute == 'FEATURE' + id) { }var route = 'FEATURE_' + msg.featureId;
if (route.contains('FEATURE_')) { }+ 拼字符串,contains 判包含。布尔一律 == true,别依赖隐式转换。
出错时会看到:不报错但结果不符预期
04C 基础对照表
你会的 C 已经覆盖 TBEL 九成语法。这里只讲一样的、不一样的、和会坑你的。
跟 C 一样,直接用
var i = 0;/Integer n = 5;/Long l = 10L;—— 变量声明for (var i=0; i<5; i++) { }、while (i<3) { }—— 循环if / else if / else、break—— 分支与跳出循环switch (m) { case 'A': ... break; }—— 支持,但 case 和它的语句要分行,同行会报unknown class or illegal statement+= -= *= /=、++、--、%—— 运算符&& || !、三元a ? b : cfunction f(v) { ... }—— 可以定义在文件任意位置,也能互相调用(现有代码顶部那些 number()、valid() 就是)arr[i]下标取值;长度用arr.size()或arr.length
跟 C 不一样,必须记住四条
continue不支持。报unresolvable property or identifier: continue。把循环体反过来用if包住即可。- 顶层不能写裸大括号代码块。放进函数里,或用
if块。 - 数组用
add()不是push();判断包含用contains()。字符串长度.length(),子串判断.contains(),没有strstr。 - 没有指针、没有
typeof、没有显式类型转换语法。转字符串.toString(),转数字parseDouble()/parseInt()。
最关键的一节:TBEL 比 C 宽松,很多错不报错,只静默算错。C 里指针写错立刻段错误;TBEL 里写错会安静返回 false,要等设备不动作才发现。
| 按 C 习惯写 | TBEL 实际 | 正确做法 |
|---|---|---|
| if (msg.temp1 > 30) | 缺失时返回 false | 先判 != null,别指望"缺失会崩" |
| if (t > 30 && t != null) | 返回 false,不报错 | 保护写反也不报错。必须先判 null:t != null && t > 30 |
| if (x == '1') / if (pump == 1) | '1'==1、true==1 都为真 | 布尔一律 == true;数字先 parseDouble |
| parseDouble(metadata.ss_missing) | 报错 Value: "null" is not numeric | 属性可能是空字符串,转换前判空 |
| 5 / 0 | 得到 Infinity | 不报错。除法前确认分母非 0 |
| 5 % 0 | 报错 / by zero | 取模前确认非 0 |
实测清单(34 条)
跟 C 一样,已验证可用10 条
C 风格 for 循环可用
var s=0; for(var i=0;i<5;i++){ s=s+i; } msg.r=s; return {msg:msg,metadata:metadata,msgType:msgType};
while 循环可用
var i=0; while(i<3){ i++; } msg.r=i; return {msg:msg,metadata:metadata,msgType:msgType};
复合赋值 += -= *= /= 可用
var x=10; x+=5; x-=2; x*=2; x/=4; msg.r=x; return {msg:msg,metadata:metadata,msgType:msgType};
自增 ++ 自减 -- 可用
var i=0; i++; i++; i--; msg.r=i; return {msg:msg,metadata:metadata,msgType:msgType};
取余 % 可用(现用于判断整数占空比)
msg.r = (50.5 % 1 != 0); return {msg:msg,metadata:metadata,msgType:msgType};
函数可以在文件任意位置定义
msg.r=twice(msg.n);
function twice(v){ return v*2; }
return {msg:msg,metadata:metadata,msgType:msgType};
函数里可以写 if 和 return
function pick(t){ if(t==null){ return -1; } if(t>30){ return 1; } return 0; }
msg.r=pick(msg.n); return {msg:msg,metadata:metadata,msgType:msgType};
switch/case 可用(case 与语句必须换行)
var m='B'; var r=0;
switch(m){
case 'A':
r=1;
break;
case 'B':
r=2;
break;
}
msg.r=r; return {msg:msg,metadata:metadata,msgType:msgType};
for 里 break 可用
var s=0;
for(var i=0;i<9;i++){
if(i==4){ break; }
s=s+i;
}
msg.r=s; return {msg:msg,metadata:metadata,msgType:msgType};
数组有 .size() 和 .length 两种写法
var a=[1,2,3]; msg.r = a.size() + a.length; return {msg:msg,metadata:metadata,msgType:msgType};
跟 C 不一样,已验证9 条
continue 不支持(用 if 包住循环体替代)
var s=0;
for(var i=0;i<5;i++){
if(i==2){ continue; }
s=s+i;
}
顶层裸大括号代码块不合法
var a=1;
{
msg.r=a+1;
}
空值与数字比较不报错,静默返回 false
msg.r = (msg.missing > 30);
短路保护写反了不会报错,只会算错
var t=msg.missing; msg.r = (t > 30 && t != null);
字符串与数字宽松相等(1=="1" 为真)
msg.r = ('1' == 1);
布尔与数字宽松相等(true==1 为真)
msg.r = (true == 1);
parseDouble(null) 会报错,要先判空
msg.r = parseDouble(metadata.ss_missing);
除以 0 得到 Infinity,不是报错
msg.r = 5 / 0;
对 0 取模会报错(/ by zero)
msg.r = 5 % 0;
05哪些能点、哪些要写码
直接回答"TB 不是可以可视化配置吗":结构和参数能点,判断条件是代码。
| 要做的事 | 方式 |
|---|---|
| 新建/删除节点、拖线、改关系类型 | 可视化 |
| 改节点参数(看门狗延迟、RPC 超时、读哪些属性、存到哪个作用域) | 可视化 |
| 告警等级、类型、是否传播 | 可视化 |
| 告警阈值、目标温度、输送量、回差 | 改属性 |
| "温度低于目标减回差"、"泵开了但没流量" | 写码 · 几行 if |
| 状态机(自动温控、定量输送的全部决策) | 写码 · 已写好 |
结论:现场九成需求用"可视化 + 改属性"就能解决。真正要动脚本的只有三种:加一个现在没有的判断条件(在现有脚本末尾加一行 if)、吸收设备新字段(其实不用改,遥测键会自动保存)、改自动控制行为(走文件重新生成)。
06怎么改:按需求查
每一条都标了"改哪里"。标"改属性"的完全不用碰规则链。
改属性改告警阈值
改哪里:设备服务端属性
- 打开设备 iot_race → 属性 → 服务端属性
- 改 tempLowLimit / tempHighLimit / maxTempDiff / minFlow / minPressure / maxPressure
- 下一帧遥测立即生效,不用部署
告警阈值 tempHighLimit 与自动控制的过温上限 hardMaxTemp 是两套独立参数。想让自动任务比告警更早收手,应保持 hardMaxTemp ≤ tempHighLimit。
改属性改目标温度 / 回差 / 测温通道
改哪里:autoConfig
- control.cjs configure --sensor temp1 --target 38 --hysteresis 1.5
- 或面板「自动温控」页签填表后点保存
任务启动时会锁定参数快照。改参数必须:stop → 等停机确认 → 用新 runId 重新启动。
改属性改输送量 / 占空比 / 流量上限
改哪里:autoConfig
- 先把泵和加热器都关掉并确认遥测
- control.cjs dose --liters 2 --duty 60 --maxFlow 20
定量输送启动前泵和加热器必须都是关闭的,这是状态机里的硬检查(START_REQUIRES_STOPPED)。
改属性校准流量系数 flowScale
改哪里:autoConfig.flowScale
- 做一次已知体积的输送,比如用量杯接 1 L
- 比较 autoRuntime.totalL 与实测体积
- 新 flowScale = 旧值 × (实测 ÷ 平台累计)
默认值 1 是未校准的。约 1 秒采样加网络延迟,不要承诺毫升级精度。
可视化改看门狗延迟
改哪里:「等待遥测超时检查」节点
- 规则链编辑器里双击该节点
- 改 periodInSeconds(当前 6)
这是纯表单参数,不用写脚本。但“多久算失联”的 5 秒判定在脚本里,两者要搭配合理。
界面改脚本新增一个告警维度
改哪里:统一键名节点 + 安全状态分流节点
- 在「统一键名并计算状态」脚本末尾加一行:msg.tempJumpAlarm = msg.tempDiff != null && msg.tempDiff > 3;
- 在「安全状态分流」脚本里追加出口:result.push(msg.tempJumpAlarm ? 'TEMPJUMP_ALARM' : 'TEMPJUMP_CLEAR');
- 新建一个创建告警节点和一个清除告警节点,告警类型字符串必须完全一致
- 从分流节点连出 TEMPJUMP_ALARM 和 TEMPJUMP_CLEAR 两条线
- 改完把规则链导出覆盖回 iot_race_rule_chain.json
阈值想可配的话,要在「读取安全阈值」节点的 serverAttributeNames 数组里补上新属性名,否则脚本读到的是空值。
可视化告警时发邮件
改哪里:告警节点后面
- 把一个发送邮件节点拖到「温度异常告警」的下游
- 连接类型选 Created
- 配置收件人和正文模板
要接在告警节点后面,不要接在分流节点后面——接错会在每次遥测(包括清除分支)都发一次通知。
改文件重建改停机重试间隔
改哪里:controller.tbel 里的 8000
- 改 controller.tbel 中停机段的 now - r.lastCommandAt >= 8000
- build.cjs → verify.cjs → sync-dashboard.cjs
自动控制的脚本必须在文件里改再重新生成。在平台上直接改会在下次同步时被覆盖。
改属性 / 可视化临时停用自动控制但保留告警
改哪里:autoConfig
- 方式一:control.cjs stop,自动模式变 OFF,告警完全不受影响
- 方式二:把 controlCapabilities 里对应功能的 enabled 改成 false(handled 保持 true)
- 方式三:删掉「保存遥测与派生状态」到「读取自动控制参数」的连线(排障用,记得改回来)
日常做法是方式一。方式三会留下一条难查的隐式差异。
发布流程(改脚本时)
node competition/iot_race/automation/build.cjs node competition/iot_race/automation/verify.cjs node competition/iot_race/automation/verify-dashboard.cjs node competition/iot_race/automation/sync-dashboard.cjs
最后一步才真正写平台,且要求当前没有运行中的任务、mode=OFF,同步前会自动备份。
最高频的翻车方式:在平台上直接改自动控制的脚本,当时生效,但下次 sync-dashboard.cjs 会覆盖掉,看起来像"昨天还好好的,今天自己变了"。自动控制相关的脚本一律改文件 + 重新生成。
07故障速查
自动控制的故障几乎都写在 autoRuntime.reason 里。先跑 status 看一眼。
| 现象 | 原因 | 处理 |
|---|---|---|
| 一直 IDLE 不起来 | mode 还是 OFF / runId 与上次相同 / 遥测不新鲜 / 参数越界 | 核对 autoConfig 的 mode 和 runId |
| FAULT: NO_CIRCULATION | 泵没开、流量低于 minFlow、流量键名不对 | 先手动开泵并确认流量数字在动 |
| FAULT: INVALID_TEMPERATURE | 温度读到 -127 或超出 -20~80 | 查传感器接线,脚本没错 |
| FAULT: TELEMETRY_GAP | 上报间隔接近或超过 5 秒 | 加快上报;或改脚本里的 5 秒判定 |
| FAULT: COMMAND_NOT_CONFIRMED | 设备没回 heater/pump 字段或回的是字符串 | 让设备遥测带上布尔值 |
| FAULT: START_REQUIRES_STOPPED | 定量启动时泵或加热器还开着 | 先关掉并确认遥测 |
| 停在 STOPPING | 设备没回报执行器已关闭 | 看遥测里 heater 是否 false(定量还要 pump false) |
| 改参数没生效 | 运行中锁定了启动时的参数快照 | stop → 等确认 → 新 runId 重启 |
| 面板点启动没反应 | 遥测超 5 秒 / 泵未开(温控)/ 泵未关(定量)/ 有任务没结束 | 面板会给拒绝原因 |
| 告警不清除 | 创建与清除节点的告警类型拼写不一致 | 逐字比对类型字符串 |
| 一直报传感器缺失 | 设备发了字符串数字,如 "25.1" | 设备端改成 JSON 数字 |
| 输送量偏差大 | flowScale 没校准 / 单位不是 L/min | 用量杯标定 |
| 规则链改了没生效 | 改的是平台脚本没同步文件 / 改完没部署 | check-live.cjs 核对一致性 |
| 扩展命令一直 REJECTED | 没有注册扩展处理器,设计如此 | 不用处理 |
最好用的定位手段:规则链右上角打开调试(Debug),设备发一条数据,就能看到消息在每个节点的输入输出,包括 autoPlan、autoWrite、autoCommands 的实际值。用完关掉。
08自动化不行了怎么退
按顺序退,不要现场硬撑。
node competition/iot_race/automation/control.cjs stop再
status 确认:STOPPING 不等于已停机,要看到 STOPPED / COMPLETED / FAULT。
competition/iot_race/backups/,按时间戳分目录,before.json 是改动前。回滚前先确认设备输出已关闭。
硬底线:没把握时不要让自动任务控制加热器。过温、干烧、失联的最终保护应在 STM32 本地实现;服务端看门狗在设备断网时无法保证命令送达。宁可手动,不要赌。
时间不够时的优先级
- 设备上报正常 + 遥测曲线能看 —— 不依赖任何脚本改动
- 告警能触发和清除 —— 改阈值即可,效果直观
- 手动控制泵和加热器 —— 点一下就有反应
- 自动温控 —— 先手动开泵,参数用默认 35°C
- 定量输送 —— 依赖流量精度,最容易偏差,放最后
09规则链全链路
iot_race 通用水循环控制 · 53 节点 / 56 连线。共 7 组,点开看每组在干什么。
三条入口路径:
- 遥测(Post telemetry)→ 统一键名 → 存遥测 → 一分为二:① 四维告警 ② 自动控制状态机
- RPC 请求(RPC Request to Device)→ 手动接管判断 → 是 Pump/Heater/setPump 就退出自动模式 → 原样下发
- 属性更新(Attributes Updated)→ 扩展命令入口 → 校验协议 → 分发(当前无注册处理器,一律拒绝)
通用数据与告警13 个
原有模板部分。遥测统一键名、存时序、跑四个维度的告警。这条线跟自动控制完全独立,自动控制关掉它照常工作。
消息类型分流
唯一入口。按消息类型分三路:遥测走主数据路径,RPC 请求先过手动接管判断,属性更新进扩展命令入口。
读取安全阈值
从设备服务端属性读 6 个阈值放进 metadata。所以改阈值不用动规则链。
统一键名并计算状态
把 temperature1/t1/flow/press 等别名统一成标准键,并算出 tempAvg、tempDiff、sensorFault、temperatureAlarm、pressureAlarm、flowAlarm。
保存遥测与派生状态
存所有键为遥测,用设备时间戳。自动控制判断“数据是否新鲜”就靠它。
安全状态分流
按 4 个独立维度输出最多 8 个分支,互不压制。
温度异常告警
清除温度告警
压力异常告警
清除压力告警
开泵无流量告警
清除无流量告警
传感器缺失告警
清除传感器告警
自动控制:状态机6 个
读意图和事实 → TBEL 状态机算决策 → 存状态 → 存遥测。
读取自动控制参数
读 autoConfig(意图)和 autoRuntime(事实)。
计算自动控制状态
整条链的核心:TBEL 状态机。判新鲜度、认新 runId、校验参数、累计输送量、决定是否发指令、决定是否停机、判断停机是否已确认。
需要保存自动状态
只有 autoWrite=true 才往下走。不新鲜或重复的帧在这里被丢掉。
保存自动控制状态
把 autoRuntime 存回服务端属性。先记账、后动作。
生成自动控制遥测
把 autoMode/autoStatus/autoReason/deliveredLiters 变成遥测,供仪表板显示。
保存自动控制遥测
自动控制:指令下发10 个
有指令时逐条下发并记录设备反馈。停机是“先关加热器、再停泵”,串行执行。
自动控制动作分流
按 metadata 分流:COMMAND=有指令要发,WATCH=要安排看门狗。
生成第一条自动指令
把 autoPlan.commands[0] 还原成 RPC。停机时第一条通常是关加热器。
下发第一条自动指令
下发 RPC,超时 3 秒。Success 和 Failure 都接同一结果节点,超时也留痕。
记录第一条指令结果
比对设备真实反馈与期望值,写出 autoRpcConfirmed。
保存第一条指令结果
还有第二条停机指令
commands 超过 1 条时继续,通常意味着“关加热器之后还要停泵”。
生成第二条停机指令
下发第二条停机指令
记录第二条指令结果
保存第二条指令结果
看门狗与超时分支9 个
每帧运行任务安排一次 6 秒后的检查。设备失联就锁定任务并尝试停机。这套节点是复制出来的,脚本内容由 build.cjs 自动同步。
准备遥测看门狗
生成空消息,类型改为 AUTO_WATCHDOG。6 秒后重新读状态,不用现在的快照。
等待遥测超时检查
延迟 6 秒放行。这个秒数在节点表单里改,不用写脚本。
超时读取自动控制参数
超时计算自动控制状态
同一个状态机走 watch 分支:任务还在跑且超过 5 秒没遥测,就判 TELEMETRY_TIMEOUT 并要求停机。
超时需要保存自动状态
超时保存自动控制状态
超时生成自动控制遥测
超时保存自动控制遥测
需要超时停机指令
手动接管6 个
面板点泵/加热器开关会走这条。先把自动模式置 OFF,再执行你的指令。不依赖状态机,任何时候可用。
手动控制接管判断
判断 RPC 方法是不是 Pump/Heater/setPump。是就进接管流程。
保存原始手动请求
读取手动接管前状态
手动接管退出自动模式
把 autoConfig.mode 置 OFF,autoRuntime 置 MANUAL / MANUAL_TAKEOVER。
保存手动接管状态
恢复手动控制请求
把用户原始请求原样还原并下发。接管只关自动模式,不动别的执行器。
RPC 落地与属性保存2 个
手动指令与自动指令最终都汇总到这里发给设备。
下发泵和加热器 RPC
手动指令真正落地的地方,只在这里下发一次。
保存客户端属性
保存设备上报的客户端属性,仅在值变化时更新。
扩展功能命令入口7 个
给未来功能预留的协议化通道。当前没有注册处理器,未知功能会被拒绝并回写原因。
扩展功能命令入口
只有带 controlCommand 的属性更新才继续。
读取扩展命令回执
校验扩展命令协议
校验版本、requestId、时间戳、operation,并按已注册处理器判断功能是否支持。
扩展命令去重
保存扩展命令回执
恢复扩展命令参数
后续功能分发接口
按 extensionRoute 输出分支。当前没有注册任何 FEATURE_ 分支,所以扩展命令一律被拒绝。
两个看起来奇怪的地方:
- 看门狗分支里有 9 个节点和正常分支几乎重名。这是刻意的:延迟节点之后如果回到原来的状态机节点,规则链里会出现环路。脚本内容由 build.cjs 自动复制,不是手写两遍。
- 告警分支是终点,没有回到主干的线。创建/清除告警后消息就结束了。