钉钉开发机器人单聊业务实现
背景
特定业务场景下的对具体业务实现方式的取舍
公司OA系统升级, 需要将OA系统内的消息和系统外的消息联系. 为此产品提出需求. 当用户在OA系统收到消息时,可同时在钉钉收到待办消息提醒,提醒用户前往OA系统查看消息,处理相关事宜
. 因此着手做了钉钉待办. 但是钉钉待办实现后效果并不理想.因此采用了钉钉卡片进行通知. 而采用卡片通知又分群聊和单聊场景. 并且卡片通知整个流程也挺曲折, 下面我将 把我走过的坑重新梳理下, 让更多需要的人更快搭建此场景.
钉钉待办
待办是钉钉的一个协同办公产品,帮助企业员工更高效的进行事项(工作任务)管理。钉钉待办提供了强大的开放能力,各类业务系统或企业自建应用可低成本的接入。因为主要介绍的是钉钉机器人单聊发送卡片业务实现, 所以在这里简单提一下.
经过自己自测发现, 钉钉现在做逻辑是, 如果在待办中有配置pc端和app的跳转url. 那么给你创建的就是工作待办.
这种待办提醒比较醒目(会有红点, 以及待办数提示)
但是如果在代码中没有加上双端应用跳转地址, 则不会创建工作类型的待办, 而是创建个人类型的待办.
个人类型的待办不会再钉钉待办框提示, 并且只有手动点进去才能进行提醒,
这样的话体验效果就较差, 谁能每次会主动进入待办中去查看个人待办的东西呢?
你会觉得钉钉那边肯定有考虑了, 一定有参数可以设置待办类型的.
但目前的情况是, 就其提供的现有参数是无法创建没有跳转的工作待办的.
如果你可以实现, 也欢迎大家尝试来告诉下我, 让我也来学习下.
因为以上的原因, 我们觉得采用钉钉卡片, 以微应用或机器人的形式进行单聊发送.
进而实现将OA系统消息推送到钉钉的这样一个需求
钉钉卡片
钉钉互动卡片是一种即时交互、多人协同、数据驱动的轻量卡片,它能够将原本复杂的应用解构成一个个轻量级的卡片在钉钉的各个场域(场域的解释请参考下文名词解释部分)上运行。用户可以在卡片上完成互动协同,提高用户的沟通效率,同时帮助业务更好地触达用户。钉钉的互动卡片官方介绍地址,
习惯啃官方文档的可以看这个. 里面的内容也就是我今天想讲的, 现在我把我对这块的理解以及我踩过的坑来给大家分享一下.
在我花费漫长的时间阅读官方文档以及调试中发现, 钉钉互动卡片的发送不止一种, 而是达到了惊人的三种!
分别是发送卡片类型的工作通知, 机器人发送互动卡片(普通版), 以及直接创建并投放卡片.
下面我来简单介绍下三种创建方式的区别.
卡片类型的工作通知
此种方式创建的卡片, 格式固定, 自由度较低, 无法自己编辑通知卡片格式
创建并投放卡片
-
创建并投放卡片
卡片的创建十分复杂. 主要步骤分为:- 开启卡片权限和机器人权限(单聊)
- 创建卡片模板(编辑模板, 绑定参数)
- 为卡片模板新增场域并创建卡片实例
- 配置场域并投放测试
- web接口页面测试
- 生成相关代码
- 封装api并使用
并且我在成功之后, 进行web接口页面测试接口发下, 不仅需要n多个参数, 而且参数的解释也不全,
花了很长时间请求之后, 接口请求成功, 但是没有显示投放结果并且钉钉上未收到卡片信息.
而且, 就接口请求相关问题去问钉钉客服, 只能通过工单,
而我提的工单, 截止到发文已经两个工作日都没有回复了…
机器人发送互动卡片
- 机器人发送互动卡片(普通版)
因为之前主要就是研究创建并投放卡片的接口, 去立即并测试相关参数. 机
器人单聊和场域相关的参数好不容易补充好, 结果最终还是请求失败.
但是在阅读官方文档的时, 无意中发现, 在机器人这一章节, 有发送卡片的功能以及接口.
因此就在这里测试了. 结果没花几分钟就搞好了.
此种方式创建卡片通知, 参数简单, 调用方便, 并且支持调用卡片模板和变量传入
所以, 最终还是敲定使用此方案进行业务功能实现
实现
基于机器人发送互动卡片的实现以及搭建过程和踩坑介绍
过程搭建
整个流程可以分为:
- 授权
- 创建机器人
- 创建卡片
- 卡片投放测试
- web端调用机器人发送卡片接口
- 生成待办并封装接口
- 调用测试
下面, 我们来按照上面流程来介绍实现过程
1. 授权
这里授权主要是为了方便之后接口调用时, 没有权限导致的无法调用的问题
在钉钉开放平台-> 应用管理 -> 权限管理中, 搜索机器人, 卡片. 寻找对应的权限并开启
2. 创建机器人
配置用于后续进行发送卡片的单聊机器人的相关信息. 这里主要是获取到RobotCode, 用于后续接口测试时使用
在钉钉开放平台-> 应用管理->消息和推送中创建机器人
3. 创建卡片
创建卡片的目的主要就是开发人员根据需求去创建符合自己业务环境的卡片. 通过变量来绑定自己业务数据, 通过按钮来绑定自己跳转链接 卡片支持的交互组件
卡片的话, 可以参照钉钉卡片这一模块的案例介绍. 慢慢摸索总能够做好.
这里主要提两点: 变量创建和链接问题.
变量创建
在卡片左侧数据源这里, 编辑后面需要的变量, 在卡片设计中, 通过el表达式进行填充. 主要格式就是 ${变量名}
. 我们可以填写好Mock数据来进行模板的渲染效果的展示
链接问题
互动卡片支持链接的跳转。默认情况下,如果给卡片配置的是一个普通的 HTTP 协议的链接,如 https://dingtalk.com ,那么它会以新窗口的方式打开(在 PC 端以系统默认浏览器打开,移动端以普通 H5 页面打开)。
但钉钉除了常规的新窗口打开之外,还支持其他多种打开方式,如侧边栏打开、半浮层打开等。通过合理使用钉钉的统一跳转协议,即可实现这些跳转效果。但钉钉除了常规的新窗口打开之外,还支持其他多种打开方式,如侧边栏打开、半浮层打开等。通过合理使用钉钉的统一跳转协议,即可实现这些跳转效果。
这里主要介绍一下弹窗方式打开链接
格式:
dingtalk://dingtalkclient/page/link?popup_wnd=true&url=${url}&title=${title}&width=${width}&height=${height}
参数介绍:
根据上面要求, 我将某个连接以窗口形式加载, 并且窗口名为test, 链接应构造成
dingtalk://dingtalkclient/page/link?popup_wnd=true&url=https://open.dingtalk.com/document/orgapp/link-jump-specification&title=%E9%92%89%E9%92%89%E9%93%BE%E6%8E%A5%E8%A7%84%E8%8C%83&width=1000&height=800
不过经过测试, 目前弹窗的标题和弹窗的宽高配置之后都未生效, 已经向钉钉提交了相关问题, 希望能够早日处理吧
下面再说一下双端跳转, 即在手机端和pc端分别跳转不同地址
双端跳转
这部分是对上一部分的拓展,
如果想要实现在 PC 端以某种方式打开链接,在移动端又是通过另外一种方式进行打开,那么可以使用以下这个跳转协议:dingtalk://dingtalkclient/action/open_platform_link?pcLink=${pcLink}&mobileLink=${mobileLink}
-
URI encode工具网站:
https://www.matools.com/app/url
-
结合上述pcLink和mobileLink的示例值,最终的链接如下:
dingtalk://dingtalkclient/action/open_platform_link?pcLink=dingtalk%3A%2F%2Fdingtalkclient%2Fpage%2Flink%3Furl%3Dhttps%253A%252F%252Fwww.dingtalk.com%26pc_slide%3Dtrue&mobileLink=dingtalk%3A%2F%2Fdingtalkclient%2Faction%2Fim_open_hybrid_panel%3FpanelHeight%3Dpercent83%26hybridType%3Donline%26pageUrl%3Dhttps%253A%252F%252Fwww.dingtalk.com
-
测试链接是否配置成功
将链接放入钉钉聊天框, 然后分别用手机和移动端点击, 查看是否跳转到不同地址
-
测试无误后, 配置到上一步卡片按钮组件的链接值上
4. 卡片投放测试
卡片编辑完成并绑定变量, 参数之后, 可以进行发布. 发布后的卡片可以创建卡片实例.用于对具体场景的投放.
投放后, 我们便可以在钉钉中看到该消息
-
发布卡片
-
在卡片实例管理中创建卡片实例
后面创建好卡片实例之后, 就会生成 outTrackId , 而 outTrackId在后面接口调试中会用到
-
实例创建是, 类型一般选择静态数据即可.
此静态数据指的是, 卡片在发送之后, 只会拉取一次数据.
而动态数据指的是, 卡片在发送之后, 可以按照指定周期去实时动态拉取消息.
-
投放卡片实例
目前投放只能选择单聊场域, 选择投放之后便可进行投放
投放成功之后, 钉钉内便可以收到此通知, 此步成功说明卡片创建成功
-
返回模板列表, 这里的模板ID后面接口调试会用到
5. web端调用机器人发送卡片接口
进入接口调试页面, 输入指定参数后发起调用, 执行成功之后点击示例代码即可获取消息调用的api
下面将分享机器人发送单聊互动卡片的代码
在开发环境运行下面代码需要下载jar包, 在下一章我将进行分享
// java代码
package com.aliyun.sample;
import com.aliyun.tea.*;
public class Sample {
/**
* 使用 Token 初始化账号Client
* @return Client
* @throws Exception
*/
public static com.aliyun.dingtalkim_1_0.Client createClient() throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
config.protocol = "https";
config.regionId = "central";
return new com.aliyun.dingtalkim_1_0.Client(config);
}
public static void main(String[] args_) throws Exception {
java.util.List<String> args = java.util.Arrays.asList(args_);
com.aliyun.dingtalkim_1_0.Client client = Sample.createClient();
com.aliyun.dingtalkim_1_0.models.SendRobotInteractiveCardHeaders sendRobotInteractiveCardHeaders = new com.aliyun.dingtalkim_1_0.models.SendRobotInteractiveCardHeaders();
sendRobotInteractiveCardHeaders.xAcsDingtalkAccessToken = "<your access token>";
com.aliyun.dingtalkim_1_0.models.SendRobotInteractiveCardRequest sendRobotInteractiveCardRequest = new com.aliyun.dingtalkim_1_0.models.SendRobotInteractiveCardRequest()
.setSingleChatReceiver("{\"userId\":\"用户userId\"}")
.setCardData("卡片模板变量填充的串")
.setCardBizId("卡片唯一id")
.setCardTemplateId("卡片模板id")
.setRobotCode("机器人码RobotCode");
try {
client.sendRobotInteractiveCardWithOptions(sendRobotInteractiveCardRequest, sendRobotInteractiveCardHeaders, new com.aliyun.teautil.models.RuntimeOptions());
} catch (TeaException err) {
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
} catch (Exception _err) {
TeaException err = new TeaException(_err.getMessage(), _err);
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
}
}
}
# python代码
import sys
from typing import List
from alibabacloud_dingtalk.im_1_0.client import Client as dingtalkim_1_0Client
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_dingtalk.im_1_0 import models as dingtalkim__1__0_models
from alibabacloud_tea_util import models as util_models
from alibabacloud_tea_util.client import Client as UtilClient
class Sample:
def __init__(self):
pass
@staticmethod
def create_client() -> dingtalkim_1_0Client:
"""
使用 Token 初始化账号Client
@return: Client
@throws Exception
"""
config = open_api_models.Config()
config.protocol = 'https'
config.region_id = 'central'
return dingtalkim_1_0Client(config)
@staticmethod
def main(
args: List[str],
) -> None:
client = Sample.create_client()
send_robot_interactive_card_headers = dingtalkim__1__0_models.SendRobotInteractiveCardHeaders()
send_robot_interactive_card_headers.x_acs_dingtalk_access_token = '<your access token>'
send_robot_interactive_card_request = dingtalkim__1__0_models.SendRobotInteractiveCardRequest(
single_chat_receiver='{"userId":"userId"}',
card_data='卡片模板变量填充的json串',
card_biz_id='chy123123',
card_template_id='卡片唯一id',
robot_code='机器人码RobotCode'
)
try:
client.send_robot_interactive_card_with_options(send_robot_interactive_card_request, send_robot_interactive_card_headers, util_models.RuntimeOptions())
except Exception as err:
if not UtilClient.empty(err.code) and not UtilClient.empty(err.message):
# err 中含有 code 和 message 属性,可帮助开发定位问题
pass
@staticmethod
async def main_async(
args: List[str],
) -> None:
client = Sample.create_client()
send_robot_interactive_card_headers = dingtalkim__1__0_models.SendRobotInteractiveCardHeaders()
send_robot_interactive_card_headers.x_acs_dingtalk_access_token = '<your access token>'
send_robot_interactive_card_request = dingtalkim__1__0_models.SendRobotInteractiveCardRequest(
single_chat_receiver='{"userId":"01335649490826229549"}',
card_data='{ "notifyTitle": "您有一个审核带申请", "notifyType": "审核通知", "notifyContent": "王康提交的一个新闻中心待申请", "handleType": "待审核", "createTime": "2023-03-15 21:40" }',
card_biz_id='chy123123',
card_template_id='592fafad-f6ff-4937-b713-73779152c555',
robot_code='dingm1iglijknxgpjrvc'
)
try:
await client.send_robot_interactive_card_with_options_async(send_robot_interactive_card_request, send_robot_interactive_card_headers, util_models.RuntimeOptions())
except Exception as err:
if not UtilClient.empty(err.code) and not UtilClient.empty(err.message):
# err 中含有 code 和 message 属性,可帮助开发定位问题
pass
if __name__ == '__main__':
Sample.main(sys.argv[1:])
6. 生成待办并封装接口
我这里我采用将上面参数通过对象进行封装, 然后配置对象通过配置文件进行配置的方式进行封装
-
钉钉sds包的下载 下载页面传送门. 这里我选择下载新版sdk, 且版本号为最新的1.5.55
-
实体类的构建
/**
* info:卡片通知相关
*
* @Author caoHaiYang
* @Date 2023/3/16 14:05
*/
public class CartNotifyDTO {
/**
* 通知标题
*/
private String notifyTitle;
/**
* 通知类型
*/
private String notifyType;
/**
* 通知内容
*/
private String notifyContent;
/**
* 处理类型
*/
private String handleType;
/**
* 创建时间
*/
private String createTime;
/**
* 卡片接受者的用户id
*/
@JsonIgnore
private String receiveUserId;
public String getNotifyTitle() {
return notifyTitle;
}
public void setNotifyTitle(String notifyTitle) {
this.notifyTitle = notifyTitle;
}
public String getNotifyType() {
return notifyType;
}
public void setNotifyType(String notifyType) {
this.notifyType = notifyType;
}
public String getNotifyContent() {
return notifyContent;
}
public void setNotifyContent(String notifyContent) {
this.notifyContent = notifyContent;
}
public String getHandleType() {
return handleType;
}
public void setHandleType(String handleType) {
this.handleType = handleType;
}
public String getCreateTime() {
return createTime;
}
public void setCreateTime(String createTime) {
this.createTime = createTime;
}
public String getReceiveUserId() {
return receiveUserId;
}
public void setReceiveUserId(String receiveUserId) {
this.receiveUserId = receiveUserId;
}
- 单聊机器人发送卡片业务方法
/**
* 使用 Token 初始化账号Client
* @return Client
* @throws Exception
*/
public static com.aliyun.dingtalkim_1_0.Client createClient2() throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
config.protocol = "https";
config.regionId = "central";
return new com.aliyun.dingtalkim_1_0.Client(config);
}
@Override
public void setCardNotify(CartNotifyDTO cardNotifyDTO) throws Exception {
com.aliyun.dingtalkim_1_0.Client client = createClient2();
com.aliyun.dingtalkim_1_0.models.SendRobotInteractiveCardHeaders sendRobotInteractiveCardHeaders = new com.aliyun.dingtalkim_1_0.models.SendRobotInteractiveCardHeaders();
sendRobotInteractiveCardHeaders.xAcsDingtalkAccessToken = "这里注意accessToken有效期, 需要全局进行定时刷新, 所有的接口请求都要调用获取企业accessToken接口";
com.aliyun.dingtalkim_1_0.models.SendRobotInteractiveCardRequest sendRobotInteractiveCardRequest = new com.aliyun.dingtalkim_1_0.models.SendRobotInteractiveCardRequest()
.setSingleChatReceiver("{\"userId\":\""+cardNotifyDTO.getReceiveUserId()+"\"}")
.setCardData(JSONObject.toJSONString(cardNotifyDTO))
.setCardBizId(UUIDUtil.getUuid())
.setCardTemplateId(dingTalkConfig.getCardTemplateId())
.setRobotCode(dingTalkConfig.getRobotCode());
try {
SendRobotInteractiveCardResponse sendRobotInteractiveCardResponse = client.sendRobotInteractiveCardWithOptions(sendRobotInteractiveCardRequest, sendRobotInteractiveCardHeaders, new RuntimeOptions());
} catch (TeaException err) {
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
log.error(err.code, err.getMessage());
}
} catch (Exception _err) {
TeaException err = new TeaException(_err.getMessage(), _err);
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
log.error(err.code, err.getMessage());
}
}
}
新发现
在资料搜寻时, 遇到了一个优秀的博主 Grandpa_Rick, 将钉钉发送互动消息做成了组件. 只需要将组件dingtalk-module项目打包, 然后放到demo项目中, 即可轻松使用, 至今依旧在持续更新. 博主人很棒, 向他请教了很多问题, 都很热心的解答. 感兴趣的也可以看看 DingTalk钉钉机器人单聊互动卡片消息的一次实现(附仓库).
基于该博主的demo代码. 只配置下面三个地方也能实现上述功能(但是需要自己重写一下dingtalk-module组件的DingBotMessageHandler类下的sendInteractiveMsgToIndividual方法, 在方法头部方法参数中新增卡片模板参数实体, 然后在方法中将参数实体转换成map放入到cardData.setCardParamMap中. 调用重写后的方法).
下面是配置修改内容
修改一: 基础信息的修改
修改二: 卡片模板id填入
修改三: 用户手机号(可以根据钉钉提供其他接口改成userId或union来实现单聊机器人卡片推送)
总结
不得不感慨, 阿里钉钉开放平台是一款优秀的企业级应用开发平台,具有便捷易用、功能完善、安全可靠、社区生态等优点,能够满足企业在数字化转型和业务创新方面的需求。其自身善于将钉钉内部开发的功能开放出来, 独立做成功能, 供各企业使用. 并且对各种功能进行组合, 实现业务场景的开发. 各种场景的组合则为企业提供了在管理和运营时的解决方案. 并且, 各个公司提供的钉钉场景解决方案还能在钉钉中进行商业化, 从而更好的促进钉钉更多的场景和解决方案的出现. 这种模式值得广大互联网公司学习.
但是, 仍然美中不足的是. 作为业界标杆, 钉钉在第三方企业进行对接仍存在不少问题. 比如: 接口部分文档描述较差, 跨场景关联性较差, 接口关键参数定义不清晰, 工单响应不及时, 接口sdk分新旧两版, 新版个版本具有哪些功能未说明.
良好的生态是需要有良好开放环境所支持的. 希望钉钉再接再厉, 在商业化的同时也不要忘了对开发者的良性支持. 这样才能让开放平台更加的开放!