前言
众说周知,微信个人未经过认证的订阅号在接口权限上面有非常大的限制,例如:只能回复用户消息而不能主动推送;回复消息只能在三次微信推送的15秒内;回复用户消息有字符限制等等。这里主要是为了平衡国内调用ChatGPT接口的速度和微信公众号限制。
扫码关注下面公众号,即可免费体验,还可以领取第三方代理的chatgpt密钥,免费调用chatgpt
最新版本请访问:微信公众号接入ChatGPT的dalle-2/3 绘图模型,实现公众号文生图功能详解,并支持docker部署
一、准备工作
- 申请一个个人订阅号(很简单,不说了)
- 到微信公众号的管理界面,点击 设置与开发 —> 基本配置
开启服务器配置,自定义令牌,选择明文模式。
- 如下所示,填写到配置文件中
#wechatmp
wechatmp:
#这里就是服务器配置中自己填写的 令牌(Token)
token: xxxxxxxxxxxx
#chatgpt
chatgpt:
model: gpt-3.5-turbo-1106
# openAI 的接口
apikey:
- sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#oepnai 接口基础地址 https://openai.xxx.com/ 或者使用自己的代理地址
baseUrl: https://openai.xxxx.com/
二、验证服务器配置中的服务器URL
当我们填写服务器配置的URL时候,是需要验证URL地址的,验证代码如下:
- Controller
@GetMapping("")
public ResponseEntity<Object> checkSignature(WeChatBean weChatBean) {
//验证是否为微信消息
String signatureHashcode = weChatService.checkWeChatSignature(weChatBean);
if (!signatureHashcode.equals(weChatBean.getSignature())) {
return ResponseEntity.ok("非法数据");
}
//微信公众号接口认证
if (StringUtils.isNotBlank(weChatBean.getEchostr())) {
return ResponseEntity.ok(weChatBean.getEchostr());
}
return ResponseEntity.ok(null);
}
- Service
@Override
public String checkWeChatSignature(WeChatBean weChatBean) {
String hashSignature = null;
if (StringUtils.isBlank(weChatBean.getTimestamp()) || StringUtils.isBlank(weChatBean.getNonce())) {
return hashSignature;
}
hashSignature = SignatureUtils.generateEventMessageSignature(wechatMpConfig.getToken(),
weChatBean.getTimestamp(), weChatBean.getNonce());
return hashSignature;
}
三、 接受订阅号用户对话,调用chatgpt接口
1、微信回复的处理
- 过滤掉不是文本的对话信息(暂时只处理文本对话)
if (!params.get("MsgType").equals("text")) {
return getReplyWeChat(weChatBean.getOpenid(), params.get("ToUserName"), "暂时只支持接收文本信息");
}
对于处理ChatGPT接口回复逻辑,这里主要分为两方面:
- 当前对话是第一次调用,即:问问题
//调用chatgpt
final String msgKey = String.format(CommonConstant.CHAT_WX_USER_MSG_REPLY_KEY, msgId);
if (!redisCacheUtils.hasKey(msgKey)) {
redisCacheUtils.setCacheObject(msgKey, success, 30, TimeUnit.SECONDS);
AsyncManager.me().execute(new TimerTask() {
@Override
public void run() {
if (StringUtils.isNotBlank(content)) {
redisCacheUtils.deleteObject(waitKey);
chatgptService.singleChatStreamToWX(weChatBean.getOpenid(), msgId, content);
}
}
});
}
while (messageCountMap.containsKey(msgId)) {
String replay = checkMessageCountMap(msgId, currentMsgCount, start, toUserName, weChatBean);
if (null != replay) {
return replay;
}
Object o = redisCacheUtils.getCacheObject(msgKey);
if (!success.equals(String.valueOf(o))) {
messageCountMap.remove(msgId);
redisCacheUtils.deleteObject(Arrays.asList(msgKey, waitKey));
return getReplyWeChat(weChatBean.getOpenid(), toUserName, String.valueOf(o));
}
}
- 当前对话是为了取出ChatGPT的回答,即:输入 “继续”
if (StringUtils.isNotBlank(content) && content.equals("继续")) {
while (messageCountMap.containsKey(msgId)) {
String replay = checkMessageCountMap(msgId, currentMsgCount, start, toUserName, weChatBean);
if (null != replay) {
return replay;
}
if (redisCacheUtils.hasKey(waitKey)) {
Object o = redisCacheUtils.getCacheObject(waitKey);
Integer contentLength = getByteSize(String.valueOf(o));
messageCountMap.remove(msgId);
if (contentLength < 2048) {
redisCacheUtils.deleteObject(waitKey);
return getReplyWeChat(weChatBean.getOpenid(), toUserName, String.valueOf(o));
} else {
String replyContent = String.valueOf(o).substring(0, 580);
redisCacheUtils.setCacheObject(waitKey, String.valueOf(o).replace(replyContent, ""), 60, TimeUnit.MINUTES);
return getReplyWeChat(weChatBean.getOpenid(), toUserName, replyContent + "\n (公众号回复字符限制,输入\"继续\"查看后续内容)");
}
}
}
return success;
}
注:这里两处的 while循环操作,主要是为了尽可能的在三次微信消息推送的15秒内给出ChatGPT的回答,而不是让用户多次输入继续。
2、调用chatgpt接口处理
- 调用接口分为单轮对话和多轮对话(这里使用了开源的openai调用sdk)
<dependency>
<groupId>com.unfbx</groupId>
<artifactId>chatgpt-java</artifactId>
<version>1.1.5</version>
</dependency>
/**
* 多轮会话
* @param openId 用户openid
* @param msgId 消息id
* @param content 问话内容
*/
@Override
public void multiChatStreamToWX(String openId, String msgId, String content) {
OpenAiStreamClient streamClient = getStreamClient();
WeChatEventSourceListener weChatEventSourceListener = new WeChatEventSourceListener(openId, msgId);
//获取历史会话记录
List<Message> messages = getWxMessageList(openId, content);
ChatCompletion chatCompletion = ChatCompletion.builder().stream(true).messages(messages).build();
streamClient.streamChatCompletion(chatCompletion, weChatEventSourceListener);
}
/**
* 单轮会话
* @param openId 用户openid
* @param msgId 消息id
* @param content 问话内容
*/
@Override
public void singleChatStreamToWX(String openId, String msgId, String content) {
OpenAiStreamClient streamClient = getStreamClient();
WeChatEventSourceListener weChatEventSourceListener = new WeChatEventSourceListener(openId, msgId);
Message message = Message.builder().role(BaseMessage.Role.USER).content(content).build();
ChatCompletion chatCompletion = ChatCompletion.builder().stream(true).messages(Arrays.asList(message)).build();
streamClient.streamChatCompletion(chatCompletion, weChatEventSourceListener);
}
- 这里处理opeAI接口接口返回,都是使用sse的形式,因此所有的结果处理都是在WeChatEventSourceListener中
public WeChatEventSourceListener(String openId, String msgId) {
this.openId = openId;
this.msgId = msgId;
this.sb = new StringBuffer();
}
@Override
public void onClosed(@NotNull EventSource eventSource) {
log.info("OpenAI关闭sse连接...");
//缓存回复到redis
redisCacheUtils.setCacheObject(waitKey, sb.toString(), 60, TimeUnit.MINUTES);
redisCacheUtils.setCacheObject(msgKey, sb.toString(), 60, TimeUnit.SECONDS);
eventSource.cancel();
}
@Override
public void onEvent(@NotNull EventSource eventSource, @Nullable String id, @Nullable String type, @NotNull String data) {
log.debug("OpenAI返回数据:{}", data);
if (!"[DONE]".equals(data)) {
ChatCompletionResponse response = JSON.parseObject(data, ChatCompletionResponse.class);
if (null == response.getChoices().get(0).getFinishReason()) {
String content = response.getChoices().get(0).getDelta().getContent();
sb.append(content);
}
} else {
log.info("OpenAI返回数据结束了");
}
}
结束语
以上只是实现了简单的订阅号对话ChatGPT,下面是本项目开源地址
项目开源地址: wechatgpt 微信订阅号对接 ChatGPT4.0