钉钉群自定义机器人接入
场景介绍
企业内部有较多系统支撑着公司的核心业务流程,譬如CRM系统、交易系统、监控报警系统等。通过钉钉的自定义机器人,可以将这些系统事件同步到钉钉的聊天群。
说明
当前机器人尚不支持应答机制,该机制指的是群里成员在聊天@机器人的时候,钉钉回调指定的服务地址,即Outgoing机器人。
调用频率限制
由于消息发送太频繁会严重影响群的使用体验,因此自定义机器人发送消息的频率限制如下:
- 每个机器人每分钟最多发送20条消息到群里,如果超过20条,会限流10分钟。
注意
如果你有大量发消息的场景(譬如系统监控报警)可以将这些信息进行整合,通过markdown消息以摘要的形式发送到群里。
具体步骤
步骤一:获取自定义机器人Webhook
-
选择需要添加机器人的群聊,然后依次单击群设置 > 智能群助手。
-
在机器人管理页面选择自定义机器人,输入机器人名字并选择要发送消息的群,同时可以为机器人设置机器人头像。
-
完成必要的安全设置,勾选我已阅读并同意《自定义机器人服务及免责条款》,然后单击完成。
以自定义关键词为例,最多可以设置10个关键词,消息中至少包含其中1个关键词才可以发送成功。
例如添加了一个自定义关键词:监控报警,则这个机器人所发送的消息,必须包含监控报警这个词,才能发送成功。
- 完成安全设置后,复制出机器人的Webhook地址,可用于向这个群发送消息,格式如下:
https://oapi.dingtalk.com/robot/send?access_token=XXXXXX
注意
请保管好此Webhook 地址,不要公布在外部网站上,泄露后有安全风险。
步骤二:使用自定义机器人
获取到Webhook地址后,用户可以向该地址发起HTTP POST 请求,即可实现给该钉钉群发送消息。
注意
- 已默认开通使用自定义机器人发消息的权限,无需申请。即向Webhook地址发请求时,无需申请权限。
- 发起POST请求时,必须将字符集编码设置成UTF-8。
- 每个机器人每分钟最多发送20条。消息发送太频繁会严重影响群成员的使用体验,大量发消息的场景 (譬如系统监控报警) 可以将这些信息进行整合,通过markdown消息以摘要的形式发送到群里。
当前自定义机器人支持以下消息类型,请根据自己的使用场景选择合适的类型,详情参见消息类型及数据格式。
- 文本 (text)
- 链接 (link)
- markdown(markdown)
- ActionCard
- FeedCard
自定义机器人发送消息时,可以通过手机号码指定“被@人列表”。在“被@人列表”里面的人员收到该消息时,会有@消息提醒。免打扰会话仍然通知提醒,首屏出现“有人@你”。
步骤三:测试自定义机器人
通过以下方法,可以快速验证自定义机器人是否可以正常工作:
- 使用命令行工具curl。
说明
为避免出错,将以下命令逐行复制到命令行,需要将xxxxxxxx替换为真实access_token;若测试出错,请检查复制的命令是否和测试命令一致,多特殊字符会报错。
curl 'https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxx' \
-H 'Content-Type: application/json' \
-d '{"msgtype": "text","text": {"content":"我就是我, 是不一样的烟火"}}'
- SDK请求示例(Java)
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/robot/send?access_token=566cc69da782ec******");
OapiRobotSendRequest request = new OapiRobotSendRequest();
request.setMsgtype("text");
OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text();
text.setContent("测试文本消息");
request.setText(text);
OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
at.setAtMobiles(Arrays.asList("132xxxxxxxx"));
// isAtAll类型如果不为Boolean,请升级至最新SDK
at.setIsAtAll(true);
at.setAtUserIds(Arrays.asList("109929","32099"));
request.setAt(at);
request.setMsgtype("link");
OapiRobotSendRequest.Link link = new OapiRobotSendRequest.Link();
link.setMessageUrl("https://www.dingtalk.com/");
link.setPicUrl("");
link.setTitle("时代的火车向前开");
link.setText("这个即将发布的新版本,创始人xx称它为红树林。而在此之前,每当面临重大升级,产品经理们都会取一个应景的代号,这一次,为什么是红树林");
request.setLink(link);
request.setMsgtype("markdown");
OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
markdown.setTitle("杭州天气");
markdown.setText("#### 杭州天气 @156xxxx8827\n" +
"> 9度,西北风1级,空气良89,相对温度73%\n\n" +
"> ![screenshot](https://gw.alicdn.com/tfs/TB1ut3xxbsrBKNjSZFpXXcXhFXa-846-786.png)\n" +
"> ###### 10点20分发布 [天气](http://www.thinkpage.cn/) \n");
request.setMarkdown(markdown);
OapiRobotSendResponse response = client.execute(request);
消息类型及数据格式
- text类型
{
"at": {
"atMobiles":[
"180xxxxxx"
],
"atUserIds":[
"user123"
],
"isAtAll": false
},
"text": {
"content":"我就是我, @XXX 是不一样的烟火"
},
"msgtype":"text"
}
- link类型
{
"msgtype": "link",
"link": {
"text": "这个即将发布的新版本,创始人xx称它为红树林。而在此之前,每当面临重大升级,产品经理们都会取一个应景的代号,这一次,为什么是红树林",
"title": "时代的火车向前开",
"picUrl": "",
"messageUrl": "https://www.dingtalk.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI"
}
}
- markdown类型
{
"msgtype": "markdown",
"markdown": {
"title":"杭州天气",
"text": "#### 杭州天气 @150XXXXXXXX \n > 9度,西北风1级,空气良89,相对温度73%\n > ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png)\n > ###### 10点20分发布 [天气](https://www.dingtalk.com) \n"
},
"at": {
"atMobiles": [
"150XXXXXXXX"
],
"atUserIds": [
"user123"
],
"isAtAll": false
}
}
目前只支持markdown语法的子集,具体支持的元素如下:
标题
# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题
引用
> A man who stands for nothing will fall for anything.
文字加粗、斜体
**bold**
*italic*
链接
[this is a link](http://name.com)
图片(建议不要超过20张)
![](http://name.com/pic.jpg)
无序列表
- item1
- item2
有序列表
1. item1
2. item2
- 整体跳转ActionCard类型
{
"actionCard": {
"title": "乔布斯 20 年前想打造一间苹果咖啡厅,而它正是 Apple Store 的前身",
"text": "![screenshot](https://gw.alicdn.com/tfs/TB1ut3xxbsrBKNjSZFpXXcXhFXa-846-786.png)
### 乔布斯 20 年前想打造的苹果咖啡厅
Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划",
"btnOrientation": "0",
"singleTitle" : "阅读全文",
"singleURL" : "https://www.dingtalk.com/"
},
"msgtype": "actionCard"
}
通过整体跳转ActionCard类型消息发出的消息样式如下:
- 独立跳转ActionCard类型
{
"msgtype": "actionCard",
"actionCard": {
"title": "我 20 年前想打造一间苹果咖啡厅,而它正是 Apple Store 的前身",
"text": "![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png) \n\n #### 乔布斯 20 年前想打造的苹果咖啡厅 \n\n Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划",
"btnOrientation": "0",
"btns": [
{
"title": "内容不错",
"actionURL": "https://www.dingtalk.com/"
},
{
"title": "不感兴趣",
"actionURL": "https://www.dingtalk.com/"
}
]
}
}
通过独立跳转ActionCard类型消息发出的消息样式如下:
- FeedCard类型
{
"msgtype":"feedCard",
"feedCard": {
"links": [
{
"title": "时代的火车向前开1",
"messageURL": "https://www.dingtalk.com/",
"picURL": "https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png"
},
{
"title": "时代的火车向前开2",
"messageURL": "https://www.dingtalk.com/",
"picURL": "https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png"
}
]
}
}
通过FeedCard类型消息发出的消息样式如下:
错误码
SDK请求示例错误码
安全设置错误码
当出现以下错误时,表示消息校验未通过,请查看机器人的安全设置。
自定义请求代码
点击查看DingTalkAlertService
package com.demo.alert.service;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Maps;
import com.demo.alert.entity.AlertMsgLog;
import com.demo.alert.repository.AlertMsgLogRepository;
import com.demo.alert.valobj.AlertWay;
import com.demo.configuration.domain.configration.entity.Configuration;
import com.demo.configuration.domain.configration.enums.AlertConfigEnum;
import com.demo.configuration.domain.configration.service.ConfigurationService;
import com.demo.notice.application.exception.NoticeBusinessException;
import com.demo.notice.domain.noticetemplate.entity.NoticeTemplate;
import com.demo.notice.domain.noticetemplate.entity.NoticeTemplateType;
import com.demo.notice.domain.noticetemplate.entity.SendStatus;
import com.demo.notice.domain.noticetemplate.repository.NoticeTemplateRepository;
import org.apache.commons.lang3.text.StrSubstitutor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: 钉钉群机器人推送告警服务类
* @Author:
* @Date: 2022/3/21 11:16
*/
@Service
public class DingTalkAlertService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private NoticeTemplateRepository noticeTemplateRepository;
@Autowired
private ConfigurationService configurationService;
@Autowired
private AlertMsgLogRepository alertMsgLogRepository;
/**
* 向目标群钉钉机器人推送告警信息(不含安全码)<br/>
* <i>注:通过总服务AlertService扩展、调用,否则无法设置安全key及告警序列号</i>
* @param alertMsg 告警消息
* @param isAtAll 是否艾特全员
* @param serialNo 告警序列号
*/
protected void sendAlertToAll(String alertMsg, Boolean isAtAll, String serialNo) {
// 组装告警消息与模板
alertMsg = this.assembleAlertMsg(alertMsg);
// 构造报文头
HttpHeaders headers = this.constructHttpHeaders();
// 组装钉钉请求报文体
String requestBody = this.assembleDingTalkRequestBody(alertMsg, isAtAll);
// 拼接报文头报文体
HttpEntity<String> formEntity = new HttpEntity<String>(requestBody, headers);
// 推送消息
this.executeSendAlert(formEntity, serialNo);
}
/**
* 向目标群钉钉机器人推送告警信息(含安全码)<br/>
* <i>注:通过总服务AlertService扩展、调用,否则无法设置安全key及告警序列号</i>
* @param alertMsg 告警消息
* @param safeCode 安全码
* @param isAtAll 是否艾特全员
* @param serialNo 告警序列号
*/
protected void sendAlertToAllWithSafeCode(String alertMsg, String safeCode, Boolean isAtAll, String serialNo) {
// 组装告警消息与模板
alertMsg = this.assembleAlertMsgWithSafeCode(alertMsg, safeCode);
// 构造报文头
HttpHeaders headers = this.constructHttpHeaders();
// 组装钉钉请求报文体
String requestBody = this.assembleDingTalkRequestBody(alertMsg, isAtAll);
// 拼接报文头与报文体内容
HttpEntity<String> formEntity = new HttpEntity<String>(requestBody, headers);
// 推送消息
this.executeSendAlert(formEntity, serialNo);
}
/**
* 组装告警消息与模板(不含安全码)
* @param alertMsg 告警消息
* @return
*/
private String assembleAlertMsg(String alertMsg) {
NoticeTemplate noticeTemplate = noticeTemplateRepository.findByNoticeTemplateType(NoticeTemplateType.PLATFORM_ALERT_MSG);
if (noticeTemplate == null) {
throw new NoticeBusinessException(NoticeBusinessException.CodeOption.NOTICE_TEMPLATE_NOT_FOUND);
}
// 告警信息
final HashMap<String, String> argMaps = Maps.newHashMap();
argMaps.put("msg", alertMsg);
StrSubstitutor strSubstitutor = new StrSubstitutor(argMaps);
return strSubstitutor.replace(noticeTemplate.getContent());
}
/**
* 组装告警消息与模板(含安全码)<br/>
* <i>注:通过总服务AlertService扩展、调用,否则无法设置安全key</i>
* @param alertMsg 告警消息
* @param safeCode 安全码
* @return
*/
private String assembleAlertMsgWithSafeCode(String alertMsg, String safeCode) {
NoticeTemplate noticeTemplate = noticeTemplateRepository.findByNoticeTemplateType(NoticeTemplateType.PLATFORM_ALERT_MSG_WITH_SAFE_CODE);
if (noticeTemplate == null) {
throw new NoticeBusinessException(NoticeBusinessException.CodeOption.NOTICE_TEMPLATE_NOT_FOUND);
}
// 告警信息
final HashMap<String, String> argMaps = Maps.newHashMap();
argMaps.put("msg", alertMsg);
argMaps.put("code", safeCode);
StrSubstitutor strSubstitutor = new StrSubstitutor(argMaps);
return strSubstitutor.replace(noticeTemplate.getContent());
}
/**
* 统一构造Http报文头
* @return
*/
private HttpHeaders constructHttpHeaders() {
// 报文头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
return headers;
}
/**
* 组装钉钉请求报文体<br/>
* <i>{"msgtype": "text", "text":{"content": "消息内容"}, "at": {"atMobiles": ["18100000000", "18100000001"], "isAtAll": true}}</i>
* @param alertMsg 告警消息
* @param isAtAll 是否艾特全员
* @return
*/
private String assembleDingTalkRequestBody(String alertMsg, Boolean isAtAll) {
// 组装钉钉请求报文体,{"msgtype": "text", "text":{"content": "消息内容"}, "at": {"atMobiles": ["18100000000", "18100000001"], "isAtAll": true}}
Map<String, Object> requestBody = new HashMap<>();
Map<String, Object> content = new HashMap<>();
content.put("content", alertMsg);
Map<String, Object> at = new HashMap<>();
at.put("isAtAll", isAtAll);
requestBody.put("msgtype", "text");
requestBody.put("text", content);
requestBody.put("at", at);
return JSONObject.toJSONString(requestBody);
}
/**
* 执行发送告警消息
* @param formEntity Post请求实体
* @param serialNo 告警序列号
*/
private void executeSendAlert(HttpEntity<String> formEntity, String serialNo) {
// 告警日志
AlertMsgLog alertMsgLog = null;
// 消息推送的目标机器人url
String url = null;
// 初始化发送状态为失败
SendStatus sendStatus = SendStatus.FAIL;
// 错误信息
String errorMsg = null;
try {
// 获取对应环境的钉钉消息推送的目标机器人url
Configuration config = configurationService.getConfig(AlertConfigEnum.DINGTALK_ALERT_ROBOT_URL.getKey());
url = config.getConfValue();
// 发起POST请求
ResponseEntity<Map> response = restTemplate.postForEntity(url, formEntity, Map.class);
// 钉钉请求返回业务代码&错误信息,成功推送code为0,msg为ok
Integer dingTalkSuccessCode = 0;
Integer errCode = (Integer) response.getBody().get("errcode");
String errMsg = (String) response.getBody().get("errmsg");
// 完整响应体信息
String resBodyStr = JSONObject.toJSONString(response.getBody());
// HTTP请求返回码成功,且钉钉业务代码为0,即为推送成功
HttpStatus httpStatus = response.getStatusCode();
if (HttpStatus.OK.equals(httpStatus) && dingTalkSuccessCode.equals(errCode)) {
sendStatus = SendStatus.SUCCESS;
} else {
errorMsg = "HTTP响应码:".concat(String.valueOf(httpStatus.value())).concat(",响应信息:").concat(resBodyStr);
}
alertMsgLog = new AlertMsgLog(url, formEntity.toString(), AlertWay.DING_TALK, sendStatus, errorMsg, serialNo);
} catch (Exception e) {
// 生成告警记录,批量发送时不抛出异常,避免中断其他人员的发送
errorMsg = "HTTP请求异常:".concat(e.getMessage());
alertMsgLog = new AlertMsgLog(url, formEntity.toString(), AlertWay.DING_TALK, sendStatus, errorMsg, serialNo);
} finally {
alertMsgLogRepository.save(alertMsgLog);
}
}
}
点击查看AlertMsgLog
package com.demo.alert.entity;
import com.demo.alert.valobj.AlertWay;
import com.demo.common.model.BaseEntity;
import com.demo.notice.domain.noticetemplate.entity.SendStatus;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Type;
import javax.persistence.*;
/**
* @Description:
* @Author:
* @Date: 2022/3/18 16:07
*/
@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "alert_msg_log")
public class AlertMsgLog extends BaseEntity {
/**
* 主键ID
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 接收者手机/邮箱,钉钉群为机器人Url
*/
private String receiver;
/**
* 告警内容
*/
@Lob
@Type(type = "org.hibernate.type.TextType")
private String alertMsg;
/**
* 告警方式
*/
@Enumerated(EnumType.STRING)
private AlertWay alertWay;
/**
* 告警信息发送状态
*/
@Enumerated(EnumType.STRING)
private SendStatus sendStatus;
/**
* 发送失败时的异常信息
*/
@Lob
@Type(type = "org.hibernate.type.TextType")
private String errorMsg;
/**
* 告警序列号(同一批次告警序列号相同)
*/
private String serialNo;
/**
* 告警日志
* @param receiver 接收者
* @param alertMsg 告警信息
* @param sendStatus 发送状态
* @param alertWay 告警方式
* @param errorMsg 错误信息
*/
public AlertMsgLog(String receiver, String alertMsg, AlertWay alertWay, SendStatus sendStatus, String errorMsg, String serialNo) {
this.receiver = receiver;
this.alertMsg = alertMsg;
this.alertWay = alertWay;
this.sendStatus = sendStatus;
this.errorMsg = errorMsg;
this.serialNo = serialNo;
}
}