汇率说明:本文出现的美元金额按 2026-10 基准汇率 1 USD ≈ 6.71 元换算,人民币后括号内标注原币种。

一、告警系统失败的方式,通常不是"没告警"

绝大多数物联网项目对告警的期待是"出事就响"。但真正把告警系统拖垮的,从来不是漏报,而是没人看的告警。

几组公开数据可以说明问题的规模:

数据 数值 来源
安全运营中心日均告警量 约 2,992 条/天,其中约 63% 未被处理 Vectra AI 2026
同上(另一口径) 约 2,047 条/天,其中 300 条(15%)因资源不足未被检查 Binalyze《2026 State of the SOC》(Sapio 调研 400 名 CISO)
信息过载的后果 91% 的 CISO 承认组织存在信息过载,77% 因此漏掉过告警或漏洞 同上
工业现场的误报率 传感器告警中 40%–90% 最终是误报(漂移、环境噪声、标定衰减) 多份工业运维研究的一致区间
重复告警的代价 每重复一次同类告警,运维人员真正采取行动的概率下降约 30% 工业告警管理研究

一个可以换算的参考:一家 180 台设备的工厂,若误报率 35%,每年约有 1,200 个工时消耗在"追假告警"上,折合约 28.2 万元(原报告 4.2 万美元)。

(需要说明的是,前两组来自安全运营领域,不是物联网领域。但机制是一样的:噪声越大,真正重要的那一条越容易被淹没。)

所以本文不讨论"怎么让告警更多",只讨论三件事:

  1. 不迟到——该响的时候,最迟在下一个上报周期内响;
  2. 不刷屏——同一件事只产生一条告警,恢复后自动消失;
  3. 找对人——紧急的进手机,不紧急的只进列表,别把两者混在一起。

二、LoRaWAN 场景下,先算清三笔时间账

在 LoRaWAN 上做告警,第一件事不是写脚本,是接受一个物理事实:电池供电的终端不会为了告警而多醒一次。由此决定了三条时间线:

环节 决定因素 典型量级 你能做的
检测延迟 终端上报周期(采集周期 + COV 变化上报) 5 分钟 – 60 分钟;COV 未触发时更长 缩短采集周期,或把 COV 阈值调小
下行延迟 Class A 终端只在自己上行之后开窗(RX1 约 1 秒、RX2 约 2 秒,LoRaWAN 1.0.4 默认值);错过窗口就要等下一次上行 最好 1–2 秒,最坏=一个完整上报周期 关键回路用 Class C;否则把延迟写进 SLA
失联判定 平台按"最后活动时间 + 2 × 心跳周期(heart_period)"判离线 由项目配置的心跳周期决定 心跳周期设成上报周期的 2–3 倍

这三笔账的意义在于:LoRaWAN 告警做不到秒级,但完全可以做到"不迟过一个上报周期"。把这个结论明明白白写进方案书,比事后被客户追问"为什么告警晚了 20 分钟"要好得多。

三、ThinkLink 的告警三件套

ThinkLink 的告警不是"在设备上配阈值",而是三个部件协作:

部件 职责 配置位置
触发模型 每次上行数据到达后执行脚本,判断条件,输出动作 模型管理 → 触发模型 → 挂载到设备
内置 ALARM RPC 接收 action: new / clear,创建或清除告警记录,并按通知组分发出去 运维管理 → 设备详情 → RPC → 新增 ALARM
通知组 决定"发给谁、通过什么渠道" 告警管理 → 通知组

配置顺序固定为三步:先给设备挂 ALARM RPC,再挂触发模型,最后(可选)配通知组。三者缺一个,告警就是"哑"的——不存在"配了脚本就自动会响"这件事。

一个设备可以挂载多个触发模型,多条件场景不用挤在一个脚本里。

四、触发脚本的运行环境:写裸函数体,跑在 vm2 沙盒里

触发脚本有一个容易踩的细节:编辑器里写的是裸函数体,平台会把它包进 function triggerRunner() { ... } 再调用,所以不需要(也不应该)再套一层 function trigger_script(...) {}。文档里的 trigger_script 写法只是为了说明可用变量。

运行环境是 vm2 沙盒,限制如下:

限制项 值
执行超时 1000 ms,超时本次触发直接跳过
eval / WebAssembly 禁用
async / await 不支持,脚本同步执行
require / import 不支持
setTimeout / setInterval / console 不可用;延时执行要用返回值的 delayms 字段,调试用触发模型日志
process / path / fs 不可用
Buffer SafeBuffer,仅开放 alloc / from / isBuffer / byteLength / compare / concat

