前言

众说周知,微信个人未经过认证的订阅号在接口权限上面有非常大的限制,例如:只能回复用户消息而不能主动推送;回复消息只能在三次微信推送的15秒内;回复用户消息有字符限制等等。这里主要是为了平衡国内调用ChatGPT接口的速度和微信公众号限制。

可以先关注订阅号体验一下,回复 #chatgptkey 可获得不限次数的密钥(国内不需要代理)用于学习或者测试。

微信公众号二维码

一、准备工作

  • 申请一个个人订阅号(很简单,不说了)
  • 到微信公众号的管理界面,点击 设置与开发 —> 基本配置
    开启服务器配置,自定义令牌,选择明文模式。
    微信公众平台配置
  • 如下所示,填写到配置文件中
#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接口回复逻辑,这里主要分为两方面:

  1. 当前对话是第一次调用,即:问问题
        //调用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));
            }
        }
  1. 当前对话是为了取出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