使用chatgpt实现微信聊天小程序(秒回复),github开源(附带链接)

Posted 秃头披风侠.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用chatgpt实现微信聊天小程序(秒回复),github开源(附带链接)相关的知识,希望对你有一定的参考价值。

文章目录

前言

我在前一段时间突发奇想,就使用java来调用chatgpt的接口,然后写了一个简单小程序,也上了热榜第一,java调用chatgpt接口,实现专属于自己的人工智能助手,事实上,这个程序毛病挺多的,最不能让人接受的一点就是返回速度非常缓慢(即使使用非常好多外网服务器)。

现在,我改进了一下程序,使用异步请求的方式,基本可以实现秒回复。并且还基于webSocket编写了一个微信小程序来进行交互,可以直接使用微信小程序来进行体验。

现在我将所有代码都上传了github(链接在文章结尾),大家可以clone下来,部署到服务器上,真正实现自己的聊天机器人!!!

ps:网上好多的小程序或者网站提供了chatgpt的聊天功能,但是多数都收费或者限制次数,我就在想,作为一个学计算机的,具备开源分享精神不是最基础的吗???本来官方提供的chatgpt3.5接口就是免费的,既然网上的不免费,那我就自己写一个免费的


效果展示

部分截图如下




原理说明

java调用chatgpt接口,实现专属于自己的人工智能助手 我说明了java调用chatgpt的基本原理,这里的代码就是对这个代码的改进,使用异步请求的方式来进行。

注意看官方文档,我们在请求时可以提供一个参数stream,然后就可以实现按照流的形式进行返回,这种方式基本可以做到没有延迟就给出答案。

由于这次改进的思路主要就是将请求改为了异步,其他的基本一样,所以就不做解释,直接给出代码了,代码上面都有注释

    /**
     * 这个方法用于测试的,可以在控制台打印输出结果
     *
     * @param chatGptRequestParameter 请求的参数
     * @param question                问题
     */
    public void printAnswer(ChatRequestParameter chatGptRequestParameter, String question) 
        asyncClient.start();
        // 创建一个post请求
        AsyncRequestBuilder asyncRequest = AsyncRequestBuilder.post(url);

        // 设置请求参数
        chatGptRequestParameter.addMessages(new ChatMessage("user", question));

        // 请求的参数转换为字符串
        String valueAsString = null;
        try 
            valueAsString = objectMapper.writeValueAsString(chatGptRequestParameter);
         catch (JsonProcessingException e) 
            e.printStackTrace();
        

        // 设置编码和请求参数
        ContentType contentType = ContentType.create("text/plain", charset);
        asyncRequest.setEntity(valueAsString, contentType);
        asyncRequest.setCharset(charset);

        // 设置请求头
        asyncRequest.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
        // 设置登录凭证
        asyncRequest.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey);

        // 下面就是生产者消费者模型
        CountDownLatch latch = new CountDownLatch(1);
        // 用于记录返回的答案
        StringBuilder sb = new StringBuilder();
        // 消费者
        AbstractCharResponseConsumer<HttpResponse> consumer = new AbstractCharResponseConsumer<HttpResponse>() 
            HttpResponse response;

            @Override
            protected void start(HttpResponse response, ContentType contentType) throws HttpException, IOException 
                setCharset(charset);
                this.response = response;
            

            @Override
            protected int capacityIncrement() 
                return Integer.MAX_VALUE;
            

            @Override
            protected void data(CharBuffer src, boolean endOfStream) throws IOException 
                // 收到一个请求就进行处理
                String ss = src.toString();
                // 通过data:进行分割,如果不进行此步,可能返回的答案会少一些内容
                for (String s : ss.split("data:")) 
                    // 去除掉data:
                    if (s.startsWith("data:")) 
                        s = s.substring(5);
                    
                    // 返回的数据可能是(DONE)
                    if (s.length() > 8) 
                        // 转换为对象
                        ChatResponseParameter responseParameter = objectMapper.readValue(s, ChatResponseParameter.class);
                        // 处理结果
                        for (Choice choice : responseParameter.getChoices()) 
                            String content = choice.getDelta().getContent();
                            if (content != null && !"".equals(content)) 
                                // 保存结果
                                sb.append(content);
                                // 将结果使用webSocket传送过去
                                System.out.print(content);
                            
                        
                    
                
            

            @Override
            protected HttpResponse buildResult() throws IOException 
                return response;
            

            @Override
            public void releaseResources() 
            
        ;

        // 执行请求
        asyncClient.execute(asyncRequest.build(), consumer, new FutureCallback<HttpResponse>() 

            @Override
            public void completed(HttpResponse response) 
                latch.countDown();
                chatGptRequestParameter.addMessages(new ChatMessage("assistant", sb.toString()));
                System.out.println("回答结束!!!");
            

            @Override
            public void failed(Exception ex) 
                latch.countDown();
                System.out.println("failed");
                ex.printStackTrace();
            

            @Override
            public void cancelled() 
                latch.countDown();
                System.out.println("cancelled");
            

        );
        try 
            latch.await();
         catch (InterruptedException e) 
            e.printStackTrace();
        
    