可直接使用的注入变量:device、thingModelId、org_params(组织参数)、env、plugins、Buffer,以及 tklHelper 的六个类(BufferTKL、PayloadParser、MSparser、Utils、TriggerHelper、RPCHelper)。

device 对象里与告警最相关的字段:

字段 说明
device.telemetry_data[thingModelId] 该物模型的最新遥测快照(取数入口)
device.server_attrs 平台侧属性,适合放阈值、通知组、联动 EUI
device.shared_attrs 与设备双向同步的属性
device.active_time 最近一次上行时间(ISO 8601)
device.heart_period 心跳周期(秒),用于失联判断
device.eui / device.name 设备标识与名称

注意:device 与 tklHelper 各类都以冻结方式注入,赋值不生效,脚本唯一的输出通道是返回值。

五、返回值四个字段,其中一个是平台自带的降噪开关

触发脚本返回 null(什么都不做)或一个包含下列字段的对象:

字段 类型 说明
delayms Number 执行 actions 前的延迟(毫秒)
abort_previous_timer Boolean true 时取消上一次未执行的延迟任务,用本次替代
should_dispatch Boolean 默认 false:本次数据与上次相同时跳过下发。设为 true 则强制执行
actions Array 按顺序执行的 RPC 调用数组

actions 每一项的字段:

字段 说明
method RPC 的 Method 名称(告警场景固定为 alarm)
methodId 与 method 二选一,重名时优先用 methodId
params._eui 目标设备 EUI,写在 params 里,不是 action 的顶层字段

should_dispatch 默认 false 是平台送的第一层降噪——重复上报相同数据时不会重复派发动作。很多人不知道这一点,又自己去写了一套去重逻辑。

六、一条可以照抄的告警脚本

下面是一个温度 + 电池双条件告警,阈值和通知组全部外置到 server_attrs,改阈值不用改脚本:

// 触发脚本写裸函数体,平台会包进 function triggerRunner(){...} let tdata = device?.telemetry_data?.[thingModelId]; if (!tdata) return null;

const LIMIT_TEMP = device.server_attrs?.alarm_temp ?? 60; const LIMIT_BAT = device.server_attrs?.alarm_battery ?? 20; const notify = device.server_attrs?.notify ?? [];

let actions = [];

// 1)过热:等级与标题保持固定,便于平台去重 let hot = tdata.temperature >= LIMIT_TEMP; actions.push({ method: "alarm", params: { _eui: device.eui, name: "overheat_alarm", title: "[过热] " + device.name, level: "urgent", desc: "温度超过上限,请立即处理", action: hot ? "new" : "clear", notify: notify } });

// 2)低电量:只进列表,不打扰(notify 传空数组) let lowBat = tdata.battery <= LIMIT_BAT; actions.push({ method: "alarm", params: { _eui: device.eui, name: "low_battery_alarm", title: "[低电量] " + device.name, level: "mid", desc: "电池电量低于阈值,建议排入更换计划", action: lowBat ? "new" : "clear", notify: [] } });

return { delayms: 0, abort_previous_timer: true, actions };

ALARM RPC 的参数说明:

参数 说明
name 告警事件唯一标识,一种告警一个名字
action new 产生,clear 清除,no 不动作
title 界面显示的标题
desc 描述信息
level low / mid / high / urgent
notify 通知组名称数组,逐项分发;空数组=只记不送

配套写在设备 server_attrs 里的配置:

字段 示例值 作用
alarm_temp 60 温度阈值
alarm_battery 20 电量阈值
notify ["ops-team"] 通知组列表

如果不想手写返回对象,可以继承 TriggerHelper 并重写 _alarmProcess(info):构造函数会自动从 device.server_attrs.notify 读取通知组,并自动生成标准返回结构(其中参数名为 group,对应 ALARM RPC 的 notify)。_alarmProcess 返回 false 或 action 为 no 时,alarm() 返回 null。

七、四级告警,以及一张分级矩阵

告警等级只有四个:low(提示)、mid(警告)、high(错误)、urgent(紧急)。等级本身不决定任何行为,决定行为的是"等级 × 通知组"的组合:

场景 建议等级 notify 响应预期
水浸、可燃气体、机房高温 urgent 值班组(企业微信 WebHook) 立即
越限但可容忍(温湿度偏差) high 运维组邮件 当班处理
电池低电、信号质量劣化 mid 空数组,只进列表 纳入计划
参数被改、固件版本不一致 low 空数组 巡检时看

这张表建议在项目启动会上跟客户对齐一次。告警分级不是技术问题,是责任划分问题——每一级告警都要有一个明确的"谁负责",否则 urgent 也会变成没人看。

