通过企业微信发送消息通知有两种方式:
- 群机器人;
- 企业内部自建应用;
两者的对比
接入难易程度 | 发送范围 | 是否可以接收回调 | |
---|---|---|---|
群机器人 | 简单,任何人都可以创建,并添加到群组中(默认任何人,但管理员可以开启白名单) | 只能往群组中发送消息 | 不能,只能发送,不能接收 |
企业自建应用 | 需要管理员添加,并需要配置发送消息服务器的 IP 白名单 | 可以指定给某人或某几人或某部门发送消息(在管理员指定范围内) | 可以接收消息,实现互动 |
适用范围
- 群机器人:适合小范围推送,或者群内所有人或大部分人都关心的消息,否则容易对群内其他人造成困扰。如服务的监控消息、每周一次的科技信息等。
- 企业自建应用:适合精准推送,推送一些偏私密性的消息,或者只需要让他自己知道就足够了。比如禅道的任务、bug 状态变动、内容增删等的通知;比如流水线的通知(谁触发谁接收通知)等;
群机器人 #
群机器人,顾名思义,是只能在群组中才能添加的机器人。二人对话的聊天框中是无法添加的,但我们可以通过一些技巧来实现。
如何创建小群测试 #
有时候我们想单独测试下我们的机器人,又不想打扰别人,可以用如下的方式操作。
先默默拉几个人建一个群,注意,不要发送消息、不要改群名。不要进行任何操作。然后再默默把其他人踢掉,就可以形成二人群或者一人群了。其他人是完全不感知的,他们是不知道自己被拉群了,然后又被踢掉了。
比如这个群,这群里只有我一个人:
添加群机器人 #
群里任何人都可以添加机器人。
添加成功后,就会有一个对应的 webhook 地址,其他应用使用这个地址,就可以通过群机器人发送消息了。 注意,不要泄露您的 webhook 地址,避免他人通过该地址发送垃圾消息。
如何发送消息 #
群机器人发送消息还是比较简单的,按照文档配置即可。开发者可以按以下说明向这个地址发起 HTTP POST 请求,即可实现给该群组发送消息。
官方发送不同格式消息的地址:https://developer.work.weixin.qq.com/document/path/91770
如在 shell 脚本中:
curl 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxx-yyyyyy-zzzzzz' \
-H 'Content-Type: application/json' \
-d '
{
"msgtype": "text",
"text": {
"content": "hello world"
}
}'
比如我在流水线是用的群机器人,来提示我流水线的启动和结束。gitlab 流水线可以执行 shell 脚本,我们直接编写 shell 脚本即可。
很奇怪,这段代码在博客里一直发布不成功,就改成截图了。
有的同学会通过后台服务发送一些信息,如在 nodejs 中:
const axios = require("axios");
const send = async () => {
const result = await axios({
url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxx-yyyyyy-zzzzzz",
method: "post",
data: {
msgtype: "news",
news: {
articles: [
{
title: "中秋节礼品领取",
description: "今年中秋节公司有豪礼相送",
url: "www.qq.com",
picurl:
"http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png",
},
],
},
},
});
if (result.status === 200 && result.data?.errcode === 0) {
console.log("success", result.data);
} else {
console.error("fail");
}
};
send();
把机器人发布到公司 #
若您觉得您的群机器人功能很不错,后台服务也比较稳定,有的同学会搞一些笑话机器人、每周信息汇总机器人、每日一语机器人等等。就可以将其发布到公司范围内,其他人可以就把他添加到别的群组里。
添加机器人时,可以从这里选择已发布到公司的机器人。
对于没有发布到公司的群机器人,只能靠大家的口口相传了。
点评 #
企业微信机器人引入简单,使用方便。相应的,能力也有很大的局限性,比如无法鉴权,谁拿到这个地址,都能发送消息;同时,只有在有这个机器人的群里,才能收到消息,否则就无法感知。
比如上面的那个群里,群里只有我自己,我就可以接收到通知,但其他人,虽然他们触发流水产生的群机器人消息,但因为他们不在这个群里,就无法收到通知。
企业自建应用 #
如何创建企业自建应用 #
这需要拥有该企业微信的管理员权限,才能创建自建应用。
一. 进入管理后台,选择应用管理,下拉到底部,点击创建应用;
二. 填入提前准备好的 logo 图片(建议使用 750*750,1M 以内的 jpg、png 图片)、应用名称和应用介绍(选填),然后选择可见范围;
同时,还要准备好域名和几个 IP,这里需要从管理后台下载一个验证文件,放到该域名对应的服务器根目录中,域名验证通过后就可以添加 IP 白名单了。
- 主动推送消息:可以在设置了本地机器 IP 白名单后,在本地机器就可以发起测试;
- 接收并回复消息:只能在已验证的域名上操作;
三. 创建成功;得到如下的几条数据(获取方式:https://developer.work.weixin.qq.com/document/path/90665):
- corpId: 即企业 ID,在“我的企业”tab 的最底部;
- secret: 即该应用的密钥,点击进入刚才创建的应用内进行查看;
- agentId: 该应用的 id(主动发送消息时用不到,但接收消息时需要);
若您不是管理员,还请将第 2 序列中准备好的数据给到公司的管理员,待管理员创建成功后,得到第 3 序列中需要的数据。
自建应用的特点:
- 只有能由有管理员权限的人员,进行创建;
- 可以定向给企业内的任何成员发送消息,对其他成员无干扰;
- 可以接收消息,并进行相应的回复(关键词的自动回复和接入程序的更复杂的回复等);
- 需要验证域名、IP 白名单,并且需要该企业对应的 corpId,才能正常使用;
获取 access_token #
企业自建应用的任何功能,都要首先获取到 access_token。
官网地址:https://developer.work.weixin.qq.com/document/path/91039。
通过已获取到的 corpId 和 secret 就可以拿到 access_token 了。不过 token 接口有请求频率的限制,并且获取到的 token 有 7200s(2 小时)的有效期,程序需要缓存该 token。
const axios = require("axios");
const { corpId, secret } = require("./utils/secret");
const cache = require("./utils/cache");
const getAccessToken = async () => {
const cacheToken = await cache.get("token");
if (cacheToken) {
return cacheToken;
}
const { status, data } = await axios({
url: "https://qyapi.weixin.qq.com/cgi-bin/gettoken",
params: {
corpid: corpId,
corpsecret: secret,
},
});
if (status === 200 && data.errcode === 0) {
cache.set("token", data.access_token);
return data.access_token;
}
return null;
};
module.exports = getAccessToken;
发送消息推送 #
企业自建应用,除消息推送外,还有很多其他的功能,如通讯录管理、身份验证、消息推送、创建群聊等,不过我们这里主讲消息推送这块。
跟群机器人类似,消息也有很多种类型,每种类型的消息所需要的字段也不一样,大家可自行查阅文档。这里仅举一个发送文本消息的例子:
const axios = require("axios");
const getAccessToken = require("./src/get-access-token");
const { agentId } = require("./src/utils/secret");
const sendTextMsg = async () => {
const accessToken = await getAccessToken();
const { status, data } = await axios({
url: `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${accessToken}`,
method: "post",
data: {
touser: "xiaowenzi", // 多个用户,用 | 隔开
// toparty: 'PartyID1|PartyID2', // 部门 id
// totag: 'TagID1 | TagID2',
msgtype: "text",
agentid: agentId,
text: {
content:
'你的快递已到,请携带工卡前往邮件中心领取。\n 出发前可查看<a href="https://www.xiabingbao.com">邮件中心视频实况</a>,聪明避开排队。',
},
},
});
console.log(status, data);
};
我们在创建好「消息提醒」的自建应用后,其他的内网服务(如禅道、知识库、流水线、错误率警告等服务),都可以通过该应用向用户或部门发送通知,方便周知各种内容的变更。
接收并回复消息 #
自建应用还有一个很重要的功能,就是可以接收每个用户发送过来的任何消息(包括底部菜单的点击),然后再针对该消息,进行相应的回复。
这里的配置相对来说比较麻烦一些:
- 所有的操作只能在管理后台配置的链接进行;
- 接收和要回复的消息均是 xml 格式或 xml string 格式的;
在接收消息之前,首先要在管理后台配置接收消息的线上地址,在保存地址时,就会校验这个地址的有效性,即企业微信会以 GET 请求的方式,携带一些参数,请求该 url,若能正常解密 url 参数中的内容并返回,即为配置成功。
加密和解密的算法与官方已有库 #
在接收并回复消息模块中,有三个过程:
- 验证 url:接收参数并解析出参数中的内容,然后返回;主要是在保存 url 地址时使用;
- 接收消息:企业微信会以 POST 方式并携带参数请求我们的保存的地址 ,根据参数和 body 中的数据,解析出真正的消息;
- 回复消息:把要回复的消息,连同时间戳、随机数等进行加密,然后返回给企业微信;
第 2、3 过程是连续的,若要回复消息,则直接返回即可;若不想回复消息或回复需要很长的时间(官方会等待 5 秒时间),可以直接在第 2 步中返回 200(即以空串为返回包),然后再通过上面的“发送消息推送”,主动给相关用户推送消息。
各种加密解密算法也挺绕的,官方也出了一些相关语言的包(库),可以直接拿来使用(https://developer.work.weixin.qq.com/document/path/90307)。我这里只有一个能运行 php 的主机,因此选择了 php 语言的库。各位可以根据自己的需要,选择相应的库即可。
我这里使用了 codeigniter 框架。
验证 url #
// 检测 url 的合法性
private function checkValidUrl()
{
$this->load->library('WXBizMsgCrypt');
$this->wxbizmsgcrypt->init($this->token, $this->encodingAesKey, $this->corpId);
$sVerifyMsgSig = $this->input->get("msg_signature");
$sVerifyTimeStamp = $this->input->get("timestamp");
$sVerifyNonce = $this->input->get("nonce");
$sVerifyEchoStr = $this->input->get("echostr");
// 需要返回的明文
$sEchoStr = "";
$errCode = $this->wxbizmsgcrypt->VerifyURL($sVerifyMsgSig, $sVerifyTimeStamp, $sVerifyNonce, $sVerifyEchoStr, $sEchoStr);
if ($errCode == 0) {
echo $sEchoStr;
} else {
print("ERR: " . $errCode . "\n\n");
}
}
接收消息 #
接收消息这里很特殊,企业微信发送过来的是一个 xml string 类型的。我也是好久没写过 php 了,不知道怎么接收这个数据,用 post 方式尝试了 N 多次,也没成功。后来才查到相关资料是用file_get_contents("php://input")
的方式来接收。
接收到所有的数据,再通过官方提供的解密函数,解析出真实的 xml 信息。注意,这里并不是单纯的消息,还有各种如发送用户、发送的消息类型、发送的时间等信息。还得需要通过 xml 的进一步解析,才能解析出各个字段的值。
/**
* 接收消息
* 参数接收一些加密参数,具体消息是通过post的body传过来的,
* 在php中,若body是一个纯字符串,需要用 file_get_contents('php://input') 的方式来接收
*
* 关于file_get_contents和post的区别:
* @see https://www.cnblogs.com/phpper/p/9574419.html
*/
private function decodeMsg()
{
$this->load->library('WXBizMsgCrypt');
$this->wxbizmsgcrypt->init($this->token, $this->encodingAesKey, $this->corpId);
$sReqMsgSig = $this->input->get("msg_signature");
$sReqTimeStamp = $this->input->get("timestamp");
$sReqNonce = $this->input->get("nonce");
// post请求的密文数据
$sReqData = file_get_contents('php://input');
$sMsg = ""; // 解析之后的明文
$errCode = $this->wxbizmsgcrypt->DecryptMsg($sReqMsgSig, $sReqTimeStamp, $sReqNonce, $sReqData, $sMsg);
if ($errCode == 0) {
// 解密成功,sMsg即为xml格式的明文
echo ($sMsg);
return $sMsg;
// TODO: 对明文的处理
/*
"<xml><ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName>
<FromUserName><![CDATA[mycreate]]></FromUserName>
<CreateTime>1409659813</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[hello]]></Content>
<MsgId>4561255354251345929</MsgId>
<AgentID>218</AgentID>
</xml>"
*/
} else {
print("ERR: " . $errCode . "\n\n");
}
}
回复消息 #
回复消息与接收消息差不多,根据官方要求的字段格式,拼接 xml,然后再以 string 类型进行加密。
private function sendMsg()
{
$this->load->library('WXBizMsgCrypt');
$this->wxbizmsgcrypt->init($this->token, $this->encodingAesKey, $this->corpId);
// 接收消息
$getMsg = $this->decodeMsg();
// 接收到消息后,经过处理,然后需要返回给用户消息了
$now = time();
// 需要发送的明文
$sRespData = "<xml><ToUserName><![CDATA[xiaowenzi]]></ToUserName><FromUserName><![CDATA[{$this->corpId}]]></FromUserName><CreateTime>{$now}</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[{$getMsg}]]></Content><AgentID>{$this->agentId}</AgentID></xml>";
$sReqTimeStamp = $now;
$sReqNonce = rand();
$sEncryptMsg = ""; //xml格式的密文
$errCode = $this->wxbizmsgcrypt->EncryptMsg($sRespData, $sReqTimeStamp, $sReqNonce, $sEncryptMsg);
if ($errCode == 0) {
echo ($sEncryptMsg);
// print("done \n");
// TODO:
// 加密成功,企业需要将加密之后的sEncryptMsg返回
// HttpUtils.SetResponce($sEncryptMsg); //回复加密之后的密文
} else {
print("ERR: " . $errCode . "\n\n");
// exit(-1);
}
}
在群聊会话中发通知 #
比如一些活动报名、或者中奖名单等,有一长串的用户名单,需要拉群知会一些消息。若要手动创建的话,那每次都得搜索拉入,需要很长时间。但若通过接口创建的话,几秒钟就可以。
创建群聊 #
创建群聊官方地址:https://developer.work.weixin.qq.com/document/path/90245。
{
"name": "NAME",
"owner": "userid1",
"userlist": ["userid1", "userid2", "userid3"],
"chatid": "CHATID"
}
设置好群聊名称、群主、群成员、群 Id(可选),就可以创建了。创建成功后,会返回该群聊的 id:
// 创建群聊
const createGroupChat = async () => {
const accessToken = await getAccessToken();
const { status, data } = await axios({
url: `https://qyapi.weixin.qq.com/cgi-bin/appchat/create?access_token=${accessToken}`,
method: "post",
data: {
// chatid: Date.now(),
name: `企业内建应用创建的群聊-${Date.now().toString(36)}`,
owner: "xiaowenzi",
userlist: ["xiaowenzi", "dawenzi"], // 成员名单至少需要2个人
},
});
// 创建成功后,不会立即在聊天框中展示出来,需要通过发送消息,来激活该群聊
console.log("createGroupChat", status, data); // chatid: wruVG5OwAAQF_MwwspiyXmD8T7NQt8yA
};
群聊创建成功后,会返回该群聊的 id,用于后续的比如修改群聊标题、群聊成员、发送群聊消息等操作。而且,群聊刚创建成功时,是不会立即在聊天框中展示出来的,需要通过发送消息,来激活该群聊。
向群聊中发送消息 #
这里也有一个发送消息的接口,但这里的接口跟上面的“发送消息推送”不是同一个接口。而且,这里的接口还需要指定群 id(即 chatid)才能发送消息。
const sendMsgToGroup = async () => {
const accessToken = await getAccessToken();
const { status, data } = await axios({
url: `https://qyapi.weixin.qq.com/cgi-bin/appchat/send?access_token=${accessToken}`,
method: "post",
data: {
chatid: "chatid", // 修改自己的群聊id
msgtype: "markdown",
markdown: {
content:
'# 你的快递已到\n请携带工卡前往邮件中心领取\n<a href="http://work.weixin.qq.com">邮件中心视频实况</a>,聪明避开排队',
},
safe: 0,
},
});
console.log("sendMsgToGroup", status, data);
};
这里发送消息的格式,也是有多种格式。
总结 #
群机器人和企业自建应用有着不同的接入难度和接入场景,各位可以根据自己的需要,来选择适合自己的方式。