大家代码可以直接不看,反正最终的效果就是可以实现问了问题就返回结果。运行效果如下


可以发现,输出就类似于官方的那种效果,一个字一个字的输出


服务器端代码说明

我使用java搭建了一个简单的服务器端程序,提供最基础的用户登录校验功能,以及提供了WebSocket通信。

用户校验的代码

package com.ttpfx.controller;

import com.ttpfx.entity.User;
import com.ttpfx.service.UserService;
import com.ttpfx.utils.R;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author ttpfx
 * @date 2023/3/29
 */
@RestController
@RequestMapping("/user")
public class UserController 

    @Resource
    private UserService userService;

    public static ConcurrentHashMap<String, User> loginUser = new ConcurrentHashMap<>();

    public static ConcurrentHashMap<String, Long> loginUserKey = new ConcurrentHashMap<>();
    @RequestMapping("/login")
    public R login(String username, String password) 
        if (username == null) return R.fail("必须填写用户名");


        User user = userService.queryByName(username);
        if (user == null) return R.fail("用户名不存在");
        String targetPassword = user.getPassword();
        if (targetPassword == null) return R.fail("用户密码异常");
        if (!targetPassword.equals(password)) return R.fail("密码错误");

        loginUser.put(username, user);
        loginUserKey.put(username, System.currentTimeMillis());
        return R.ok(String.valueOf(loginUserKey.get(username)));
    

    @RequestMapping("/logout")
    public R logout(String username) 
        loginUser.remove(username);
        loginUserKey.remove(username);
        return R.ok();
    

    @RequestMapping("/checkUserKey")
    public R checkUserKey(String username, Long key)
        if (username==null || key == null)return R.fail("用户校验异常");
        if (!Objects.equals(loginUserKey.get(username), key))
            return R.fail("用户在其他地方登录!!!");
        
        return R.ok();
    

    @RequestMapping("/loginUser")
    public R loginUser()
        return R.ok("success",loginUser.keySet());
    


基于webSocket通信的代码

package com.ttpfx.server;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.ttpfx.entity.UserLog;
import com.ttpfx.model.ChatModel;
import com.ttpfx.service.UserLogService;
import com.ttpfx.service.UserService;
import com.ttpfx.vo.chat.ChatRequestParameter;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author ttpfx
 * @date 2023/3/28
 */
