简介:面向Java开发者的钉钉机器人自动发送消息源码包,完整演示了通过调用钉钉Webhook接口向群聊推送自定义文本信息的实现过程。项目核心包含AlarmService类,封装了HTTP POST请求、JSON消息结构构建、返回状态判断等关键步骤,并预留扩展点便于接入定时任务或告警场景。
包体共161个文件,压缩后仅382KB。文件以Java源码、XML配置、JS脚本、JSON数据、CSS样式和Maven工程文件为主,其中XML多用于项目与Spring配置,JS与CSS配合前端页面展示,properties保存环境参数,源代码可直接导入IDE运行调试。目前已有2296人学习下载。
借助该源码,可快速掌握钉钉机器人接入流程、HttpClient工具类的用法以及消息推送的通用设计思路。下载后能直接获得完整工程与配置文件,既可用于团队通知、系统监控报警、任务进度播报等实际场景,也可作为学习Java网络编程和第三方API调用的实用范例。
1. 钉钉机器人自动发送自定义信息到钉钉群:一条 Webhook 就能把重复通知变成定时任务
每天早上把昨天的报表、报警记录、订单状态挨个打开看一眼,再手动复制粘贴到群里,这个动作一次两次无所谓,天天做就成了纯消耗。钉钉机器人自动发送自定义信息到钉钉群,就是把这一步从「人肉通知」变成「定时任务」的常见做法:在群里建一个自定义机器人,得到一个 Webhook 地址,任何能发 HTTPS 请求的脚本都能以机器人身份把文本、Markdown 表格甚至带跳转链接的卡片推进群里。很多人搜钉钉机器人消息推送,要的其实就是这个能力,不需要开放平台权限,也不需要一台独立服务。标题里的「对应源码」说明这套方案通常自带可直接改着用的脚本,而不是只有原理。这篇按从一个空群到定时推送的完整路线写:建机器人、读懂源码里的消息模型、写发送脚本、排掉最常见的坑,照着做完,半个小时能跑通第一版。
2. 从建群到拿到 Webhook:三步建好机器人并选对安全校验方式
2.1 创建钉钉群机器人的完整路径:群设置、自定义机器人、密钥
先纠正一个普遍误解:钉钉机器人自动发送这事,机器人不是在钉钉开放平台创建的,而是在群设置里加的。打开目标群,点右上角“...”,找到「群机器人」入口,选「添加机器人」,在类型列表里选「自定义」。这里很多人会选错成「企业内部机器人」,那是给正经集成开发用的,要申请 AppKey、走应用发布流程;而「自定义」机器人建完直接给你一串 Webhook 地址,脚本往这个地址发请求就算发消息。标题里说的对应源码,基本都围绕这个自定义机器人方向写,因为接入成本最低,不需要管理员审批,群主或群管理员在手机端和电脑端都能操作。
具体路径按顺序走一遍:群设置 → 群机器人 → 添加机器人 → 自定义 → 输入机器人名称 → 选择安全设置 → 完成。机器人名称建议带上环境标识,比如「线上告警」「报表机器人」,不然三五个机器人全叫「小助手」,运维排错时根本分不清消息从哪来。头像可以默认,也可以传团队 logo,这一步不影响功能。完成后钉钉会展示一个 Webhook 地址,形如https://oapi.dingtalk.com/robot/send?access_token=一串字符,这串字符就是群的身份凭证,泄露给别人,别人也能往群里发消息,所以后续脚本里要把它当密码处理,别直接写死在代码里到处传。
还有一个边界要提前说清楚:自定义机器人只能往它被创建的那个群发消息,不能跨群发送,也不能读取群消息,更不会自动回复。它只有「发消息」这一项能力。看清这个边界后才好判断它能干什么——定时报表推送、监控告警、版本上线通知、运营活动播报,这些单向通知场景都合适;如果要和群成员的回复做交互,那就不是这个方案能覆盖的范围了。
另外记一个失效规则:机器人被群管理员移除或群被解散后,旧 Webhook 立即失效;重新添加得到的是新的 access_token。所以脚本里的 Webhook 不要散落各处,我会把它放在单独配置或环境变量里,换群时只改一个地方。
2.2 三种安全校验方式怎么选:关键词、加签、IP白名单
建机器人时「安全设置」这一步必须选,钉钉默认不让你跳过。三种方式各有取舍,先看对照:
| 安全方式 | 原理 | 适合场景 | 自动化脚本的代价 |
|---|---|---|---|
| 自定义关键词 | 消息内容必须包含至少一个预设关键词 | 人工测试、内容固定 | 每条消息都要带关键词,自由文本受限 |
| 加签 | 用 Secret 计算签名拼进请求 URL | 自动化推送、内容不固定 | 每次发送要现算 timestamp 和 sign |
| IP 白名单 | 只允许指定来源 IP 调用 | 出口 IP 固定的服务器 | 动态 IP 环境会频繁失效 |
如果选了「自定义关键词」,后续所有消息的内容里必须包含至少一个预设关键词,否则钉钉在网关层直接拒绝,返回类似「关键词不匹配」的错误。给团队演示时这个最省事,因为内容里写上词就行;但对自动化链路有点难受,比如关键词设成「报表」,那每一条自定义信息都得记得把「报表」两个字塞进去,消息内容被绑住了。
自动化推送我一般默认选「加签」。流程是:钉钉给一个以SEC开头的密钥,发送时把当前毫秒级时间戳和密钥拼成timestamp\nsecret,用 HMAC-SHA256 计算、Base64 编码、再 URL 编码,最后拼到 Webhook 的 query 参数上。钉钉服务端用同一份密钥做校验,能对上就放行。这个模式不会约束消息内容,正好匹配「自定义信息」随意变化的需求。
IP 白名单适合服务器出口固定、多服务共用一个推群机器人的场景;但小团队办公网和个人电脑基本都是动态出口 IP,今天能发明天被拒,排查一圈才发现是 IP 变了,所以不建议把它当首选。
实际部署时三种方式可以组合,比如「关键词 + 加签」同时启用。我一般只开加签,重点提醒一句:加签用的SEC密钥只在创建时完整展示一次,之后在群设置里只能看到 Webhook 地址,看不到密钥内容。第一次拿到就保存好,丢了只能删掉机器人重新建。
2.3 先拿 curl 验证 Webhook 通不通:最小可用请求
在写 Python 脚本之前,先用 curl 把链路打通,确认 Webhook 没抄错。如果机器人选的是「自定义关键词」或没设安全设置,直接发:
curl 'https://oapi.dingtalk.com/robot/send?access_token=你的TOKEN' \ -H 'Content-Type: application/json' \ -d '{"msgtype":"text","text":{"content":"钉钉机器人连通测试"}}'返回{"errcode":0,"errmsg":"ok"},测试群里出现这条消息,说明 Webhook 可用。如果内容里没带预设关键词,返回的 errcode 非 0,也能顺手验证安全设置是否真的生效。
如果建机器人时选了「加签」,上面这个裸 curl 是发不出去的,必须先算签名再拼 URL,这一步放到第四章的脚本里处理。先验证再编码的好处是缩小排错范围:链路不通时先怀疑 Webhook 本身,链路通了再谈签名和消息体,后面加变量时才不会一团乱麻。
3. 看懂源码里的消息模型:text、markdown、link 三种消息体的字段对照
3.1 text 消息体:最少字段也能发,@ 指定人的正确写法
打开对应源码包,不管它用 Python、Java 还是 Go 写,核心都在构造一段 JSON 消息体。钉钉机器人自动发送自定义信息,本质就是把信息塞进固定结构的 JSON,然后用 POST 丢给 Webhook。最基础的是 text 类型:
{ "msgtype": "text", "text": { "content": "巡检完成,一切正常" }, "at": { "atMobiles": ["13800138000"], "isAtAll": false } }顶层msgtype告诉钉钉这是文本消息;text.content就是要展示的原文,直接填字符串就行。at字段可选:atMobiles接收一个手机号数组,表示要提醒哪些群成员;isAtAll设为 true 会 @ 全员。日常定时通知如果没有紧急情况,建议把isAtAll保持 false,避免半夜一个成功通知打扰所有人。
一个消息体常见误区:有人会把@13800138000拼进 content,又把同一手机号放进 atMobiles,结果群里出现一段带 @ 符号的正文外加一条提醒,看起来很啰嗦。正确做法是 content 里不写 @,提醒由 atMobiles 负责。另外 atMobiles 里的手机号必须是当前钉钉组织内已激活的账号,写在普通外部联系人群里可能不提醒,这是钉钉侧的限制,脚本多写了也无解。
3.2 markdown 消息体:标题、文本、表格的渲染边界
报表、告警摘要这类结构信息,用 text 发一大段纯文本很难读,这时用 markdown 类型:
{ "msgtype": "markdown", "markdown": { "title": "每日订单报表", "text": "## 订单统计\n\n| 日期 | 订单数 |\n| --- | --- |\n| 2025-03-21 | 128 |\n\n> 数据来自线上库" }, "at": { "isAtAll": false } }markdown.title显示在会话列表和通知栏,作用类似邮件标题,要起得一看就懂;markdown.text才是正文,写 Markdown 源文本。钉钉渲染的是 Markdown 子集,不是 GitHub 完整风味:标题、粗斜体、有序无序列表、表格、引用块基本都支持,但不支持复杂 HTML、也不支持 base64 内嵌图片,图片只能放外链 URL。
表格是自动化推送里最常用也最容易摔跤的部分。钉钉要求表头、分隔行、数据行之间结构完整,前后要有空行。text字段里的\n是真实的换行符,源码里如果只是简单"\n".join(),大概率被渲染成一整段连续文本。我习惯在每行之间留一个空行,也就是两个换行符,像上面的示例一样。单元格内容里的竖线、换行必须清洗掉,否则表格会被拦腰截断,这一节的坑在第五章还会展开。
3.3 link 消息体:一条带跳转入口的通知
自动推送到最后往往还要给人一个落脚点:想看详情点哪里。link 类型就是干这个的:
{ "msgtype": "link", "link": { "text": "点击查看完整报表与昨日明细", "title": "订单日报已生成", "picUrl": "", "messageUrl": "https://your-report.example.com/daily" } }link.title是卡片主标题,link.text是概要描述,link.messageUrl是点击卡片后打开的地址,必须是完整 http/https 链接;picUrl是缩略图地址,可留空。适合版本上线通知、跳转报表、跳转工单这类场景。源码里它常常配合 text 一起用:先发一条 markdown 汇总,再补一条 link 给人入口。
看完三种消息体就会发现,源码里发送函数往往像这样封装公共逻辑:
def send_payload(webhook: str, payload: dict) -> dict: resp = requests.post(webhook, json=payload, timeout=5) return resp.json()消息体统一用 dict 构造,再交给requests.post的json参数序列化,而不是自己拿字符串拼 JSON。原因很朴素:自定义信息里经常混着中文引号、双引号、换行、百分号,手工拼字符串极易转义错误,一个引号就让你排查半天。用 dict 构造让序列化库去处理转义,是源码里最值得保留的习惯。
4. 用 Python 把自定义信息推进钉钉群:最小脚本、Excel 报表转 Markdown 与定时触发
4.1 完整可用的发送脚本:加签、命令行参数、文本消息三合一
把第三章的消息模型落到脚本上,第一版只做一件事:接收命令行参数,发一条 text 消息,同时支持加签。代码可以收敛到一个文件:
import argparse import base64 import hashlib import hmac import time import urllib.parse import requests def build_signed_url(webhook: str, secret: str) -> str: # 没有 secret 说明机器人没开加签,直接返回原始链接 if not secret: return webhook timestamp = str(round(time.time() * 1000)) string_to_sign = f"{timestamp}\n{secret}" hmac_code = hmac.new( secret.encode("utf-8"), string_to_sign.encode("utf-8"), digestmod=hashlib.sha256, ).digest() sign = urllib.parse.quote_plus(base64.b64encode(hmac_code).decode("utf-8")) return f"{webhook}×tamp={timestamp}&sign={sign}" def send_text(webhook: str, secret: str, content: str, mobiles=None): url = build_signed_url(webhook, secret) payload = { "msgtype": "text", "text": {"content": content}, "at": {"atMobiles": mobiles or [], "isAtAll": False}, } return requests.post(url, json=payload, timeout=5).json() if __name__ == "__main__": parser = argparse.ArgumentParser(description="钉钉机器人自动发送自定义信息") parser.add_argument("--webhook", required=True, help="群设置里复制的完整 Webhook 地址") parser.add_argument("--secret", default="", help="加签模式下的 SEC 开头密钥,没加签可留空") parser.add_argument("--content", required=True, help="要发送的自定义文本内容") parser.add_argument("--mobiles", nargs="*", default=[], help="需要 @ 的群成员手机号,多个用空格分隔") args = parser.parse_args() result = send_text(args.webhook, args.secret, args.content, args.mobiles) print(result) if result.get("errcode") != 0: raise SystemExit(1)传给函数的主要参数见下:
| 命令行参数 | 含义与示例 |
|---|---|
--webhook | 完整 Webhook 地址,形如https://oapi.dingtalk.com/robot/send?access_token=xxx |
--secret | 加签模式下的SEC密钥;没开加签就不传 |
--content | 推送到群里的自定义信息,比如「巡检完成,一切正常」 |
--mobiles | 被 @ 的手机号,多个用空格分隔 |
build_signed_url是加签的全部秘密:时间戳必须用毫秒,string_to_sign顺序是「时间戳 + 换行 + Secret」,算完 HMAC-SHA256 后 Base64 编码,最后还要 URL 编码一次。sign里可能带+、/、=,不 URL 编码直接拼进链接,会被解析成空格或截断,签名对不上。用dict构造 payload 后交给requests.post(url, json=payload),requests 会自动设置 Content-Type,也省去手工json.dumps的中文转义麻烦。
注意:如果机器人创建时没选「加签」,
--secret留空即可;脚本里if not secret会直接返回原始 Webhook。如果确认加了签却一直报验签失败,先跳到 5.1 对一遍常见错误。
4.2 用 Python 将 Excel 使用钉钉机器人推送到群聊天消息:报表转 Markdown
很多群里的日报、周报其实是把 Excel 内容复制进去。这个动作完全可以用标题里的方案替代:用 pandas 读 Excel,转成一条 markdown 表格,一次推送进群。先装依赖:
pip install requests pandas openpyxlopenpyxl负责读.xlsx文件;如果手上还有旧版.xls,额外装一个xlrd。
读取并转换成 markdown 的函数可以这样写:
import pandas as pd def build_markdown_from_excel(path: str, max_rows: int = 20) -> str: df = pd.read_excel(path).head(max_rows).fillna("") header = "| " + " | ".join(str(col) for col in df.columns) + " |" divider = "| " + " | ".join(["---"] * len(df.columns)) + " |" lines = [header, divider] for _, row in df.iterrows(): cells = [] for value in row: text = str(value).replace("\r", "").replace("\n", " ").replace("|", "/") if len(text) > 40: text = text[:40] + "..." cells.append(text) lines.append("| " + " | ".join(cells) + " |") return f"## 数据报表\n\n" + "\n\n".join(lines)单元格清洗是这段的关键:原始 Excel 单元格里的换行符会直接破坏表格结构,竖线|会和 markdown 分隔符冲突,超长文本在手机端会把排版撑乱,所以分别替换成空格、斜杠、截断加省略号。最后用"\n\n".join(lines)把每一行表格之间加一个空行,这和第三章提到的钉钉 markdown 渲染要求一致。
配合发送函数使用:
text = build_markdown_from_excel("/data/report.xlsx", max_rows=20) resp = send_text(args.webhook, args.secret, text) if resp.get("errcode") == 0: print("推送成功") else: print("推送失败:", resp)这里直接用send_text没问题,因为钉钉对消息类型的识别只看msgtype字段。要发 markdown,把 payload 换成msgtype: markdown并补上title就行。行数超过 20 行的表建议只发汇总,或提供 link 跳转完整报表;行数越多,单条消息越容易被限制,详情见 5.3。
4.3 定时触发:Linux crontab 和 Windows 计划任务的最小写法
脚本能手动跑通只是第一步,真正解放人力的是定时执行。Linux 上最常用 crontab:
0 9 * * 1-5 cd /opt/dingtalk && /usr/bin/python3 send_daily.py \ --webhook "https://oapi.dingtalk.com/robot/send?access_token=xxx" \ --secret "SECxxx" \ --content "每日巡检完成" >> /var/log/dingtalk.log 2>&1从左到右依次是分钟、小时、日期、月份、星期,0 9是每天 9 点,1-5是周一到周五。cd /opt/dingtalk保证脚本能读到相对路径文件;>> /var/log/dingtalk.log 2>&1把输出和错误都落到日志,排错时翻日志比盯屏幕有效。留意 shell 会把未加引号的&解释成后台执行,所以 Webhook 地址一定要用双引号包住,这是 cron 里最容易翻车的一个细节。
Windows 机器用计划任务:
schtasks /create /tn "DingTalkReport" \ /tr "python D:\dingtalk\send_daily.py --webhook \"你的URL\" --secret \"你的SEC\"" \ /sc daily /st 09:00 /f/sc daily表示每天,/st 09:00指定开始时间,/f强制覆盖同名任务。如果 Python 不在系统 PATH 里,/tr里要写完整解释器路径,比如C:\Python312\python.exe。任务创建后建议先手动运行一次,再改计划时间;不然「创建成功但到点没消息」很难判断是任务没触发还是脚本报错。
5. 钉钉机器人自动发送避坑指南:签名、限流、换行截断这几个坑我全踩过
5.1 加签机器人报「验签失败」:时间戳单位与拼接顺序错一个都不行
现象:脚本第一次跑,返回{"errcode":310000,"errmsg":"sign not match"};同一个脚本换成没加签的 Webhook 就能发出去。
原因:验签失败最常见有三个来源。第一,时间戳用了秒而不是毫秒,time.time()取整后只有 10 位,钉钉要求 13 位毫秒时间戳。第二,签名串顺序写成secret + "\n" + timestamp,而官方规定是timestamp + "\n" + secret,两者算出的 HMAC 完全不一样。第三,Base64 结果没做 URL 编码,+号在 query 里被当成空格,服务端还原出来的签名对不上。
解决:发出前给脚本加三个断言,比肉眼检查快得多:
assert len(timestamp) == 13, "timestamp must be milliseconds" assert string_to_sign.startswith(timestamp), "timestamp must come first" assert "%" in sign or ("+" not in sign and "/" not in sign), "sign must be urlencoded"第三个断言不太好使,因为 URL 编码后%一定出现,而未编码的+会被 URL 解析丢失。更直接的办法是把url打印出来人工核对一遍:timestamp、sign是否都出现在链接里,SEC是否完整。
5.2 返回 ok 但群里没消息:别把 errcode 当送达证明
现象:send_text返回{"errcode":0,"errmsg":"ok"},但盯着业务群就是看不到消息。
原因:这个 ok 只代表钉钉网关收下了请求,不代表消息已经进群。最常见情况是 Webhook 从旧群复制过来,旧机器人已被移除或群已解散,网关对旧 token 有时只回 ok 不下发;另一种是发送的超长文本被钉钉服务端处理时丢弃,但响应仍是 ok。依靠返回值做「送达审计」会测不出问题。
解决:脚本里把发送时间、Webhook 尾部 6 位、errcode 写进日志;测试时先手动 curl 当前 Webhook,看群里到底能不能出消息,再跑业务脚本。如果怀疑超长,把 content 截到前 500 字分两条发送,观察哪一段消失就能确认边界。
5.3 Excel 推 100 行被限流:把逐行发送改成单条 Markdown 表格
现象:脚本 for 循环读取 Excel 每一行,逐行调用发送函数,前十几条正常,突然开始返回限流错误,后面再也没有消息进群。
原因:自定义机器人对单个 Webhook 有每分钟消息数限制,逐行推送等于短时间内打几十次调用,必然会触顶。限流错误的具体码在不同版本钉钉上有差异,但返回格式都是errcode非 0,且错误码集中在限流相关区间。
解决:数据量小时用一条 markdown 表格打包发送,把 20 行合并成一条消息,这是最省配额的做法。数据量大时只发送汇总统计加抽样明细,完整数据通过 link 消息跳转。如果业务真要求逐行发,就在循环里加time.sleep(1.5)并记录已发送行号,宁可慢一点也不要半夜被限流打断;但一条 markdown 表格在手机端的阅读体验其实比几十条单行消息好得多,我会优先改合并方案。
5.4 markdown 表格只有第一行:换行符和管道符的处理顺序
现象:同一个 markdown 文本在本地预览正常,推送到群里只剩表头和第一行,后面全变成普通文本串。
原因:钉钉要求表格的每一行之间有空行,源码里如果只用\n换行而不留空行,解析器会把多行归成一个段落,表格结构断掉。另一个元凶是单元格里的竖线没做处理,多一个|就多一列,对齐错位后后面的行全被忽略。
解决:统一在表格行之间用"\n\n".join(lines)拼接,而不是"\n".join;单元格内容里的换行符替换成空格,竖线替换成全角竖线|或斜杠/。这正好对上了第四章build_markdown_from_excel里那两行 replace 的作用,当初踩这个坑时,我在源码里加了很长一段注释才记住优先级:先清数据,再拼行,最后才进消息体。
6. 收尾:给自动发送加一层重试、日志和「预览」习惯,别让机器人变黑匣子
源码跑通不难,难的是第二天没人管它也能跑通。我现在凡是上生产的推送脚本,都先加一个最小重试模板:请求超时后等 10 秒重试,最多三次,并且每次都把返回的错误码和发送时间落日志。
for attempt in range(3): resp = requests.post(url, json=payload, timeout=5) data = resp.json() if data.get("errcode") == 0: break time.sleep(10)日志里不要打印完整 Webhook 和 sign,里面带着 access_token 和签名信息,打日志等于把密钥撒得到处都是。我一般只记录 errcode、errmsg、发送时间和 Webhook 尾号 4 位,既能定位问题,又不会把敏感信息散出去。
另一个坚持下来的习惯是:任何机器人第一次接入,先在一个人少的管理测试群发一条完全相同的消息,肉眼看过渲染效果,再让脚本去切生产群。markdown 表格这种排版,代码里和手机上看到的永远是两个世界。很多次我以为脚本没问题,结果测试群一跑就露出 5.4 里说的表格断裂,也正因为提前预览,才没在业务群里当众翻车。
半年下来,我觉得这个方向最有价值的地方反而不是省了多少复制粘贴,而是让数据能用一种固定格式、固定时间出现在该出现的地方,其他人不用追问「今天报表发了没」。希望帮到你。
本文还有配套的精品资源,点击获取