八、降噪六招

  1. 别关掉 should_dispatch 的默认去重。默认 false 意味着相同数据不重复派发,这是白送的一层降噪。只有在"每次都要强制下发"的场景才设 true。

  2. 让 title、level、desc、name 保持稳定。平台对同名且标题、描述、等级完全相同的告警会做抑制,不重复创建。所以不要把当前数值拼进 desc——温度 61.2 和 61.5 会被判成两条不同的告警,去重失效,一晚上刷出几十条。要写数值,就写区间或等级("温度超过上限")。

  3. clear 默认化。把 action 默认设成 clear,只在条件满足时切成 new。这样每次上行都是一次"恢复确认",条件正常了告警自动消失,不留僵尸告警。反过来(只在超限时发 new),一旦设备离线,告警会永远挂着。

  4. 用 delayms 做持续确认。抖动型测点(门磁、水浸、瞬时越限)不要一超限就告警,可以返回 delayms 延后执行,配合 abort_previous_timer: true,让"上一次还没执行的定时动作"被本次替代——只有持续越限的那一趟才能真正落地成告警。

  5. 阈值和通知组放 server_attrs。改阈值不用改脚本、不用产生新版本,也不用重新挂载。多设备共享一个触发模型、各自配自己的阈值,这是规模化部署的前提。

  6. 多参数确认。温度高且湿度同步变化,和温度单独跳一下,是两种可信度。单一参数越限进 mid,两个参数同时越限才升到 high,能砍掉相当一部分误报。

九、通知组:能力边界如实说

通知组(告警管理 → 通知组)的必填项是 Id Name、名称、通知方式、联系人,标签和备注可选。

项 实际情况
内置通知方式 邮件(默认)与企业微信 WebHook
联系人 可多选
启用开关 默认关闭,配完必须手动打开
钉钉 / Slack / Telegram 不在内置表单里,需要走自定义 RPC 接入

这一点值得单独强调:把钉钉、Slack、Telegram 写成"平台内置渠道"是不准确的。它们的接入路径是自定义 RPC——能实现,但不是开箱即用的勾选项。方案书上要按实际口径写,否则验收时会对不上。

实践建议:在设备 server_attrs 里以数组形式存通知组名(如 notify: ["ops-team"]),脚本读出来直接传给 notify。这样"分级路由"就变成了改属性,而不是改脚本——同一个触发模型挂 500 台设备,可以各走各的组。

十、三个可直接套用的配方

场景 触发条件 等级 通知 恢复方式
KS52 水浸 water_leak == 1 urgent 值班组企业微信 数据恢复即 clear
电池低电 battery <= 20 mid 空数组(只进列表) 换电池后 clear
设备失联 active_time 超过 2 × heart_period high 运维组邮件 下次上行即 clear

失联告警需要说明:平台对在线状态有三个不同的口径(列表与统计的活动时间口径、server_attrs.online 检测器、MQTT 的 data.online 通知),三者不能直接互换。用触发脚本做失联判断时,建议明确写清自己用的是 active_time 与 heart_period 这一路,并在文档里注明。

十一、上线前自查清单

十二、八个常踩的坑

现象 原因
脚本完全不执行 触发模型没挂到设备上,只创建了模型
脚本执行了但没告警 设备上没挂 ALARM RPC
告警刷屏 desc 里拼了实时数值,去重被绕过
告警永远不消失 只发 new 不发 clear,设备离线后告警挂死
改了阈值没生效 阈值硬编码在脚本里,改的是 server_attrs
脚本报 ReferenceError 用了 console / setTimeout / process 等沙盒不注入的对象
误以为脚本卡住 用了 async/await,沙盒不支持异步
以为"全部已读"=故障恢复 那只是阅读状态,与告警是否 clear、通知是否送达是三件事

结语

一套能用的告警系统,技术含量不在脚本本身,而在这三个判断:什么是真的异常、谁必须知道、什么时候闭嘴。ThinkLink 把这三个判断都交给了脚本和属性配置——好处是灵活(同一套模型可以服务 500 台阈值不同的设备),代价是需要有人在上线前把规则想清楚。

本文的六招降噪里,最容易被忽略的是第 2 条(保持 desc 稳定)和第 3 条(clear 默认化)。这两条不写,系统上线第一周可能一切正常,第二周开始就没人愿意看告警列表了。

下一期预告(第 23 期):数据在本地不出厂区——ThinkLink Edge(TKE)边缘部署实战,从硬件选型、Docker Compose 起步到与云端物模型的双向迁移。