# WorkBuddy 实战:半小时搭出浙政钉消息推送助手,让渔船风险预警自动"找人"
> **摘要**:预警信息躺在系统里没人看,是政务业务系统最常见的"最后一公里"失效。本文记录我用 WorkBuddy 从 0 到 1 搭建浙政钉机器人消息推送助手的全过程:从群机器人配置、六类消息格式、可视化推送台搭建,到对接渔船安全风险研判专报自动推送,并附上 7 个真实踩过的坑。全程零框架依赖,一个 Node 服务 + 一个单页前端即可跑通。
>
> **标签**:#WorkBuddy #钉钉机器人 #政务信息化 #自动化推送 #Node.js
---
## 一、先说痛点:预警不是发不出去,是没人第一时间看
我所在的场景是**渔船安全风险研判**。系统每天会产出大量预警:沉船报警、设备按钮报警、驾驶室无人值守、伏休期违规出港……这些预警最初只存在于业务系统的列表页里,值班人员要主动登录、翻列表、筛选,才能发现"刚刚有一条红色预警"。
问题就出在这——**预警的价值随时间衰减得极快**。一条沉船报警晚 10 分钟被看到,性质完全不同。
但现实是:
- 值班人员不会 8 小时盯着后台页面;
- 邮件没人看,短信成本高且字数受限;
- 让业务系统直接写死推送代码,每次改文案都要开发介入、重新发版;
- 不同业务群要推的内容、格式、@的人都不一样,硬编码会迅速变成一团乱麻。
我们需要的是:**一个独立于业务系统的、可配置的、所见即所得的推送台**,把"要推什么、推给谁、用什么格式推"这三件事交给业务人员自己决定,业务系统只负责在关键时刻调一次接口。
而浙政钉(浙江政务协同办公平台)基于钉钉底座,群机器人的 Webhook 能力与钉钉开放平台同源——这让整件事的成本几乎为零。
---
## 二、整体架构:三层,越薄越好

三个设计取舍,事后看是对的:
1. **推送逻辑与业务逻辑解耦**。业务系统不知道推送助手的存在,推送助手也不知道业务数据的来源。中间只靠一个 HTTP 调用和一个 JSON 体连接。
2. **配置外置**。机器人 Webhook、加签密钥、安全关键字、API 域名全部存在配置文件/界面里,改文案不重启服务。
3. **先干跑,再发送**。这是整个工具里最值钱的一个功能,后面细说。
---
## 三、准备:拿到 Webhook、加签密钥和 API 域名
这一步在浙政钉客户端里完成,2 分钟:
1. 打开目标政务群 → 群设置 → **智能群助手** → 添加机器人 → **自定义机器人**;
2. 安全设置三选一,建议选**加签**(比"自定义关键词"灵活,比"IP 白名单"省事):
- 复制 Webhook 里的 `access_token`;
- 复制加签密钥:`SEC` 开头的字符串;
3. **顺手记下接口域名**。浙政钉开放平台和公网钉钉**不是同一个域名**,工具里专门留了「API 域名」一栏可覆盖,默认值是 `https://openplatform-pro.ding.zj.gov.cn`。这一步很多人会漏——照搬公网教程里的 `oapi.dingtalk.com` 在内网里是发不通的。
4. 机器人名字起一个能自证身份的,比如"渔安值守助手"——政务群里一个说不清来源的机器人,管理员迟早会把它禁掉。
> **政务内网特别注意**:浙政钉环境常常限制出网。开工前先确认服务器能访问浙政钉开放平台域名,否则会卡在最后一步莫名其妙的超时上。这是我在政务项目里踩过最多次的坑,没有之一。
加签算法本身很简单,`timestamp + "\n" + secret` 做 HMAC-SHA256 再 Base64,最后 URL 编码:
```js
// server.js —— 加签:网上教程多是 openssl 版本,Windows 机器上没装 openssl 就用这 4 行
const crypto = require('crypto');
const timestamp = Date.now(); // 必须是 13 位毫秒
const stringToSign = timestamp + '\n' + secret; // 注意是换行,且与密钥之间无空格
const sign = encodeURIComponent(
crypto.createHmac('sha256', secret).update(stringToSign).digest('base64')
);
url += `×tamp=${timestamp}&sign=${sign}`; // 拿到的是最终可 POST 的完整地址
```
这篇文章要做的事,就是把这个只能改脚本参数的原始版本,升级成业务人员能自己操作的推送台。
---
## 四、让 WorkBuddy 干活:我用的提示词
我没有从空白页开始写,而是把已有的加签脚本和一份专报样例 JSON 一起丢给 WorkBuddy,配这样一段需求:
> 基于附件里的推送脚本和样例 JSON,做一个可配置的前端页面,用来管理浙政钉机器人的消息推送。要求:
>
> 1. 支持多个机器人的增删改查,每个机器人配置名称、access_token、加签密钥、安全关键字、API 域名、备注;
> 2. 支持 **6 种消息类型**:文本、图文 Markdown、链接、整卡跳转、图文列表、舆情报告,切换类型时表单字段跟着变;
> 3. 支持 @指定人和 @所有人,手机号多个用逗号分隔;
> 4. **发送前必须能预览最终 payload**(干跑),并且要和脚本真实发出的请求体完全一致;
> 5. 有发送历史,能看每次发的原始 JSON 和平台返回,失败可以一键重发。
>
> 技术栈:Node.js 原生 http 模块 + 单文件前端,不要引入框架和构建工具,配置和留痕用 json 文件存,端口 4600。
**这里最关键的是第 4 条**——"必须和脚本真实发出的请求体完全一致"。如果不写这句,AI 很可能会"顺手优化"一下 payload 结构,发出来的东西看着差不多,但到了线上就是不行。给它一个可比对的标准,是从"生成代码"走向"生成可用代码"的分水岭。
WorkBuddy 生成的项目结构:
```
ding-notify-assistant/
├── server.js # Node 原生 http,零依赖
├── public/
│ └── index.html # 单文件前端
├── config/
│ └── robots.json # 机器人配置
└── data/
└── ding-log.json # 发送留痕(json-file 存储,无需数据库)
```
启动就一条命令,浏览器打开 `http://localhost:4600`:
```bash
node server.js
```
打开后是这样一页——上面机器人配置、中间消息推送、下面发送历史,三块在一块屏里:

