mirror of https://github.com/jeecgboot/jeecg-boot
【3.6.3版本发布】ai聊天模块新增代码
parent
f7538c1ed8
commit
e15e9d80c4
|
@ -17,6 +17,7 @@ import org.jeecg.config.shiro.filters.CustomShiroFilterFactoryBean;
|
||||||
import org.jeecg.config.shiro.filters.JwtFilter;
|
import org.jeecg.config.shiro.filters.JwtFilter;
|
||||||
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
|
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.DependsOn;
|
import org.springframework.context.annotation.DependsOn;
|
||||||
|
@ -25,10 +26,12 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
|
||||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.filter.DelegatingFilterProxy;
|
||||||
import redis.clients.jedis.HostAndPort;
|
import redis.clients.jedis.HostAndPort;
|
||||||
import redis.clients.jedis.JedisCluster;
|
import redis.clients.jedis.JedisCluster;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import javax.servlet.DispatcherType;
|
||||||
import javax.servlet.Filter;
|
import javax.servlet.Filter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
@ -50,7 +53,7 @@ public class ShiroConfig {
|
||||||
private JeecgBaseConfig jeecgBaseConfig;
|
private JeecgBaseConfig jeecgBaseConfig;
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private RedisProperties redisProperties;
|
private RedisProperties redisProperties;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter Chain定义说明
|
* Filter Chain定义说明
|
||||||
*
|
*
|
||||||
|
@ -181,6 +184,20 @@ public class ShiroConfig {
|
||||||
return shiroFilterFactoryBean;
|
return shiroFilterFactoryBean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
||||||
|
@Bean
|
||||||
|
public FilterRegistrationBean shiroFilterRegistration() {
|
||||||
|
FilterRegistrationBean registration = new FilterRegistrationBean();
|
||||||
|
registration.setFilter(new DelegatingFilterProxy("shiroFilterFactoryBean"));
|
||||||
|
registration.setEnabled(true);
|
||||||
|
registration.addUrlPatterns("/*");
|
||||||
|
//支持异步
|
||||||
|
registration.setAsyncSupported(true);
|
||||||
|
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
|
||||||
|
return registration;
|
||||||
|
}
|
||||||
|
//update-end---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
||||||
|
|
||||||
@Bean("securityManager")
|
@Bean("securityManager")
|
||||||
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
|
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
|
||||||
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
|
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
|
||||||
|
|
|
@ -16,6 +16,12 @@
|
||||||
<groupId>org.jeecgframework.boot</groupId>
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
<artifactId>jeecg-boot-base-core</artifactId>
|
<artifactId>jeecg-boot-base-core</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- chatgpt -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
|
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
34
jeecg-module-demo/src/main/java/org/jeecg/modules/demo/gpt/cache/LocalCache.java
vendored
Normal file
34
jeecg-module-demo/src/main/java/org/jeecg/modules/demo/gpt/cache/LocalCache.java
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package org.jeecg.modules.demo.gpt.cache;
|
||||||
|
|
||||||
|
import cn.hutool.cache.CacheUtil;
|
||||||
|
import cn.hutool.cache.impl.TimedCache;
|
||||||
|
import cn.hutool.core.date.DateUnit;
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天记录本地缓存
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024/1/26 20:06
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//update-end---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
|
@ -0,0 +1,74 @@
|
||||||
|
package org.jeecg.modules.demo.gpt.controller;
|
||||||
|
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.modules.demo.gpt.service.ChatService;
|
||||||
|
import org.jeecg.modules.demo.gpt.vo.ChatHistoryVO;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: chatGpt-聊天接口
|
||||||
|
* @Author: chenrui
|
||||||
|
* @Date: 2024/1/9 16:30
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/ai/chat")
|
||||||
|
public class ChatController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
ChatService chatService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建sse连接
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/send")
|
||||||
|
public SseEmitter createConnect(@RequestParam(name = "topicId", required = false) String topicId, @RequestParam(name = "message", required = true) String message) {
|
||||||
|
SseEmitter sse = chatService.createChat();
|
||||||
|
chatService.sendMessage(topicId, message);
|
||||||
|
return sse;
|
||||||
|
}
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20240223 for:[QQYUN-8225]聊天记录保存------------
|
||||||
|
/**
|
||||||
|
* 保存聊天记录
|
||||||
|
* @param chatHistoryVO
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024/2/22 13:54
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/history/save")
|
||||||
|
@ResponseBody
|
||||||
|
public Result<?> saveHistory(@RequestBody ChatHistoryVO chatHistoryVO) {
|
||||||
|
return chatService.saveHistory(chatHistoryVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询聊天记录
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024/2/22 14:03
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/history/get")
|
||||||
|
@ResponseBody
|
||||||
|
public Result<ChatHistoryVO> getHistoryByTopic() {
|
||||||
|
return chatService.getHistoryByTopic();
|
||||||
|
}
|
||||||
|
//update-end---author:chenrui ---date:20240223 for:[QQYUN-8225]聊天记录保存------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭连接
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/close")
|
||||||
|
public void closeConnect() {
|
||||||
|
chatService.closeChat();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
//update-end---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
|
@ -0,0 +1,136 @@
|
||||||
|
package org.jeecg.modules.demo.gpt.listeners;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.unfbx.chatgpt.entity.chat.ChatCompletionResponse;
|
||||||
|
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.jetbrains.annotations.NotNull;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
||||||
|
/**
|
||||||
|
* OpenAI的SSE监听
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024/1/26 20:06
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class OpenAISSEEventSourceListener extends EventSourceListener {
|
||||||
|
|
||||||
|
private long tokens;
|
||||||
|
|
||||||
|
private SseEmitter sseEmitter;
|
||||||
|
|
||||||
|
private String topicId;
|
||||||
|
|
||||||
|
public OpenAISSEEventSourceListener(SseEmitter sseEmitter) {
|
||||||
|
this.sseEmitter = sseEmitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OpenAISSEEventSourceListener(String topicId,SseEmitter sseEmitter){
|
||||||
|
this.topicId = topicId;
|
||||||
|
this.sseEmitter = sseEmitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onOpen(@NotNull EventSource eventSource, @NotNull Response response) {
|
||||||
|
log.info("OpenAI建立sse连接...");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
@Override
|
||||||
|
public void onEvent(@NotNull EventSource eventSource, String id, String type, @NotNull String data) {
|
||||||
|
log.debug("OpenAI返回数据:{}", data);
|
||||||
|
tokens += 1;
|
||||||
|
if (data.equals("[DONE]")) {
|
||||||
|
log.info("OpenAI返回数据结束了");
|
||||||
|
sseEmitter.send(SseEmitter.event()
|
||||||
|
.id("[TOKENS]")
|
||||||
|
.data("<br/><br/>tokens:" + tokens())
|
||||||
|
.reconnectTime(3000));
|
||||||
|
sseEmitter.send(SseEmitter.event()
|
||||||
|
.id("[DONE]")
|
||||||
|
.data("[DONE]")
|
||||||
|
.reconnectTime(3000));
|
||||||
|
// 传输完成后自动关闭sse
|
||||||
|
sseEmitter.complete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
ChatCompletionResponse completionResponse = mapper.readValue(data, ChatCompletionResponse.class); // 读取Json
|
||||||
|
try {
|
||||||
|
sseEmitter.send(SseEmitter.event()
|
||||||
|
.id(this.topicId)
|
||||||
|
.data(completionResponse.getChoices().get(0).getDelta())
|
||||||
|
.reconnectTime(3000));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.getMessage(),e);
|
||||||
|
eventSource.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClosed(@NotNull EventSource eventSource) {
|
||||||
|
log.info("流式输出返回值总共{}tokens", tokens() - 2);
|
||||||
|
log.info("OpenAI关闭sse连接...");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NotNull EventSource eventSource, Throwable t, Response response) {
|
||||||
|
String errMsg = "";
|
||||||
|
ResponseBody body = null == response ? null:response.body();
|
||||||
|
if (Objects.nonNull(body)) {
|
||||||
|
log.error("OpenAI sse连接异常data:{},异常:{}", body.string(), t.getMessage());
|
||||||
|
errMsg = body.string();
|
||||||
|
} else {
|
||||||
|
log.error("OpenAI sse连接异常data:{},异常:{}", response, t.getMessage());
|
||||||
|
errMsg = t.getMessage();
|
||||||
|
}
|
||||||
|
eventSource.cancel();
|
||||||
|
sseEmitter.send(SseEmitter.event()
|
||||||
|
.id("[ERR]")
|
||||||
|
.data(Message.builder().content(explainErr(errMsg)).build())
|
||||||
|
.reconnectTime(3000));
|
||||||
|
sseEmitter.send(SseEmitter.event()
|
||||||
|
.id("[DONE]")
|
||||||
|
.data("[DONE]")
|
||||||
|
.reconnectTime(3000));
|
||||||
|
sseEmitter.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String explainErr(String errMsg){
|
||||||
|
if(StringUtils.isEmpty(errMsg)){
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if(errMsg.contains("Rate limit")){
|
||||||
|
return "请求频率太快了,请等待20秒再试.";
|
||||||
|
}
|
||||||
|
return errMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tokens
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public long tokens() {
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//update-end---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
|
@ -0,0 +1,56 @@
|
||||||
|
package org.jeecg.modules.demo.gpt.service;
|
||||||
|
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.modules.demo.gpt.vo.ChatHistoryVO;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI助手聊天Service
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024/1/26 20:08
|
||||||
|
*/
|
||||||
|
public interface ChatService {
|
||||||
|
/**
|
||||||
|
* 创建SSE
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
SseEmitter createChat();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭SSE
|
||||||
|
*/
|
||||||
|
void closeChat();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端发送消息到服务端
|
||||||
|
*
|
||||||
|
* @param topicId
|
||||||
|
* @param message
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024/1/26 20:01
|
||||||
|
*/
|
||||||
|
void sendMessage(String topicId, String message);
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20240223 for:[QQYUN-8225]聊天记录保存------------
|
||||||
|
/**
|
||||||
|
* 保存聊天记录
|
||||||
|
* @param chatHistoryVO
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024/2/22 13:37
|
||||||
|
*/
|
||||||
|
Result<?> saveHistory(ChatHistoryVO chatHistoryVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询聊天记录
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024/2/22 13:59
|
||||||
|
*/
|
||||||
|
Result<ChatHistoryVO> getHistoryByTopic();
|
||||||
|
//update-end---author:chenrui ---date:20240223 for:[QQYUN-8225]聊天记录保存------------
|
||||||
|
}
|
||||||
|
|
||||||
|
//update-end---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
|
@ -0,0 +1,199 @@
|
||||||
|
package org.jeecg.modules.demo.gpt.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import com.alibaba.fastjson.JSONArray;
|
||||||
|
import com.unfbx.chatgpt.OpenAiStreamClient;
|
||||||
|
import com.unfbx.chatgpt.entity.chat.ChatCompletion;
|
||||||
|
import com.unfbx.chatgpt.entity.chat.Message;
|
||||||
|
import com.unfbx.chatgpt.exception.BaseException;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
|
import org.jeecg.common.util.UUIDGenerator;
|
||||||
|
import org.jeecg.modules.demo.gpt.cache.LocalCache;
|
||||||
|
import org.jeecg.modules.demo.gpt.listeners.OpenAISSEEventSourceListener;
|
||||||
|
import org.jeecg.modules.demo.gpt.service.ChatService;
|
||||||
|
import org.jeecg.modules.demo.gpt.vo.ChatHistoryVO;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI助手聊天Service
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024/1/26 20:07
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class ChatServiceImpl implements ChatService {
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20240223 for:[QQYUN-8225]聊天记录保存------------
|
||||||
|
private static final String CACHE_KEY_PREFIX = "ai:chart:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private static final String CACHE_KEY_MSG_CONTEXT = "msg_content";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private static final String CACHE_KEY_MSG_HISTORY = "msg_history";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
RedisTemplate redisTemplate;
|
||||||
|
//update-end---author:chenrui ---date:20240223 for:[QQYUN-8225]聊天记录保存------------
|
||||||
|
|
||||||
|
private OpenAiStreamClient openAiStreamClient = null;
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20240131 for:[QQYUN-8212]fix 没有配置启动报错------------
|
||||||
|
public ChatServiceImpl() {
|
||||||
|
try {
|
||||||
|
this.openAiStreamClient = SpringContextUtils.getBean(OpenAiStreamClient.class);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防止client不能成功注入
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024/2/3 23:08
|
||||||
|
*/
|
||||||
|
private OpenAiStreamClient ensureClient(){
|
||||||
|
if(null == this.openAiStreamClient){
|
||||||
|
this.openAiStreamClient = SpringContextUtils.getBean(OpenAiStreamClient.class);
|
||||||
|
}
|
||||||
|
return this.openAiStreamClient;
|
||||||
|
}
|
||||||
|
//update-end---author:chenrui ---date:20240131 for:[QQYUN-8212]fix 没有配置启动报错------------
|
||||||
|
|
||||||
|
private String getUserId() {
|
||||||
|
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||||
|
return sysUser.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SseEmitter createChat() {
|
||||||
|
String uid = getUserId();
|
||||||
|
//默认30秒超时,设置为0L则永不超时
|
||||||
|
SseEmitter sseEmitter = new SseEmitter(-0L);
|
||||||
|
//完成后回调
|
||||||
|
sseEmitter.onCompletion(() -> {
|
||||||
|
log.info("[{}]结束连接...................",uid);
|
||||||
|
LocalCache.CACHE.remove(uid);
|
||||||
|
});
|
||||||
|
//超时回调
|
||||||
|
sseEmitter.onTimeout(() -> {
|
||||||
|
log.info("[{}]连接超时...................", uid);
|
||||||
|
});
|
||||||
|
//异常回调
|
||||||
|
sseEmitter.onError(
|
||||||
|
throwable -> {
|
||||||
|
try {
|
||||||
|
log.info("[{}]连接异常,{}", uid, throwable.toString());
|
||||||
|
sseEmitter.send(SseEmitter.event()
|
||||||
|
.id(uid)
|
||||||
|
.name("发生异常!")
|
||||||
|
.data(Message.builder().content("发生异常请重试!").build())
|
||||||
|
.reconnectTime(3000));
|
||||||
|
LocalCache.CACHE.put(uid, sseEmitter);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error(e.getMessage(),e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
sseEmitter.send(SseEmitter.event().reconnectTime(5000));
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error(e.getMessage(),e);
|
||||||
|
}
|
||||||
|
LocalCache.CACHE.put(uid, sseEmitter);
|
||||||
|
log.info("[{}]创建sse连接成功!", uid);
|
||||||
|
return sseEmitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void closeChat() {
|
||||||
|
String uid = getUserId();
|
||||||
|
SseEmitter sse = (SseEmitter) LocalCache.CACHE.get(uid);
|
||||||
|
if (sse != null) {
|
||||||
|
sse.complete();
|
||||||
|
//移除
|
||||||
|
LocalCache.CACHE.remove(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendMessage(String topicId, String message) {
|
||||||
|
String uid = getUserId();
|
||||||
|
if (StrUtil.isBlank(message)) {
|
||||||
|
log.info("参数异常,message为null");
|
||||||
|
throw new BaseException("参数异常,message不能为空~");
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(topicId)) {
|
||||||
|
topicId = UUIDGenerator.generate();
|
||||||
|
}
|
||||||
|
//update-begin---author:chenrui ---date:20240223 for:[QQYUN-8225]聊天记录保存------------
|
||||||
|
log.info("话题id:{}", topicId);
|
||||||
|
String cacheKey = CACHE_KEY_PREFIX + uid + "_" + topicId;
|
||||||
|
String messageContext = (String) redisTemplate.opsForHash().get(cacheKey, CACHE_KEY_MSG_CONTEXT);
|
||||||
|
List<Message> msgHistory = new ArrayList<>();
|
||||||
|
if (StrUtil.isNotBlank(messageContext)) {
|
||||||
|
List<Message> messages = JSONArray.parseArray(messageContext, Message.class);
|
||||||
|
msgHistory = messages == null ? new ArrayList<>() : messages;
|
||||||
|
}
|
||||||
|
Message currentMessage = Message.builder().content(message).role(Message.Role.USER).build();
|
||||||
|
msgHistory.add(currentMessage);
|
||||||
|
|
||||||
|
SseEmitter sseEmitter = (SseEmitter) LocalCache.CACHE.get(uid);
|
||||||
|
if (sseEmitter == null) {
|
||||||
|
log.info("聊天消息推送失败uid:[{}],没有创建连接,请重试。", uid);
|
||||||
|
throw new JeecgBootException("聊天消息推送失败uid:[{}],没有创建连接,请重试。~");
|
||||||
|
}
|
||||||
|
OpenAISSEEventSourceListener openAIEventSourceListener = new OpenAISSEEventSourceListener(topicId, sseEmitter);
|
||||||
|
ChatCompletion completion = ChatCompletion
|
||||||
|
.builder()
|
||||||
|
.messages(msgHistory)
|
||||||
|
.model(ChatCompletion.Model.GPT_3_5_TURBO.getName())
|
||||||
|
.build();
|
||||||
|
ensureClient().streamChatCompletion(completion, openAIEventSourceListener);
|
||||||
|
redisTemplate.opsForHash().put(cacheKey, CACHE_KEY_MSG_CONTEXT, JSONUtil.toJsonStr(msgHistory));
|
||||||
|
//update-end---author:chenrui ---date:20240223 for:[QQYUN-8225]聊天记录保存------------
|
||||||
|
Result.ok(completion.tokens());
|
||||||
|
}
|
||||||
|
|
||||||
|
//update-begin---author:chenrui ---date:20240223 for:[QQYUN-8225]聊天记录保存------------
|
||||||
|
@Override
|
||||||
|
public Result<?> saveHistory(ChatHistoryVO chatHistoryVO) {
|
||||||
|
String uid = getUserId();
|
||||||
|
String cacheKey = CACHE_KEY_PREFIX + CACHE_KEY_MSG_HISTORY + ":" + uid;
|
||||||
|
redisTemplate.opsForValue().set(cacheKey, chatHistoryVO.getContent());
|
||||||
|
return Result.OK("保存成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<ChatHistoryVO> getHistoryByTopic() {
|
||||||
|
String uid = getUserId();
|
||||||
|
String cacheKey = CACHE_KEY_PREFIX + CACHE_KEY_MSG_HISTORY + ":" + uid;
|
||||||
|
String historyContent = (String) redisTemplate.opsForValue().get(cacheKey);
|
||||||
|
ChatHistoryVO chatHistoryVO = new ChatHistoryVO();
|
||||||
|
chatHistoryVO.setContent(historyContent);
|
||||||
|
return Result.OK(chatHistoryVO);
|
||||||
|
}
|
||||||
|
//update-end---author:chenrui ---date:20240223 for:[QQYUN-8225]聊天记录保存------------
|
||||||
|
}
|
||||||
|
|
||||||
|
//update-end---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
|
@ -0,0 +1,25 @@
|
||||||
|
package org.jeecg.modules.demo.gpt.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 聊天记录
|
||||||
|
* @Author: chenrui
|
||||||
|
* @Date: 2024/2/22 13:36
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ChatHistoryVO implements Serializable {
|
||||||
|
private static final long serialVersionUID = 3238429500037511283L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 话题id
|
||||||
|
*/
|
||||||
|
String topicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天记录内容
|
||||||
|
*/
|
||||||
|
String content;
|
||||||
|
}
|
|
@ -261,6 +261,20 @@ jeecg:
|
||||||
password:
|
password:
|
||||||
type: STANDALONE
|
type: STANDALONE
|
||||||
enabled: true
|
enabled: true
|
||||||
|
# ChartGPT对接配置
|
||||||
|
ai-chat:
|
||||||
|
# 是否开启;必须。
|
||||||
|
enabled: false
|
||||||
|
# openAi接口秘钥,填写自己的apiKey;必须。
|
||||||
|
apiKey: "????"
|
||||||
|
# openAi域名,有代理就填代理的域名。默认:openAI官方apiHost
|
||||||
|
apiHost: "https://api.openai.com"
|
||||||
|
# 超时时间单位:s。默认 60s
|
||||||
|
timeout: 60
|
||||||
|
# 本地代理地址
|
||||||
|
# proxy:
|
||||||
|
# host: "http://127.0.0.1"
|
||||||
|
# port: "7890"
|
||||||
#cas单点登录
|
#cas单点登录
|
||||||
cas:
|
cas:
|
||||||
prefixUrl: http://cas.example.org:8443/cas
|
prefixUrl: http://cas.example.org:8443/cas
|
||||||
|
|
12
pom.xml
12
pom.xml
|
@ -23,7 +23,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>2.7.10</version>
|
<version>2.7.18</version>
|
||||||
<relativePath/>
|
<relativePath/>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
<ojdbc6.version>11.2.0.3</ojdbc6.version>
|
<ojdbc6.version>11.2.0.3</ojdbc6.version>
|
||||||
<sqljdbc4.version>4.0</sqljdbc4.version>
|
<sqljdbc4.version>4.0</sqljdbc4.version>
|
||||||
<mysql-connector-java.version>8.0.27</mysql-connector-java.version>
|
<mysql-connector-java.version>8.0.27</mysql-connector-java.version>
|
||||||
<hutool.version>5.8.23</hutool.version>
|
<hutool.version>5.8.25</hutool.version>
|
||||||
<!-- 国产数据库驱动 -->
|
<!-- 国产数据库驱动 -->
|
||||||
<dm8.version>8.1.1.49</dm8.version>
|
<dm8.version>8.1.1.49</dm8.version>
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
<minidao.version>1.9.5</minidao.version>
|
<minidao.version>1.9.5</minidao.version>
|
||||||
|
|
||||||
<!-- 积木报表-->
|
<!-- 积木报表-->
|
||||||
<jimureport-spring-boot-starter.version>1.6.6</jimureport-spring-boot-starter.version>
|
<jimureport-spring-boot-starter.version>1.7.1</jimureport-spring-boot-starter.version>
|
||||||
<commons.version>2.6</commons.version>
|
<commons.version>2.6</commons.version>
|
||||||
<aliyun-java-sdk-dysmsapi.version>2.1.0</aliyun-java-sdk-dysmsapi.version>
|
<aliyun-java-sdk-dysmsapi.version>2.1.0</aliyun-java-sdk-dysmsapi.version>
|
||||||
<aliyun.oss.version>3.11.2</aliyun.oss.version>
|
<aliyun.oss.version>3.11.2</aliyun.oss.version>
|
||||||
|
@ -390,6 +390,12 @@
|
||||||
<artifactId>jimureport-nosql-starter</artifactId>
|
<artifactId>jimureport-nosql-starter</artifactId>
|
||||||
<version>1.6.0</version>
|
<version>1.6.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- chatgpt -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jeecgframework.boot</groupId>
|
||||||
|
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
|
||||||
|
<version>${jeecgboot.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue