SSE[Server-Sent Events]实现页面流式数据输出(模拟ChatGPT流式输出)

news2024/11/18 12:48:15

文章目录

    • 前言
    • SSE 简介
      • 应用场景区分
      • 浏览器支撑性
    • 实现过程
    • Web VUE核心解析数据代码
    • 实例demo
    • 参考

前言

        服务端向客户端推送消息,除了用WebSocket可实现,还有一种服务器发送事件(Server-Sent Events)简称 SSE,这是一种服务器端到客户端(浏览器)的单向消息推送。ChatGPT 就是采用的 SSE。对于需要长时间等待响应的对话场景,ChatGPT 采用了一种巧妙的策略:它会将已经计算出的数据“推送”给用户,并利用 SSE 技术在计算过程中持续返回数据。这样做的好处是可以避免用户因等待时间过长而选择关闭页面。

SSE 简介

        SSE 基于 HTTP 协议的,我们知道一般意义上的 HTTP 协议是无法做到服务端主动向客户端推送消息的,但 SSE 是个例外,它变换了一种思路。
在这里插入图片描述
SSE 在服务器和客户端之间打开一个单向通道,服务端响应的不再是一次性的数据包而是text/event-stream类型的数据流信息,在有数据变更时从服务器流式传输到客户端。整体的实现思路有点类似于在线视频播放,视频流会连续不断的推送到浏览器,你也可以理解成,客户端在完成一次用时很长(网络不畅)的下载。
在这里插入图片描述
SSE 与 WebSocket 作用相似,都可以建立服务端与浏览器之间的通信,实现服务端向客户端推送消息,但还是有些许不同:

  • SSE 是基于 HTTP 协议的,它们不需要特殊的协议或服务器实现即可工作;WebSocket 需单独服务器来处理协议。
  • SSE 单向通信,只能由服务端向客户端单向通信;WebSocket 全双工通信,即通信的双方可以同时发送和接受信息。
  • SSE 实现简单开发成本低,无需引入其他组件;WebSocket 传输数据需做二次解析,开发门槛高一些
  • SSE 默认支持断线重连;WebSocket 则需要自己实现。
  • SSE 只能传送文本消息,二进制数据需要经过编码后传送;WebSocket 默认支持传送二进制数据。

应用场景区分

  • WebSocket: 提供更丰富的协议来执行双向、全双工通信。对于游戏、即时通信以及需要双向近乎实时更新的场景,拥有双向通道更具吸引力。
  • 而某些情况下,不需要从客户端发送数据,而只需要一些服务器操作的更新。比如:站内信、未读消息数、状态更新、股票行情、监控数量等场景。SSE 具有 WebSocket 在设计上缺乏的多种功能,例如:自动重新连接、事件 ID 和发送任意事件的能力。前端只需进行一次 HTTP 请求,带上唯一 ID,打开事件流,监听服务端推送的事件就可以反向不定期的推送实时数据。SEE 从实现的难易和成本上都更有优势。

浏览器支撑性

SSE 不支持 IE 浏览器,对其他主流浏览器兼容性做的还不错。
在这里插入图片描述

实现过程

环境提示:JDK8 、 Spring Boot 2.5.15 、Spring framework 5.3.27
1、配置与初始化工作
导入pom依赖

<dependency>
    <groupId>com.unfbx</groupId>
    <artifactId>chatgpt-java</artifactId>
    <version>1.0.12</version>
</dependency>

启动类自定义OkHttpClient客户端和OpenAi流客户端OpenAiStreamClient使用Bean对象

package com.merak;
import com.unfbx.chatgpt.OpenAiStreamClient;
import com.unfbx.chatgpt.function.KeyRandomStrategy;
import com.unfbx.chatgpt.interceptor.OpenAILogger;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
 * 启动程序
 */
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
@ComponentScan(basePackages = "com.*")
public class MerakAdminApplication {

    public static void main(String[] args)
    {
        SpringApplication.run(MerakAdminApplication.class, args);
    }

    @Bean
    public OpenAiStreamClient openAiStreamClient() {
        //本地开发需要配置代理地址
//        Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 8002));
        HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new OpenAILogger());
        //!!!!!!测试或者发布到服务器千万不要配置Level == BODY!!!!
        httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
        //自定义OkHttpClient客户端
        OkHttpClient okHttpClient = new OkHttpClient
                .Builder()
//                .proxy(proxy)
                .addInterceptor(httpLoggingInterceptor)
                .connectTimeout(30, TimeUnit.SECONDS)
                .writeTimeout(600, TimeUnit.SECONDS)
                .readTimeout(600, TimeUnit.SECONDS)
                .build();
        //OpenAi流客户端OpenAiStreamClient,http://127.0.0.1:8080/ 为服务侧查询数据的流式接口,也必须返回text/event-stream类型
        return OpenAiStreamClient
                .builder()
                .apiHost("http://127.0.0.1:8080/")
                .apiKey(Arrays.asList("1","2"))
                //自定义key使用策略 默认随机策略
                .keyStrategy(new KeyRandomStrategy())
                .okHttpClient(okHttpClient)
                .build();
    }
}

定义SSE服务类[SseService]和实现类[SseServiceImpl]

public interface SseService {
    /**
     * 创建SSE
     * @param uid
     * @return
     */
    SseEmitter createSse(String uid);

    /**
     * 关闭SSE
     * @param uid
     */
    void closeSse(String uid);

    /**
     * 客户端发送消息到服务端
     */
    boolean sseChat(String uid, String chatRequestInfo, JSONObject jsonObject, Long userId);
}

实现类代码见源代码

LocalCache配置类
定义缓存对象CACHE存储每个请求UUid和SseEmitter关系,并定时清理

package com.merak.web.controller.stream.config;

import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.TimedCache;
import cn.hutool.core.date.DateUnit;

/**
 * 定义缓存对象CACHE存储每个请求UUid和SseEmitter关系,并定时清理
 *
 */
public class LocalCache {
    /**
     * 缓存时长
     */
    public static final long TIMEOUT = 5 * DateUnit.MINUTE.getMillis();
    /**
     * 清理间隔
     */
    private static final long CLEAN_TIMEOUT = 5 * DateUnit.MINUTE.getMillis();
    /**
     * 缓存对象
     */
    public static final TimedCache<String, Object> CACHE = CacheUtil.newTimedCache(TIMEOUT);

    static {
        //启动定时任务
        CACHE.schedulePrune(CLEAN_TIMEOUT);
    }
}

定义OpenAISSEEventSourceListener类
继承、重载EventSourceListener抽象类建立sse连接-onOpen、监听事件->解析响应数据-onEvent、关闭-onClosed以及异常onFailure 等方法。

package com.merak.web.controller.stream.listener;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.unfbx.chatgpt.entity.chat.MerakChatCompletionResponse;
import com.unfbx.chatgpt.entity.chat.Message;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.*;
/**
 * 描述:定义OpenAISSEEventSourceListener类,继承EventSourceListener抽象类
 */
@Slf4j
public class OpenAISSEEventSourceListener extends EventSourceListener {
    private static final Logger log = LoggerFactory.getLogger(OpenAISSEEventSourceListener.class);
    private SseEmitter sseEmitter;
    private String uid;
    private JSONObject jsonObject;
    private Long userId;

    /**
     * 结构化
     */
    public OpenAISSEEventSourceListener(SseEmitter sseEmitter, String uid, JSONObject jsonObject, Long userId) {
        this.sseEmitter = sseEmitter;
        this.uid = uid;
        this.jsonObject = jsonObject;
        this.userId = userId;
    }

    /**
     * 建立sse连接
     */
    @Override
    public void onOpen(EventSource eventSource, Response response) {
        log.info("response=" + response);
        log.info("sseEmitter uid=" + uid);
        log.info("OpenAI建立sse连接...");
    }

    /**
     * 监听事件->解析响应数据
     */
    @Override
    public void onEvent(EventSource eventSource, String id, String type, String data) {
        log.info("sseEmitter uid=" + uid + ",OpenAI返回数据:{}", data);
        if (data.equals("[DONE]")) {
            log.info("OpenAI返回数据结束了");
            try {
                String answerContext = "[DONE]";
                sseEmitter.send(answerContext);// .id("[DONE]" reconnectTime(30000)
                // 传输完成后自动关闭sse
                sseEmitter.complete();
                return;
            } catch (IOException e) {
                log.error("onEvent error msg=" + e.getMessage());
            }
        }
        ObjectMapper mapper = new ObjectMapper();
        //由于业务特性需要自定义MerakChatCompletionResponse -> 重写原响应消息对象CompletionResponse [说明:没要求可以不重写]
        MerakChatCompletionResponse completionResponse = null;
        String answerContext = "";
        try {
            completionResponse = mapper.readValue(data, MerakChatCompletionResponse.class); //转换成自定义MerakChatCompletionResponse
            String finishReason = completionResponse.getChoices().get(0).getFinishReason();
            if ("stop".equals(finishReason)) {
                //在原始Json串的"delta"节点增加自定义的业务数据节点bussinessDataNodeJson
                String bussinessDataNodeJson = "{\"userId\":\"" + userId + "\"}";
                //{"id": "chatcmpl-3BpHEcUKNMUk7jbWkKB2gU", "object": "chat.completion.chunk", "created": 1699844126, "model": "chatglm2-6b-32k", "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]}
                JSONObject originalObj = JSON.parseObject(data);
                originalObj.remove("body");
                JSONObject targetObj = JSON.parseObject(bussinessDataNodeJson);
                //将自定义的目标Json串添加到原始Json串的"delta"节点后面
                originalObj.getJSONArray("choices").getJSONObject(0).put("delta", targetObj);
                String lastData = originalObj.toJSONString();
                sseEmitter.send(SseEmitter.event()
                        .id(completionResponse.getId())
                        .data(lastData)
                        .reconnectTime(3000));
            } else {
                //过滤Message delta节点有内容的有效内容
                Message delta = completionResponse.getChoices().get(0).getDelta();
                if (null != delta) {
                    answerContext = delta.getContent();
                    if (null != answerContext && !StringUtils.isBlank(answerContext)) {
                        Thread.sleep(50);
                        log.info("休眠50毫秒");
                        sseEmitter.send(SseEmitter.event()
                                .id(completionResponse.getId())
                                .data(data)
                                .reconnectTime(3000));
                    }
                }
            }
        } catch (Exception e) {
            log.error("sse信息推送失败!");
            eventSource.cancel();
            log.error("sse信息推送失败,error msg=" + e.getMessage());
        }
    }