---
## 五、功能实操:五个真正用得上的点
### 5.1 多机器人管理
政务场景几乎不可能只推一个群。我们实际配置的是这样的矩阵:
| 机器人 | 目标群 | 默认消息类型 | 默认@人 |
| --- | --- | --- | --- |
| 值班值守 | 渔船值守群 | 图文 Markdown | 值班手机号 |
| 指挥调度 | 应急指挥群 | 整卡跳转 | 无 |
| 日报推送 | 领导阅示群 | 图文 Markdown | 无 |
机器人列表给的字段就是配置时真正要填的那些:名称、`access_token`、安全关键字、加签状态、API 域名,操作列放测试 / 编辑 / 删除三个按钮。**token 在列表和表单里都只显示前 8 位 + 后 6 位**,避免有人在会议室投屏时一眼泄露;密钥只保存在服务端,页面和接口都不返回明文。

这里有个省事的细节:「安全关键字」不是让你每次手打,工具会在发送时**自动给正文加「【】」前缀**(图3 里那台机器人填的是"通知",所以每条消息首行都是 `【通知】…`,图5 的 payload 里能看到),只要和群机器人安全设置里的关键词一致,就不会被平台拦。
### 5.2 六种消息类型怎么选
这是最容易选错的地方,我的判断标准只有一句:**有没有需要人点一下的动作**。
| 界面名称 | 协议 msgtype | 什么时候用 | 我们的用法 |
| --- | --- | --- | --- |
| 文本 | `text` | 纯通知,无需排版 | 连通性测试、短通知 |
| 图文 Markdown | `markdown` | **主力类型**,日常 90% 场景 | 风险预警、今日要情 |
| 链接 | `link` | 一条标题 + 摘要 + 跳转 | 单条通知配详情页 |
| 整卡跳转 | `actionCard` | 需要人做决策(处置/忽略) | 红色高危预警,带"立即处置"按钮 |
| 图文列表 | `feedCard` | 多条并列的信息流 | 多条要情并列 |
| 舆情报告 | `report` | **多篇报告聚合**,一次发一批 | 今日要情参考,N 篇合并成一条 |
前五种是钉钉协议原生类型,第六种 `report` 是**业务侧自定义的聚合类型**——它的价值在于把"N 篇报告"的组装逻辑从业务系统挪到了工具里:业务方只管把 N 篇丢进来,工具负责拼标题、加序号、配图、分隔线,最后归一成 markdown 发出去(下一节会讲这个归一化)。
一个真实的 `actionCard` payload,红色预警用法:
```json
{
"msgtype": "actionCard",
"actionCard": {
"title": "【通知】【红色预警】疑似沉船报警",
"text": "### 【红色预警】疑似沉船报警\n\n- **船名**:浙台渔 xxxxx\n- **时间**:2026-09-16 21:05\n- **位置**:28°xx′N / 121°xx′E\n- **研判**:AIS 信号消失 12 分钟,同区域 3 船报告异常\n\n请值班人员 **5 分钟内** 确认处置。",
"btnOrientation": "0",
"singleTitle": "立即处置",
"singleURL": "http://10.xx.xx.xx:4600/risk/detail?id=xxxxx"
}
}
```
注意标题前缀 `【通知】`——这就是安全关键字自动加上的,不需要人记。

