
Dingtalk Message
- 1.5k installs
- 97 repo stars
- Updated June 26, 2026
- breath57/dingtalk-skills
钉钉消息发送。当用户提到"钉钉消息"、"发消息"、"发通知"、"群通知"、"群消息"、"Webhook"、"机器人消息"、"机器人发消息"、"工作通知"、"单聊消息"、"群聊消息"、"撤回消息"、"消息已读"、"发送Markdown"、"发卡片消息"、"ActionCard"、"@某人"、"@员工"、"at某人"、"提醒某人"、"dingtalk message"、"send message"、"
About
The dingtalk message skill 钉钉消息发送。当用户提到"钉钉消息"、"发消息"、"发通知"、"群通知"、"群消息"、"Webhook"、"机器人消息"、"机器人发消息"、"工作通知"、"单聊消息"、"群聊消息"、"撤回消息"、"消息已读"、"发送Markdown"、"发卡片消息"、"ActionCard"、"@某人"、"@员工"、"at某人"、"提醒某人"、"dingtalk message"、"send message"、"robot message"、"work notification"时使用此技能。支持:群自定义 Webhook 机器人(文本/Markdown/ActionCard/Link/FeedCard + 加签 + @某人)、企业内部应用机器人单聊和群聊发送、消息撤回、已读查询、工作通知等全部消息类操作。. Documentation covers workflows, commands, and guardrails agents should follow when users invoke this capability. Key documented areas include **识别通道** → 按 api.md「场景路由」判断属于哪个通道; **校验配置** → `bash scripts/dt_helper.sh --get KEY` 读取该通道所需键值; **收集缺失项** → 一次性询问并 `bash scripts/dt_helper.sh --set KEY=VALUE` 写入; **获取 Token** → 机器人用 `--token`;工作通知用 `--old-token`;Webhook/sessionWebhook 无需. Reference commands include set -e; HELPER="./scripts/dt_helper.sh". Use when developers or agents need structured guidance for dingtalk message tasks with evidence grounded in the bundled SKILL.md rather than generic advice. **识别通道** → 按 api.md「场景路由」判断属于哪个通道 **校验配置** → `bash scripts/dt_helper.sh --get KEY` 读取该通道所需键值 **收集缺失项** → 一次性询问并 `bash scripts/dt_helper.sh --set KEY=VALUE` 写入 **获取 Token** → 机器人用 `--token`;工作通知用 `--old-token`;Webho.
- **识别通道** → 按 api.md「场景路由」判断属于哪个通道
- **校验配置** → `bash scripts/dt_helper.sh --get KEY` 读取该通道所需键值
- **收集缺失项** → 一次性询问并 `bash scripts/dt_helper.sh --set KEY=VALUE` 写入
- **获取 Token** → 机器人用 `--token`;工作通知用 `--old-token`;Webhook/sessionWebhook 无需
- **执行 API** → 多行逻辑写入 `/tmp/<task>.sh` 再执行;禁止 heredoc
Dingtalk Message by the numbers
- 1,522 all-time installs (skills.sh)
- +8 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #209 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
dingtalk-message capabilities & compatibility
- Capabilities
- **识别通道** → 按 api.md「场景路由」判断属于哪个通道 · **校验配置** → `bash scripts/dt_helper.sh get key` · **收集缺失项** → 一次性询问并 `bash scripts/dt_helper.sh · **获取 token** → 机器人用 ` token`;工作通知用 ` old token · **执行 api** → 多行逻辑写入 `/tmp/<task>.sh` 再执行;禁止 here
npx skills add https://github.com/breath57/dingtalk-skills --skill dingtalk-messageAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.5k |
|---|---|
| repo stars | ★ 97 |
| Security audit | 2 / 3 scanners passed |
| Last updated | June 26, 2026 |
| Repository | breath57/dingtalk-skills ↗ |
How do I handle dingtalk message tasks with agent guidance?
钉钉消息发送。当用户提到"钉钉消息"、"发消息"、"发通知"、"群通知"、"群消息"、"Webhook"、"机器人消息"、"机器人发消息"、"工作通知"、"单聊消息"、"群聊消息"、"撤回消息"、"消息已读"、"发送Markdown"、"发卡片消息"、"ActionCard"、"@某人"、"@员工"、"at某人"、"提醒某人"、"dingtalk message"、"send message"、"
Who is it for?
Teams needing documented dingtalk message workflows.
Skip if: Teams outside DingTalk ecosystems who only need Slack, Microsoft Teams, or email notification channels.
When should I use this skill?
钉钉消息发送。当用户提到"钉钉消息"、"发消息"、"发通知"、"群通知"、"群消息"、"Webhook"、"机器人消息"、"机器人发消息"、"工作通知"、"单聊消息"、"群聊消息"、"撤回消息"、"消息已读"、"发送Markdown"、"发卡片消息"、"ActionCard"、"@某人"、"@员工"、"at某人"、"提醒某人"、"dingtalk message"、"send message"、"
What you get
Structured workflow from dingtalk message documentation applied to the user request.
- JSON message payloads
- Signed request examples
- API field reference
By the numbers
- Webhook robots limited to 20 messages per minute
- Covers 3 message channels: Webhook robot, internal app bot, and work notifications
Files
钉钉消息技能
负责钉钉消息发送的所有操作。本文件为策略指南;完整 API 请求格式、场景路由、消息类型速查、身份标识、错误码等见 references/api.md。
dt_helper.sh位于本SKILL.md同级目录的scripts/dt_helper.sh。
四种消息通道
| 通道 | 适用场景 | Token | 特点 |
|---|---|---|---|
| Webhook 机器人 | 往指定群发通知 | 无需 | 最简单;URL 自带凭证 |
| 企业内部应用机器人 | 单聊私信 / 群聊 | --token(新版) | 可撤回、查已读 |
| 工作通知 | 应用推送到"工作通知" | --old-token(旧版) | 可推全员/部门 |
| sessionWebhook | 回调中直接回复 | 无需 | 回调消息自带临时 URL |
工作流程(每次执行前)
1. 识别通道 → 按 api.md「场景路由」判断属于哪个通道 2. 校验配置 → bash scripts/dt_helper.sh --get KEY 读取该通道所需键值 3. 收集缺失项 → 一次性询问并 bash scripts/dt_helper.sh --set KEY=VALUE 写入 4. 获取 Token → 机器人用 --token;工作通知用 --old-token;Webhook/sessionWebhook 无需 5. 执行 API → 多行逻辑写入 /tmp/<task>.sh 再执行;禁止 heredoc
各通道所需配置
| 通道 | 所需配置 | 来源说明 |
|---|---|---|
| Webhook | DINGTALK_WEBHOOK_URL(加签额外需 DINGTALK_WEBHOOK_SECRET) | 群设置 → 智能群助手 → 添加自定义机器人 |
| 机器人消息 | DINGTALK_APP_KEY + DINGTALK_APP_SECRET | 开放平台 → 应用管理 → 凭证信息 |
| 工作通知 | DINGTALK_APP_KEY + DINGTALK_APP_SECRET + DINGTALK_AGENT_ID | agentId 在应用管理 → 基本信息 |
-robotCode=appKey(完全一致,无需额外配置)
- 凭证禁止在输出中完整打印,确认时仅显示前 4 位 + ****- 消息内容缺失时:先询问用户想发什么,不要自行编造
身份标识
消息 API 只接受 userId(staffId),不接受 unionId。详见 grep -A 20 "^## 身份标识" references/api.md。
- unionId → userId 转换:
bash scripts/dt_helper.sh --to-userid <unionId> - 群消息发送方式选择:发群消息时须先询问用户用 Webhook 还是企业机器人,详见
grep -A 20 "^### 群消息发送方式选择" references/api.md
执行脚本模板
#!/bin/bash
set -e
HELPER="./scripts/dt_helper.sh"
TOKEN=$(bash "$HELPER" --token)
USER_ID=$(bash "$HELPER" --get DINGTALK_MY_USER_ID | cut -d= -f2)
# 机器人单聊示例
RESP=$(curl -s -w '\nHTTP_STATUS:%{http_code}' -X POST "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend" \
-H "x-acs-dingtalk-access-token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"robotCode":"'$(bash "$HELPER" --get DINGTALK_APP_KEY | cut -d= -f2)'","userIds":["'$USER_ID'"],"msgKey":"sampleText","msgParam":"{\"content\":\"测试消息\"}"}')
echo "$RESP"#!/bin/bash
set -e
HELPER="./scripts/dt_helper.sh"
OLD_TOKEN=$(bash "$HELPER" --old-token)
AGENT_ID=$(bash "$HELPER" --get DINGTALK_AGENT_ID | cut -d= -f2)
USER_ID=$(bash "$HELPER" --get DINGTALK_MY_USER_ID | cut -d= -f2)
# 工作通知示例
RESP=$(curl -s -w '\nHTTP_STATUS:%{http_code}' -X POST "https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2?access_token=$OLD_TOKEN" \
-H "Content-Type: application/json" \
-d '{"agent_id":"'$AGENT_ID'","userid_list":"'$USER_ID'","msg":{"msgtype":"text","text":{"content":"测试工作通知"}}}')
echo "$RESP"Token 异常时:bash "$HELPER" --token --nocache或bash "$HELPER" --old-token --nocache
references/api.md 查阅索引
grep -A 196 "^## 一、群自定义 Webhook 机器人" references/api.md
grep -A 192 "^## 二、企业内部应用机器人" references/api.md
grep -A 145 "^## 三、工作通知" references/api.md
grep -A 60 "^## 四、sessionWebhook" references/api.md
grep -A 30 "^## 场景路由" references/api.md
grep -A 20 "^### 群消息发送方式选择" references/api.md
grep -A 25 "^## 身份标识" references/api.md
grep -A 30 "^## 消息类型速查" references/api.md
grep -A 33 "^## 错误码" references/api.md
grep -A 12 "^## 所需应用权限" references/api.md钉钉消息 API 参考
本文档覆盖三类消息发送方式:群自定义 Webhook 机器人、企业内部应用机器人、工作通知。
---
一、群自定义 Webhook 机器人
基础地址:群设置中获取的 Webhook URL 认证:无需 accessToken,URL 中自带 access_token 参数 限制:每个机器人每分钟最多 20 条消息
---
发送消息
POST https://oapi.dingtalk.com/robot/send?access_token=<WEBHOOK_TOKEN>
Content-Type: application/json若配置了加签安全模式,还需附加 ×tamp=<ms>&sign=<签名>---
文本消息
{
"msgtype": "text",
"text": {
"content": "项目 v2.1 已上线,请验收。"
},
"at": {
"atMobiles": ["138xxxx1234"],
"atUserIds": ["user123"],
"isAtAll": false
}
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
msgtype | string | ✅ | 固定 "text" |
text.content | string | ✅ | 消息内容 |
at.atMobiles | string[] | ❌ | 按手机号 @ 人 |
at.atUserIds | string[] | ❌ | 按 userId @ 人 |
at.isAtAll | boolean | ❌ | 是否 @所有人 |
返回示例:
{ "errcode": 0, "errmsg": "ok" }---
Markdown 消息
{
"msgtype": "markdown",
"markdown": {
"title": "部署通知",
"text": "## v2.1 部署完成\n\n- **环境**:production\n- **时间**:2026-03-10 15:00\n- **状态**:✅ 成功"
},
"at": {
"isAtAll": false
}
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
markdown.title | string | ✅ | 推送通知展示标题 |
markdown.text | string | ✅ | Markdown 正文 |
支持的 Markdown 语法:标题(#~######)、加粗、链接、图片、有序/无序列表、引用。
---
ActionCard(整体跳转)
{
"msgtype": "actionCard",
"actionCard": {
"title": "技术评审邀请",
"text": "## 技术评审\n\n请参加明天 14:00 的架构评审会议",
"singleTitle": "查看详情",
"singleURL": "https://example.com/review"
}
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
actionCard.title | string | ✅ | 消息标题 |
actionCard.text | string | ✅ | Markdown 正文 |
actionCard.singleTitle | string | ✅ | 单按钮文字 |
actionCard.singleURL | string | ✅ | 按钮跳转链接 |
---
ActionCard(多按钮)
{
"msgtype": "actionCard",
"actionCard": {
"title": "选择操作",
"text": "## 审批请求\n\n张三提交了报销申请",
"btnOrientation": "0",
"btns": [
{ "title": "同意", "actionURL": "https://example.com/approve" },
{ "title": "拒绝", "actionURL": "https://example.com/reject" }
]
}
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
actionCard.btnOrientation | string | ❌ | "0" 竖排(默认)、"1" 横排 |
actionCard.btns | array | ✅ | 按钮列表 |
btns[].title | string | ✅ | 按钮文字 |
btns[].actionURL | string | ✅ | 按钮跳转链接 |
---
Link 消息
{
"msgtype": "link",
"link": {
"title": "版本发布公告",
"text": "v2.1 正式发布,包含性能优化和安全修复。",
"messageUrl": "https://example.com/release",
"picUrl": "https://example.com/logo.png"
}
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
link.title | string | ✅ | 消息标题 |
link.text | string | ✅ | 消息摘要 |
link.messageUrl | string | ✅ | 点击跳转链接 |
link.picUrl | string | ❌ | 缩略图 URL |
---
FeedCard 消息
{
"msgtype": "feedCard",
"feedCard": {
"links": [
{
"title": "需求评审会议纪要",
"messageURL": "https://example.com/doc/1",
"picURL": "https://example.com/img1.png"
},
{
"title": "技术方案设计文档",
"messageURL": "https://example.com/doc/2",
"picURL": "https://example.com/img2.png"
}
]
}
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
feedCard.links | array | ✅ | FeedCard 链接列表 |
links[].title | string | ✅ | 单条标题 |
links[].messageURL | string | ✅ | 点击跳转链接 |
links[].picURL | string | ❌ | 缩略图 URL |
---
加签计算(HMAC-SHA256)
适用于配置了"加签"安全模式的自定义机器人。开启加签后,所有请求都必须带签名,否则返回 310000 sign not match。
签名算法:
1. timestamp = 当前毫秒级时间戳 2. string_to_sign = timestamp + "\n" + secret 3. sign = URL-Safe Base64 编码(HMAC-SHA256(secret, string_to_sign)的二进制结果)
最终请求 URL:
https://oapi.dingtalk.com/robot/send?access_token=<TOKEN>×tamp=<timestamp>&sign=<sign>timestamp 与钉钉服务器时差不能超过 1 小时,否则签名验证失败。
---
二、企业内部应用机器人
基础地址:https://api.dingtalk.com/v1.0/robot 认证:请求头 x-acs-dingtalk-access-token: <accessToken>
robotCode 等于应用的 appKey(完全一致)。
钉钉身份标识体系
钉钉有三种用户 ID,使用场景各不相同:
| 标识 | 说明 | 作用域 | 消息 API 支持 |
|---|---|---|---|
userId(也叫 staffId) | 企业内部的用户 ID | 单个企业内唯一 | ✅ 机器人和工作通知 API 均只接受此 ID |
unionId | 跨企业/跨应用的用户 ID | 同一法人跨组织唯一 | ❌ 不能直接用于发送消息 |
openId | 第三方应用作用域的用户 ID | 单个第三方应用内唯一 | 企业内部应用不涉及 |
重要:所有消息发送 API(机器人单聊、群聊、工作通知)均只接受 userId,传入 unionId 会被判定为无效用户。
userId 获取方式
1. 机器人回调(最常用):消息体中 senderStaffId 字段即为发送者的 userId 2. unionId → userId 转换:POST /topapi/user/getbyunionid?access_token=<旧版token>,body: {"unionid": "<unionId>"} 3. 钉钉管理后台:PC 端钉钉 → 工作台 → 管理后台 → 通讯录 → 成员详情 4. API 查询:POST /topapi/v2/user/getbymobile(按手机号查),或遍历部门成员
userId ↔ unionId 互转
| 方向 | API | 请求 body | 返回值 |
|---|---|---|---|
| userId → unionId | POST /topapi/v2/user/get?access_token=<旧版token> | {"userid": "<userId>"} | result.unionid(注意:无下划线的 unionid 有值,union_id 可能为空) |
| unionId → userId | POST /topapi/user/getbyunionid?access_token=<旧版token> | {"unionid": "<unionId>"} | result.userid |
openConversationId 获取方式
群聊 API 需要 openConversationId(以 cid 开头),获取方式:
1. 机器人回调(推荐):在群中 @机器人,回调消息体的 conversationId 字段即为该值 2. 调用 IM 接口创建群时返回
回调中conversationType: "2"表示群聊,"1"表示单聊
---
批量发送单聊消息
POST /v1.0/robot/oToMessages/batchSend
Content-Type: application/json
{
"robotCode": "<appKey>",
"userIds": ["user001", "user002"],
"msgKey": "sampleText",
"msgParam": "{\"content\": \"你好,这是机器人消息\"}"
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
robotCode | string | ✅ | 机器人 robotCode,通常等于 appKey |
userIds | string[] | ✅ | 用户 staffId(userId)列表,最多 20 个 |
msgKey | string | ✅ | 消息类型 key(见下方消息类型表) |
msgParam | string | ✅ | 消息参数 JSON 字符串 |
返回示例:
{
"processQueryKey": "abc123",
"invalidStaffIdList": [],
"flowControlledStaffIdList": []
}---
发送群聊消息
POST /v1.0/robot/groupMessages/send
Content-Type: application/json
{
"robotCode": "<appKey>",
"openConversationId": "<群 openConversationId>",
"msgKey": "sampleText",
"msgParam": "{\"content\": \"群通知:明天 10:00 开周会\"}"
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
robotCode | string | ✅ | 机器人 robotCode |
openConversationId | string | ✅ | 群会话 ID |
msgKey | string | ✅ | 消息类型 key |
msgParam | string | ✅ | 消息参数 JSON 字符串 |
返回:{ "processQueryKey": "xxx" }
---
查询单聊消息已读状态
GET /v1.0/robot/oToMessages/readStatus?robotCode=<appKey>&processQueryKey=<processQueryKey>注意:此接口为 GET 方法,参数通过 query string 传递,不是 POST body。
返回示例:
{
"messageReadInfoList": [
{
"name": "张三",
"userId": "user001",
"readStatus": "read",
"readTimestamp": 1710000000000
}
]
}| 字段 | 类型 | 说明 |
|---|---|---|
readStatus | string | read(已读)或 unread(未读) |
readTimestamp | long | 阅读时间戳(毫秒),未读时为 0 |
---
撤回单聊消息
POST /v1.0/robot/otoMessages/batchRecall
Content-Type: application/json
{
"robotCode": "<appKey>",
"processQueryKeys": ["<processQueryKey1>"]
}返回示例:
{
"successResult": ["processQueryKey1"],
"failedResult": {}
}---
撤回群聊消息
POST /v1.0/robot/groupMessages/recall
Content-Type: application/json
{
"robotCode": "<appKey>",
"openConversationId": "<openConversationId>",
"processQueryKeys": ["<processQueryKey>"]
}返回示例:
{
"successResult": ["processQueryKey"],
"failedResult": {}
}---
消息类型(msgKey & msgParam)
| msgKey | 类型 | msgParam 示例 |
|---|---|---|
sampleText | 文本 | {"content": "消息内容"} |
sampleMarkdown | Markdown | {"title": "标题", "text": "# 正文\n内容"} |
sampleActionCard | ActionCard(整体跳转) | {"title": "标题", "text": "正文", "singleTitle": "按钮", "singleURL": "https://..."} |
sampleActionCard2 | ActionCard(按钮竖排) | {"title": "标题", "text": "正文", "actionTitle1": "按钮1", "actionURL1": "url1", "actionTitle2": "按钮2", "actionURL2": "url2"} |
sampleActionCard3 | ActionCard(按钮横排) | 同上 |
sampleLink | 链接 | {"title": "标题", "text": "描述", "messageUrl": "url", "picUrl": "图片url"} |
sampleImageMsg | 图片 | {"photoURL": "https://..."} |
sampleAudio | 语音 | {"mediaId": "xxx", "duration": "3000"} |
msgParam 必须是 JSON 字符串,而非 JSON 对象。---
三、工作通知
基础地址:https://oapi.dingtalk.com 认证:查询参数 access_token=<旧版 access_token>(通过 /gettoken 获取)
---
发送工作通知
POST /topapi/message/corpconversation/asyncsend_v2?access_token=<token>
Content-Type: application/json
{
"agent_id": "<agentId>",
"userid_list": "user001,user002",
"to_all_user": false,
"msg": {
"msgtype": "text",
"text": { "content": "你的报销申请已审批通过" }
}
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
agent_id | long | ✅ | 应用 agentId |
userid_list | string | 条件 | 逗号分隔的 userId(最多 100 个),与 dept_id_list 二选一 |
dept_id_list | string | 条件 | 逗号分隔的部门 ID |
to_all_user | boolean | ❌ | 是否推送全员(true 时忽略 userid_list/dept_id_list) |
msg | object | ✅ | 消息体 |
返回示例:
{ "errcode": 0, "errmsg": "ok", "task_id": 123456 }---
工作通知消息类型
文本:
{ "msgtype": "text", "text": { "content": "消息内容" } }Markdown:
{ "msgtype": "markdown", "markdown": { "title": "标题", "text": "## 正文\n内容" } }ActionCard(整体跳转):
{
"msgtype": "action_card",
"action_card": {
"title": "标题",
"markdown": "## 正文内容",
"single_title": "查看详情",
"single_url": "https://example.com"
}
}ActionCard(多按钮):
{
"msgtype": "action_card",
"action_card": {
"title": "标题",
"markdown": "## 正文内容",
"btn_orientation": "0",
"btn_json_list": [
{ "title": "同意", "action_url": "https://example.com/approve" },
{ "title": "拒绝", "action_url": "https://example.com/reject" }
]
}
}OA 消息:
{
"msgtype": "oa",
"oa": {
"message_url": "https://example.com/detail",
"head": { "bgcolor": "FFBBBBBB", "text": "正文标题" },
"body": {
"title": "报销审批",
"form": [
{ "key": "申请人", "value": "张三" },
{ "key": "金额", "value": "¥3,200" }
],
"content": "请尽快处理"
}
}
}---
查询工作通知发送结果
POST /topapi/message/corpconversation/getsendresult?access_token=<token>
Content-Type: application/json
{
"agent_id": "<agentId>",
"task_id": 123456
}返回示例:
{
"errcode": 0,
"send_result": {
"invalid_user_id_list": [],
"forbidden_user_id_list": [],
"read_user_id_list": ["user001"],
"unread_user_id_list": ["user002"],
"failed_user_id_list": []
}
}---
撤回工作通知
POST /topapi/message/corpconversation/recall?access_token=<token>
Content-Type: application/json
{
"agent_id": "<agentId>",
"msg_task_id": 123456
}返回:{ "errcode": 0, "errmsg": "ok" }
---
四、sessionWebhook(回调临时回复通道)
当机器人收到用户消息回调时,消息体中包含 sessionWebhook 字段。这是一个临时 Webhook URL,可直接 POST 回复消息,无需 accessToken。
回调消息体关键字段
{
"conversationId": "cidXXXXX==",
"conversationType": "2",
"senderStaffId": "25262904",
"senderUnionId": "K1mxiiGFgkVfWYR5tNM04lAiEiE",
"senderNick": "张三",
"senderCorpId": "dingxxxxx",
"robotCode": "dingxxxxxx",
"text": { "content": " 用户发送的内容" },
"msgtype": "text",
"atUsers": [
{ "dingtalkId": "xxx", "staffId": "25262904", "unionId": "K1mxiiGFgkVfWYR5tNM04lAiEiE" }
],
"sessionWebhook": "https://oapi.dingtalk.com/robot/sendBySession?session=xxxxx",
"sessionWebhookExpiredTime": 1773216797267
}| 字段 | 说明 |
|---|---|
conversationId | 会话 ID,群聊时即 openConversationId |
conversationType | "1" 单聊、"2" 群聊 |
senderStaffId | 发送者的 userId(企业内部群始终存在;外部群中的外部用户可能为空) |
senderUnionId | 发送者的 unionId(跨组织通用,始终存在) |
senderCorpId | 发送者所在企业的 corpId |
atUsers[].staffId | 被@用户的 userId(外部群中的外部用户此字段为空) |
atUsers[].unionId | 被@用户的 unionId(始终存在) |
robotCode | 机器人 robotCode(= appKey) |
sessionWebhook | 临时回复 URL(约 1.5 小时有效) |
sessionWebhookExpiredTime | 过期时间戳(毫秒) |
通过 sessionWebhook 回复
POST <sessionWebhook>
Content-Type: application/json
{
"msgtype": "text",
"text": { "content": "收到,正在处理..." }
}支持所有 Webhook 消息格式(text/markdown/actionCard/link/feedCard),返回:
{ "errcode": 0, "errmsg": "ok" }无需加签、无需 accessToken,是机器人回复消息最简单的方式。
---
场景路由(收到用户请求后的判断逻辑)
用户想发消息
├─ 发到群里?
│ ├─ 通用群消息(含 @某人)→ 询问用户选择方式(见「群消息发送方式选择」)
│ ├─ 明确需要撤回或查已读 → 企业机器人群聊
│ └─ 正在处理机器人回调,直接回复 → sessionWebhook
├─ 发给个人?
│ ├─ 以机器人身份发私信 → 企业机器人单聊
│ └─ 以应用身份推工作通知 → 工作通知
├─ 撤回/查已读?
│ ├─ 机器人消息 → 企业机器人的撤回/已读 API
│ └─ 工作通知 → 工作通知的查询/撤回 API
└─ 回复机器人收到的消息? → sessionWebhook群消息发送方式选择
用户发起群消息请求时,必须先询问选择哪种方式:
| 方式 | 需要提供 | 如何获取 | 说明 |
|---|---|---|---|
| Webhook 机器人 | WEBHOOK_URL | 群设置 → 智能群助手 → 添加自定义机器人 → 复制 URL | 无需应用权限,配置最简单;支持 @某人(at.atUserIds) |
| 企业内部应用机器人 | openConversationId(群会话 ID) | 机器人收到群消息时,回调体的 conversationId 字段即为该值 | 需要 APP_KEY/APP_SECRET;支持撤回、查已读 |
推荐 Webhook,只需一个 URL 即可,无需任何应用权限。
- 选 Webhook:收集
DINGTALK_WEBHOOK_URL(若启用加签还需DINGTALK_WEBHOOK_SECRET),持久化后执行 - 选 企业机器人:收集
openConversationId,复用已有的APP_KEY/APP_SECRET,调用groupMessages/send
---
身份标识
所有消息发送 API 均只接受 userId(staffId),不接受 unionId。
| 标识 | 作用域 | 能否用于发消息 |
|---|---|---|
userId(= staffId) | 单个企业内唯一 | ✅ 唯一接受的 ID |
unionId | 跨组织唯一 | ❌ 会被判定无效用户 |
userId 获取方式: 1. 机器人回调:消息体 senderStaffId 字段 2. unionId → userId:bash scripts/dt_helper.sh --to-userid <unionId> 3. 手机号 → userId:POST /topapi/v2/user/getbymobile 4. 管理后台:PC 端钉钉 → 工作台 → 管理后台 → 通讯录
回调消息中的身份字段:
| 字段 | 含义 | 可靠性 |
|---|---|---|
senderStaffId | 发送者 userId | 企业内部群始终存在;外部群中外部用户可能为空 |
senderUnionId | 发送者 unionId | 始终存在 |
注意result.unionid(无下划线)有值,result.union_id(有下划线)在部分企业中为空。
---
消息类型速查
Webhook 消息类型
直接在请求 body 的 msgtype 字段指定:text | markdown | actionCard | link | feedCard
机器人消息类型
通过 msgKey + msgParam(JSON 字符串)指定:
| msgKey | 类型 | msgParam 关键字段 |
|---|---|---|
sampleText | 文本 | content |
sampleMarkdown | Markdown | title, text |
sampleActionCard | ActionCard | title, text, singleTitle, singleURL |
sampleLink | 链接 | title, text, messageUrl, picUrl |
sampleImageMsg | 图片 | photoURL |
重要:msgParam 必须是 JSON 字符串,不是对象。工作通知消息类型
在 msg 对象的 msgtype 字段指定:text | markdown | action_card
注意工作通知的action_card用下划线(不同于 Webhook 的actionCard)。
---
错误码
Webhook 错误码
| errcode | 说明 | 处理建议 |
|---|---|---|
0 | 成功 | — |
310000 | keywords not in content | 消息内容须包含自定义关键词 |
310000 | sign not match | 检查签名计算或 timestamp 是否过期(±1h) |
310000 | token is not exist | Webhook URL 无效或已被删除 |
302033 | send too fast | 超过 20 条/分钟限制,等待后重试 |
机器人错误码
| 错误 | 说明 | 处理建议 |
|---|---|---|
| 401 | accessToken 过期 | 重新获取 |
| 403 | 权限不足 | 开通 Robot.Message.Send 等权限 |
invalidStaffIdList 非空 | userId 无效 | 确认用户在组织内 |
flowControlledStaffIdList 非空 | 被限流 | 稍候重试 |
工作通知错误码
| errcode | 说明 | 处理建议 |
|---|---|---|
0 | 成功 | — |
33 | access_token 过期 | 重新调用 /gettoken |
40035 | 参数不合法 | 检查 agent_id、userid_list 格式 |
88 | agent_id 不存在 | 确认应用 agentId |
90018 | userid_list 超过 100 | 分批发送 |
---
所需应用权限
| 功能 | 权限 |
|---|---|
| 机器人单聊消息 | Robot.Message.Send |
| 机器人群聊消息 | Robot.GroupMessage.Send |
| 消息已读查询 | Robot.Message.Query |
| 消息撤回 | Robot.Message.Recall |
| 工作通知发送 | Message.CorpConversation.AsyncSend |
| 工作通知撤回 | Message.CorpConversation.Recall |
| Webhook | 无需应用权限 |
| sessionWebhook | 无需应用权限(回调消息自带) |
#!/bin/bash
# =============================================================================
# dt_helper.sh — 钉钉开放平台辅助工具
# 路径: scripts/common/dt_helper.sh
# 用法: bash scripts/common/dt_helper.sh <命令> [参数]
# =============================================================================
set -e
CONFIG="${DINGTALK_CONFIG:-$HOME/.dingtalk-skills/config}"
# ─────────────────────────────────────────────────────────────────────────────
# 帮助信息
# ─────────────────────────────────────────────────────────────────────────────
show_help() {
cat <<'EOF'
钉钉开放平台辅助工具 (dt_helper.sh)
用法: bash scripts/common/dt_helper.sh <命令> [参数]
Token 管理(两种 token 互不兼容,按域名区分):
--token [--nocache] 获取新版 accessToken(用于 api.dingtalk.com 域名的所有接口)
适用:待办、文档、AI 表格等 api.dingtalk.com 域名下所有版本的接口
请求头:x-acs-dingtalk-access-token: <token>
有缓存且未过期则直接返回,否则自动刷新并缓存
--nocache:跳过缓存,强制重新获取(token 被提前吊销时使用)
--token-info 查看新版 token 缓存状态(是否有效、剩余有效秒数)
--clear-token 清除缓存的新版 token(下次 --token 时强制重新获取)
--old-token [--nocache]
获取旧版 access_token(用于 oapi.dingtalk.com 域名的所有接口)
适用:群消息/工作通知/userId↔unionId 转换等 oapi.dingtalk.com 接口
不适用:api.dingtalk.com 接口(如待办、文档、AI表格)
⚠️ 新旧两种 token 互不兼容,混用会导致 401/403
--nocache:跳过缓存,强制重新获取(token 被提前吊销时使用)
身份转换:
--to-unionid [userId] 将 userId 转换为 unionId
不传参数:转换配置中的 DINGTALK_MY_USER_ID(操作者自身),
结果首次自动写入 DINGTALK_MY_OPERATOR_ID
传入参数:动态转换指定 userId,仅返回结果,不写入配置
--to-userid [unionId] 将 unionId 反向转换为 userId(需传入参数)
配置管理:
--config 查看 ~/.dingtalk-skills/config 中的所有配置项(敏感项脱敏显示)
--get KEY [KEY...] 获取一个或多个配置项的值(敏感项脱敏显示)
--set KEY=VALUE 将配置项持久化写入配置文件(已存在则更新,不存在则追加,目录自动创建)
帮助:
--help, -h 显示此帮助信息
环境变量:
DINGTALK_CONFIG 覆盖默认配置文件路径(默认 ~/.dingtalk-skills/config)
配置文件:
~/.dingtalk-skills/config key=value 格式,存储以下键:
DINGTALK_APP_KEY 应用 Client ID(AppKey)
DINGTALK_APP_SECRET 应用 Client Secret(AppSecret)
DINGTALK_MY_USER_ID 企业员工 ID(userId,管理后台通讯录可查)
DINGTALK_MY_OPERATOR_ID 操作者 unionId(由 --to-unionid 自动生成)
DINGTALK_ACCESS_TOKEN 新版 token 缓存
DINGTALK_TOKEN_EXPIRY 新版 token 过期时间戳(Unix 秒)
DINGTALK_OLD_TOKEN 旧版 token 缓存
DINGTALK_OLD_TOKEN_EXPIRY 旧版 token 过期时间戳(Unix 秒)
EOF
}
# ─────────────────────────────────────────────────────────────────────────────
# 工具函数
# ─────────────────────────────────────────────────────────────────────────────
# 从配置文件读取指定键的值
cfg_get() {
local key="$1"
grep "^${key}=" "$CONFIG" 2>/dev/null | head -1 | cut -d= -f2-
}
# 写入或更新配置文件中的键值
cfg_set() {
local key="$1"
local value="$2"
mkdir -p "$(dirname "$CONFIG")"
touch "$CONFIG"
if grep -q "^${key}=" "$CONFIG" 2>/dev/null; then
sed -i "s|^${key}=.*|${key}=${value}|" "$CONFIG"
else
echo "${key}=${value}" >> "$CONFIG"
fi
}
# 从配置文件删除指定键
cfg_del() {
local key="$1"
sed -i "/^${key}=/d" "$CONFIG" 2>/dev/null || true
}
# 确保必须的配置项存在,否则报错退出
require_cfg() {
local key="$1"
local val
val=$(cfg_get "$key")
if [ -z "$val" ]; then
echo "❌ 缺少配置项 ${key},请先运行: bash scripts/common/dt_helper.sh --set ${key}=<值>" >&2
exit 1
fi
echo "$val"
}
# ─────────────────────────────────────────────────────────────────────────────
# Token 管理
# ─────────────────────────────────────────────────────────────────────────────
cmd_token() {
local force="${1:-}" app_key app_secret cached expiry now resp token expire_in
app_key=$(require_cfg DINGTALK_APP_KEY)
app_secret=$(require_cfg DINGTALK_APP_SECRET)
now=$(date +%s)
if [ "$force" != "--nocache" ]; then
cached=$(cfg_get DINGTALK_ACCESS_TOKEN)
expiry=$(cfg_get DINGTALK_TOKEN_EXPIRY)
if [ -n "$cached" ] && [ -n "$expiry" ] && [ "$now" -lt "$expiry" ]; then
echo "$cached"
return 0
fi
fi
# 过期或无缓存,重新获取
resp=$(curl -s -X POST "https://api.dingtalk.com/v1.0/oauth2/accessToken" \
-H "Content-Type: application/json" \
-d "{\"appKey\":\"${app_key}\",\"appSecret\":\"${app_secret}\"}")
token=$(echo "$resp" | grep -o '"accessToken":"[^"]*"' | cut -d'"' -f4)
expire_in=$(echo "$resp" | grep -o '"expireIn":[0-9]*' | cut -d: -f2)
if [ -z "$token" ]; then
echo "❌ 获取 token 失败: $resp" >&2
exit 1
fi
cfg_set DINGTALK_ACCESS_TOKEN "$token"
cfg_set DINGTALK_TOKEN_EXPIRY "$((now + expire_in - 200))"
echo "$token"
}
cmd_token_info() {
local cached expiry now remaining
cached=$(cfg_get DINGTALK_ACCESS_TOKEN)
expiry=$(cfg_get DINGTALK_TOKEN_EXPIRY)
now=$(date +%s)
if [ -z "$cached" ]; then
echo "状态: 无缓存(从未获取或已清除)"
return 0
fi
if [ -z "$expiry" ] || [ "$now" -ge "$expiry" ]; then
echo "状态: 已过期"
echo "Token: ${cached:0:20}..."
else
remaining=$((expiry - now))
echo "状态: 有效"
echo "Token: ${cached:0:20}..."
echo "剩余: ${remaining} 秒(约 $((remaining / 60)) 分钟)"
fi
}
cmd_clear_token() {
cfg_del DINGTALK_ACCESS_TOKEN
cfg_del DINGTALK_TOKEN_EXPIRY
echo "✅ 新版 Token 缓存已清除"
}
cmd_old_token() {
# 旧版 access_token,用于所有 oapi.dingtalk.com 接口:
# - 群消息、工作通知、互动卡片(dingtalk-message)
# - userId ↔ unionId 转换
# ⚠️ 不可用于 api.dingtalk.com 接口(待办、文档、AI表格等)
local force="${1:-}" app_key app_secret resp token cached expiry now
app_key=$(require_cfg DINGTALK_APP_KEY)
app_secret=$(require_cfg DINGTALK_APP_SECRET)
now=$(date +%s)
if [ "$force" != "--nocache" ]; then
cached=$(cfg_get DINGTALK_OLD_TOKEN)
expiry=$(cfg_get DINGTALK_OLD_TOKEN_EXPIRY)
if [ -n "$cached" ] && [ -n "$expiry" ] && [ "$now" -lt "$expiry" ]; then
echo "$cached"
return 0
fi
fi
resp=$(curl -s "https://oapi.dingtalk.com/gettoken?appkey=${app_key}&appsecret=${app_secret}")
token=$(echo "$resp" | grep -o '"access_token":"[^"]*"' | cut -d'"' -f4)
expires_in=$(echo "$resp" | grep -o '"expires_in":[0-9]*' | cut -d: -f2)
if [ -z "$token" ]; then
echo "❌ 获取旧版 token 失败: $resp" >&2
exit 1
fi
cfg_set DINGTALK_OLD_TOKEN "$token"
cfg_set DINGTALK_OLD_TOKEN_EXPIRY "$((now + expires_in - 200))"
echo "$token"
}
# ─────────────────────────────────────────────────────────────────────────────
# 身份转换
# ─────────────────────────────────────────────────────────────────────────────
cmd_to_unionid() {
local user_id="$1"
local is_self=false
local old_token resp union_id
# 未传参 → 使用配置中的操作者自身 userId,转换结果写入配置
if [ -z "$user_id" ]; then
user_id=$(require_cfg DINGTALK_MY_USER_ID)
is_self=true
fi
old_token=$(cmd_old_token)
resp=$(curl -s -X POST \
"https://oapi.dingtalk.com/topapi/v2/user/get?access_token=${old_token}" \
-H "Content-Type: application/json" \
-d "{\"userid\":\"${user_id}\"}")
# 注意:使用无下划线的 unionid 字段(有下划线的 union_id 可能为空)
union_id=$(echo "$resp" | grep -o '"unionid":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$union_id" ]; then
echo "❌ userId→unionId 转换失败: $resp" >&2
exit 1
fi
# 仅当转换的是操作者自身时,才写入配置(动态转换他人 userId 不写入)
if "$is_self" && [ -z "$(cfg_get DINGTALK_MY_OPERATOR_ID)" ]; then
cfg_set DINGTALK_MY_OPERATOR_ID "$union_id"
echo "✅ 自身 unionId 已写入配置 DINGTALK_MY_OPERATOR_ID" >&2
fi
echo "$union_id"
}
cmd_to_userid() {
local union_id="$1"
local old_token resp user_id
if [ -z "$union_id" ]; then
echo "❌ 请提供 unionId 参数" >&2
exit 1
fi
old_token=$(cmd_old_token)
resp=$(curl -s -X POST \
"https://oapi.dingtalk.com/topapi/user/getbyunionid?access_token=${old_token}" \
-H "Content-Type: application/json" \
-d "{\"unionid\":\"${union_id}\"}")
user_id=$(echo "$resp" | grep -o '"userid":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$user_id" ]; then
echo "❌ unionId→userId 转换失败: $resp" >&2
exit 1
fi
echo "$user_id"
}
# ─────────────────────────────────────────────────────────────────────────────
# 配置管理
# ─────────────────────────────────────────────────────────────────────────────
cmd_config() {
if [ ! -f "$CONFIG" ]; then
echo "配置文件不存在: $CONFIG"
echo "使用 --set KEY=VALUE 写入配置项"
return 0
fi
echo "配置文件: $CONFIG"
echo "─────────────────────────────────"
# 脱敏显示 SECRET 和 TOKEN
while IFS= read -r line; do
key="${line%%=*}"
val="${line#*=}"
case "$key" in
DINGTALK_APP_SECRET|DINGTALK_ACCESS_TOKEN|DINGTALK_OLD_TOKEN)
echo "${key}=${val:0:6}***(已脱敏)"
;;
*)
echo "$line"
;;
esac
done < "$CONFIG"
}
cmd_get() {
if [ $# -eq 0 ]; then
echo "❌ 请提供至少一个键名,用法: --get KEY [KEY2 ...]" >&2
exit 1
fi
for key in "$@"; do
val=$(cfg_get "$key")
if [ -z "$val" ]; then
echo "${key}=(未设置)"
else
case "$key" in
DINGTALK_APP_SECRET|DINGTALK_ACCESS_TOKEN|DINGTALK_OLD_TOKEN)
echo "${key}=${val:0:6}***(脱敏)"
;;
*)
echo "${key}=${val}"
;;
esac
fi
done
}
cmd_set() {
local kv="$1"
if [ -z "$kv" ] || [[ "$kv" != *"="* ]]; then
echo "❌ 格式错误,用法: --set KEY=VALUE" >&2
exit 1
fi
local key="${kv%%=*}"
local value="${kv#*=}"
cfg_set "$key" "$value"
echo "✅ 已设置 ${key}"
}
# ─────────────────────────────────────────────────────────────────────────────
# 入口:解析命令
# ─────────────────────────────────────────────────────────────────────────────
CMD="${1:-}"
case "$CMD" in
--help|-h|"")
show_help
;;
--token)
cmd_token "${2:-}"
;;
--token-info)
cmd_token_info
;;
--clear-token)
cmd_clear_token
;;
--old-token)
cmd_old_token "${2:-}"
;;
--to-unionid)
cmd_to_unionid "${2:-}"
;;
--to-userid)
cmd_to_userid "${2:-}"
;;
--config)
cmd_config
;;
--get)
shift
cmd_get "$@"
;;
--set)
cmd_set "${2:-}"
;;
*)
echo "❌ 未知命令: $CMD" >&2
echo "运行 --help 查看用法" >&2
exit 1
;;
esac
Related skills
FAQ
What does dingtalk message do?
钉钉消息发送。当用户提到"钉钉消息"、"发消息"、"发通知"、"群通知"、"群消息"、"Webhook"、"机器人消息"、"机器人发消息"、"工作通知"、"单聊消息"、"群聊消息"、"撤回消息"、"消息已读"、"发送Markdown"、"发卡片消息"、"ActionCard"、"@某人"、"@员工"、"at某人"、"提醒某人"、"dingtalk message"、"send message"、"
When should I invoke dingtalk message?
钉钉消息发送。当用户提到"钉钉消息"、"发消息"、"发通知"、"群通知"、"群消息"、"Webhook"、"机器人消息"、"机器人发消息"、"工作通知"、"单聊消息"、"群聊消息"、"撤回消息"、"消息已读"、"发送Markdown"、"发卡片消息"、"ActionCard"、"@某人"、"@员工"、"at某人"、"提醒某人"、"dingtalk message"、"send message"、"
What are key capabilities?
**识别通道** → 按 api.md「场景路由」判断属于哪个通道
Is Dingtalk Message safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.