    @Override
    public void onClosed(EventSource eventSource) {
        log.info("OpenAI关闭sse连接...");
    }


    @SneakyThrows
    @Override
    public void onFailure(EventSource eventSource, Throwable t, Response response) {
        log.info("OpenAI 连接操作失败...");
        if (Objects.isNull(response)) {
            return;
        }
        ResponseBody body = response.body();
        if (Objects.nonNull(body)) {
            try {
                log.error("OpenAI sse连接异常data:{},异常:{}", body.string(), t);
            } catch (IOException e) {
                log.error("OpenAI sse连接异常,error msg=" + e.getMessage());
            }
        } else {
            log.error("OpenAI  sse连接异常data:{},异常:{}", response, t);
        }
        eventSource.cancel();
    }

}

2、业务控制类:智能问答,接收Web客户端请求并向调用数据服务

package com.merak.web.controller.knowledge;
import com.alibaba.fastjson2.JSONObject;
import com.merak.common.core.controller.BaseController;
import com.merak.common.utils.uuid.UUID;
import com.merak.web.controller.stream.config.LocalCache;
import com.merak.web.service.AianswerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
 * 4.7.机器人智能问答管理
 */
@RestController
@RequestMapping("/aianswer")
public class AianswerController extends BaseController {
    @Autowired
    private AianswerService aianswerService;

    /**
     * 机器人智能问答:客户端发送消息到服务端-流式响应
     * @param jsonObject 智能问答对象 {"robotId":"1","aiQuestion":"请介绍chatgpt"}
     * @return emitter SseEmitter
     * produces:request请求头中的(Accept)类型包含text/event-stream时,指定返回内容类型text/event-stream;
     */
    @PostMapping(path = "/streamAsk", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public ResponseBodyEmitter streamAskBak(@RequestBody JSONObject jsonObject) {
        SseEmitter emitter = null;
        try {
            String uid = UUID.randomUUID().toString().replace("-", "");
            Long userId = 1L;
            boolean blnAnswer = aianswerService.aianswerStreamAsk(jsonObject, userId, uid);
            logger.info("sse 完成连接和发送请求,blnAnswer=" + blnAnswer);
            if (blnAnswer) {
                emitter = (SseEmitter) LocalCache.CACHE.get(uid);
            }
        } catch (Exception e) {
            logger.info("机器人智能问答新增error msg=" + e.getMessage());
        }
        return emitter;
    }

}

智能问答实现方法-aianswerStreamAsk

package com.merak.web.service;
import com.alibaba.fastjson2.JSONObject;
import com.merak.web.controller.stream.service.SseService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
/**
 * 智能问答-服务层
 */
@Service
public class AianswerService {
    private static final Logger logger = LoggerFactory.getLogger(AianswerService.class);
    @Autowired
    private SseService sseService;

