引子   

  最近最火的莫过于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,让他完成”低智”工作,让自己从机械的工作中解放,提升自我效率,将会变得非常重要。

   成为一名优秀的程序员!

GPT接入企微应用 - 让工作快乐起来的更多相关文章

  1. Prometheus + Alertmanager 实现企微告警

    上一篇:二进制安装Prometheus  下面准备在监控的流程中呈现到告警到企微 查看企业ID,用于后续配置文件 四.安装Alertmanager1.准备安装的包 --选择上面链接给的Linux的ta ...

  2. 企微云CRM操作指南 – 道一云|企微

    企微云CRM操作指南 – 道一云|企微https://wbg.do1.com.cn/xueyuan/2568.html 线索及线索池 – 道一云|企微https://wbg.do1.com.cn/xu ...

  3. SpringBoot 整合 Zookeeper 接入Starring微服务平台

    背景 最近接的一个项目是基于公司产品Starring做的微服务支付平台,纯后台项目,实现三方支付公司和银行接口来完成用户账户扣款,整合成通用支付接口发布给前端调用. 但是扯蛋了,这边前端什么都不想做, ...

  4. Spring Security整合企业微信的扫码登录,企微的API震惊到我了

    本文代码: https://gitee.com/felord/spring-security-oauth2-tutorial/tree/wwopen/ 现在很多企业都接入了企业微信,作为私域社群工具, ...

  5. k8s event监控利器kube-eventer对接企微告警

    背景 监控是保障系统稳定性的重要组成部分,在Kubernetes开源生态中,资源类的监控工具与组件监控百花齐放. cAdvisor:kubelet内置的cAdvisor,监控容器资源,如容器cpu.内 ...

  6. 极速体验|使用 Erda 微服务观测接入 Jaeger Trace

    在大型网站系统设计中,随着分布式架构,特别是微服务架构的流行,我们将系统解耦成更小的单元,通过不断的添加新的.小的模块或者重用已经有的模块来构建复杂的系统.随着模块的不断增多,一次请求可能会涉及到十几 ...

  7. 关于工作与生活——HP大中华区总裁孙振耀撰文谈退休并畅谈人生

    转自:http://blog.csdn.net/adaptiver/article/details/7494121 我有个有趣的观察,外企公司多的是25-35岁的白领, 40岁以上的员工很少,二三十岁 ...

  8. 我们工作到底为了什么 (HP大中华区总裁孙振耀退休感言)

    我们工作到底为了什么 (HP大中华区总裁孙振耀退休感言) 一.关于工作与生活    我有个有趣的观察,外企公司多的是25-35岁的白领,40岁以上的员工很少,二三十岁的外企员工是意气风发的,但外企公司 ...

  9. Nepxion Discovery【探索】微服务企业级解决方案

    Nepxion Discovery[探索]微服务企业级解决方案] Nepxion Discovery[探索]使用指南,基于Spring Cloud Greenwich版.Finchley版和Hoxto ...

  10. Spring Cloud Alibaba 新一代微服务解决方案

    本篇是「跟我学 Spring Cloud Alibaba」系列的第一篇, 每期文章会在公众号「架构进化论」进行首发更新,欢迎关注. 1.Spring Cloud Alibaba 是什么 Spring ...

随机推荐

  1. 升级adb

    adb 是没有自动升级的命令的,如果想要更新adb的版本,需要在网上找到自己想要的版本进行更新. 为什么要更新呢? 肯定是在使用中遇到了什么问题必须升级版本才能解决,如果不影响使用,那都无所谓.这里提 ...

  2. SQL查询 错误 [1843] [22008]: ORA-01843: 无效的月份

    dbeaver客户端运行sql查询Oracle库报错. 正确示例: select count(*) from PRODUCTS WHERE CREATE_TIME > '15-7月-2021 ' ...

  3. mysql之存储引擎-第二篇

    什么是存储引擎? 数据库存储引擎是数据库底层软件组件,数据库管理系统使用数据引擎进行创建,查询,更新和删除数据操作.不同的存储引擎提供了不同的存储机制,索引技巧及特定功能. 存储引擎类型 InnoDB ...

  4. react实现转盘动画

    转盘动画方法如下: /** * 点击转动转盘 */ const turnCircle = () => { let runDeg = +(Math.random() * 360).toFixed( ...

  5. 【alive-progress】Python控制台输出动态进度条

    简介 alive-progress是一种具有实时吞吐量和非常酷的动画新型的进度条python库. 使用 from alive_progress import alive_bar import time ...

  6. JAVA基础Day2-基本运算符/自增自减运算符/逻辑运算符、位运算符/包机制

    一.基本运算符 算术运算符:+.-.*./.%.++.-- 赋值运算符:= 关系运算符:>.<.>=.<=.==.!= instanceof 逻辑运算符:&&. ...

  7. uniapp的子组件,当父组件下来刷新时,子组件一同刷新。

    最近做uniapp项目的时候,使用给父组件一个刷新属性,父组件有效果,但是子组件没有反应,网上查找了很多方法,最终采取通过刷新时,函数传值,子组件监听的方式.具体做法可以参照:https://blog ...

  8. Java学习笔记2-1

    2.对象容器(1)   今天学习一下Java里面的一些容器的基本功能,今天先来Arraylist. 一.Arraylist   容器类主要是为了存放一些按某些方式排列的对象,arraylist是一种容 ...

  9. DTO的理解

    首要的作用,我认为就是减少原生对象的多余参数.包括为了安全,有时候也为了节约流量.例如:密码,你就不能返回到前端.因为不安全. 其次假如说:获取博客列表的时候,也不能返回博客全文吧.顶多就返回标题,i ...

  10. [Leetcode 111]二叉树的最短深度 BFS/DFS

    题目 给定二叉树,求最短路径包含的节点个数 https://leetcode.com/problems/minimum-depth-of-binary-tree/ Given a binary tre ...