Skip to content

通过 RPC 推送告警到企业微信

1. 场景说明

ThinkLink(TKL)平台的 RPC 支持调用外部 HTTP 接口(见 RPC 模型 §1.3.2 类型四),可以把设备告警、运行状态、自定义事件实时推送到企业微信群机器人。当设备触发告警条件时,相关人员可以在企业微信中第一时间收到通知。

本文档介绍如何在 RPC 中向企业微信推送消息,包含三种接入方式(按推荐顺序):

方式适用场景鉴权(存放位置)
群机器人(§2–§3)把消息推送到固定的一个微信群Webhook URL(组织参数 wecom_webhook_url
智能机器人(§6)一个机器人需要同时进多个群,或要使用模板卡片、回调等高级能力Webhook URL(组织参数)
自建应用(§7)定向推送给指定人 / 部门 / 标签(不依赖群聊)corpid + corpsecret + agentid(组织参数)

2. 准备工作

2.1 创建企业微信群机器人

  1. 在企业微信群中点击 群设置 → 群机器人 → 添加机器人
  2. 设置机器人名称和头像后,复制生成的 Webhook 地址
  3. Webhook 地址形如:
    https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
    URL 中的 key 参数就是机器人的访问凭证。

2.2 妥善保管 Webhook 密钥(必读)

⚠️ 安全提示:Webhook URL 中的 key 等同于群机器人的发送权限凭证,泄漏后任何人都可以向群中发消息。

请遵守以下规则:

  • 不要把密钥放在设备的 server_attrs、资产属性、触发模型等任何调用方可读的位置;这些字段会随设备/资产一起被有权限的运维查看,密钥外溢面会扩大
  • 推荐做法:把整个 webhook URL 配置到 系统管理 → 服务器配置 → 组织参数(org_params),RPC 脚本通过 org_params.wecom_webhook_url 读取。原因:
    • 组织参数页仅组织管理员可见——普通运维、调用方都看不到密钥
    • 换 key 时只改服务器配置一处,所有引用该 RPC 的触发模型、定时任务自动生效,无需重新发布 RPC 脚本
    • 同一组织内多个机器人(生产/测试、告警/日报)可分别用不同的 key 名称,互不干扰
    • 服务器配置 §1.6 组织参数
  • 建议至少 每季度更换一次 Webhook 密钥(在群机器人设置中点击"重置 webhook 地址",然后只更新组织参数的 value 即可)
  • 严格控制群成员范围,避免外部人员通过加入群获取 webhook
  • 一旦发现密钥可能泄漏,立即在群机器人设置中重置,回到组织参数页面更新 value,并检查群消息日志是否有异常推送

3. 在 RPC 中调用企业微信

3.1 设计原则

把企业微信告警 RPC 当作一个"通知服务"来设计:

  • 密钥封装在内:webhook URL 配置到组织参数(org_params.wecom_webhook_url),仅组织管理员可见
  • 接口暴露在外:调用方通过 params 传入消息内容、告警等级、@ 人员等业务字段
  • 多机器人时:在组织参数里维护多个 key(例如 wecom_webhook_alarm / wecom_webhook_daily),脚本通过 params.channel 路由到不同的 org_params key

3.2 准备工作:把 webhook 写到组织参数

进入 系统管理 → 服务器配置 → 组织参数,新增一条:

KeyValueRemark
wecom_webhook_urlhttps://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx-...企业微信告警机器人 webhook,每季度轮换

保存后所有 RPC 立即生效,无需重启服务。

3.3 RPC 脚本:发送文本消息

javascript
function rpc_script({ device, params, alarms, logger, org_params }) {
    let webhook = org_params?.wecom_webhook_url;
    if (!webhook) {
        logger.error("wecom_webhook_url not configured in org_params");
        return null;
    }
    let content = params.content || `设备 ${device.name}(${device.eui}) 告警通知`;

    return [
        {
            sleepTimeMs: 0,
            type: "axios",
            dnMsg: {
                method: "POST",
                url: webhook,
                headers: { "Content-Type": "application/json" },
                data: {
                    msgtype: "text",
                    text: { content: content }
                },
                timeout: 5000
            }
        }
    ];
}

3.4 RPC 脚本:告警通知(推荐,与 ALARM RPC 配套)

本脚本与平台内置的 ALARM RPC 接收相同的入参,由同一个触发模型同时调用,实现"平台告警记录 + 企业微信推送"双通道。

关键设计点:

  • action = "new":推送一条"设备告警"消息
  • action = "clear":先通过 alarms[name] 判断该告警当前是否活跃 —— 只有活跃告警被清除时才推送"告警恢复"消息;条件长期正常时不会每次上行都刷屏
  • 入参与 ALARM RPC 对齐name / action / title / level / desc 字段名和含义完全一致,触发脚本里同一份参数可以同时喂给两个 RPC
  • webhook 来自 org_params:脚本不再硬编码 key
javascript
function rpc_script({ device, params, alarms, logger, org_params }) {
    let webhook = org_params?.wecom_webhook_url;
    if (!webhook) {
        logger.error("wecom_webhook_url not configured in org_params");
        return null;
    }

    let name   = params.name;
    let action = params.action;
    let title  = params.title || name || "";
    let level  = params.level || "mid";
    let desc   = params.desc  || "";

    if (action === "clear") {
        // 仅当该 alarm_name 当前处于活跃状态时才推送恢复通知,
        // 避免条件持续正常的情况下,每次上行都触发一条 clear 消息
        if (!alarms || alarms[name] === undefined) {
            return null;
        }
    } else if (action !== "new") {
        // 未知 action(如 "no"):不推送
        return null;
    }

    let levelColor = {
        urgent: "warning",
        high:   "warning",
        mid:    "comment",
        low:    "info"
    }[level] || "info";

    let header = action === "new" ? `### 设备告警` : `### 告警恢复`;

    let content = [
        header,
        `> **设备名称**:${device.name}`,
        `> **设备 EUI**:\`${device.eui}\``,
        `> **告警事件**:\`${name}\``,
        `> **告警标题**:${title}`,
        `> **告警等级**:<font color="${levelColor}">${level.toUpperCase()}</font>`,
        `> **${action === "new" ? "告警内容" : "恢复说明"}**:${desc || "无"}`,
        `> **时间**:${new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}`
    ].join("\n");

    return [
        {
            sleepTimeMs: 0,
            type: "axios",
            dnMsg: {
                method: "POST",
                url: webhook,
                headers: { "Content-Type": "application/json" },
                data: {
                    msgtype: "markdown",
                    markdown: { content: content }
                },
                timeout: 5000
            }
        }
    ];
}

💡 alarms 是 RPC 入参之一,是以 alarm_name 为键的活跃告警映射表。键存在表示对应告警当前活跃,键不存在(undefined)表示未产生或已清除。详见告警管理 §1.6.2

4. 配置 RPC 参数

在 RPC 模型管理界面,为该 RPC 添加以下参数字段(与 ALARM RPC 完全对齐,便于触发脚本复用同一份 params):

字段标识类型别名说明
namestring告警事件名称唯一标识,必须与 ALARM RPC 的 name 一致,用于 alarms[name] 去重判断
actionstring动作new 产生告警 / clear 恢复(仅当原告警活跃时才推送)
titlestring告警标题推送消息中的标题字段
levelstring告警等级low / mid / high / urgent
descstring告警描述new 时为告警详情,clear 时可填恢复说明

文本模式(§3.3)的 RPC 是另一种通用调用形态,参数为 content,不属于本告警链路,可作为独立 RPC 留作非告警类通知(日报、对账等)使用。

5. 集成到告警流程

推荐做法:在同一个触发模型中同时调用 alarmwecom_notify,两者共享同一份参数。alarm 负责在平台中创建/清除告警记录,wecom_notify 负责把消息推送到企业微信群。

javascript
function trigger_script(device, thingModelId) {
    const ACTION = { no: "no", new: "new", clear: "clear" };
    const LEVEL  = { low: "low", mid: "mid", high: "high", urgent: "urgent" };

    let tdata = device?.telemetry_data?.[thingModelId];
    if (tdata === undefined) { return null; }

    let name   = "overheat_alarm";
    let title  = "过热告警: [" + device.name + "]";
    let level  = LEVEL.high;
    let desc   = "";
    let action = ACTION.clear;     // 默认:清除(条件未满足)
    let group  = device?.server_attrs?.group ?? {};

    if (tdata.temperature !== undefined && tdata.temperature >= device.server_attrs?.alarm_temp) {
        desc   = "[" + device.name + "] 温度 " + tdata.temperature + "℃,超过阈值 " + device.server_attrs.alarm_temp + "℃";
        action = ACTION.new;       // 条件满足:产生告警
    }

    // 一份参数同时喂给 alarm 和 wecom_notify
    let alarmParams = {
        _eui:   device.eui,
        action: action,
        name:   name,
        title:  title,
        level:  level,
        desc:   desc,
        group:  group
    };

    return {
        delayms: 0,
        abort_previous_timer: true,
        actions: [
            { method: "alarm",        params: alarmParams },
            { method: "wecom_notify", params: alarmParams }
        ]
    };
}

⚠️ 若实测发现 clearwecom_notify 收到的 alarms[name] 总是 undefined(说明平台先执行 alarm 清除了记录、再执行 wecom_notify 时已读不到),可将 actions 顺序调整为 wecom_notify 在前、alarm 在后;或在触发脚本中自行用 device.server_attrs 记录"上一次是否处于告警态"作为判定依据。

6. 使用智能机器人推送

智能机器人与群机器人都走 Webhook,但管理形态完全不同:

群机器人智能机器人
创建入口群内 → 群设置 → 群机器人 → 添加企业微信管理后台 → 应用管理 → 智能机器人
权限要求群成员即可需企业管理员
服务范围仅所在的一个群可加入多个群,统一管理
支持消息text / markdown / image / news / file / template_card同左 + 模板卡片按钮交互回调
接收用户消息✅(可配置回调 URL)

6.1 创建智能机器人

  1. 进入 企业微信管理后台 → 应用管理 → 智能机器人 → 添加机器人
  2. 配置名称、头像、可见范围
  3. 把机器人加入需要接收告警的群(一个机器人可加入多个群)
  4. 在机器人详情中复制 Webhook 地址,格式与群机器人一致:
    https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=<智能机器人唯一KEY>

6.2 RPC 脚本

智能机器人的发送接口与群机器人完全一致——只需在组织参数里新增/替换 wecom_webhook_url 为智能机器人的 webhook,§3.4 的脚本代码无需改动,同一份 RPC 即可同时推送到该机器人加入的所有群。

若需要同时保留群机器人和智能机器人,可在组织参数里分别配置 wecom_webhook_groupwecom_webhook_smart,再让调用方通过 params.channel 路由:

javascript
let webhook = params.channel === "smart"
    ? org_params?.wecom_webhook_smart
    : org_params?.wecom_webhook_group;

6.3 安全提醒

  • 与群机器人一样,key 只写在组织参数里(见 §2.2)
  • 智能机器人 key 的泄漏面比群机器人更大——它可能同时在多个群中,一旦泄漏所有群都会被骚扰;建议管理员每季度在管理后台重置一次,并同步更新组织参数 value

7. 使用自建应用推送

当告警需要发给特定的人、部门或标签(而不是固定的群),用自建应用消息接口更合适。

7.1 准备

  1. 企业微信管理后台 → 应用管理 → 自建 → 创建应用
  2. 记录三个关键字段:
    • corpid:在"我的企业"页面获取
    • agentid:应用详情页中的"AgentId"
    • corpsecret:应用详情页中的"Secret"
  3. 在应用的"可见范围"中勾选可以接收消息的成员

⚠️ corpsecret 一旦泄漏,攻击者可冒充该应用向可见范围内任意成员发任意消息。务必按 §2.2 的方式存放在组织参数中,不要放在 server_attrs、触发模型或调用方可读的任何位置。

把以下三个参数写到 系统管理 → 服务器配置 → 组织参数

KeyValue 示例Remark
wecom_corp_idww1234567890abcdef企业微信 corpid
wecom_corp_secretxxxxxxxxxxxxxxxxxxxxxxxxxxxx自建应用 secret,每季度轮换
wecom_agent_id1000002自建应用 AgentId

7.2 调用流程

发送一条消息需要两步 HTTP 调用:

  1. 拿 access_tokenGET https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=...&corpsecret=...
  2. 发送消息POST https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=...

由于第二步依赖第一步的响应,必须在 RPC 脚本里用 await axios() 先把 token 拿到,再返回发送动作(参考 RPC 模型 §1.3.1 axios 入参)。

7.3 RPC 脚本

javascript
async function rpc_script({ device, params, alarms, logger, org_params }) {
    let corpId     = org_params?.wecom_corp_id;
    let corpSecret = org_params?.wecom_corp_secret;
    let agentId    = org_params?.wecom_agent_id;
    if (!corpId || !corpSecret || !agentId) {
        logger.error("wecom_corp_id / wecom_corp_secret / wecom_agent_id not configured in org_params");
        return null;
    }

    let name   = params.name;
    let action = params.action;
    let title  = params.title || name || "";
    let level  = params.level || "mid";
    let desc   = params.desc  || "";

    if (action === "clear") {
        if (!alarms || alarms[name] === undefined) return null;
    } else if (action !== "new") {
        return null;
    }

    // 1) 先拿 access_token
    let tokenResp = await axios({
        method: "GET",
        url:    "https://qyapi.weixin.qq.com/cgi-bin/gettoken",
        params: { corpid: corpId, corpsecret: corpSecret },
        timeout: 5000
    });
    let token = tokenResp.data?.access_token;
    if (!token) {
        logger.error("get wecom access_token failed", { resp: tokenResp.data });
        return null;
    }

    let header = action === "new" ? "## 设备告警" : "## 告警恢复";
    let content = [
        header,
        `**设备**:${device.name} (\`${device.eui}\`)`,
        `**告警**:${title}`,
        `**等级**:${level.toUpperCase()}`,
        `**${action === "new" ? "说明" : "恢复"}**:${desc || "无"}`,
        `**时间**:${new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}`
    ].join("\n\n");

    // 2) 返回发送动作(响应不需要进一步处理时,用 axios 类型动作交给平台异步执行)
    return [{
        sleepTimeMs: 0,
        type: "axios",
        dnMsg: {
            method: "POST",
            url:    `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${token}`,
            headers: { "Content-Type": "application/json" },
            data: {
                touser:  params.touser  || "@all",
                toparty: params.toparty,
                totag:   params.totag,
                msgtype: "markdown",
                agentid: Number(agentId),
                markdown: { content: content }
            },
            timeout: 5000
        }
    }];
}

7.4 接收人参数

touser / toparty / totag 由调用方在 params 里传入,至少要指定其一:

字段类型说明
touserstring用户 ID 列表,多人用 | 分隔;@all 表示该应用可见范围内所有人
topartystring部门 ID 列表,多个用 | 分隔
totagstring标签 ID 列表,多个用 | 分隔

7.5 access_token 的缓存策略

access_token 有效期 7200 秒,且企业微信对 /gettoken 接口有频次限制。两种策略二选一:

  • 每次现取(上面脚本采用):实现最简单,告警量低时(每天几十次以内)够用
  • 缓存复用:把 token 和过期时间通过 type: "modifyAttrs" 写到设备 server_attrs,下次先读 device.server_attrs.wecom_token 判断是否过期,过期才重取。告警量大时启用,可大幅减少 /gettoken 调用

8. 故障排查

现象可能原因
群中收不到消息Webhook key 错误;机器人被移出群;服务器到 qyapi.weixin.qq.com 网络不���
RPC 日志显示 errcode != 0消息格式不符合企业微信规范;@人时手机号格式不正确
偶尔丢消息触发企业微信频率限制(单 webhook 每分钟 20 条)
自建应用 errcode = 40014access_token 已过期或无效,检查是否启用了缓存但未做过期处理
自建应用 errcode = 60011touser / toparty / totag 中的成员不在应用可见范围
自建应用 errcode = 45033触发了 /gettoken 调用频次限制,应启用 §7.5 缓存策略

9. 注意事项

  • 单条消息正文长度建议控制在 2048 字符以内
  • 消息中不要携带客户身份证、手机号、Token 等敏感业务数据;只放定位问题所需的关键信息
  • 同一个机器人服务多个用途时,建议在消息开头加上来源标识(如 [生产环境][测试环境]
  • 重要告警建议同时配置多个通知通道(如企业微信 + 邮件),避免单一通道异常导致漏报