### 5.3 @人:别指望群成员自己看
钉钉的 `@` 有个反直觉的设计:**`atMobiles` 里的手机号,必须同时出现在 `text.content` 里才会真正生效**。只填 `atMobiles` 而不在正文写 `@138xxxx`,收不到提醒。
所以工具里的处理是:把 @手机号追加到正文末尾,同时写进 `at` 字段——两处都写:
```json
{
"msgtype": "text",
"text": { "content": "【通知】【红色预警】请立即核实 \n\n@138xxxx8888" },
"at": {
"atMobiles": ["138xxxx8888"],
"isAtAll": false
}
}
```
另外 `@所有人` 要慎用。一个天天@全体的机器人,三天之内必然被人静音——那就等于白做了。
### 5.4 干跑预览:救命的功能
点"发送"之前,先点 **🔍 预览 Payload**。它会把最终要 POST 出去的 JSON 原样展示,包括加签后的完整 URL,并且明确告诉你这就是实际请求体、没有真发:

为什么必须有这一步?因为在调试阶段我遇到的失败,80% 不是代码问题,而是:
- 模板里某个变量没替换,发出去是 `${shipName}`;
- markdown 里混进了平台不认的语法,整条消息渲染成纯文本;
- 安全关键字漏了,被平台静默拦截;
- 手机号填错,@ 了个不在群里的人(平台会静默脱敏,不报错,就是不提醒)。
能看到最终 JSON,这些问题 3 秒钟就能定位;看不到,就得去翻服务端日志,一轮十分钟。
实现上有个关键点:**干跑和真实发送必须共用同一个 payload 构造函数**,干跑接口只做构造不发送、不留痕。否则预览和实发两套逻辑迟早会漂移,"所见即所发"就成了空话。
### 5.5 发送历史:可追溯与一键重发
每次发送(含失败)都落一条记录:时间、目标机器人、类型、内容摘要、结果、平台返回,顶部还给了"最近 N 条 / 累计 N 条"的计数。

