引子
最近最火的莫過于ChatGPT了,在自己體驗后就想著如何其他同事也能方便的起起來,畢竟獨樂樂不如眾樂樂,自己注冊又是V-P-N,又是國外手機驗證,對于大部分同事來說門檻還是高的,現在也有不少小程式,公眾號集成了這個能力,但作業中大家還是在企微中,如果能讓大家在企微,釘釘這樣的作業臺上使用,與作業無縫對接,這豈不快哉,
本來周末遛娃一天已經筋疲力盡了,收拾完娃子也跟著睡覺了,哪成想,等把他們哄睡后,23點突然來了精神,就開始搗鼓起來了,
創建企微應用
最近公司剛把內部溝通平臺從釘釘切換到企微了,就開始摸索可能的對接方式, 一開始想著和釘釘一樣,@機器人時,決議內容,自動回復,結果一查,企微的群機器人沒這個功能,只能配置有限的規則,自動回復,在無奈之時,甚至想先用釘釘對接起來,但是沒能過了心里這關,畢竟本月剛剛完成了釘釘到企微的全面遷移,遷移程序還問題多多,原本以為此路不通,即將放棄之時,突然看了社區有個回復自建應用可以實作接收用戶的訊息并主動回復內容(介面檔案點這里),一開始覺得用群,熱鬧一些,效果好些;不過從后來大家問的問題來看,單獨應用時,大家放得開些, 喧鬧與寂靜從來都是相輔相成,企微走了通了,全鏈路也就用了,(因為國內無法使用的情況,要有一個服務器做請求代理)

話不多說,進入企微后臺,進入“應用管理”模塊,創建自建應用,如下圖,我創建了一個GTP機器應用,配置訊息回呼地址,IP限制等資訊就可以了,配置回呼地址時,會有資料驗證,下面會說到,


配置代理服務器
因為一些原因,國內無法直接訪問,于是購買了一臺香港的服務器,安裝JKD jdk-8u202-linux-x64.tar.gz,使用RestTemplate發起Http請求, 測驗代碼比較簡單, 代碼如下,
HttpHeaders headers = new HttpHeaders(); headers.add("Content-Type", "application/json;charset=UTF-8"); headers.add("Authorization", "Bearer key"); headers.add("OpenAI-Organization", "org-id"); JSONObject textMsg=new JSONObject(); textMsg.put("model", question.getModel()); textMsg.put("temperature",0.7); JSONArray messages = new JSONArray(); JSONObject message = new JSONObject(); message.put("role", "user"); message.put("content", question.getQuestion()); messages.add(message); textMsg.put("messages", messages); HttpEntity<String> request = new HttpEntity<>(JSONObject.toJSONString(textMsg), headers); ResponseEntity<JSONObject> responseBody = restTemplate.postForEntity(URL, request, JSONObject.class); JSONObject httpBody = responseBody.getBody(); String answer = httpBody.getJSONArray("choices").getJSONObject(0).getJSONObject("message").getString("content"); System.out.println("介面回傳引數:" + httpBody.toJSONString());
其中 key 從api keys 中生成,ogi-id 在組織配置中查詢,


通過 Artifacts 方式生成jar, 通過命令 nohup java -jar gpt-test.jar & 跑起來,驗證下介面,一次通過,抬頭看時間已經2點多了,看起來簡單的事兒,也用了快3小時了,真是須知書上得來終覺淺、絕知此事要躬行,

