草庐IT

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

秃头披风侠. 2023-04-14 原文

文章目录

前言

我在前一段时间突发奇想,就使用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 {
        chatWebSocketMap.get(toUserId).sendMessage(message);
    }
}

我们只需要编写简单的前端代码,就可以实现和后端的socket通信。对于后端,我们只需要改一下apiKey和数据库配置就可以直接运行了。


微信小程序代码说明

我写了一个简单微信小程序来和后端进行通信,界面如下



大家只需要下载源代码,然将程序中的ip改为自己服务器的ip即可


代码链接

github的地址为 https://github.com/c-ttpfx/chatgpt-java-wx
可以直接使用 git clone https://github.com/c-ttpfx/chatgpt-java-wx.git 下载代码到本地

我在github里面说明了安装使用的基本步骤,大家按照步骤使用即可

总结

上面聊天小程序就是我花2天写出来的,可能会有一些bug,我自己测试的时候倒是没有怎么遇到bug,聊天和登录功能都能正常使用。

对于微信小程序,由于我不是专业搞前端的,就只东拼西凑实现了最基本的功能(登录、聊天),大家可以自己写一个,反正后端接口都提供好了嘛,也不是很难,不想写也可以将就使用我的。

最后,也是最重要的,大家帮我的代码star一下!!! 感谢大家了(≥▽≤)/(≥▽≤)/

有关使用chatgpt实现微信聊天小程序(秒回复),github开源(附带链接)的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  4. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  5. ruby - 在 Ruby 中使用匿名模块 - 2

    假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

  6. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  7. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  8. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  9. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  10. ruby - 在 64 位 Snow Leopard 上使用 rvm、postgres 9.0、ruby 1.9.2-p136 安装 pg gem 时出现问题 - 2

    我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po

随机推荐