通过 Webhook 接入自定义服务,使用自定义机器人,支持在企业内部群和普通钉钉群内发送群消息,不支持发送单聊消息。
详情参考:
每个机器人每分钟最多发送20条消息到群里,如果超过20条,会限流10分钟。
重要
如果你有大量发消息的场景(譬如系统监控报警)可以将这些信息进行整合,通过markdown
消息以摘要的形式发送到群里。
请求方式:POST
请求地址:https://oapi.dingtalk.com/robot/send
名称 | 类型 | 是否必填 | 示例值 | 描述 |
---|---|---|---|---|
access_token | String | 是 | BE3xxxx | 自定义机器人调用接口的凭证。自定义机器人安装后webhook地址中的access_token值。详情参考获取自定义机器人 Webhook 地址。 |
重要
如果自定义机器人的安全设置使用的是加签方式,调用本接口发送消息时,需要拼接timestamp和sign参数,示例为:
shhttps://oapi.dingtalk.com/robot/send?access_token=XXXXXX×tamp=XXX&sign=XXX
名称 | 类型 | 是否必填 | 示例值 | 描述 |
---|---|---|---|---|
msgtype | String | 是 | text | 消息类型,自定义机器人可发送的消息类型参见自定义机器人发送消息的消息类型。 |
text | Object | 否 | 文本类型消息。 | |
at | Object | 否 | 被@的群成员信息。 | |
link | Object | 否 | 链接类型消息。 | |
markdown | Object | 否 | markdown类型消息。 | |
actionCard | Object | 否 | actionCard类型消息。 | |
feedCard | Object | 否 | feedCard类型消息。 |
名称 | 类型 | 示例值 | 描述 |
---|---|---|---|
errmsg | String | ok | errmsg |
errcode | Number | 0 | errcode |
请求示例(HTTP)
POST https://oapi.dingtalk.com/robot/send?access_token=ACCESS_TOKEN
请求正文
json{
"msgtype": "text", // 消息类型,可为 text、link、markdown、actionCard、feedCard
"text": {
"content": "这是一条文本消息内容"
},
"link": {
"messageUrl": "https://www.example.com", // 跳转链接
"picUrl": "https://example.com/image.png", // 图片链接
"text": "这是一条链接消息内容", // 消息内容
"title": "链接消息标题" // 消息标题
},
"markdown": {
"title": "Markdown消息标题",
"text": "#### 这是Markdown消息内容 \n "
},
"actionCard": {
"title": "ActionCard消息标题",
"text": "#### 这是ActionCard内容 \n ",
"btnOrientation": "0", // 0-按钮竖直排列,1-按钮横向排列
"singleTitle": "阅读全文", // 单个按钮标题
"singleURL": "https://www.example.com", // 单个按钮跳转链接
"btns": [
{
"title": "按钮1",
"actionURL": "https://www.example.com/btn1"
},
{
"title": "按钮2",
"actionURL": "https://www.example.com/btn2"
}
]
},
"feedCard": {
"links": [
{
"title": "FeedCard标题1",
"messageURL": "https://www.example.com/1",
"picURL": "https://example.com/image1.png"
},
{
"title": "FeedCard标题2",
"messageURL": "https://www.example.com/2",
"picURL": "https://example.com/image2.png"
}
]
},
"at": {
"isAtAll": false, // 是否@所有人
"atUserIds": ["user001", "user002"], // 被@的用户ID列表
"atMobiles": ["15xxx", "18xxx"] // 被@的手机号列表
}
}
json{
"at":{
"isAtAll":"false",
"atUserIds":["user001","user002"],
"atMobiles":["15xxx","18xxx"]
},
//链接消息
"link":{
"messageUrl":"1",
"picUrl":"1",
"text":"1",
"title":"1"
},
//markdown消息
"markdown":{
"text":"1",
"title":"1"
},
//feedCard消息
"feedCard":{
"links":{
"picURL":"1",
"messageURL":"1",
"title":"1"
}
},
//文本消息
"text":{
"content":"123"
},
"msgtype":"text",
//actionCard消息
"actionCard":{
"hideAvatar":"1",
"btnOrientation":"1",
"singleTitle":"1",
"btns":[{
"actionURL":"1",
"title":"1"
}],
"text":"1",
"singleURL":"1",
"title":"1"
}
}
请求示例(JAVA SDK)
jsonimport com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiRobotSendRequest;
import com.dingtalk.api.response.OapiRobotSendResponse;
import com.taobao.api.ApiException;
import java.util.Arrays;
public class TextMsgRobot {
//自定义机器人发送文本消息
public static void main(String[] args) {
try {
// 机器人webhook对应的access_token的值,不是client_id和client_secret生成的应用access_token
String accessToken = "机器人webhook对应的access_token的值";
// 创建钉钉客户端
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/robot/send");
// 构建请求对象
OapiRobotSendRequest req = new OapiRobotSendRequest();
req.setMsgtype("text"); // 消息类型为text
OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text();
text.setContent("123"); // 文本内容
req.setText(text);
// 构建@信息
OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
at.setIsAtAll(false); // 是否@所有人
// 指定@的用户ID列表
at.setAtUserIds(Arrays.asList("024759202052757723", "4525232859750548"));
req.setAt(at);
// 发送请求
OapiRobotSendResponse rsp = client.execute(req, accessToken);
System.out.println(rsp.getBody()); // 输出返回结果
} catch (ApiException e) {
e.printStackTrace();
}
}
}
markdown 消息:
jsonimport com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiRobotSendRequest;
import com.dingtalk.api.response.OapiRobotSendResponse;
import com.taobao.api.ApiException;
import java.util.Arrays;
public class MarkdownMsgRobot {
//自定义机器人发送markdown消息
public static void main(String[] args) {
try {
// 机器人webhook对应的access_token的值,不是client_id和client_secret生成的应用access_token
String accessToken = "机器人webhook对应的access_token的值";
// 创建钉钉客户端
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/robot/send");
// 构建请求对象
OapiRobotSendRequest req = new OapiRobotSendRequest();
req.setMsgtype("markdown"); // 消息类型为markdown
OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
markdown.setTitle("标题"); // 标题
markdown.setText("@024759202052757723 @4525232859750548 \n #### 钉钉,让进步发生 \n - 钉钉,让进步发生"); // markdown内容
req.setMarkdown(markdown);
// 构建@信息
OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
at.setIsAtAll(false); // 是否@所有人
// 指定@的用户ID列表
at.setAtUserIds(Arrays.asList("024759202052757723", "4525232859750548"));
req.setAt(at);
// 发送请求
OapiRobotSendResponse rsp = client.execute(req, accessToken);
System.out.println(rsp.getBody()); // 输出返回结果
} catch (ApiException e) {
e.printStackTrace();
}
}
}
link(链接消息)不支持@能力:
json
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiRobotSendRequest;
import com.dingtalk.api.response.OapiRobotSendResponse;
import com.taobao.api.ApiException;
public class LinkMsgRobot {
//自定义机器人发送link消息
public static void main(String[] args) {
try {
// 机器人webhook对应的access_token的值,不是client_id和client_secret生成的应用access_token
String accessToken = "机器人webhook对应的access_token的值";
// 创建钉钉客户端
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/robot/send");
// 构建请求对象
OapiRobotSendRequest req = new OapiRobotSendRequest();
req.setMsgtype("link"); // 消息类型为link
OapiRobotSendRequest.Link link = new OapiRobotSendRequest.Link();
link.setMessageUrl("https://www.dingtalk.com"); // 跳转链接
link.setPicUrl("https://example.com/ipad.png"); // 图片链接
link.setText("测试消息内容"); // 消息内容
link.setTitle("标题"); // 消息标题
req.setLink(link);
// 发送请求
OapiRobotSendResponse rsp = client.execute(req, accessToken);
System.out.println(rsp.getBody()); // 输出返回结果
} catch (ApiException e) {
e.printStackTrace();
}
}
}
json
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiRobotSendRequest;
import com.dingtalk.api.response.OapiRobotSendResponse;
import com.taobao.api.ApiException;
import java.util.Collections;
public class FeedCardMsgRobot {
//自定义机器人发送feedCard消息
public static void main(String[] args) {
try {
// 机器人webhook对应的access_token的值,不是client_id和client_secret生成的应用access_token
String accessToken = "机器人webhook对应的access_token的值";
// 创建钉钉客户端
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/robot/send");
// 构建请求对象
OapiRobotSendRequest req = new OapiRobotSendRequest();
req.setMsgtype("feedCard"); // 消息类型为feedCard
OapiRobotSendRequest.Feedcard feedCard = new OapiRobotSendRequest.Feedcard();
OapiRobotSendRequest.Links link = new OapiRobotSendRequest.Links();
link.setTitle("钉钉"); // 卡片标题
link.setMessageURL("https://www.dingtalk.com"); // 跳转链接
link.setPicURL("https://example.com/ipad.png"); // 图片链接
feedCard.setLinks(Collections.singletonList(link));
req.setFeedCard(feedCard);
// 发送请求
OapiRobotSendResponse rsp = client.execute(req, accessToken);
System.out.println(rsp.getBody()); // 输出返回结果
} catch (ApiException e) {
e.printStackTrace();
}
}
}
actionCard 消息:
json
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiRobotSendRequest;
import com.dingtalk.api.response.OapiRobotSendResponse;
import com.taobao.api.ApiException;
import java.util.Arrays;
public class ActionCardMsgRobot {
//自定义机器人发送actionCard消息
public static void main(String[] args) {
try {
// 机器人webhook对应的access_token的值,不是client_id和client_secret生成的应用access_token
String accessToken = "机器人webhook对应的access_token的值";
// 创建钉钉客户端
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/robot/send");
// 构建请求对象
OapiRobotSendRequest req = new OapiRobotSendRequest();
req.setMsgtype("actionCard"); // 消息类型为actionCard
OapiRobotSendRequest.Actioncard actionCard = new OapiRobotSendRequest.Actioncard();
actionCard.setTitle("乔布斯 20 年前想打造一间苹果咖啡厅,而它正是 Apple Store 的前身"); // 标题
actionCard.setText("@024759202052757723 @4525232859750548 \n  \n ### 乔布斯 20 年前想打造的苹果咖啡厅 \n Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划"); // 内容
actionCard.setBtnOrientation("0"); // 按钮排列方向
actionCard.setSingleTitle("阅读全文"); // 单个按钮标题
actionCard.setSingleURL("https://www.dingtalk.com/"); // 单个按钮跳转链接
req.setActionCard(actionCard);
// 构建@信息
OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
at.setIsAtAll(false); // 是否@所有人
// 指定@的用户ID列表
at.setAtUserIds(Arrays.asList("024759202052757723", "4525232859750548"));
req.setAt(at);
// 发送请求
OapiRobotSendResponse rsp = client.execute(req, accessToken);
System.out.println(rsp.getBody()); // 输出返回结果
} catch (ApiException e) {
e.printStackTrace();
}
}
}
返回示例
json{
"errcode":"0",
"errmsg":"ok"
}
错误码(errorcode) | 错误码描述(errmsg) | 解决方案 |
---|---|---|
-1 | 系统繁忙 | 请稍后重试 |
40035 | 缺少参数 json | 请补充消息json |
43004 | 无效的HTTP HEADER Content-Type | 请设置具体的消息参数 |
400013 | 群已被解散 | 请向其他群发消息 |
400101 | access_token不存在 | 请确认access_token拼写是否正确 |
400102 | 机器人已停用 | 请联系管理员启用机器人 |
400105 | 不支持的消息类型 | 请使用文档中支持的消息类型 |
400106 | 机器人不存在 | 请确认机器人是否在群中 |
410100 | 发送速度太快而限流 | 请降低发送速度 |
430101 | 含有不安全的外链 | 请确认发送的内容合法 |
430102 | 含有不合适的文本 | 请确认发送的内容合法 |
430103 | 含有不合适的图片 | 请确认发送的内容合法 |
430104 | 含有不合适的内容 | 请确认发送的内容合法 |
json安全设置错误码
当出现以下错误时,表示消息校验未通过,请查看机器人的安全设置。
错误码(errorcode)
错误码描述(errmsg)
解决方案
310000
keywords not in content 消息内容中不包含任何关键词
invalid timestamp timestamp 无效
sign not match 签名不匹配
ip X.X.X.X not in whitelist IP地址不在白名单
上面是钉钉官方机器人使用方法文档示例
下面有一个利用go语言写的插件,在cnb平台发送钉钉机器人信息,目前只开发了txt与md两种支持 ,具体文件代码如下
https://cnb.cool/cnb/plugins/tencentcom/dingtalk-bot-msg/-/tree/main
dockerfile内容如下:
dockerfileFROM golang:1.21-alpine3.18 ADD ./ /plugins RUN chmod +x /plugins/entrypoint.sh ENTRYPOINT /plugins/entrypoint.sh
是MIT许可
txtThe MIT License (MIT) Copyright (c) 2024-present, cnb.cool Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
原作者写的readme.md
md# dingtalk-bot-msg
发送钉钉机器人消息
## 在 云原生构建 上使用
```yml
main:
push:
- stages:
- name: dingtalk-bot-msg
imports: https://xxx/envs.yaml
image: tencentcom/dingtalk-bot-msg:latest
settings:
content: "your message"
c_type: "text"
secret: $SECRET
webhook: $WEBHOOK
at: "199xxxxxx"
isAtAll: false
envs.yaml文件示例:
yml
WEBHOOK: xxx
SECRET: xxx
content
:消息内容
c_type
:消息类型。可选:text,markdown
webhook
:钉钉机器人 WebHook [需要在PC端钉钉客户端创建]。
参考文档
secret
: 安全设置加签的密钥。可选(推荐)。
参考文档
at
:需要 at 的人。填写需要 at 人的手机号,多个以 ";" 隔开
isAtAll
:是否 at 所有人。bool
更多用法参考:钉钉开放平台帮助文档
entrypoint.sh这个入口脚本内容如下 ```sh go run /plugins/main.go --content "$PLUGIN_CONTENT" \ --c_type "$PLUGIN_C_TYPE" \ --webhook "$PLUGIN_WEBHOOK" \ --at "$PLUGIN_AT" \ --secret "$PLUGIN_SECRET" \ --isAtAll ${PLUGIN_ISATALL:-false}
主文件 main.go 内容如下
gopackage main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net"
"net/http"
"strings"
"time"
)
var (
content = flag.String("content", "", "")
cType = flag.String("c_type", "", "")
webhook = flag.String("webhook", "", "")
secret = flag.String("secret", "", "")
at = flag.String("at", "", "")
isAtAll = flag.String("isAtAll", "false", "")
)
var (
atMobiles = make([]string, 0)
)
// 全局获取 http client 的方法
var httpClient func() *http.Client
// TimeOut 全局请求超时设置,默认1分钟
var TimeOut time.Duration = 60 * time.Second
type Response struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
type Media struct {
Response
Type string `json:"type"`
MediaId string `json:"media_id"`
}
type TextMessage struct {
// 消息类型,此时固定为:text
MsgType string `json:"msgtype"`
Text struct {
// 消息内容,最长不超过2048个字节,超过将截断(支持id转译)
Content string `json:"content"`
} `json:"text"`
At struct {
AtMobiles []string `json:"atMobiles"`
AtUserIds []string `json:"atUserIds"`
IsAtAll bool `json:"isAtAll"`
} `json:"at"`
}
type MarkdownMessage struct {
// 消息类型,此时固定为:text
MsgType string `json:"msgtype"`
Markdown struct {
Title string `json:"title"`
// 消息内容,最长不超过2048个字节,超过将截断(支持id转译)
Text string `json:"text"`
} `json:"markdown"`
At struct {
AtMobiles []string `json:"atMobiles"`
AtUserIds []string `json:"atUserIds"`
IsAtAll bool `json:"isAtAll"`
} `json:"at"`
}
func SendTextMessage(content string, isAtAll bool, atMobiles ...string) error {
message := TextMessage{
MsgType: "text",
Text: struct {
Content string `json:"content"`
}(struct{ Content string }{
Content: content,
}),
At: struct {
AtMobiles []string `json:"atMobiles"`
AtUserIds []string `json:"atUserIds"`
IsAtAll bool `json:"isAtAll"`
}(struct {
AtMobiles []string
AtUserIds []string
IsAtAll bool
}{AtMobiles: atMobiles, AtUserIds: []string{}, IsAtAll: isAtAll}),
}
return SendMessage(message)
}
func SendMarkdownMessage(content string, isAtAll bool, atMobiles ...string) error {
message := MarkdownMessage{
MsgType: "markdown",
Markdown: struct {
Title string `json:"title"`
Text string `json:"text"`
}(struct {
Title string
Text string
}{Title: "CODING", Text: content}),
At: struct {
AtMobiles []string `json:"atMobiles"`
AtUserIds []string `json:"atUserIds"`
IsAtAll bool `json:"isAtAll"`
}(struct {
AtMobiles []string
AtUserIds []string
IsAtAll bool
}{AtMobiles: atMobiles, AtUserIds: []string{}, IsAtAll: isAtAll}),
}
return SendMessage(message)
}
// 生成钉钉要求的加签(签名)和时间戳
func generateSign(secret string) (string, int64) {
timestamp := time.Now().UnixNano() / 1e6 // 毫秒级时间戳
stringToSign := fmt.Sprintf("%d\n%s", timestamp, secret)
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(stringToSign))
sign := base64.StdEncoding.EncodeToString(h.Sum(nil))
return sign, timestamp
}
func SendMessage(data interface{}) error {
url := *webhook
if *secret != "" {
sign,timestamp := generateSign(*secret)
url = fmt.Sprintf("%s×tamp=%d&sign=%s", *webhook, timestamp, sign)
}
return sendMessage(url, data)
}
func sendMessage(url string, data interface{}) error {
client := &http.Client{
Transport: &http.Transport{
Dial: func(netw, addr string) (net.Conn, error) {
conn, err := net.DialTimeout(netw, addr, time.Second*15) //设置建立连接超时
if err != nil {
return nil, err
}
_ = conn.SetDeadline(time.Now().Add(time.Second * 15)) //设置发送接受数据超时
return conn, nil
},
ResponseHeaderTimeout: time.Second * 2,
},
}
body, err := json.Marshal(data)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return err
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("send message status code err: %d", resp.StatusCode)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
fmt.Println("dingding server resp:", string(b))
var messageResp Response
err = json.Unmarshal(b, &messageResp)
if err != nil {
return err
}
if messageResp.ErrCode == 0 {
return nil
}
return fmt.Errorf("%s", messageResp.ErrMsg)
}
// httpClient() 带超时的http.Client
func defaultHTTPClient() *http.Client {
return &http.Client{Timeout: TimeOut}
}
func checkRequisite() {
if *content == "" {
panic("不允许发送空消息")
}
if *webhook == "" {
panic("WebHook不能为空")
}
}
func transUser() {
for _, v := range strings.Split(*at, ";") {
atMobiles = append(atMobiles, v)
}
fmt.Println("at list:", atMobiles)
}
func init() {
httpClient = defaultHTTPClient
}
func main() {
flag.Parse()
checkRequisite()
transUser()
var err error
isAtAllBool := *isAtAll == "true"
fmt.Println("isAtAll:", "true")
fmt.Println("isAtAll:", *isAtAll)
fmt.Println("isAtAllBool:", isAtAllBool)
switch *cType {
case "text":
err = SendTextMessage(*content, isAtAllBool, atMobiles...)
case "markdown":
err = SendMarkdownMessage(*content, isAtAllBool, atMobiles...)
default:
panic("发送消息类型不合法")
}
if err != nil {
panic(err)
}
}
plugin-meta.json
{ "name": "dingtalk-bot-msg", "description": "发送钉钉机器人消息", "mark": "official", "tags": [ "dingtalk" ], "images": "https://hub.docker.com/r/tencentcom/dingtalk-bot-msg/tags", "source": "https://cnb.cool/cnb/plugins/tencentcom/dingtalk-bot-msg", "bugs": "https://cnb.cool/cnb/plugins/tencentcom/dingtalk-bot-msg/-/issues", "locales": { "en": { "name": "dingtalk-bot-msg", "description": "Send DingTalk robot messages", "readme": "README.en.md" } } }
test.sh
#!/bin/bash docker build -t ding:v1 . docker run --rm -it -v $(pwd):$(pwd) -w $(pwd) \ -e PLUGIN_CONTENT="lalala" \ -e PLUGIN_C_TYPE="text" \ -e PLUGIN_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=xxx" \ -e PLUGIN_SECRET="xxx" \ -e PLUGIN_AT="xxx" \ -e PLUGIN_ISATALL="true" \ ding:v1
能给我扩展所有的文件格式吗?
基于您提供的钉钉机器人文档和现有Go代码,我将扩展所有钉钉支持的消息格式。以下是完整的实现:
在main.go
中添加所有钉钉支持的消息类型结构体:
go// TextMessage 文本消息
type TextMessage struct {
MsgType string `json:"msgtype"`
Text struct {
Content string `json:"content"`
} `json:"text"`
At struct {
AtMobiles []string `json:"atMobiles"`
AtUserIds []string `json:"atUserIds"`
IsAtAll bool `json:"isAtAll"`
} `json:"at"`
}
// MarkdownMessage Markdown消息
type MarkdownMessage struct {
MsgType string `json:"msgtype"`
Markdown struct {
Title string `json:"title"`
Text string `json:"text"`
} `json:"markdown"`
At struct {
AtMobiles []string `json:"atMobiles"`
AtUserIds []string `json:"atUserIds"`
IsAtAll bool `json:"isAtAll"`
} `json:"at"`
}
// LinkMessage 链接消息
type LinkMessage struct {
MsgType string `json:"msgtype"`
Link struct {
Text string `json:"text"`
Title string `json:"title"`
PicUrl string `json:"picUrl"`
MessageUrl string `json:"messageUrl"`
} `json:"link"`
}
// ActionCardMessage 整体跳转ActionCard消息
type ActionCardMessage struct {
MsgType string `json:"msgtype"`
ActionCard struct {
Title string `json:"title"`
Text string `json:"text"`
BtnOrientation string `json:"btnOrientation"`
SingleTitle string `json:"singleTitle"`
SingleURL string `json:"singleURL"`
} `json:"actionCard"`
}
// MultiActionCardMessage 独立跳转ActionCard消息
type MultiActionCardMessage struct {
MsgType string `json:"msgtype"`
ActionCard struct {
Title string `json:"title"`
Text string `json:"text"`
BtnOrientation string `json:"btnOrientation"`
Btns []struct {
Title string `json:"title"`
ActionURL string `json:"actionURL"`
} `json:"btns"`
} `json:"actionCard"`
}
// FeedCardMessage FeedCard消息
type FeedCardMessage struct {
MsgType string `json:"msgtype"`
FeedCard struct {
Links []struct {
Title string `json:"title"`
MessageURL string `json:"messageURL"`
PicURL string `json:"picURL"`
} `json:"links"`
} `json:"feedCard"`
}
go// SendTextMessage 发送文本消息
func SendTextMessage(content string, isAtAll bool, atMobiles ...string) error {
message := TextMessage{
MsgType: "text",
Text: struct {
Content string `json:"content"`
}{
Content: content,
},
At: struct {
AtMobiles []string `json:"atMobiles"`
AtUserIds []string `json:"atUserIds"`
IsAtAll bool `json:"isAtAll"`
}{
AtMobiles: atMobiles,
AtUserIds: []string{},
IsAtAll: isAtAll,
},
}
return SendMessage(message)
}
// SendMarkdownMessage 发送Markdown消息
func SendMarkdownMessage(title, content string, isAtAll bool, atMobiles ...string) error {
message := MarkdownMessage{
MsgType: "markdown",
Markdown: struct {
Title string `json:"title"`
Text string `json:"text"`
}{
Title: title,
Text: content,
},
At: struct {
AtMobiles []string `json:"atMobiles"`
AtUserIds []string `json:"atUserIds"`
IsAtAll bool `json:"isAtAll"`
}{
AtMobiles: atMobiles,
AtUserIds: []string{},
IsAtAll: isAtAll,
},
}
return SendMessage(message)
}
// SendLinkMessage 发送链接消息
func SendLinkMessage(title, text, picUrl, messageUrl string) error {
message := LinkMessage{
MsgType: "link",
Link: struct {
Text string `json:"text"`
Title string `json:"title"`
PicUrl string `json:"picUrl"`
MessageUrl string `json:"messageUrl"`
}{
Text: text,
Title: title,
PicUrl: picUrl,
MessageUrl: messageUrl,
},
}
return SendMessage(message)
}
// SendSingleActionCard 发送整体跳转ActionCard消息
func SendSingleActionCard(title, text, singleTitle, singleURL string, btnOrientation string) error {
message := ActionCardMessage{
MsgType: "actionCard",
ActionCard: struct {
Title string `json:"title"`
Text string `json:"text"`
BtnOrientation string `json:"btnOrientation"`
SingleTitle string `json:"singleTitle"`
SingleURL string `json:"singleURL"`
}{
Title: title,
Text: text,
BtnOrientation: btnOrientation,
SingleTitle: singleTitle,
SingleURL: singleURL,
},
}
return SendMessage(message)
}
// SendMultiActionCard 发送独立跳转ActionCard消息
func SendMultiActionCard(title, text, btnOrientation string, btns []map[string]string) error {
message := MultiActionCardMessage{
MsgType: "actionCard",
ActionCard: struct {
Title string `json:"title"`
Text string `json:"text"`
BtnOrientation string `json:"btnOrientation"`
Btns []struct {
Title string `json:"title"`
ActionURL string `json:"actionURL"`
} `json:"btns"`
}{
Title: title,
Text: text,
BtnOrientation: btnOrientation,
},
}
// 添加按钮
for _, btn := range btns {
message.ActionCard.Btns = append(message.ActionCard.Btns, struct {
Title string `json:"title"`
ActionURL string `json:"actionURL"`
}{
Title: btn["title"],
ActionURL: btn["actionURL"],
})
}
return SendMessage(message)
}
// SendFeedCard 发送FeedCard消息
func SendFeedCard(links []map[string]string) error {
message := FeedCardMessage{
MsgType: "feedCard",
FeedCard: struct {
Links []struct {
Title string `json:"title"`
MessageURL string `json:"messageURL"`
PicURL string `json:"picURL"`
} `json:"links"`
}{},
}
// 添加链接
for _, link := range links {
message.FeedCard.Links = append(message.FeedCard.Links, struct {
Title string `json:"title"`
MessageURL string `json:"messageURL"`
PicURL string `json:"picURL"`
}{
Title: link["title"],
MessageURL: link["messageURL"],
PicURL: link["picURL"],
})
}
return SendMessage(message)
}
gofunc main() {
flag.Parse()
checkRequisite()
transUser()
var err error
isAtAllBool := *isAtAll == "true"
switch *cType {
case "text":
err = SendTextMessage(*content, isAtAllBool, atMobiles...)
case "markdown":
err = SendMarkdownMessage("通知", *content, isAtAllBool, atMobiles...)
case "link":
// 解析content为link消息参数,格式为 title|text|picUrl|messageUrl
parts := strings.Split(*content, "|")
if len(parts) != 4 {
panic("链接消息格式错误,应为 title|text|picUrl|messageUrl")
}
err = SendLinkMessage(parts[0], parts[1], parts[2], parts[3])
case "actionCard":
// 解析content为actionCard消息参数,格式为 title|text|btnOrientation|singleTitle|singleURL
parts := strings.Split(*content, "|")
if len(parts) != 5 {
panic("ActionCard消息格式错误,应为 title|text|btnOrientation|singleTitle|singleURL")
}
err = SendSingleActionCard(parts[0], parts[1], parts[3], parts[4], parts[2])
case "multiActionCard":
// 解析content为multiActionCard消息参数,格式为 title|text|btnOrientation|btn1Title,btn1Url|btn2Title,btn2Url...
parts := strings.Split(*content, "|")
if len(parts) < 4 {
panic("多ActionCard消息格式错误,应为 title|text|btnOrientation|btn1Title,btn1Url|btn2Title,btn2Url...")
}
var btns []map[string]string
for i := 3; i < len(parts); i++ {
btnParts := strings.Split(parts[i], ",")
if len(btnParts) != 2 {
panic("按钮格式错误,应为 btnTitle,btnUrl")
}
btns = append(btns, map[string]string{
"title": btnParts[0],
"actionURL": btnParts[1],
})
}
err = SendMultiActionCard(parts[0], parts[1], parts[2], btns)
case "feedCard":
// 解析content为feedCard消息参数,格式为 title1,messageUrl1,picUrl1|title2,messageUrl2,picUrl2...
var links []map[string]string
linkParts := strings.Split(*content, "|")
for _, part := range linkParts {
linkInfo := strings.Split(part, ",")
if len(linkInfo) != 3 {
panic("FeedCard链接格式错误,应为 title,messageUrl,picUrl")
}
links = append(links, map[string]string{
"title": linkInfo[0],
"messageURL": linkInfo[1],
"picURL": linkInfo[2],
})
}
err = SendFeedCard(links)
default:
panic("发送消息类型不合法,支持: text, markdown, link, actionCard, multiActionCard, feedCard")
}
if err != nil {
panic(err)
}
}
markdown# dingtalk-bot-msg
发送钉钉机器人消息,支持所有钉钉机器人消息类型
## 在 云原生构建 上使用
```yml
main:
push:
- stages:
- name: dingtalk-bot-msg
imports: https://xxx/envs.yaml
image: tencentcom/dingtalk-bot-msg:latest
settings:
content: "your message"
c_type: "text" # 支持: text, markdown, link, actionCard, multiActionCard, feedCard
secret: $SECRET
webhook: $WEBHOOK
at: "199xxxxxx" # 多个用分号(;)分隔
isAtAll: false
ymlc_type: "text"
content: "文本消息内容"
ymlc_type: "markdown"
content: "#### Markdown标题\n- 项目1\n- 项目2"
ymlc_type: "link"
content: "标题|消息内容|图片URL|跳转URL"
ymlc_type: "actionCard"
content: "标题|消息内容|按钮方向(0-垂直,1-水平)|按钮标题|跳转URL"
ymlc_type: "multiActionCard"
content: "标题|消息内容|按钮方向(0-垂直,1-水平)|按钮1标题,按钮1URL|按钮2标题,按钮2URL"
ymlc_type: "feedCard"
content: "标题1,跳转URL1,图片URL1|标题2,跳转URL2,图片URL2"
content
:消息内容,格式根据消息类型有所不同c_type
:消息类型。支持: text, markdown, link, actionCard, multiActionCard, feedCardwebhook
:钉钉机器人 WebHooksecret
:安全设置加签的密钥(可选)at
:需要 @ 的人手机号,多个用分号(;)分隔isAtAll
:是否 @ 所有人,bool值## 测试脚本更新 更新`test.sh`以测试所有消息类型: ```bash #!/bin/bash docker build -t ding:v1 . # 测试文本消息 docker run --rm -it -v $(pwd):$(pwd) -w $(pwd) \ -e PLUGIN_CONTENT="测试文本消息" \ -e PLUGIN_C_TYPE="text" \ -e PLUGIN_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=xxx" \ -e PLUGIN_SECRET="xxx" \ -e PLUGIN_AT="13800138000" \ -e PLUGIN_ISATALL="false" \ ding:v1 # 测试Markdown消息 docker run --rm -it -v $(pwd):$(pwd) -w $(pwd) \ -e PLUGIN_CONTENT="#### Markdown测试\n- 项目1\n- 项目2\n" \ -e PLUGIN_C_TYPE="markdown" \ -e PLUGIN_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=xxx" \ -e PLUGIN_SECRET="xxx" \ ding:v1 # 测试链接消息 docker run --rm -it -v $(pwd):$(pwd) -w $(pwd) \ -e PLUGIN_CONTENT="链接标题|链接描述|https://example.com/image.png|https://example.com" \ -e PLUGIN_C_TYPE="link" \ -e PLUGIN_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=xxx" \ ding:v1 # 测试ActionCard消息 docker run --rm -it -v $(pwd):$(pwd) -w $(pwd) \ -e PLUGIN_CONTENT="卡片标题|卡片内容|0|查看详情|https://example.com" \ -e PLUGIN_C_TYPE="actionCard" \ -e PLUGIN_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=xxx" \ ding:v1 # 测试多ActionCard消息 docker run --rm -it -v $(pwd):$(pwd) -w $(pwd) \ -e PLUGIN_CONTENT="多卡片标题|卡片内容|0|按钮1,https://example.com/btn1|按钮2,https://example.com/btn2" \ -e PLUGIN_C_TYPE="multiActionCard" \ -e PLUGIN_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=xxx" \ ding:v1 # 测试FeedCard消息 docker run --rm -it -v $(pwd):$(pwd) -w $(pwd) \ -e PLUGIN_CONTENT="标题1,https://example.com/1,https://example.com/image1.png|标题2,https://example.com/2,https://example.com/image2.png" \ -e PLUGIN_C_TYPE="feedCard" \ -e PLUGIN_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=xxx" \ ding:v1
支持所有钉钉机器人消息类型:
参数处理优化:
错误处理:
兼容性:
这个扩展实现覆盖了钉钉机器人所有支持的消息类型,并提供了清晰的文档说明和测试用例,便于用户使用各种消息类型。
以下是针对高级定制建议的完整实现方案,我将为您增强现有的配置,增加输入验证、消息模板和速率限制功能:
.cnb/web_trigger.yml
yamlbranch:
- buttons:
- name: 钉钉消息测试
description: 高级钉钉机器人消息测试
event: advanced_dingtalk_test
env:
DINGTALK_WEBHOOK: $DINGTALK_WEBHOOK
DINGTALK_SECRET: $DINGTALK_SECRET
inputs:
test_scenario:
name: 测试场景
description: 选择预设测试场景或自定义
required: true
type: select
default: custom
options:
- name: 自定义消息
value: custom
- name: 系统告警测试
value: alert
- name: 每日报告测试
value: daily_report
- name: 任务通知测试
value: task_notice
message_type:
name: 消息类型
description: 选择消息类型
required: true
type: select
default: text
options:
- name: 文本
value: text
- name: Markdown
value: markdown
- name: 链接
value: link
- name: 行动卡片
value: actionCard
- name: 多行动卡片
value: multiActionCard
- name: Feed卡片
value: feedCard
content:
name: 消息内容
description: 根据下方提示输入内容
required: true
type: textarea
default: ""
at_mobiles:
name: @手机号
description: "多个用分号分隔,如: 13800138000;13900139000"
required: false
type: input
default: ""
is_at_all:
name: "@所有人"
type: switch
default: "false"
options:
- name: 是
value: "true"
- name: 否
value: "false"
rate_limit:
name: 速率限制
description: 每条消息间隔(毫秒)
type: input
default: "3500"
regex: "^[0-9]{3,5}$"
regex_error: "请输入100-99999之间的数字"
.cnb.yml
流水线yaml"**":
advanced_dingtalk_test:
- stages:
- name: 参数预处理
script: |
# 根据测试场景生成默认内容
case "$test_scenario" in
"alert")
case "$message_type" in
"text")
CONTENT="【系统告警】服务器CPU使用率超过95%"
;;
"markdown")
CONTENT="### 【系统告警】\n**主机**: web-server-01\n**指标**: CPU使用率\n**当前值**: 95%\n**阈值**: 90%"
;;
"actionCard")
CONTENT="系统告警|服务器CPU使用率超过95%,请立即处理|0|查看详情|https://example.com/alerts"
;;
*)
CONTENT="$content"
;;
esac
;;
"daily_report")
case "$message_type" in
"markdown")
CONTENT="### 每日运营报告\n- PV: 120万\n- UV: 80万\n- 订单数: 5,328\n[查看详情](https://example.com/daily)"
;;
"feedCard")
CONTENT="运营日报,https://example.com/report,https://example.com/report.png|系统监控,https://example.com/monitor,https://example.com/monitor.png"
;;
*)
CONTENT="$content"
;;
esac
;;
*)
CONTENT="$content"
;;
esac
# 验证内容格式
case "$message_type" in
"link")
if [ $(echo "$CONTENT" | tr '|' '\n' | wc -l) -ne 4 ]; then
echo "链接消息格式错误,应为: 标题|描述|图片URL|跳转URL"
exit 1
fi
;;
"actionCard")
if [ $(echo "$CONTENT" | tr '|' '\n' | wc -l) -ne 5 ]; then
echo "ActionCard格式错误,应为: 标题|内容|按钮方向|按钮标题|跳转URL"
exit 1
fi
;;
"multiActionCard")
if [ $(echo "$CONTENT" | tr '|' '\n' | wc -l) -lt 4 ]; then
echo "多ActionCard格式错误,应为: 标题|内容|按钮方向|按钮1标题,按钮1URL|..."
exit 1
fi
;;
"feedCard")
if [ -z "$(echo "$CONTENT" | grep '|')" ]; then
echo "FeedCard格式错误,应包含至少一个|分隔符"
exit 1
fi
;;
esac
echo "CONTENT=$CONTENT" >> $CI_ENV
echo "RATE_LIMIT=${rate_limit:-3500}" >> $CI_ENV
- name: 发送钉钉消息
image: tencentcom/dingtalk-bot-msg:latest
settings:
content: $CONTENT
c_type: $message_type
webhook: $DINGTALK_WEBHOOK
secret: $DINGTALK_SECRET
at: $at_mobiles
isAtAll: $is_at_all
script: |
echo "正在发送消息..."
echo "等待 ${RATE_LIMIT}ms 防止限流..."
sleep $(echo "scale=3; $RATE_LIMIT/1000" | bc)
- name: 发送结果通知
script: |
echo "钉钉消息发送完成 √"
echo "消息类型: $message_type"
echo "测试场景: $test_scenario"
echo "内容预览:"
echo "----------------------------------------"
echo "$CONTENT" | head -n 5
echo "----------------------------------------"
if [ "$message_type" = "text" ] || [ "$message_type" = "markdown" ]; then
echo "@设置: ${at_mobiles:-无} ${is_at_all:-false}"
fi
echo "速率限制: ${RATE_LIMIT}ms"
预设测试场景:
智能内容生成:
严格的输入验证:
增强的速率控制:
完善的反馈信息:
选择预设场景:
自定义消息:
高级控制:
消息模板库:
yaml# 可以扩展更多预设场景
options:
- name: 版本发布通知
value: release
- name: 会议提醒
value: meeting
内容预览功能:
yaml# 在inputs中添加预览按钮
preview_button:
name: 内容预览
type: button
action: preview
发送历史记录:
yaml# 添加发送日志记录
- name: 记录发送日志
script: |
echo "$(date) - $message_type - $test_scenario" >> dingtalk_test.log
这套方案提供了企业级的安全消息测试能力,既保持了易用性,又提供了必要的控制和验证机制,同时符合钉钉机器人的最佳实践。
本文作者:任浪漫
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!