    public boolean aianswerStreamAsk(JSONObject jsonObject, Long userId, String uid) {
        boolean bln = false;
        String prompt = "";//提示词
        try {
            //1.拼装问答提问入参chatCompletion
            String robotId = (String) jsonObject.get("robotId");//提问机器人
            String originalQuestion = (String) jsonObject.get("aiQuestion");//提问问题
            Map<String, Object> chatInfo = new HashMap();
            chatInfo.put("source_question", originalQuestion);
            chatInfo.put("prompt", prompt);
            chatInfo.put("temperature", 1);
            chatInfo.put("top_p", 1);
            chatInfo.put("history", null);
            String chatCompletion = JSONObject.toJSONString(chatInfo);

            //2.创建SSE
            sseService.createSse(uid);
            logger.info("chatCompletion=" + chatCompletion);
            //3.客户端发送消息到服务端
            bln = sseService.sseChat(uid, chatCompletion, jsonObject, userId);
        } catch (Exception e) {
            logger.error("机器人智能问答失败,error msg=" + e.getMessage());
        }
        return bln;
    }
}

客户端发送消息到服务端-sseService.sseChat

@Override
    public boolean sseChat(String uid, String chatCompletion, JSONObject jsonObject, Long userId) {
        boolean bln = true;
        try {
            //从缓存对象TimedCache内获取SseEmitter
            SseEmitter sseEmitter = (SseEmitter) LocalCache.CACHE.get(uid);
            if (sseEmitter == null) {
                log.info("消息推送失败uid:[{}],没有创建连接,请重试。", uid);
                throw new BaseException("消息推送失败uid:[{}],没有创建连接,请重试。~");
            }
            OpenAISSEEventSourceListener openAIEventSourceListener = new OpenAISSEEventSourceListener(sseEmitter, uid, jsonObject, userId);
            openAiStreamClient.streamChatCompletion(chatCompletion, openAIEventSourceListener);
        }catch (Exception e){
            bln = false;
            log.info("[{}]消息推送失败失败!", uid);
        }
        return bln;
    }

服务端调用ChatGPT API方法streamChatCompletion 实现同最底层数据提供服务方通信
即调用openAiStreamClient.streamChatCompletion(chatCompletion, openAIEventSourceListener);方法
发送的API接口地址为:String streamUrl = this.apiHost + "v1/completions";

响应的实时数据

{"id": "chatcmpl-42rNbjJ9WFAnMGF9cNZTwn", "model": "chatglm2-6b-32k", "choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": null}]}
{"id": "chatcmpl-42rNbjJ9WFAnMGF9cNZTwn", "model": "chatglm2-6b-32k", "choices": [{"index": 0, "delta": {"content": "尊敬"}, "finish_reason": null}]}
{"id": "chatcmpl-42rNbjJ9WFAnMGF9cNZTwn", "model": "chatglm2-6b-32k", "choices": [{"index": 0, "delta": {"content": "的"}, "finish_reason": null}]}
{"id": "chatcmpl-42rNbjJ9WFAnMGF9cNZTwn", "model": "chatglm2-6b-32k", "choices": [{"index": 0, "delta": {"content": "客户"}, "finish_reason": null}]}
{"id": "chatcmpl-42rNbjJ9WFAnMGF9cNZTwn", "model": "chatglm2-6b-32k", "choices": [{"index": 0, "delta": {"content": ","}, "finish_reason": null}]}
{"id": "chatcmpl-42rNbjJ9WFAnMGF9cNZTwn", "model": "chatglm2-6b-32k", "choices": [{"index": 0, "delta": {"content": "您好"}, "finish_reason": null}]}
。。。。。。
{"id": "chatcmpl-42rNbjJ9WFAnMGF9cNZTwn", "model": "chatglm2-6b-32k", "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]}
[DONE]

3、监听响应的实时数据
EventSourceListener onEvent()监听事件->解析响应数据:

    /**
     * 监听事件->解析响应数据
     */
    @Override
    public void onEvent(EventSource eventSource, String id, String type, String data) {
        log.info("sseEmitter uid=" + uid + ",OpenAI返回数据:{}", data);
        if (data.equals("[DONE]")) {
            log.info("OpenAI返回数据结束了");
            try {
                String answerContext = "[DONE]";
                sseEmitter.send(answerContext);// .id("[DONE]" reconnectTime(30000)
                // 传输完成后自动关闭sse
                sseEmitter.complete();
                return;
            } catch (IOException e) {
                log.error("onEvent error msg=" + e.getMessage());
            }
        }
        ObjectMapper mapper = new ObjectMapper();
        //由于业务特性需要自定义MerakChatCompletionResponse -> 重写原响应消息对象CompletionResponse [说明:没要求可以不重写]
        MerakChatCompletionResponse completionResponse = null;
        String answerContext = "";
        try {
            completionResponse = mapper.readValue(data, MerakChatCompletionResponse.class); //转换成自定义MerakChatCompletionResponse
            String finishReason = completionResponse.getChoices().get(0).getFinishReason();
            if ("stop".equals(finishReason)) {
                //在原始Json串的"delta"节点增加自定义的业务数据节点bussinessDataNodeJson
                String bussinessDataNodeJson = "{\"userId\":\"" + userId + "\"}";
                //{"id": "chatcmpl-3BpHEcUKNMUk7jbWkKB2gU", "object": "chat.completion.chunk", "created": 1699844126, "model": "chatglm2-6b-32k", "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]}
                JSONObject originalObj = JSON.parseObject(data);
                originalObj.remove("body");
                JSONObject targetObj = JSON.parseObject(bussinessDataNodeJson);
                //将自定义的目标Json串添加到原始Json串的"delta"节点后面
                originalObj.getJSONArray("choices").getJSONObject(0).put("delta", targetObj);
                String lastData = originalObj.toJSONString();
                sseEmitter.send(SseEmitter.event()
                        .id(completionResponse.getId())
                        .data(lastData)
                        .reconnectTime(3000));
            } else {
                //过滤Message delta节点有内容的有效内容
                Message delta = completionResponse.getChoices().get(0).getDelta();
                if (null != delta) {
                    answerContext = delta.getContent();
                    if (null != answerContext && !StringUtils.isBlank(answerContext)) {
                        Thread.sleep(50);
                        log.info("休眠50毫秒");
                        sseEmitter.send(SseEmitter.event()
                                .id(completionResponse.getId())
                                .data(data)
                                .reconnectTime(3000));
                    }
                }
            }
        } catch (Exception e) {
            log.error("sse信息推送失败!");
            eventSource.cancel();
            log.error("sse信息推送失败,error msg=" + e.getMessage());
        }
    }

4、sseEmitter向Web客户端实时推送数据,将数据实时写入outputMessage Body体内

 sseEmitter.send(SseEmitter.event().id(completionResponse.getId())
           .data(lastData).reconnectTime(3000));

流程说明:

  sseEmitter.send() -> 
  ResponseBodyEmitter.sendInternal(Object object, @Nullable MediaType mediaType)  ->
  this.handler.send(object, mediaType) ->
  ResponseBodyEmitterReturnValueHandler.sendInternal(T data, @Nullable MediaType mediaType)  ->
   {  converter.write(data, mediaType, this.outputMessage);
     this.outputMessage.flush(); 
   }                                  ->
   
   public final void write(final T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        HttpHeaders headers = outputMessage.getHeaders();
        this.addDefaultHeaders(headers, t, contentType);
        if (outputMessage instanceof StreamingHttpOutputMessage) {
            StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage)outputMessage;
            streamingOutputMessage.setBody((outputStream) -> {
                this.writeInternal(t, new HttpOutputMessage() {
                    public OutputStream getBody() {
                        return outputStream;
                    }
                    public HttpHeaders getHeaders() {
                        return headers;
                    }
                });
            });
        } else {
            this.writeInternal(t, outputMessage);
            outputMessage.getBody().flush();
        }

    }