两个实际价值:
- **甩锅防御**:业务方说"我没收到预警",历史记录一翻,几点几分发的、`errcode=0 ok`、平台返回原文都在,一目了然;
- **一键重发**:配置改错了导致发送失败,改完直接点重发,不用重新填一遍表单。
内容摘要是按类型生成的,这个小设计很实用——文本取前 200 字,图文取标题,图文列表取条数,舆情报告取"标题 + N 篇",这样一眼扫过去就知道发的是什么。
---
## 六、对接真实业务:今日要情 / 风险研判专报自动推送
工具跑通之后,把它接进生产链路才是目的。我们现在的链路是:**调度器定时生成 → 先干跑核对 → 再真实推送 → 留痕可追溯**。
```bash
#!/usr/bin/env bash
# 每天 09:00 推送当日「今日要情参考」,由调度器触发
set -euo pipefail
BASE="http://localhost:4600"
# 1. 业务侧生成要情 JSON(结构见 report-example.json)
python3 /opt/fvs/gen_daily_brief.py --slot morning > /tmp/brief.json
# 2. 先干跑:只构造 payload,不发送、不留痕
curl -sS -X POST "${BASE}/api/ding/dryrun" \
-H 'Content-Type: application/json' \
-d '{"robotId":2,"msg":{"msgtype":"report","title":"今日要情参考","reports":[]}}'
# 3. 核对无误后真实推送
curl -sS -X POST "${BASE}/api/ding/send" \
-H 'Content-Type: application/json' \
-d '{"robotId":2,"msg":{"msgtype":"report","title":"今日要情参考","reports":[]}}'
```
**这里踩到一个真实的坑**:业务侧把消息类型写成 `report`,而浙政钉接口只认标准 msgtype。如果直接把 `report` 当协议类型 POST 出去,会返回:
```json
{"errcode": 400105, "errmsg": "不支持的消息类型"}
```
解决办法是在推送助手内部做一层**类型归一化**——`report` 只是内部聚合类型,真正进协议前统一变成 `markdown`:
```js
// server.js —— dingBuildPayload():类型归一化,内部聚合类型 → 协议类型
function dingBuildPayload(robot, msg) {
// 安全关键字自动加【】前缀;@手机号同时写进正文与 at 字段
const tag = String(robot.keywords || '').split(/[,,]/).map(s => s.trim()).filter(Boolean)
.map(k => '【' + k + '】').join('');
let payload = { msgtype: msg.msgtype };
switch (msg.msgtype) {
// ... text / markdown / link / actionCard / feedCard 直接使用原生结构
case 'report': {
// 多篇报告聚合成一条 markdown:主标题 + 每篇 ### 序号标题 + 摘要 + 配图 + 详情链接
const lines = ['## ' + tag + String(msg.title || '今日要情参考'), ''];
(msg.reports || []).forEach((r, i) => {
lines.push('### ' + (i + 1) + '. ' + String(r.title || '未命名'));
if (r.summary) { lines.push(String(r.summary), ''); }
if (r.image) { lines.push('', ''); }
if (r.url) { lines.push('[查看详情](' + r.url + ')', ''); }
lines.push('---', '');
});
payload.markdown = { title: tag + String(msg.title || ''), text: lines.join('\n') };
payload.msgtype = 'markdown'; // 关键一行:内部类型不进协议
break;
}
default:
throw new Error('不支持的消息类型: ' + msg.msgtype);
}
return payload;
}
```
这层映射看着多余,实际上是把"业务语言"和"协议语言"隔开的关键——业务系统想叫什么就叫什么,协议侧的变更也不会反向污染业务代码。
---
## 七、踩坑清单(7 条,都是真踩过的)
| # | 现象 | 根因 | 解法 |
| --- | --- | --- | --- |
| 1 | 请求超时,无响应 | 政务内网未开通出网白名单 | 提前申请浙政钉开放平台域名出网权限;注意别照抄公网 `oapi.dingtalk.com` |
| 2 | `errcode: 310000`(sign not match) | 机器人安全设置为「加签」但没带 `timestamp+sign`,或 sign 未 URL 编码,或服务器时间偏差大 | 补 SEC 密钥;sign 必须 urlencode;校准服务器时间 |
| 3 | `errcode: 310000`(关键词不匹配) | 安全设置用了「自定义关键词」,正文里没有该词 | 工具里填「安全关键字」,发送时自动加 `【】` 前缀 |
| 4 | `errcode: 400105` | msgtype 传了业务自定义值(如 `report`) | 加类型归一化层,内部类型不进协议 |
| 5 | @ 了但没人收到提醒 | `atMobiles` 与正文 `@手机号` 不一致,或手机号不在群内 | 两处都写,且手机号必须在群内 |
| 6 | `errcode: 410100` | 单机器人超过 20 条/分钟 | 批量推送加队列 + 间隔,被限流后停 10 分钟 |
| 7 | 内容发出去但排版全乱 / 渲染成纯文本 | 直接把网页端富文本结构原文发给机器人,浙政钉只认简化 Markdown | 写一层排版转换器:去 H1 大标题、条目标题用 `####`、来源与链接合并成一行、条目间加分隔线 |
第 6 条特别提醒:**限流是"静默"的**,不会报错给调用方感知,只是消息发不出去。批量场景一定要自己做发送队列。
第 7 条是我们后来才补上的:网页上排版漂亮的要情正文,直接原文丢进机器人会变成一坨——**网页的文档结构和推送渠道的简化 Markdown 不是一回事**,中间必须有一层转换。这件事和"干跑预览"是同一个思路:发送前把最终形态看清楚。
---
## 八、下一步:从工具到生产模块
这个助手现在已经从"独立原型"并进了生产系统,路线和当初设想的几乎一致:
1. **配置入库**:机器人配置从 `robots.json` 迁到 MySQL,与业务系统的权限体系打通;
2. **前端模块化**:作为子模块嵌入业务系统的菜单树,不再单独起服务;
3. **后端复用**:直接用系统现有的 `mysql2` 数据库操作封装,不另起炉灶;
4. **留痕升级**:发送记录从 json 文件迁到数据表,并且和要情记录做外键关联,能反查"这条要情推给了哪些群、什么时间、返回什么";
5. **定时推送闭环**:调度器每 30 秒检查一次,到点自动"生成 → 干跑核对 → 推送 → 留痕"。
回头看,从"一个 shell 脚本"到"一套可配置的推送能力",真正花时间的不是写代码,而是**想清楚哪些配置该暴露给人、哪些该锁死在系统里**。AI 帮我把编码这一段的成本压到了接近零,剩下的判断力活儿,还是得自己来。
---
## 结语
这套东西的价值不在于技术多复杂——本质上就是拼一个 JSON 然后 POST 出去。它的价值在于:**让"把消息推出去"这件事,从开发排期里被拿掉了**。现在业务同事想加一个推送场景,自己配一下就能上线,不用再等我。
如果你也在做政务信息化,强烈建议先把"推送通道"这件事做薄、做通用,再往上叠业务。顺序反了,后面每一次改文案都会变成一次发版。
**最后两点建议**:动手前先花 10 分钟确认内网能不能访问浙政钉开放平台域名;再把「干跑预览」当成硬性步骤——这一条能帮你省掉半天。
---
*本文基于真实项目实践整理,涉及的 Webhook、access_token、加签密钥、机器人名称、手机号等均为脱敏示例。*
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。