编辑
2025-08-08
默认分类
00

目录

基本信息
Query参数
Body参数
返回参数
示例
错误码
SDK请求示例错误码
钉钉机器人消息格式扩展
完整消息类型实现
1. 首先更新消息结构体
2. 添加对应的发送函数
3. 更新main函数处理所有消息类型
4. 更新README.md
测试脚本更新
完整实现说明
1. 增强版 .cnb/web_trigger.yml
2. 增强版 .cnb.yml 流水线
3. 主要增强功能说明
4. 使用示例
5. 扩展建议

通过 Webhook 接入自定义服务,使用自定义机器人,支持在企业内部群和普通钉钉群内发送群消息,不支持发送单聊消息。
详情参考:

每个机器人每分钟最多发送20条消息到群里,如果超过20条,会限流10分钟。

重要

如果你有大量发消息的场景(譬如系统监控报警)可以将这些信息进行整合,通过markdown消息以摘要的形式发送到群里。

基本信息

请求方式:POST

请求地址https://oapi.dingtalk.com/robot/send

Query参数

名称类型是否必填示例值描述
access_tokenStringBE3xxxx自定义机器人调用接口的凭证。自定义机器人安装后webhook地址中的access_token值。详情参考获取自定义机器人 Webhook 地址

重要

如果自定义机器人的安全设置使用的是加签方式,调用本接口发送消息时,需要拼接timestamp和sign参数,示例为:

sh
https://oapi.dingtalk.com/robot/send?access_token=XXXXXX&timestamp=XXX&sign=XXX

Body参数

名称类型是否必填示例值描述
msgtypeStringtext消息类型,自定义机器人可发送的消息类型参见自定义机器人发送消息的消息类型
textObject文本类型消息。
atObject被@的群成员信息。
linkObject链接类型消息。
markdownObjectmarkdown类型消息。
actionCardObjectactionCard类型消息。
feedCardObjectfeedCard类型消息。

返回参数

名称类型示例值描述
errmsgStringokerrmsg
errcodeNumber0errcode

示例

请求示例(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 ![图片](https://example.com/image.png)" }, "actionCard": { "title": "ActionCard消息标题", "text": "#### 这是ActionCard内容 \n ![图片](https://example.com/image.png)", "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)

  • text 文本消息:
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 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 消息:

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 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(); } } }
  • feedCard 消息,不支持@能力:
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 ![screenshot](https://gw.alicdn.com/tfs/TB1ut3xxbsrBKNjSZFpXXcXhFXa-846-786.png) \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" }

错误码

SDK请求示例错误码

错误码(errorcode)错误码描述(errmsg)解决方案
-1系统繁忙请稍后重试
40035缺少参数 json请补充消息json
43004无效的HTTP HEADER Content-Type请设置具体的消息参数
400013群已被解散请向其他群发消息
400101access_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内容如下:

dockerfile
FROM golang:1.21-alpine3.18 ADD ./ /plugins RUN chmod +x /plugins/entrypoint.sh ENTRYPOINT /plugins/entrypoint.sh

是MIT许可

txt
The 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 内容如下

go
package 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&timestamp=%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代码,我将扩展所有钉钉支持的消息格式。以下是完整的实现:

完整消息类型实现

1. 首先更新消息结构体

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"` }

2. 添加对应的发送函数

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) }

3. 更新main函数处理所有消息类型

go
func 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) } }

4. 更新README.md

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

消息类型使用说明

1. 文本消息(text)

yml
c_type: "text" content: "文本消息内容"

2. Markdown消息(markdown)

yml
c_type: "markdown" content: "#### Markdown标题\n- 项目1\n- 项目2"

3. 链接消息(link)

yml
c_type: "link" content: "标题|消息内容|图片URL|跳转URL"

4. 整体跳转ActionCard(actionCard)

yml
c_type: "actionCard" content: "标题|消息内容|按钮方向(0-垂直,1-水平)|按钮标题|跳转URL"

5. 独立跳转ActionCard(multiActionCard)