Web VUE核心解析数据代码

<script setup name="Chat">

  // 核心解析服务端方法requestStreamingChat
async function requestStreamingChat(message,index,subBussiness) {
  const url = `${window.location.origin}${import.meta.env.VITE_APP_BASE_API}/aianswer/streamAsk`

  controller = new AbortController()
  const reqTimeoutId = setTimeout(() => controller.abort(), 30000)

  try {
    let respString = ''
    fetchEventSource(url,{
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${getToken()}`,
      },
      signal: controller.signal,
      body: JSON.stringify({
        robotId: currentChatRobot.value?.robotId,
        knowledgeId:currentChatRobot.value?.knowledgeId,
        aiQuestion: message,
        aiQuestionHistory: subBussiness?bussinessProcess.historyQa:history.value,
        classifyFrom:'1',
        bussinessType:subBussiness?bussinessProcess.bussinessType:'',
        bussinessContent:subBussiness?bussinessProcess.formData:''
      }),
      async onopen(response) {
        console.log(response)
        if (response.ok && response.headers.get('content-type')?.includes('text/event-stream')) {
          // everything's good
          console.log('everything\'s good')
        } else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
          chatStore.updateChat(
            index,
            {
              dateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
              text: 'Stream Error',
              inversion: false,
              error: true,
              loading: false,
              streaming: false,
              requestOptions: { prompt: message },
            },
          )
          scrollToBottomIfAtBottom()
        } else {
          console.log('其他错误')
          finish()
        }
      },
      async onmessage(event) {
        console.log(event)
        // 表示整体结束
        if (event.data === '[DONE]') {
          console.log('结束')
          finish()
          return
        }
        if (event.data) {
          const jsonData = JSON.parse(event.data)
          // 如果等于stop表示结束
          if (jsonData.choices[0].finish_reason === 'stop') {
            //接收会话信息
            const info = jsonData.choices[0].delta
            chatStore.updateChatSome(
              index,
              {
                dateTime: info?.occurTime,
                aiId: info?.aiId,
                solveIs: info?.solveIs
              }
            )
            return
          }
          // 判断role存在,进行排除
          if (jsonData.choices[0].delta.role !== undefined) {
            respString = jsonData.choices[0].delta.role + ': '
            return
          }
          if (jsonData.choices[0].delta.content !== undefined) {
            respString += jsonData.choices[0].delta.content
            console.log(respString)
            chatStore.updateChat(
              index,
              {
                dateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),//res?.data?.occurTime,
                text: respString,
                error: false,
                loading: false,
                streaming: true,
                requestOptions: { prompt: message },
              },
            )
            scrollToBottomIfAtBottom()
          }
        }        
      },
      async onerror(error) {
        console.error('Error:', error)
        finish()
      },
      async onclose() {
        loading.value = false
        finish()
        // if the server closes the connection unexpectedly, retry:
        console.log('关闭连接')
      }
    })

    const finish = () => {      
      loading.value = false
      chatStore.updateChatSome(index, { streaming: false,loading: false })
      reqTimeoutId && clearTimeout(reqTimeoutId)
      controller.abort()
    }
  } catch (error) {
    loading.value = false
    const errorMessage = error?.message ?? '好像出错了,请稍后再试。'
    if (error.message === 'canceled') {
      chatStore.updateChatSome(
        index,
        {
          loading: false,
        },
      )
      scrollToBottomIfAtBottom()
      return
    }
    chatStore.updateChat(
      index,
      {
        dateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
        text: errorMessage,
        inversion: false,
        error: true,
        loading: false,
        streaming: false,
        requestOptions: { prompt: message },
      },
    )
    scrollToBottomIfAtBottom()
  }
  finally {
    chatStore.updateChatTime(robotId.value,dayjs().format('YYYY-MM-DD HH:mm:ss'))
  }
}

function handleClear() {
  //针对那些回答异常没有会话ID的情况
  if(!dataSources.value?.[dataSources.value.length-1]?.aiId) {
    chatStore.updateChatSome(dataSources.value.length-1, { cleanIs: 1 })
    const element = chatWrapper.value;
    if(element) {
      element.style.height = element.offsetHeight + 300 + 'px'
      scrollToBottom()
    }
    return
  }
  clearChat({
    aiId:dataSources.value?.[dataSources.value.length-1]?.aiId,
    robotId:robotId.value,
    knowledgeId:currentChatRobot?.knowledgeId
  }).then(res => {
    if(res.code == 200) {
      chatStore.updateChatSome(dataSources.value.length-1, { cleanIs: 1 })
      const element = chatWrapper.value;
      if(element) {
        element.style.height = element.offsetHeight + 300 + 'px'
        scrollToBottom()
      }
    }
  })  
}

function handleEnter(event) {
  if (event.key === 'Enter' && !event.shiftKey) {
    event.preventDefault()
    onConversation()
  }
}

function handleStop() {
  if (loading.value) {
    controller.abort()
    loading.value = false
  }
}

const searchRobotDo = () => {
  listRobot({robotName:searchRobot.value,robotStatus:0,pageNum:1,pageSize:100}).then(res => {
    if(res.code == 200) {
      robots.value = res.rows
    }
    if(!searchRobot.value) {
      upTopRobot()
    }
  })
}

//当前对话的机器人置顶
function upTopRobot(rid) {
  if(robotRef.value) {
    rid = rid || robotId.value
    const _up = robots.value.find(item=>item.robotId==rid)
    let _filter = robots.value.filter(item=>item.robotId!=rid)
    _filter.unshift(_up)
    robots.value = _filter
    robotRef.value.scrollTop = 0
  }  
}

function handleFeedResult(index,aiId,type) {
  if(type == 1) {
    feedBackChat({aiId,solveIs:type}).then(res => {
      proxy.$modal.msgSuccess('您的反馈我们已经收到,谢谢您的认可!')
      chatStore.updateChatSome(index,{solveIs:type})
    })
  } else {
    console.log(index)
    curFeedChat.value = dataSources.value[index]
    curFeedChat.value['index'] = index
    feedBackText.value = curFeedChat.value?.feedBack
    feedbackShow.value = true
  }
}

function submitFeedback() {
  if(!feedBackText.value) {
    proxy.$modal.msgWarning('请详细描述需要反馈的问题')
    return
  }
  feedBackChat({aiId:curFeedChat.value?.aiId,robotId:robotId.value,feedBack:feedBackText.value,solveIs:-1}).then(res => {
    if(res.code == 200) {
      proxy.$modal.msgSuccess('感谢您的反馈,我们将不断改进~~')
      chatStore.updateChatSome(curFeedChat.value.index,{feedBack:feedBackText.value,solveIs:-1})
    }
  }).finally(() => {
    feedbackShow.value = false
  })
}

onMounted(() => {  
  //查询所有机器人
  listRobot({robotStatus:0,pageNum:1,pageSize:100}).then(res => {
    if(res.code == 200 && res.rows?.length>0) {
      robots.value = res.rows
      if(!robotId.value || res.rows.findIndex(item=>item.robotId==robotId.value) == -1) {
        chatStore.setActive(res.rows[0]?.robotId)
      } else {
        upTopRobot()
      }
      //查询当前对话机器人的问答记录
      chatStore.getCurChat().then(res => {
        setTimeout(()=>{
          scrollToBottom()
        },0)
      })
    } else {
      proxy.$modal.msgWarning('暂无机器人可以对话~~')
    }
  })
  if (inputRef.value)
    inputRef.value?.focus()
})

onUnmounted(() => {
  if (loading.value)
    controller.abort()
})


const formatChatTime = (time) => {
  if(dayjs(time).isAfter(dayjs().startOf('day'))) {
    return dayjs(time).format('HH:mm')
  } else {
    return dayjs(time).format('MM/DD')
  }
}

const deleteChat = () => {
  proxy.$modal.confirm('是否确认清除当前AI应用的会话记录吗!').then(function () {
    return deleteChatContext({robotId:currentChatRobot.value?.robotId,classifyFrom:1})
  }).then(() => {
    chatStore.getCurChat()
    proxy.$modal.msgSuccess("清除成功");
  }).catch(() => { });
}

const viewAnswerLog = (aiId) => {
  getChatLog(aiId).then(res => {
    if(res.code == 200 && res.data) {
      logDetail.value = res.data
      showLogDetail.value = true
    } else {
      proxy.$modal.msgError("日志查询失败");
    }
  })
}

provide('handleFindRef', (aiId) => {
  showRefAiId.value = aiId
  showRefDialog.value = true
})

const onCloseDialog = () => {
  showRefDialog.value = false;
  showRefAiId.value = ''
}

provide('handleSumbitForm',async (type, flowStep, data) => {
  console.log(type, flowStep, data)
  if(type == -1) {
    proxy.$modal.confirm(`是否提前终止报价流程,已提交的数据将丢失?`).then(()=>{
      bussinessProcess.running = false
      chatStore.updateChatSome(dataSources.value.length - 1,{flowFlag:false})
      bussinessProcess.formData = {}
      bussinessProcess.historyQa = []

      prompt.value = '取消报价'
      onConversation()
    }).catch(() => { });
  } else {
    const qs = replaceTemplate(flowStep?.questionTpl,data)
    
    bussinessProcess.formData = {...bussinessProcess.formData,...data}

    chatStore.updateChatSome(dataSources.value.length - 1,{flowFlag:false})

    chatStore.addChat(
      {
        dateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
        text: qs,
        inversion: true,
        error: false,
        requestOptions: { prompt: qs },
      },
    )
    scrollToBottom()
    chatStore.updateChatTime(robotId.value,dayjs().format('YYYY-MM-DD HH:mm:ss'))
    loading.value = true

    chatStore.addChat(
      {
        dateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
        text: '',
        loading: true,
        inversion: false,
        error: false,
        requestOptions: { prompt: qs },
      },
    )
    scrollToBottom()

    if(flowStep?.step < bussinessProcess.totalStep) {
      let aw = ''
      try {
        const _tpl = JSON.parse(flowSteps.value[flowStep.step]?.formTpl)
        aw = _tpl?.rule?.[0].value
      } catch (error) {}
      bussinessProcess.historyQa.push([qs,aw])

      setTimeout(() => {
        chatStore.updateChat(
          dataSources.value.length - 1,
          {
            dateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
            text: aw,
            inversion: false,
            error: false,
            loading: false,
            requestOptions: { prompt: qs },
            flowFlag:true,
            flowStep:flowSteps.value[flowStep?.step]
          },
        )
        scrollToBottom()
        loading.value = false
      },800)
    } else {

      await currentChatRobot.value.openStream == 'true' ? requestStreamingChat(qs,dataSources.value.length - 1,true) : requestChat(qs,dataSources.value.length - 1,true)
      
      loading.value = false
      bussinessProcess.running = false
      bussinessProcess.formData = {}
      bussinessProcess.historyQa = []
      chatStore.updateChatTime(robotId.value,dayjs().format('YYYY-MM-DD HH:mm:ss'))
    }
  }
})
</script>

实例demo

demo源代码

参考

基于Spring ApplicationEventPublisherAware推送事件实现

Java语言作为后端对接 chatgpt

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1399097.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

idea上传本地项目到gitlab

1. idea上传本地项目到gitlab 1. 配置idea里本地安装的git位置 即选择 Settings -> Version Control -> Git -> Path to Git executable 2. 在idea创建本地仓库 即选择 VCS -> Create Git Repository 然后选择目录&#xff0c;默认就是选择的当前项目&#xff…

(学习日记)2024.01.19

写在前面&#xff1a; 由于时间的不足与学习的碎片化&#xff0c;写博客变得有些奢侈。 但是对于记录学习&#xff08;忘了以后能快速复习&#xff09;的渴望一天天变得强烈。 既然如此 不如以天为单位&#xff0c;以时间为顺序&#xff0c;仅仅将博客当做一个知识学习的目录&a…

C和指针课后答案

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 第八章课后答案 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参…

ISA Server 2006部署网站对比nginx

2024年了&#xff0c;我还是第1次使用ISA Server 。没办法在维护一个非常古老的项目。说到ISA Server可能有小伙们不清楚&#xff0c;但是说到nginx大家应该都知道吧。虽然他们俩定位并不相同&#xff0c;但是本文中提到的需求&#xff0c;他俩是都可以实现。 网上找的到的教程…

全网最详细丨2024年AMC8真题及答案来了

目录 前言 真题回忆 真题解析 结尾 前言 相信大家都已经知道今年AMC8出事情了吧&#xff0c;但最重要的还是要从中学到新知识。 听说今年考生被提前12分钟强制交卷了&#xff0c;肯定因为试题泄露了。 最新回复&#xff1a;我们这边已经退费了 真题回忆 需要word文档的请…

基于JavaWeb+SSM+Vue基于微信小程序的网上商城系统的设计和实现

基于JavaWebSSMVue基于微信小程序的网上商城系统的设计和实现 滑到文末获取源码Lun文目录前言主要技术系统设计功能截图订阅经典源码专栏Java项目精品实战案例《500套》 源码获取 滑到文末获取源码 Lun文目录 目录 1系统概述 1 1.1 研究背景 1 1.2研究目的 1 1.3系统设计思想…

应届生必读:Java真实项目的开发流程和常用工具

目录 1 接需求和前期设计 2 敏捷开发模式 3 开发、测试与测试环境 4 项目部署细节说明 5 监控系统&#xff0c;解决线上问题 6 项目管理和部署工具 7 代码管理工具 8 Java项目开发的常用组件 9 测试类工具 10 数据库服务器及其客户端组件 11 linux连接组件 12 总结…

Helm Dashboard — Kubernetes 中管理 Helm 版本的 GUI

Helm Dashboard 通过提供图形用户界面&#xff0c;使在 Kubernetes 中管理 Helm 版本变得更加容易&#xff0c;这是许多开发人员所期望的。它可用于在 Kubernetes 中创建、部署和更新应用程序的版本&#xff0c;并跟踪其状态。 本文将探讨 Helm Dashboard 提供的特性和优势&am…

鸿蒙 HarmonyOS ArkTS ArkUI 动画 中心扩展、向下扩展、纵向扩展

EntryComponentstruct Index {State widthA: number 0State heightA: number 0onPageShow():void{animateTo ( {duration: 2000,iterations: -1,curve:Curve.Linear}, () > {this.widthA 200this.heightA 200} )}build() {Column() {// 中心扩展Column(){}.width(this.wi…

二叉树基础oj题目

二叉树基础oj题目及思路总结 前文中&#xff0c;介绍了二叉树的基本概念及基础操作&#xff0c;进一步对于二叉树的递归遍历及子问题的处理思想有了一定的了解。本文将带来几道二叉树经典的oj题目。 目录 二叉树基础oj题目 对称二叉树平衡二叉树二叉树的层序遍历 二叉树基…

(C语言)编译和链接

前言͟͟͞͞&#x1f48c;&#xff1a;对于现在的各种编译器而言许多都是好多个功能的集成&#xff0c;那么我们的代码到底是如何去实现的呢&#xff1f;难道我们的计算机可以直接读懂我们所写的代码&#xff0c;并运行吗&#xff1f;对于很多细心的小伙伴们可能会想这样的问题…

强缓存、协商缓存(浏览器的缓存机制)是么子?

文章目录 一.为什么要用强缓存和协商缓存&#xff1f;二.什么是强缓存&#xff1f;三.什么是协商缓存&#xff1f;四.总结 一.为什么要用强缓存和协商缓存&#xff1f; 为了减少资源请求次数&#xff0c;加快资源访问速度&#xff0c;浏览器会对资源文件如图片、css文件、js文…

Spring DI

目录 什么是依赖注入 属性注入 构造函数注入 Setter 注入 依赖注入的优势 什么是依赖注入 依赖注入是一种设计模式&#xff0c;它通过外部实体&#xff08;通常是容器&#xff09;来注入一个对象的依赖关系&#xff0c;而不是在对象内部创建这些依赖关系。这种方式使得对象…

【Python学习】Python学习21- 正则表达式(2)

目录 【Python学习】Python学习21- 正则表达式&#xff08;2&#xff09; 前言字符串检索和替换repl 参数是一个函数参考 文章所属专区 Python学习 前言 本章节主要说明Python的正则表达式。 正则表达式是一个特殊的字符序列&#xff0c;它能帮助你方便的检查一个字符串是否与…

『 C++ - STL』map与set的封装 ( 万字 )

文章目录 &#x1f3a1; map与set介绍&#x1f3a1; map与set的基础结构&#x1f3a1; 红黑树的再修改&#x1f3a0;节点及树的定义&#x1f3a0;KeyOfValue的使用&#x1f3a0;插入函数&#x1f3a0;析构函数&#x1f3a0;红黑树完整代码(供参考) &#x1f3a1; 迭代器的实现&…

【C++】—— C++的IO流

在C中&#xff0c;I/O流是一项关键的编程概念&#xff0c;为程序提供了与外部世界进行交互的重要手段。通过使用C的强大I/O库&#xff0c;开发者能够实现对标准输入输出、文件、字符串等多种数据源的高效处理。接下来让我们深入探讨C的I/O流&#xff0c;了解其基本原理、常见操…

基于动态顺序表实现通讯录项目

本文中&#xff0c;我们将使用顺序表的结构来完成通讯录的实现。 我们都知道&#xff0c;顺序表实际上就是一个数组。而使用顺序表来实现通讯录&#xff0c;其内核是将顺序表中存放的数据类型改为结构体&#xff0c;将联系人的信息存放到结构体中&#xff0c;通过对顺序表的操…

GO 中高效 int 转换 string 的方法与高性能源码剖析

文章目录 使用 strconv.Itoa使用 fmt.Sprintf使用 strconv.FormatIntFormatInt 深入剖析1. 快速路径处理小整数2. formatBits 函数的高效实现 结论 Go 语言 中&#xff0c;将整数&#xff08;int&#xff09;转换为字符串&#xff08;string&#xff09;是一项常见的操作。 本文…

数据库-数据库分类

数据库可以分为关系型数据库和非关系型数据库&#xff0c;常见的数据库如下 关系型数据库 关系型数据库是一种采用关系模型来组织数据的数据库&#xff0c;它以行和列的形式存储数据&#xff0c;以便于用户理解。关系型数据库中的数据以二维表的形式组织&#xff0c;被称为表…

从零开始c++精讲:第三篇——内存管理

文章目录 一、C/C内存分布二、C语言中动态内存管理方式:malloc/calloc/realloc/free三、C中动态内存管理四、operator new与operator delete函数4.1 operator new与operator delete函数&#xff08;重点&#xff09; 五、new和delete的实现原理5.1内置类型5.2 自定义类型 六、定…