@Component
@ServerEndpoint("/chatWebSocket/username")
public class ChatWebSocketServer 

    /**
     * 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
     */
    private static int onlineCount = 0;
    /**
     * concurrent包的线程安全Map,用来存放每个客户端对应的MyWebSocket对象。
     */
    private static ConcurrentHashMap<String, ChatWebSocketServer> chatWebSocketMap = new ConcurrentHashMap<>();

    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    private Session session;
    /**
     * 接收的username
     */
    private String username = "";

    private UserLog userLog;

    private static UserService userService;
    private static UserLogService userLogService;

    @Resource
    public void setUserService(UserService userService) 
        ChatWebSocketServer.userService = userService;
    

    @Resource
    public void setUserLogService(UserLogService userLogService) 
        ChatWebSocketServer.userLogService = userLogService;
    

    private ObjectMapper objectMapper = new ObjectMapper();
    private static ChatModel chatModel;

    @Resource
    public void setChatModel(ChatModel chatModel) 
        ChatWebSocketServer.chatModel = chatModel;
    

    ChatRequestParameter chatRequestParameter = new ChatRequestParameter();

    /**
     * 建立连接
     * @param session 会话
     * @param username 连接用户名称
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("username") String username) 
        this.session = session;
        this.username = username;
        this.userLog = new UserLog();
        // 这里的用户id不可能为null,出现null,那么就是非法请求
        try 
            this.userLog.setUserId(userService.queryByName(username).getId());
         catch (Exception e) 
            e.printStackTrace();
            try 
                session.close();
             catch (IOException ex) 
                ex.printStackTrace();
            
        
        this.userLog.setUsername(username);
        chatWebSocketMap.put(username, this);
        onlineCount++;
        System.out.println(username + "--open");
    

    @OnClose
    public void onClose() 
        chatWebSocketMap.remove(username);
        System.out.println(username + "--close");
    

    @OnMessage
    public void onMessage(String message, Session session) 
        System.out.println(username + "--" + message);
        // 记录日志
        this.userLog.setDateTime(LocalDateTime.now());
        this.userLog.setPreLogId(this.userLog.getLogId() == null ? -1 : this.userLog.getLogId());
        this.userLog.setLogId(null);
        this.userLog.setQuestion(message);
        long start = System.currentTimeMillis();
        // 这里就会返回结果
        String answer = chatModel.getAnswer(session, chatRequestParameter, message);
        long end = System.currentTimeMillis();
        this.userLog.setConsumeTime(end - start);
        this.userLog.setAnswer(answer);
        userLogService.save(userLog);
    

    @OnError
    public void onError(Session session, Throwable error) 
        error.printStackTrace();
    

    public void sendMessage(String message) throws IOException 
        this.session.getBasicRemote().sendText(message);
    

    public static void sendInfo(String message, String toUserId) throws IOException <

微信小程序 | 借ChatGPT之手重构社交聊天小程序

一、 ChatGPT效果分析

体验过ChatGPT这一产品的小伙伴对于GPT模型的恢复效果不知道有没有一种让人感觉到真的在和真人交流的感觉。不管你的问题有多么的刁钻,它总是能以一种宠辱不惊的状态回复你。

但是对于一些很无理的要求,它有的时候也是很果断的😂

没有体验过的小伙伴也可以直接从效果图中看出,AI的每一句回答都是一个字一个字或者一小段一小段地给予回复,给人一种无比地丝滑感,这才是真的真的聊天啊!

那么这个时候,如果可以把ChatGPT这个AI的丝滑聊天动效直接迁移到我们现在使用的聊天场景中来,把这些死板的、一次性的消息框效果直接全量优化!让我们的社交更加具有趣味性!😜


二、关键技术点

针对这一效果我们静下心来思考一下你会发现:ChatGPT的这个聊天框的响应反馈不仅仅是有一个动态光标的存在,更重要的是它返回的真的有够快的。
试想一下,按照我们在日常开发中的发起Http请求业务开发过程中,都是在三次握手之后客户端与服务端才开始交流感情!而且都是要到后端处理完全部逻辑之后才进行数据的返回,然后前端再拿这些数据进行渲染操作,所以要做到这么快就有两种设想:

  • (1)后端处理完全部逻辑后速度返回,前端速度解析,然后配以光标效果进行渲染。(Bug:数据量一爆炸,前端的响应速度并不能保证!
  • (2)后端一边处理一边返回数据,前端同时接收并渲染。后端服务采用流式数据响应,从而实现不等待式实时渲染

2.1 前端动效的支持

ChatGPT中对话框进行文字输入的时候,我们可以明显看到,在每个文字的后面都有一个闪烁的光标,正是这一效果可以给予用户一种真在动态输入的感觉,让体验倍加丝滑!

要实现这一效果,我们可以使用定时器,每100毫秒逐个渲染出文本内容,并在文本后面添加了一个闪烁的光标。注意要在组件中设置ref属性来获取span元素的引用。

<template>
  <div>
    <span ref="text"></span><span ref="cursor" class="blink">_</span>
  </div>
</template>

<script>
export default 
  mounted() 
    const text = this.$refs.text;
    const cursor = this.$refs.cursor;
    const textContent = "这是一段需要逐个渲染的文字";
    let index = 0;
    
    setInterval(() => 
      if (index <= textContent.length) 
        text.textContent = textContent.slice(0, index);
        cursor.style.opacity = index % 2 === 0 ? 1 : 0;
        index++;
      
    , 100);
  ,
;
</script>

<style>
.blink 
  animation: blink-animation 1s steps(1) infinite;


@keyframes blink-animation 
  0% 
    opacity: 0;
  

  50% 
    opacity: 1;
  

  100% 
    opacity: 0;
  

</style>


2.2 消息数据的实时响应


在前端中,可以使用流式处理(Streaming)的方式,实时加载从 HTTP 请求返回的 JSON 数据。这种方式可以避免一次性加载大量数据所造成的性能问题,而是在数据流到达时逐步处理数据。

以下是使用流式处理加载 JSON 数据的示例代码:


function loadJSON(url, onData) 
  let xhr = new XMLHttpRequest()
  xhr.open('GET', url, true)
  xhr.responseType = 'json'
  xhr.onprogress = function() 
    let chunk = xhr.response.slice(xhr.loaded, xhr.response.length)
    onData(chunk)
  
  xhr.onload = function() 
    if (xhr.status === 200) 
      onData(xhr.response)
    
  
  xhr.send()

在上面的代码中,定义了一个 loadJSON 函数,该函数使用 XMLHttpRequest 对象发送 GET 请求,并指定 responseType:json 参数。然后,在 onprogress 事件中,获取从服务器返回的 JSON 数据的最新一块数据,并通过 onData 回调函数将数据传递给客户端。在 onload 事件中,将最后一块数据发送给客户端。


三、丝滑聊天功能实现

3.1 小程序端

  • 光标元素


  • 完整代码

<template>
	<view class="content">
		<view class="content-box" @touchstart="touchstart" id="content-box" :class="'content-showfn':showFunBtn">
			<!-- 背景图- 定位方式 -->
			<image class="content-box-bg" :src="_user_info.chatBgImg" :style=" height: imgHeight "></image>
			<view class="content-box-loading" v-if="!loading"><u-loading mode="flower"></u-loading></view>
			<view class="message" v-for="(item, index) in messageList" :key="index" :id="`msg-$item.hasBeenSentId`">
				<view class="message-item " :class="item.isItMe ? 'right' : 'left'">
					<image class="img" :src="item.fromUserHeadImg" mode="" @tap="linkToBusinessCard(item.fromUserId)"></image>
					<!-- contentType = 1 文本 -->
					<view class="content" v-if="item.contentType == 1">
						<!-- <span ref="text" value="item.content"></span><span ref="cursor" class="blink">_</span> -->
				<!-- 		 generateTextSpan(item,index) 
						<span :ref="'text'+index" :value="item.content" :index="index"></span>
						<span ref="cursor" class="blink">_</span> -->
						<chat-record :content="item.content"></chat-record>
					
					</view>
					<!-- <view class="content" v-if="item.contentType == 1"> item.content </view> -->
					<!-- contentType = 2 语音 -->
					<view
						class="content contentType2"
						:class="[ 'content-type-right': item.isItMe ]"
						v-if="item.contentType == 2"
						@tap="handleAudio(item)"
						hover-class="contentType2-hover-class"
						:style="width:`$130+(item.contentDuration*2)rpx`"
					>
						<view
							class="voice_icon"
							:class="[
								 voice_icon_right: item.isItMe ,
								 voice_icon_left: !item.isItMe ,
								 voice_icon_right_an: item.anmitionPlay && item.isItMe ,
								 voice_icon_left_an: item.anmitionPlay && !item.isItMe 
							]"
						></view>
						<view class=""> item.contentDuration ''</view>
					</view>
					<!-- contentType = 3 图片 -->
					<view 
						class="content contentType3" 	
						v-if="item.contentType == 3"
						@tap="viewImg([item.content])"
					>
						<image :src="item.content" class="img" mode="widthFix"></image>
					</view>
				</view>
			</view> 
		</view>
		
		<!-- 底部聊天输入框 -->
		<view class="input-box" :class=" 'input-box-mpInputMargin': mpInputMargin ">
			<view class="input-box-flex">
				<!-- #ifndef H5 -->
				<image v-if="chatType === 'voice'" class="icon_img" :src="require('@/static/voice.png')"  @click="switchChatType('keyboard')"></image>
				<image v-if="chatType === 'keyboard'" class="icon_img" :src="require('@/static/keyboard.png')"  @click="switchChatType('voice')"></image>
				<!-- #endif -->
				<view class="input-box-flex-grow"> 
					<input
						v-if="chatType === 'voice'"
						type="text"
						class="content"
						id="input"
						v-model="formData.content"
						:hold-keyboard="true"
						:confirm-type="'send'"
						:confirm-hold="true"
						placeholder-style="color:#DDDDDD;"
						:cursor-spacing="10"
						@confirm="sendMsg(null)"
					/>
					<view
						class="voice_title"
						v-if="chatType === 'keyboard'"
						:style=" background: recording ? '#c7c6c6' : '#FFFFFF' "
						@touchstart.stop.prevent="startVoice"
						@touchmove.stop.prevent="moveVoice"
						@touchend.stop="endVoice"
						@touchcancel.stop="cancelVoice"
					>
						 voiceTitle 
					</view>
				</view>
				
				<!-- 功能性按钮 -->
				<image class=" icon_btn_add" :src="require('@/static/add.png')" @tap="switchFun"></image>
				
				<!-- #ifdef H5 --> 
				<button class="btn" type="primary" size="mini" @touchend.prevent="sendMsg(null)">发送</button>
				<!-- #endif -->
			</view>
			
			<view class="fun-box" :class="'show-fun-box':showFunBtn">
				<u-grid :col="4"  hover-class="contentType2-hover-class" :border="false" @click="clickGrid">
					<u-grid-item v-for="(item, index) in funList" :index="index" :key="index" bg-color="#eaeaea">
						<u-icon :name="item.icon" :size="52"></u-icon>
						<view class="grid-text"> item.title </view>
					</u-grid-item>
				</u-grid>
			</view>

		</view>
		
		<!-- //语音动画 -->
		<view class="voice_an"  v-if="recording">
			<view class="voice_an_icon">
				<view id="one" class="wave"></view>
				<view id="two" class="wave"></view>
				<view id="three" class="wave"></view>
				<view id="four" class="wave"></view>
				<view id="five" class="wave"></view>
				<view id="six" class="wave"></view>
				<view id="seven" class="wave"></view>
			</view>
			<view class="text">voiceIconText</view>
		</view>
	</view>
</template>

<script>
	import chatRecord from '@/components/chatRecord/index.vue'
export default 
	components:chatRecord,
	data() 
		return 
			lines:[],
			fromUserInfo: ,
			formData: 
				content: '',
				limit: 15,
				index: 1
			,
			messageList: [],
			loading: true, //标识是否正在获取数据
			imgHeight: '1000px',
			mpInputMargin: false, //适配微信小程序 底部输入框高度被顶起的问题
			chatType:"voice",  // 图标类型 'voice'语音 'keyboard'键盘
			voiceTitle: '按住 说话',
			Recorder: uni.getRecorderManager(),
			Audio: uni.createInnerAudioContext(),
			recording: false, //标识是否正在录音
			isStopVoice: false, //加锁 防止点击过快引起的当录音正在准备(还没有开始录音)的时候,却调用了stop方法但并不能阻止录音的问题
			voiceInterval:null,
			voiceTime:0, //总共录音时长
			canSend:true, //是否可以发送
			PointY:0, //坐标位置
			voiceIconText:"正在录音...",
			showFunBtn:false, //是否展示功能型按钮
			AudioExam:null, //正在播放音频的实例
			funList: [
				 icon:"photo-fill",title:"照片",uploadType:["album"] ,
				 icon:"camera-fill",title:"拍摄",uploadType:["camera"] ,
			],
		;
	,
	 updated(<

以上是关于使用chatgpt实现微信聊天小程序(秒回复),github开源(附带链接)的主要内容,如果未能解决你的问题,请参考以下文章

微信小程序_调用openAi搭建虚拟伙伴聊天

如何突破微信小程序客服限制,在手机端实现聊天?

ChatGPT注册+微信自动回复

Python 004- 利用图灵小机器人来搭建微信聊天自动回复机器人

小程序版聊天室|聊天小程序|仿微信聊天界面小程序

小程序智能聊天机器人

(c)2006-2024 SYSTEM All Rights Reserved IT常识