yml
c_type: "multiActionCard" content: "标题|消息内容|按钮方向(0-垂直,1-水平)|按钮1标题,按钮1URL|按钮2标题,按钮2URL"

6. FeedCard消息(feedCard)

yml
c_type: "feedCard" content: "标题1,跳转URL1,图片URL1|标题2,跳转URL2,图片URL2"

参数说明

  • content:消息内容,格式根据消息类型有所不同
  • c_type:消息类型。支持: text, markdown, link, actionCard, multiActionCard, feedCard
  • webhook:钉钉机器人 WebHook
  • secret:安全设置加签的密钥(可选)
  • at:需要 @ 的人手机号,多个用分号(;)分隔
  • isAtAll:是否 @ 所有人,bool值

注意事项

  1. link、actionCard、multiActionCard和feedCard类型消息不支持@功能
  2. 消息内容中特殊字符需正确转义
  3. 每种消息类型有不同的大小限制,请参考钉钉官方文档
## 测试脚本更新 更新`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![图片](https://example.com/image.png)" \ -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

完整实现说明

  1. 支持所有钉钉机器人消息类型

    • 文本消息(text)
    • Markdown消息(markdown)
    • 链接消息(link)
    • 整体跳转ActionCard(actionCard)
    • 独立跳转ActionCard(multiActionCard)
    • FeedCard消息(feedCard)
  2. 参数处理优化

    • 使用管道符(|)分隔复杂消息类型的多个参数
    • 对于多条目消息(如FeedCard),支持用管道符分隔多个条目
  3. 错误处理

    • 增加了各种消息类型的格式验证
    • 提供更清晰的错误提示
  4. 兼容性

    • 保持与原有实现的兼容
    • 新增功能不影响原有text和markdown消息的使用

这个扩展实现覆盖了钉钉机器人所有支持的消息类型,并提供了清晰的文档说明和测试用例,便于用户使用各种消息类型。

以下是针对高级定制建议的完整实现方案,我将为您增强现有的配置,增加输入验证、消息模板和速率限制功能:

1. 增强版 .cnb/web_trigger.yml

yaml
branch: - 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之间的数字"

2. 增强版 .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"

3. 主要增强功能说明

  1. 预设测试场景

    • 新增"测试场景"选择,包含系统告警、每日报告等常见场景
    • 自动根据场景和消息类型生成合适的内容模板
  2. 智能内容生成

    • 根据选择的场景自动填充典型消息内容
    • 保留自定义输入选项,覆盖自动生成内容
  3. 严格的输入验证

    • 对每种消息类型的格式进行验证
    • 不满足格式要求时立即报错终止
  4. 增强的速率控制

    • 可自定义消息间隔时间(毫秒)
    • 添加输入验证,确保输入为数字
    • 默认3500ms(3.5秒),确保低于钉钉限制
  5. 完善的反馈信息

    • 显示格式化后的消息预览
    • 包含所有相关参数的执行结果报告

4. 使用示例

  1. 选择预设场景

    • 选择"系统告警测试"场景
    • 选择"markdown"消息类型
    • 系统自动生成带格式的告警内容
  2. 自定义消息

    • 选择"自定义消息"
    • 选择"feedCard"类型
    • 输入格式为:"标题1,URL1,图片1|标题2,URL2,图片2"
  3. 高级控制

    • 设置@特定成员
    • 调整速率限制为5000ms(5秒)
    • 系统会验证所有输入格式

5. 扩展建议

  1. 消息模板库

    yaml
    # 可以扩展更多预设场景 options: - name: 版本发布通知 value: release - name: 会议提醒 value: meeting
  2. 内容预览功能

    yaml
    # 在inputs中添加预览按钮 preview_button: name: 内容预览 type: button action: preview
  3. 发送历史记录

    yaml
    # 添加发送日志记录 - name: 记录发送日志 script: | echo "$(date) - $message_type - $test_scenario" >> dingtalk_test.log

这套方案提供了企业级的安全消息测试能力,既保持了易用性,又提供了必要的控制和验证机制,同时符合钉钉机器人的最佳实践。

本文作者:任浪漫

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!