摘要:概述最近項目中需要實現對接釘釘,并實現單向通訊錄同步釘釘服務器對接平臺本文通過一個簡單的案例快速實現相關的本文主要實現與釘釘對接。調用釘釘,主動注冊回調通知。
概述
最近項目中需要實現對接釘釘,并實現單向通訊錄同步(釘釘服務器 -> 對接平臺)本文通過一個簡單的案例快速實現相關的DEMO (本文主要實現與釘釘對接)。
釘釘API:https://open-doc.dingtalk.com...流程示意圖 準備工作
在使用回調接口前,需要做以下準備工作:
1) 提供一個接收消息的RESTful接口。
2) 調用釘釘API,主動注冊回調通知。
3) 因為涉及到消息的加密解密,默認的JDK存在一些限制,先要替換相關jar:
在官方網站下載JCE無限制權限策略文件
JDK6的下載地址:http://www.oracle.com/technet...
JDK7的下載地址:http://www.oracle.com/technet...
JDK8的下載地址:http://www.oracle.com/technet...
下載后解壓,可以看到local_policy.jar和US_export_policy.jar以及readme.txt。
如果安裝的是JRE,將兩個jar文件放到%JRE_HOME% libsecurity目錄下覆蓋原來的文件,
如果安裝的是JDK,將兩個jar文件放到%JDK_HOME%jrelibsecurity目錄下覆蓋原來文件。
4) 內網穿透映射本地RESTful接口到公網,推薦使用Ngrok: http://ngrok.ciqiuwl.cn/
具體實現 1. 提供回調接口package com.wuwenze.dingtalk.rest; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.mzlion.core.lang.Assert; import com.wuwenze.dingtalk.api.DingTalkConst; import com.wuwenze.dingtalk.encrpty.DingTalkEncryptor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import java.io.Serializable; import java.util.Map; /** * @author wwz * @version 1 (2018/7/26) * @since Java7 */ @Slf4j @RestController public class DingTalkCallbackRest { @PostMapping("/dingtalk/receive") public Mapreceive(// String signature, String timestamp, String nonce,@RequestBody String requestBody) { Assert.notNull(signature, "signature is null."); Assert.notNull(timestamp, "timestamp is null."); Assert.notNull(nonce, "nonce is null."); Assert.notNull(requestBody, "requestBody is null."); log.info("#receive 接收密文:{}", requestBody); DingTalkEncryptor dingTalkEncryptor = new DingTalkEncryptor(// DingTalkConst.CALLBACK_TOKEN, DingTalkConst.CALLBACK_AES_KEY, DingTalkConst.CORP_ID); JSONObject jsonEncrypt = JSON.parseObject(requestBody); String encryptMessage = dingTalkEncryptor.getDecryptMsg(signature, timestamp, nonce, jsonEncrypt.getString("encrypt")); log.info("#receive 密文解密后:{}", encryptMessage); // TODO: 異步處理報文,解析相關信息 // 返回加密后的success (快速響應) return dingTalkEncryptor.getEncryptedMsg("success", Long.parseLong(timestamp), nonce); } }
接口寫好之后,還需要將接口暴露在公網,如此釘釘服務器才能進行調用,下為內網穿透示意圖:
釘釘為我們開發者提供了一個Ngrok服務,在https://github.com/open-dingt...,按照操作文章指引配置即可。
我在這邊使用的是其他的Ngrok服務,官網地址是http://ngrok.ciqiuwl.cn/,配置后啟動如下圖所示:
將本地的http://127.0.0.1:8080映射到http://wuwz.ngrok.xiaomiqiu.cn,最終提供給釘釘的回調接口地址即為:http://wuwz.ngrok.xiaomiqiu.cn/dingtalk/receive
以上準備工作完后成,就可以將接口啟動起來,繼續后續的操作。
2. 主動注冊回調接口寫一個測試方法,將http://wuwz.ngrok.xiaomiqiu.cn/dingtalk/receive注冊到釘釘,后續釘釘相關的消息都會推送到此處。
package com.wuwenze.dingtalk; import com.wuwenze.dingtalk.api.DingTalkApi; import com.wuwenze.dingtalk.api.DingTalkConst; import com.wuwenze.dingtalk.enums.DingTalkCallbackTag; /** * @author wwz * @version 1 (2018/7/27) * @since Java7 */ public class TestRegisterCallback { public static void main(String[] args) { // 獲取Token String accessToken = DingTalkApi.getAccessTokenCache(); // 先刪除之前注冊的回調接口 DingTalkApi.removeCallback(accessToken); // 注冊新的回調接口 String callbackToken = DingTalkConst.CALLBACK_TOKEN; String callbackAesKey = DingTalkConst.CALLBACK_AES_KEY; String callbackUrl = "http://wuwz.ngrok.xiaomiqiu.cn/dingtalk/receive"; DingTalkCallbackTag[] callbackTags = { DingTalkCallbackTag.USER_ADD_ORG, // 增加用戶 DingTalkCallbackTag.USER_MODIFY_ORG, // 修改用戶 DingTalkCallbackTag.USER_LEAVE_ORG // 用戶離職 }; DingTalkApi.registerCallback(accessToken, callbackToken, callbackAesKey, callbackUrl, callbackTags); } }
執行代碼,如果一切不出意外的話,就注冊成功了(注冊的過程中需保證callbackUrl可以正常訪問,因為首次會向該接口發送一條check_url事件,驗證其合法性)
// 獲取Token 13:44:41.342 [main] INFO com.wuwenze.dingtalk.api.DingTalkApi - {"access_token":"9990578f789c3fb1a9d974c268df5029","errcode":0.0,"errmsg":"ok","expires_in":7200.0} // 先刪除之前注冊的回調接口 13:44:41.438 [main] INFO com.wuwenze.dingtalk.api.DingTalkApi - {"errcode":0.0,"errmsg":"ok"} // 注冊新的回調接口 13:44:41.888 [main] INFO com.wuwenze.dingtalk.api.DingTalkApi - {"errcode":0.0,"errmsg":"ok"} 13:44:41.893 [main] INFO com.wuwenze.dingtalk.api.DingTalkApi - #registerCallback 注冊回調接口 -> url: http://wuwz.ngrok.xiaomiqiu.cn/dingtalk/receive, tags: tag: user_add_org, describe: 通訊錄用戶增加 + tag: user_modify_org, describe: 通訊錄用戶更改 + tag: user_leave_org, describe: 通訊錄用戶離職
另外再來觀察一下回調接口是否收到checkUrL消息:
2018-07-27 13:44:41.823 INFO 2392 --- [nio-8080-exec-1] c.w.dingtalk.rest.DingTalkCallbackRest : #receive 接收密文:{"encrypt":"JfRo/wn+E1agXgk1uN5UQP/WDv0RvWnw8TgXC/ucatBxYm54OSUcGn5uTGCVMaGIN6Lv24ZOujH/uixB39AKxjXWgzdJQ1Eq4HD0EIJFG+QY8mjcCltvhX0QfhisFlll"} 2018-07-27 13:44:41.823 INFO 2392 --- [nio-8080-exec-1] c.w.dingtalk.rest.DingTalkCallbackRest : #receive 密文解密后:{"EventType":"check_url"}3. 測試注冊的通訊錄事件
在上一步中,注冊了USER_ADD_ORG (增加用戶)、USER_MODIFY_ORG (修改用戶)、USER_LEAVE_ORG (用戶離職|刪除)三個事件
打開釘釘后臺管理,在通訊錄中新增一個用戶:
保存成功后,在回調接口中則馬上收到了該事件的通知消息:
2018-07-27 13:49:55.985 INFO 2392 --- [nio-8080-exec-3] c.w.dingtalk.rest.DingTalkCallbackRest : #receive 接收密文:{"encrypt":"g6RsagVKTVUS2Gg7B1JSn81uJPgCpPKoaRN4kps4cMpp6CuqW1QahaDP8TcnwDP2fYyG0gwLFvF5cOWbn+lKX2kq4UYe5m08BB/FWw8lALV/4LYu7RI6OARCFDTsllBTs4W6/OUv+9AyYlWGmwK2ZYnXoFyiK4DqFt6jenp45NCXwvSgssjn8RsD/3E7kfw5DL/mfr4L3hkaBysmkU2ohaFFEqBO1r63cj+mONLsD8Dvr2lAsefBoMdZ2JV5sIIePuKhz08G6KnJDvkAqcm59naV6AIbDLouWrBK7upCP7Q="} 2018-07-27 13:49:55.985 INFO 2392 --- [nio-8080-exec-3] c.w.dingtalk.rest.DingTalkCallbackRest : #receive 密文解密后:{"TimeStamp":"1532670599144","CorpId":"dingb9875d6606f892ed35c2f4657eb6378f","UserId":["202844352662984130"],"EventType":"user_add_org"}4. 后續同步邏輯
在上面的例子中新增用戶后,收到的報文解密后的信息為只包含事件類型和用戶ID,所以后面還需要主動調用釘釘獲取用戶詳情的接口,再做具體的同步邏輯,這里就不再往下寫了,貼一下相關的API接口吧:
https://open-doc.dingtalk.com...
下面羅列了以上示例中用到的工具類封裝,不再具體講解,直接貼代碼DingTalkConst
常量池
public class DingTalkConst { public final static String CORP_ID = "dingb9875d6606f892ed35c2f4657eb6378f"; public final static Object CORP_SECRET = "到釘釘查看"; public final static String CALLBACK_TOKEN = "token"; // 回調Token public final static String CALLBACK_AES_KEY = "xxxxx7p5qnb6zs3xxxxxlkfmxqfkv23d40yd0xxxxxx"; // 回調秘鑰,43個隨機字符 }DingTalkCallbackTag
可供注冊的回調事件類型枚舉
public enum DingTalkCallbackTag { USER_ADD_ORG("通訊錄用戶增加"), USER_MODIFY_ORG("通訊錄用戶更改"), USER_LEAVE_ORG("通訊錄用戶離職"), ORG_ADMIN_ADD("通訊錄用戶被設為管理員"), ORG_ADMIN_REMOVE("通訊錄用戶被取消設置管理員"), ORG_DEPT_CREATE("通訊錄企業部門創建"), ORG_DEPT_MODIFY("通訊錄企業部門修改"), ORG_DEPT_REMOVE("通訊錄企業部門刪除"), ORG_REMOVE("企業被解散"), ORG_CHANGE("企業信息發生變更"), LABEL_USER_CHANGE("員工角色信息發生變更"), LABEL_CONF_ADD("增加角色或者角色組"), LABEL_CONF_DEL("刪除角色或者角色組"), LABEL_CONF_MODIFY("修改角色或者角色組"); private String describe; DingTalkCallbackTag(String describe) { this.describe = describe; } public String getDescribe() { return describe; } public void setDescribe(String describe) { this.describe = describe; } @Override public String toString() { return super.toString().toLowerCase(); } public String toInfoString() { return String.format("tag: %s, describe: %s", this.toString(), this.getDescribe()); } }DingTalkEncryptor
釘釘消息加密解密工作類
package com.wuwenze.dingtalk.encrpty; import com.google.common.collect.ImmutableMap; import com.mzlion.core.binary.Base64; import com.mzlion.core.lang.Assert; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayOutputStream; import java.io.Serializable; import java.nio.charset.Charset; import java.security.MessageDigest; import java.util.Arrays; import java.util.Map; import java.util.Random; /** * @author wwz * @version 1 (2018/7/26) * @since Java7 */ public class DingTalkEncryptor { private static final Charset CHARSET = Charset.forName("UTF-8"); private byte[] aesKey; private String token; private String corpId; /** * ask getPaddingBytes key固定長度 **/ private static final Integer AES_ENCODE_KEY_LENGTH = 43; /** * 加密隨機字符串字節長度 **/ private static final Integer RANDOM_LENGTH = 16; /** * 構造函數 * * @param token 釘釘開放平臺上,開發者設置的token * @param encodingAesKey 釘釘開放臺上,開發者設置的EncodingAESKey * @param corpId ISV進行配置的時候應該傳對應套件的SUITE_KEY(第一次創建時傳的是默認的CREATE_SUITE_KEY),普通企業是Corpid */ public DingTalkEncryptor(String token, String encodingAesKey, String corpId) { if (null == encodingAesKey || encodingAesKey.length() != AES_ENCODE_KEY_LENGTH) { throw new IllegalArgumentException("encodingAesKey is null"); } this.token = token; this.corpId = corpId; this.aesKey = Base64.decode(encodingAesKey + "="); } /** * 將和釘釘開放平臺同步的消息體加密,返回加密Map * * @param message 傳遞的消息體明文 * @param timeStamp 時間戳 * @param nonce 隨機字符串 * @return */ public MapPKCS7PaddinggetEncryptedMsg(String message, Long timeStamp, String nonce) { Assert.notNull(message, "plaintext is null"); Assert.notNull(timeStamp, "timeStamp is null"); Assert.notNull(nonce, "nonce is null"); String encrypt = encrypt(getRandomStr(RANDOM_LENGTH), message); String signature = getSignature(token, String.valueOf(timeStamp), nonce, encrypt); return ImmutableMap.of( "msg_signature", signature, // "encrypt", encrypt, // "timeStamp", timeStamp,// "nonce", nonce); } /** * 密文解密 * * @param msgSignature 簽名串 * @param timeStamp 時間戳 * @param nonce 隨機串 * @param encryptMsg 密文 * @return 解密后的原文 */ public String getDecryptMsg(String msgSignature, String timeStamp, String nonce, String encryptMsg) { // 校驗簽名 String signature = getSignature(token, timeStamp, nonce, encryptMsg); if (!signature.equals(msgSignature)) { throw new RuntimeException("校驗簽名失敗。"); } // 解密 return decrypt(encryptMsg); } private String encrypt(String random, String plaintext) { try { byte[] randomBytes = random.getBytes(CHARSET); byte[] plainTextBytes = plaintext.getBytes(CHARSET); byte[] lengthByte = int2Bytes(plainTextBytes.length); byte[] corpidBytes = corpId.getBytes(CHARSET); ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); byteStream.write(randomBytes); byteStream.write(lengthByte); byteStream.write(plainTextBytes); byteStream.write(corpidBytes); byte[] padBytes = PKCS7Padding.getPaddingBytes(byteStream.size()); byteStream.write(padBytes); byte[] unencrypted = byteStream.toByteArray(); byteStream.close(); Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES"); IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16); cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv); byte[] encrypted = cipher.doFinal(unencrypted); return Base64.encode(encrypted); } catch (Exception e) { throw new RuntimeException(e); } } /** * 對密文進行解密. * @param text 需要解密的密文 * @return 解密得到的明文 */ private String decrypt(String text) { byte[] originalArr; try { // 設置解密模式為AES的CBC模式 Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES"); IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16)); cipher.init(Cipher.DECRYPT_MODE, keySpec, iv); // 使用BASE64對密文進行解碼, 解密 originalArr = cipher.doFinal(Base64.decode(text)); } catch (Exception e) { throw new RuntimeException("計算解密文本錯誤"); } String plainText; String fromCorpid; try { // 去除補位字符 byte[] bytes = PKCS7Padding.removePaddingBytes(originalArr); // 分離16位隨機字符串,網絡字節序和corpId byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20); int plainTextLegth = bytes2int(networkOrder); plainText = new String(Arrays.copyOfRange(bytes, 20, 20 + plainTextLegth), CHARSET); fromCorpid = new String(Arrays.copyOfRange(bytes, 20 + plainTextLegth, bytes.length), CHARSET); } catch (Exception e) { throw new RuntimeException("計算解密文本長度錯誤"); } // corpid不相同的情況 if (!fromCorpid.equals(corpId)) { throw new RuntimeException("計算文本密碼錯誤"); } return plainText; } /** * 數字簽名 * @param token isv token * @param timestamp 時間戳 * @param nonce 隨機串 * @param encrypt 加密文本 * @return */ public String getSignature(String token, String timestamp, String nonce, String encrypt) { try { String[] array = new String[]{token, timestamp, nonce, encrypt}; Arrays.sort(array); StringBuffer sb = new StringBuffer(); for (int i = 0; i < 4; i++) { sb.append(array[i]); } String str = sb.toString(); MessageDigest md = MessageDigest.getInstance("SHA-1"); md.update(str.getBytes()); byte[] digest = md.digest(); StringBuffer hexstr = new StringBuffer(); String shaHex = ""; for (int i = 0; i < digest.length; i++) { shaHex = Integer.toHexString(digest[i] & 0xFF); if (shaHex.length() < 2) { hexstr.append(0); } hexstr.append(shaHex); } return hexstr.toString(); } catch (Exception e) { throw new RuntimeException(e); } } public static String getRandomStr(int count) { String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; Random random = new Random(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < count; i++) { int number = random.nextInt(base.length()); sb.append(base.charAt(number)); } return sb.toString(); } /** * int轉byte數組,高位在前 */ public static byte[] int2Bytes(int count) { byte[] byteArr = new byte[4]; byteArr[3] = (byte) (count & 0xFF); byteArr[2] = (byte) (count >> 8 & 0xFF); byteArr[1] = (byte) (count >> 16 & 0xFF); byteArr[0] = (byte) (count >> 24 & 0xFF); return byteArr; } /** * 高位在前bytes數組轉int * @param byteArr * @return */ public static int bytes2int(byte[] byteArr) { int count = 0; for (int i = 0; i < 4; i++) { count <<= 8; count |= byteArr[i] & 0xff; } return count; } }
package com.wuwenze.dingtalk.encrpty; import java.nio.charset.Charset; import java.util.Arrays; /** * @author wwz * @version 1 (2018/7/10) * @since Java7 */ public class PKCS7Padding { private final static Charset CHARSET = Charset.forName("utf-8"); private final static int BLOCK_SIZE = 32; /** * 填充mode字節 * @param count * @return */ public static byte[] getPaddingBytes(int count) { int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE); if (amountToPad == 0) { amountToPad = BLOCK_SIZE; } char padChr = chr(amountToPad); String tmp = new String(); for (int index = 0; index < amountToPad; index++) { tmp += padChr; } return tmp.getBytes(CHARSET); } /** * 移除mode填充字節 * @param decrypted * @return */ public static byte[] removePaddingBytes(byte[] decrypted) { int pad = (int) decrypted[decrypted.length - 1]; if (pad < 1 || pad > BLOCK_SIZE) { pad = 0; } return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad); } private static char chr(int a) { byte target = (byte) (a & 0xFF); return (char) target; } }DingTalkApi
釘釘開放API簡易封裝 (僅供測試)
package com.wuwenze.dingtalk.api; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableMap; import com.mzlion.core.lang.Assert; import com.mzlion.easyokhttp.HttpClient; import com.wuwenze.dingtalk.enums.DingTalkCallbackTag; import lombok.extern.slf4j.Slf4j; import java.io.Serializable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; /** * @author wwz * @version 1 (2018/7/26) * @since Java7 */ @Slf4j public class DingTalkApi { private final static LoadingCachemTokenCache = // CacheBuilder.newBuilder()// .maximumSize(100)// .expireAfterAccess(7200, TimeUnit.SECONDS)// .build(new CacheLoader () { @Override public String load(String key) throws Exception { // key:corpId#corpSecret String[] params = key.split("#"); if (params.length != 2) { throw new RuntimeException("#loadTokenCache error"); } return getAccessToken(params[0], params[1]); } }); public static String getAccessToken(String corpId, String corpSecret) { String url = String.format("https://oapi.dingtalk.com/gettoken?corpid=%s&corpsecret=%s",corpId, corpSecret); JSONObject jsonObject = HttpClient.get(url).asBean(JSONObject.class); assertDingTalkJSONObject(jsonObject); return jsonObject.getString("access_token"); } public static String getAccessTokenCache() { try { return mTokenCache.get(DingTalkConst.CORP_ID + "#" + DingTalkConst.CORP_SECRET); } catch (ExecutionException e) { return null; } } public static void registerCallback(String accessToken, String callbackToken, String callbackAesKey, String url, DingTalkCallbackTag ... tags) { Assert.notNull(accessToken, "accessToken is null"); Assert.notNull(callbackToken, "callbackToken is null"); Assert.notNull(callbackAesKey, "callbackAesKey is null"); Assert.notNull(url, "url is null"); if (tags.length < 1) { throw new IllegalArgumentException("至少指定一個回調事件類型。"); } String[] callbackTagArray = new String[tags.length]; for (int i = 0; i < tags.length; i++) { callbackTagArray[i] = tags[i].toString(); } ImmutableMap params = ImmutableMap.of(// "call_back_tag", callbackTagArray,// "token", callbackToken,// "aes_key", callbackAesKey, // "url", url// ); String apiUrl = "https://oapi.dingtalk.com/call_back/register_call_back?access_token=" + accessToken; assertDingTalkJSONObject(// HttpClient.textBody(apiUrl).json(JSON.toJSONString(params)).asBean(JSONObject.class) ); log.info("#registerCallback 注冊回調接口 -> url: {}, tags: {}", url, showTagsInfo(tags)); } private static String showTagsInfo(DingTalkCallbackTag ... tags) { StringBuffer stringBuffer = new StringBuffer(); for (DingTalkCallbackTag tag : tags) { stringBuffer.append(tag.toInfoString()).append(" + "); } return stringBuffer.toString(); } public static void removeCallback(String accessToken) { String apiUrl = "https://oapi.dingtalk.com/call_back/delete_call_back?access_token=" + accessToken; assertDingTalkJSONObject(// HttpClient.get(apiUrl).asBean(JSONObject.class) ); } public static void removeCallback() { removeCallback(getAccessTokenCache()); } public static void registerCallback(String url, DingTalkCallbackTag ... tags) { registerCallback(getAccessTokenCache(), DingTalkConst.CALLBACK_TOKEN, DingTalkConst.CALLBACK_AES_KEY, url, tags); } private static void assertDingTalkJSONObject(JSONObject jsonObject) { log.info(jsonObject.toJSONString()); int errcode = jsonObject.getIntValue("errcode"); if (errcode != 0) { throw new RuntimeException(jsonObject.getString("errmsg")); } } }
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76515.html
摘要:首先是添加群自定義機器人在釘釘上選擇需要發送消息的群,選擇群機器人添加機器人自定義機器人添加,如下圖如需要,可以為機器人設置頭像。 由于公司用的通訊工具是釘釘,而群機器人可以實現自動化的信息同步,因此這里總結了自己使用釘釘自定義機器人一些方法,供參考拋磚引玉。 首先是添加群自定義機器人 在釘釘上選擇需要發送消息的群,選擇群機器人-添加機器人-自定義機器人-添加,如下圖: showImg...
摘要:釘釘釘釘是阿里巴巴集團專為中國企業打造的免費溝通和協同的多端平臺,提供版,版和手機版,支持手機和電腦間文件互傳。 1:@teamhttps://www.atteam.cn/項目協作管理,越復雜越有序,足夠簡單足夠有效,@Team針對企業團隊協作所遇到的困境而研發的新一代基于云服務的企業級協同工作平臺,通過為每個企業或團隊提供專屬的私密網絡空間和全新的協作方式,幫助企業實現高效便捷的跨部...
摘要:說明群機器人是釘釘群的高級擴展功能。目前,大部分機器人在添加后,還需要進行配置,才可正常使用配置說明詳見操作流程中的幫助鏈接。安裝配置在使用本擴展之前,你需要去群機器人獲取相關信息。 說明 群機器人是釘釘群的高級擴展功能。群機器人可以將第三方服務的信息聚合到群聊中,實現自動化的信息同步。目前,大部分機器人在添加后,還需要進行Webhook配置,才可正常使用(配置說明詳見操作流程中的幫助...
摘要:目前釘釘機器人支持方式,仍屬于內側階段。方式是指被動接受通知,釘釘群中添加的群機器人默認都是該模式。截止撰寫文章時,釘釘的機器人文檔不可訪問,所以會在下面介紹下。本文同步發表于作者博客從零開始打造專屬釘釘機器人 官方定義如下: 群機器人是釘釘群的高級擴展功能。群機器人可以將第三方服務的信息聚合到群聊中,實現自動化的信息同步。目前,大部分機器人在添加后,還需要進行Webhook配置,才可...
摘要:另一種關于組件的常見說法,是組件是為了重用。這件事情是前端特有的,受限制于的結構。這一節的題目叫做混亂的組件通訊,我們來仔細掰扯一下細節,因為組件模型雖然很常說但是對通訊過程沒有約定。 這個話題很難寫。 但是反過來說,愛因斯坦有句名言:如果你不能把一個問題向一個六歲孩子解釋清楚,那么你不真的明白它。 所以解釋清楚一個問題的關鍵,不是去擴大化,而是相反,最小化。 Lets begin. ...
閱讀 1148·2021-11-24 10:43
閱讀 3102·2021-11-22 09:34
閱讀 3549·2021-10-08 10:04
閱讀 3931·2021-09-23 11:58
閱讀 3114·2019-08-30 15:44
閱讀 483·2019-08-30 13:01
閱讀 1155·2019-08-28 18:07
閱讀 1447·2019-08-26 13:42