企业机器人是钉钉为用户提供的组织内部使用的机器人,为组织数字化转型业务服务。开发者可通过本文所描述步骤进行机器人的自主开发和上架,组织内其它成员可通过方便快捷地在群内添加企业机器人,并使用机器人的能力。
基于企业机器人的outgoing(回调)机制,用户@机器人之后,钉钉会将消息内容POST到开发者的消息接收地址。开发者解析出消息内容、发送者身份,根据企业的业务逻辑,组装响应的消息内容返回,钉钉会将响应内容发送到群里。
一、创建机器人
1.进入机器人创建页
进入 open-dev.dingtalk.com/#/corprobot 如果没有开发者账号可以注册一个。
2.点击按钮"创建机器人"
3.添加相关信息
4.创建完成后得到相关凭证
5.填写相关信息
消息接收地址可以通过内网穿透,写个代理地址进行方便本地调试
6.发布机器人
点击上线,则可以将机器人加入群聊
二、完成机器人回调代码
1.验证工具类
开发者需对header中的 timestamp和sign进行验证,以判断是否是来自钉钉的合法请求,避免其他仿冒钉钉调用开发者的HTTPS服务传送数据
import org.apache.tomcat.util.codec.binary.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* 回调sign工具类
* @author Carrot
* @since 2020/9/14 0:29
*/
public class CallbackSignUtils {
/**
* 对header中的 timestamp和sign进行验证,以判断是否是来自钉钉的合法请求人的appSecret
* @param sign 回调header的sign
* @param timestamp 回调header的timestamp
* @param appSecret 机器人的appSecret
* @return 是否通过验证
*/
public static boolean verifySign(String sign,Long timestamp,String appSecret){
String stringToSign = timestamp + "\n" + appSecret;
Mac mac = null;
try {
mac = Mac.getInstance("HmacSHA256");
} catch (NoSuchAlgorithmException e) {
return false;
}
String correctSign=null;
try {
mac.init(new SecretKeySpec(appSecret.getBytes("UTF-8"), "HmacSHA256"));
byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
correctSign = new String(Base64.encodeBase64(signData));
} catch (InvalidKeyException | UnsupportedEncodingException e) {
return false;
}
return correctSign.equals(sign);
}
}
2.构建钉钉回调中body模型
import java.util.List;
/**
* 当用户@机器人时,钉钉会通过机器人开发者的HTTPS服务地址,把消息内容发送出去
* 此为body
* -------------------------------------------------------------
* {
* "msgtype": "text",
* "text": {
* "content": "我就是我, 是不一样的烟火"
* },
* "msgId": "XXXX",
* "createAt": 1487561654123,
* "conversationType": "2",
* "conversationId": "XXXX",
* "conversationTitle": "钉钉群标题",
* "senderId": "XXXX",
* "senderNick": "星星",
* "senderCorpId": "XXXX",
* "senderStaffId": "XXXX",
* "chatbotUserId":"XXXX",
* "atUsers":[
* {
* "dingtalkId":"XXXX",
* "staffId":"XXXX"
* }
* ]
* }
* -------------------------------------------------------------
* 参考 https://ding-doc.dingtalk.com/doc#/serverapi2/elzz1p
* @author Carrot
* @since 2020/9/13 23:40
*/
public class PostRobotRequest {
/**
* 消息文本
*/
static class Text{
/**
* 消息文本
*/
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return "Text{" +
"content='" + content + '\'' +
'}';
}
}
/**
* 被@人的信息
*/
public static class AtUser{
/**
* 加密的发送者ID
*/
private String dingtalkId;
/**
* 发送者在企业内的userid(企业内部群有)
*/
private String staffId;
public String getDingtalkId() {
return dingtalkId;
}
public void setDingtalkId(String dingtalkId) {
this.dingtalkId = dingtalkId;
}
public String getStaffId() {
return staffId;
}
public void setStaffId(String staffId) {
this.staffId = staffId;
}
@Override
public String toString() {
return "AtUser{" +
"dingtalkId='" + dingtalkId + '\'' +
", staffId='" + staffId + '\'' +
'}';
}
}
/**
* 目前只支持text
*/
private String msgtype;
/**
* 消息文本
*/
private Text text;
/**
* 加密的消息ID
*/
private String msgId;
/**
* 消息的时间戳,单位ms
*/
private Long createAt;
/**
* 1-单聊、2-群聊
*/
private String conversationType;
/**
* 加密的会话ID
*/
private String conversationId;
/**
* 会话标题(群聊时才有)
*/
private String conversationTitle;
/**
* 加密的发送者ID
*/
private String senderId;
/**
* 发送者昵称
*/
private String senderNick;
/**
* 发送者当前群的企业corpId(企业内部群有)
*/
private String senderCorpId;
/**
* 发送者在企业内的userid(企业内部群有)
*/
private String senderStaffId;
/**
* 加密的机器人ID
*/
private String chatbotUserId;
/**
* 被@人的信息列表
*/
private List<AtUser> atUsers;
public String getMsgtype() {
return msgtype;
}
public void setMsgtype(String msgtype) {
this.msgtype = msgtype;
}
public Text getText() {
return text;
}
public void setText(Text text) {
this.text = text;
}
public String getMsgId() {
return msgId;
}
public void setMsgId(String msgId) {
this.msgId = msgId;
}
public Long getCreateAt() {
return createAt;
}
public void setCreateAt(Long createAt) {
this.createAt = createAt;
}
public String getConversationType() {
return conversationType;
}
public void setConversationType(String conversationType) {
this.conversationType = conversationType;
}
public String getConversationId() {
return conversationId;
}
public void setConversationId(String conversationId) {
this.conversationId = conversationId;
}
public String getConversationTitle() {
return conversationTitle;
}
public void setConversationTitle(String conversationTitle) {
this.conversationTitle = conversationTitle;
}
public String getSenderId() {
return senderId;
}
public void setSenderId(String senderId) {
this.senderId = senderId;
}
public String getSenderNick() {
return senderNick;
}
public void setSenderNick(String senderNick) {
this.senderNick = senderNick;
}
public String getSenderCorpId() {
return senderCorpId;
}
public void setSenderCorpId(String senderCorpId) {
this.senderCorpId = senderCorpId;
}
public String getSenderStaffId() {
return senderStaffId;
}
public void setSenderStaffId(String senderStaffId) {
this.senderStaffId = senderStaffId;
}
public String getChatbotUserId() {
return chatbotUserId;
}
public void setChatbotUserId(String chatbotUserId) {
this.chatbotUserId = chatbotUserId;
}
public List<AtUser> getAtUsers() {
return atUsers;
}
public void setAtUsers(List<AtUser> atUsers) {
this.atUsers = atUsers;
}
@Override
public String toString() {
return "PostRobotResponse{" +
"msgtype='" + msgtype + '\'' +
", text=" + text.toString() +
", msgId='" + msgId + '\'' +
", createAt=" + createAt +
", conversationType='" + conversationType + '\'' +
", conversationId='" + conversationId + '\'' +
", conversationTitle='" + conversationTitle + '\'' +
", senderId='" + senderId + '\'' +
", senderNick='" + senderNick + '\'' +
", senderCorpId='" + senderCorpId + '\'' +
", senderStaffId='" + senderStaffId + '\'' +
", chatbotUserId='" + chatbotUserId + '\'' +
", atUsers=" + (atUsers==null?"null":atUsers.toString()) +
'}';
}
}
3.构建4种文本类型
目前支持4中文本类型可以参考ding-doc.dingtalk.com/doc#/server… 目前先展示text类型和Markdown这两种经常使用的类型。
1)通用接口
/**
* 通用接口,需要实现提供自己文本类别的方法
* @author Carrot
* @since 2020/9/14 2:06
*/
public interface Content {
/**
* 获得内容类别
*
* @return
*/
String getContentName();
}
2)text类型
import com.woshale.dingtalkrobottest.model.robot.content.interfaces.Content;
/**
* text类型
* @author Carrot
* @since 2020/9/14 2:28
*/
public class Text implements Content {
public static final String CONTENT_NAME="text";
/**
* 消息文本
*/
private String content;
@Override
public String getContentName() {
return CONTENT_NAME;
}
private Text() {
}
public Text(String content) {
this.content = content;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
3)Markdown类型
import com.woshale.dingtalkrobottest.model.robot.content.interfaces.Content;
/**
* Markdown格式
* @author Carrot
* @since 2020/9/14 0:52
*/
public class Markdown implements Content {
public static final String CONTENT_NAME="markdown";
private String title;
private String text;
private Markdown() {
}
public Markdown(String title, String text) {
this.title = title;
this.text = text;
}
@Override
public String getContentName() {
return CONTENT_NAME;
}
public String getTitle() {
return title;
}
public String getText() {
return text;
}
}
4.构建对回调响应的model
import com.fasterxml.jackson.annotation.JsonInclude;
import com.woshale.dingtalkrobottest.model.robot.content.Markdown;
import com.woshale.dingtalkrobottest.model.robot.content.Text;
import com.woshale.dingtalkrobottest.model.robot.content.interfaces.Content;
import java.util.List;
/**
* 机器人进行回应model
* 数据格式
* -------------------------------------------------------------
* {
* "msgtype": "text",
* "text": {
* "content": "我就是我, @150XXXXXXXX 是不一样的烟火"
* },
* "at": {
* "atMobiles": [
* "150XXXXXXXX"
* ],
* "isAtAll": false
* }
* }
* -------------------------------------------------------------
* @author Carrot
* @since 2020/9/14 0:44
*/
public class PostRobotResponse {
/**
* at用户目标
*/
public static class AtTarget{
/**
* at用户的电话列表
* 必须在这里声明被@用户的号码,才能在文本中通过“@156xxxxxxxx(手机号)”这样的方式去@他人
* 一般来讲,最终的文本会自动将“@156xxxxxxxx”替换成“@这个人在这个群内的昵称”
*/
private List<String> atMobiles;
/**
* 是否at所有人
*/
private boolean isAtAll;
public AtTarget() {
}
public AtTarget(List<String> atMobiles) {
this.atMobiles = atMobiles;
this.isAtAll = false;
}
public AtTarget(List<String> atMobiles, boolean isAtAll) {
this.atMobiles = atMobiles;
this.isAtAll = isAtAll;
}
public List<String> getAtMobiles() {
return atMobiles;
}
public boolean isAtAll() {
return isAtAll;
}
public void setAtMobiles(List<String> atMobiles) {
this.atMobiles = atMobiles;
}
public void setAtAll(boolean atAll) {
isAtAll = atAll;
}
}
/**
* 文本类型名称
*/
private String msgtype;
/**
* 文本类型-text
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private Content text;
/**
* 文本类型-Markdown
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private Content markdown;
/**
* 文本类型-actionCard
* todo 构造方法
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private Content actionCard;
/**
* 文本类型-feedCardext
* todo 构造方法
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private Content feedCard;
/**
* at目标
*/
private AtTarget at;
private PostRobotResponse() {
}
/**
* 构造Markdown类型
* @param content
* @param at
*/
public PostRobotResponse(Markdown content, AtTarget at) {
this.markdown=content;
this.initTypeAndAtTarget(content,at);
}
/**
* 构造text类型
* @param content
* @param at
*/
public PostRobotResponse(Text content, AtTarget at) {
this.markdown=content;
this.initTypeAndAtTarget(content,at);
}
/**
* 初始化一些构造信息
* @param content
* @param at
*/
private void initTypeAndAtTarget(Content content,AtTarget at){
this.msgtype=content.getContentName();
this.at = at;
}
public String getMsgtype() {
return msgtype;
}
public Content getText() {
return text;
}
public Content getMarkdown() {
return markdown;
}
public Content getActionCard() {
return actionCard;
}
public Content getFeedCard() {
return feedCard;
}
public AtTarget getAt() {
return at;
}
}
5.写一个回调demo接口
import com.woshale.dingtalkrobottest.model.robot.PostRobotRequest;
import com.woshale.dingtalkrobottest.model.robot.PostRobotResponse;
import com.woshale.dingtalkrobottest.model.robot.content.Markdown;
import com.woshale.dingtalkrobottest.utils.CallbackSignUtils;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
/**
* 钉钉机器人回调
* 注意:
* 开发者需对header中的 timestamp和sign进行验证,以判断是否是来自钉钉的合法请求,避免其他仿冒钉钉调用开发者的HTTPS服务传送数据,具体验证逻辑如下:
* 1. timestamp 与系统当前时间戳如果相差1小时以上,则认为是非法的请求。
*
* 2. sign 与开发者自己计算的结果不一致,则认为是非法的请求。
*
* 必须当timestamp和sign同时验证通过,才能认为是来自钉钉的合法请求。
* sign的计算方法:
*
* header中的timestamp + "\n" + 机器人的appSecret 当做签名字符串,使用HmacSHA256算法计算签名,然后进行Base64 encode,得到最终的签名值。
* @author Carrot
* @since 2020/9/13 23:15
*/
@RestController
@RequestMapping("/api/v1/callback")
public class CallbackController {
@PostMapping("/robot")
public PostRobotResponse robotCallback(HttpServletRequest req, @RequestBody PostRobotRequest body){
System.out.println(body.toString());
String reqSign=req.getHeader("sign");
Long timestamp=Long.valueOf(req.getHeader("timestamp"));
//todo 这里填写你的机器人的AppSecret
if (!CallbackSignUtils.verifySign(reqSign,timestamp,"需要填写的appSecret")){
System.out.println("sign验证错误");
//todo 验证错误情况下业务处理
}
//验证成功后,进行响应 这里用markdown举例
//获得了用户给机器人说的话
String contentHasSent=body.getText().getContent();
//todo 根据用户发的内容 构建title和text
String title="根据业务得到的标题";
String text="根据业务得到的文本";
Markdown markdown=new Markdown(title,text);
PostRobotResponse.AtTarget atTarget=new PostRobotResponse.AtTarget();
List<String> atMobiles=new ArrayList<>();
//todo 设置要@的人,可不填写
atMobiles.add("15xxxxxxxx要@人的电话号码");
atTarget.setAtMobiles(atMobiles);
return new PostRobotResponse(markdown,atTarget);
}
}
6.通过内网穿透或者部署到公网进行测试
1)填写服务地址以及服务所在白名单ip
2)测试机器人
可以正常使用了
三、后话
1.demo地址
2.机器人@人的改变
先前企业内部机器人是可以直接去@和机器人互动的人的,但是客服说实现变了,at参数不再能够填入发送者的加密userId了。导致我先前弄得机器人不能直接@人了,而且回调给的userId是加密的,我也不好通过服务器api去拿手机号,很麻烦,目前不知道怎么实现这种@互动人的方式了。
3.注意的地方
@人的话不仅要在at中声明电话号码,还要在文本中@电话号码,和以前直接在文本结尾自动@人不一样。