內容回復
之前對接企微時,資料加解密代碼已經完成,后面的流程就非常簡單了,因為呼叫GPT介面比較慢,而自建應用要求在5s內回復,所以需要使用異步呼叫介面,主動回復自建應用,
驗證URL有效性
應用配置回呼域名時,有一個get 請求校驗合法性: 解碼收的加密內容,回復訊息的明文,
String method = httpServletRequest.getMethod(); if (!"POST".equals(method)) { if (StrUtil.isNotBlank(sVerifyEchoStr)) { String sEchoStr = ""; try { sEchoStr = wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr); log.info("verifyurl echostr: " + sEchoStr); } catch (Exception e) { //驗證URL失敗,錯誤原因請查看例外 log.error("驗證失敗", e); } response.getWriter().print(sEchoStr); } }
回復
因為等待GPT的時間比較長,所以我們查以先回復一條:“回復正在生成中”,提升下體驗,本來可以使用被動回復內容,一直沒成功,無奈也用主動回復來完成了, 接著發送一個異步事件,完成介面呼叫與回復,
String accessToken = this.getAccessToken(sendPersonMessageParam); String url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=ACCESS_TOKEN".replace("ACCESS_TOKEN",accessToken); MessageBody messageBody = new MessageBody(); messageBody.setTouser(sendPersonMessageParam.getToUserId()); messageBody.setMsgtype("text"); messageBody.setAgentid(sendPersonMessageParam.getAgentId()); Text text = new Text(); text.setContent(sendPersonMessageParam.getContent()); messageBody.setText(text); messageBody.setSafe(0); String execute = HttpRequest.post(url).body(JSONObject.toJSONString(messageBody)) .execute().body(); JSONObject jsonObject = JSONObject.parseObject(execute);
CreateQuestionEvent createQuestionEvent = new CreateQuestionEvent(); createQuestionEvent.setQuestion(jsonObject.getString("Content")); createQuestionEvent.setUserId(jsonObject.getString("FromUserName")); createQuestionEvent.setTimestamp(System.currentTimeMillis()); context.publishEvent(createQuestionEvent);

主動推送訊息
不知不覺中,時間來到3點多,媳婦突然醒了,看我還在挑燈夜戰,給我一頓說,在這個關鍵時刻,我只能說再給我15分鐘就Ok了,結果,她還真是一直等著,創建一個事件監聽器,呼叫代理服務,回復訊息,程序非常順利,一把通過,
log.info("收到問題創建事件:{}", event);
JSONObject body = new JSONObject();
body.put("model", "gpt-3.5-turbo");
body.put("question", event.getQuestion());
String execute = HttpRequest.post(ProxyUrl).body(JSONObject.toJSONString(body))
.execute().body();
log.info("問題回答:{}", execute);
SendPersonMessageParam sendPersonMessageParam = new SendPersonMessageParam();
sendPersonMessageParam.setCorpId(ReceiveWeComMsgController.corpId);
sendPersonMessageParam.setSecret(ReceiveWeComMsgController.secret);
sendPersonMessageParam.setAgentId(Integer.parseInt(ReceiveWeComMsgController.agentId));
sendPersonMessageParam.setToUserId(event.getUserId());
sendPersonMessageParam.setContent(execute);
weWorkService.sendPersonMessage(sendPersonMessageParam);

趕在4點前完成了整個流程,整個程序大概不到5小時,主要時間用在對于企微應用的對接,
最后貼下企微應用對接工具類,希望大家可以比我更快的對接,
package com.stbella.base.server.qw.util; import com.google.common.io.BaseEncoding; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Random; public class WXBizMsgCrypt { static Charset CHARSET = Charset.forName("utf-8"); byte[] aesKey; String token; String receiveid; /** * 建構式 * @param token 企業微信后臺,開發者設定的token * @param encodingAesKey 企業微信后臺,開發者設定的EncodingAESKey * @param receiveid, 不同場景含義不同,詳見檔案 * * @throws AesException 執行失敗,請查看該例外的錯誤碼和具體的錯誤資訊 */ public WXBizMsgCrypt(String token, String encodingAesKey, String receiveid) throws AesException { if (encodingAesKey.length() != 43) { throw new AesException(AesException.IllegalAesKey); } this.token = token; this.receiveid = receiveid; aesKey = BaseEncoding.base64().decode(encodingAesKey + "="); } // 生成4個位元組的網路位元組序 byte[] getNetworkBytesOrder(int sourceNumber) { byte[] orderBytes = new byte[4]; orderBytes[3] = (byte) (sourceNumber & 0xFF); orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF); orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF); orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF); return orderBytes; } // 還原4個位元組的網路位元組序 int recoverNetworkBytesOrder(byte[] orderBytes) { int sourceNumber = 0; for (int i = 0; i < 4; i++) { sourceNumber <<= 8; sourceNumber |= orderBytes[i] & 0xff; } return sourceNumber; } // 隨機生成16位字串 String getRandomStr() { String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; Random random = new Random(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < 16; i++) { int number = random.nextInt(base.length()); sb.append(base.charAt(number)); } return sb.toString(); } /** * 對明文進行加密. * * @param text 需要加密的明文 * @return 加密后base64編碼的字串 * @throws AesException aes加密失敗 */ String encrypt(String randomStr, String text) throws AesException { ByteGroup byteCollector = new ByteGroup(); byte[] randomStrBytes = randomStr.getBytes(CHARSET); byte[] textBytes = text.getBytes(CHARSET); byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length); byte[] receiveidBytes = receiveid.getBytes(CHARSET); // randomStr + networkBytesOrder + text + receiveid byteCollector.addBytes(randomStrBytes); byteCollector.addBytes(networkBytesOrder); byteCollector.addBytes(textBytes); byteCollector.addBytes(receiveidBytes); // ... + pad: 使用自定義的填充方式對明文進行補位填充 byte[] padBytes = PKCS7Encoder.encode(byteCollector.size()); byteCollector.addBytes(padBytes); // 獲得最終的位元組流, 未加密 byte[] unencrypted = byteCollector.toBytes(); try { // 設定加密模式為AES的CBC模式 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); // 使用BASE64對加密后的字串進行編碼 String base64Encrypted = BaseEncoding.base64().encode(encrypted); return base64Encrypted; } catch (Exception e) { e.printStackTrace(); throw new AesException(AesException.EncryptAESError); } } /** * 對密文進行解密. * * @param text 需要解密的密文 * @return 解密得到的明文 * @throws AesException aes解密失敗 */ String decrypt(String text) throws AesException { byte[] original; try { // 設定解密模式為AES的CBC模式 Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES"); IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16)); cipher.init(Cipher.DECRYPT_MODE, key_spec, iv); // 使用BASE64對密文進行解碼 byte[] encrypted = BaseEncoding.base64().decode(text); // 解密 original = cipher.doFinal(encrypted); } catch (Exception e) { e.printStackTrace(); throw new AesException(AesException.DecryptAESError); } String xmlContent, from_receiveid; try { // 去除補位字符 byte[] bytes = PKCS7Encoder.decode(original); // 分離16位隨機字串,網路位元組序和receiveid byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20); int xmlLength = recoverNetworkBytesOrder(networkOrder); xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET); from_receiveid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), CHARSET); } catch (Exception e) { e.printStackTrace(); throw new AesException(AesException.IllegalBuffer); } // receiveid不相同的情況 if (!from_receiveid.equals(receiveid)) { throw new AesException(AesException.ValidateCorpidError); } return xmlContent; } /** * 將企業微信回復用戶的訊息加密打包. * <ol> * <li>對要發送的訊息進行AES-CBC加密</li> * <li>生成安全簽名</li> * <li>將訊息密文和安全簽名打包成xml格式</li> * </ol> * * @param replyMsg 企業微信待回復用戶的訊息,xml格式的字串 * @param timeStamp 時間戳,可以自己生成,也可以用URL引數的timestamp * @param nonce 隨機串,可以自己生成,也可以用URL引數的nonce * * @return 加密后的可以直接回復用戶的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字串 * @throws AesException 執行失敗,請查看該例外的錯誤碼和具體的錯誤資訊 */ public String EncryptMsg(String replyMsg, String timeStamp, String nonce) throws AesException { // 加密 String encrypt = encrypt(getRandomStr(), replyMsg); // 生成安全簽名 if (timeStamp == "") { timeStamp = Long.toString(System.currentTimeMillis()); } String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt); // System.out.println("發送給平臺的簽名是: " + signature[1].toString()); // 生成發送的xml String result = XMLParse.generate(encrypt, signature, timeStamp, nonce); return result; } /** * 檢驗訊息的真實性,并且獲取解密后的明文. * <ol> * <li>利用收到的密文生成安全簽名,進行簽名驗證</li> * <li>若驗證通過,則提取xml中的加密訊息</li> * <li>對訊息進行解密</li> * </ol> * * @param msgSignature 簽名串,對應URL引數的msg_signature * @param timeStamp 時間戳,對應URL引數的timestamp * @param nonce 隨機串,對應URL引數的nonce * @param postData 密文,對應POST請求的資料 * * @return 解密后的原文 * @throws AesException 執行失敗,請查看該例外的錯誤碼和具體的錯誤資訊 */ public String DecryptMsg(String msgSignature, String timeStamp, String nonce, String postData) throws AesException { // 密鑰,公眾賬號的app secret // 提取密文 Object[] encrypt = XMLParse.extract(postData); // 驗證安全簽名 String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt[1].toString()); // 和URL中的簽名比較是否相等 // System.out.println("第三方收到URL中的簽名:" + msg_sign); // System.out.println("第三方校驗簽名:" + signature); if (!signature.equals(msgSignature)) { throw new AesException(AesException.ValidateSignatureError); } // 解密 String result = decrypt(encrypt[1].toString()); return result; } /** * 驗證URL * @param msgSignature 簽名串,對應URL引數的msg_signature * @param timeStamp 時間戳,對應URL引數的timestamp * @param nonce 隨機串,對應URL引數的nonce * @param echoStr 隨機串,對應URL引數的echostr * * @return 解密之后的echostr * @throws AesException 執行失敗,請查看該例外的錯誤碼和具體的錯誤資訊 */ public String VerifyURL(String msgSignature, String timeStamp, String nonce, String echoStr) throws AesException { String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr); if (!signature.equals(msgSignature)) { throw new AesException(AesException.ValidateSignatureError); } String result = decrypt(echoStr); return result; } }
結語
本來準備部門內先試用下,看看效果,也測測bug,老大把應用權限變成全公司可見后,還沒發通知,大家就踴躍的用了起來,問啥的都有, 上傳自己企業的訓練資料,創建個性化的模型,可能企業內部真的能用起來

體驗了兩周Github Copilt 感覺真是可以少寫不少代碼,以前,我認為編程是一件充滿創造性的作業,是一次次思維的旅行,不可取代,然而GPT出現了,大部分工種的作業方式都受到沖擊,非常多的機械的動作會被取代,包括編程,
思考自身的作業如果與GPT結合,如何擁抱Ai,讓他完成”低智”作業,讓自己從機械的作業中解放,提升自我效率,將會變得非常重要,
成為一名優秀的程式員!
著作權宣告:作者:J2

編輯:妞妞
妞妞主頁
出處:http://www.cnblogs.com/jijunjian/
本文著作權歸作者和博客園共有,歡迎轉載,大家好,才是真的好!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/548398.html
標籤:Java
