Explorar o código

feat: 对话模块重构

ageerle hai 1 mes
pai
achega
37a8b7dad3

+ 0 - 3
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/api/ChatController.java

@@ -49,9 +49,6 @@ public class ChatController {
     @PostMapping("/send")
     @ResponseBody
     public SseEmitter sseChat(@RequestBody @Valid ChatRequest chatRequest, HttpServletRequest request) {
-        if (chatRequest.getModel().startsWith("ollama")) {
-            return sseService.ollamaChat(chatRequest);
-        }
         return sseService.sseChat(chatRequest,request);
     }
 

+ 24 - 0
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/factory/SseServiceFactory.java

@@ -0,0 +1,24 @@
+package org.ruoyi.chat.factory;
+
+
+import lombok.extern.slf4j.Slf4j;
+import org.ruoyi.chat.service.chat.IChatService;
+import org.ruoyi.chat.service.chat.impl.OllamaServiceImpl;
+import org.ruoyi.chat.service.chat.impl.OpenAIServiceImpl;
+
+import org.springframework.stereotype.Component;
+
+@Component
+@Slf4j
+public class SseServiceFactory {
+
+    public IChatService getSseService(String type) {
+        if ("openai".equals(type)) {
+            return new OpenAIServiceImpl();
+        } else if ("ollama".equals(type)) {
+            return new OllamaServiceImpl();
+        } else {
+            throw new IllegalArgumentException("Unknown type: " + type);
+        }
+    }
+}

+ 3 - 1
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/knowledge/vectorstore/VectorStoreFactory.java → ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/factory/VectorStoreFactory.java

@@ -1,8 +1,10 @@
-package org.ruoyi.chat.service.knowledge.vectorstore;
+package org.ruoyi.chat.factory;
 
 import cn.hutool.core.util.StrUtil;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
+import org.ruoyi.chat.service.knowledge.vectorstore.MilvusVectorStore;
+import org.ruoyi.chat.service.knowledge.vectorstore.WeaviateVectorStore;
 import org.ruoyi.domain.vo.KnowledgeInfoVo;
 import org.ruoyi.mapper.KnowledgeInfoMapper;
 import org.ruoyi.service.VectorStoreService;

+ 3 - 1
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/knowledge/vectorizer/VectorizationFactory.java → ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/factory/VectorizationFactory.java

@@ -1,9 +1,11 @@
-package org.ruoyi.chat.service.knowledge.vectorizer;
+package org.ruoyi.chat.factory;
 
 import cn.hutool.core.util.StrUtil;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 
+import org.ruoyi.chat.service.knowledge.vectorizer.BgeLargeVectorization;
+import org.ruoyi.chat.service.knowledge.vectorizer.OpenAiVectorization;
 import org.ruoyi.domain.vo.KnowledgeInfoVo;
 import org.ruoyi.service.IKnowledgeInfoService;
 import org.ruoyi.service.VectorizationService;

+ 19 - 0
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatService.java

@@ -0,0 +1,19 @@
+package org.ruoyi.chat.service.chat;
+
+import org.ruoyi.common.chat.request.ChatRequest;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+/**
+ * 对话Service接口
+ *
+ * @author ageerle
+ * @date 2025-04-08
+ */
+public interface IChatService {
+
+    /**
+     * 客户端发送消息到服务端
+     * @param chatRequest 请求对象
+     */
+    SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter);
+}

+ 1 - 10
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/ISseService.java

@@ -39,9 +39,8 @@ public interface ISseService {
      */
     ResponseEntity<Resource> textToSpeed(TextToSpeech textToSpeech);
 
-
     /**
-     * 上传文件到api服务器
+     * 上传文件到服务器
      *
      * @param file 文件信息
      * @return 返回文件信息
@@ -49,13 +48,6 @@ public interface ISseService {
     UploadFileResponse upload(MultipartFile file);
 
 
-    /**
-     * 使用ollama调用本地模型
-     * @param chatRequest 对话信息
-     * @return 流式输出返回内容
-     */
-    SseEmitter ollamaChat(ChatRequest chatRequest);
-
     /**
      * 企业应用回复
      * @param prompt 提示词
@@ -63,7 +55,6 @@ public interface ISseService {
      */
     String wxCpChat(String prompt);
 
-
     /**
      * 联网查询
      *

+ 80 - 0
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java

@@ -0,0 +1,80 @@
+package org.ruoyi.chat.service.chat.impl;
+
+import io.github.ollama4j.OllamaAPI;
+import io.github.ollama4j.models.chat.OllamaChatMessage;
+import io.github.ollama4j.models.chat.OllamaChatMessageRole;
+import io.github.ollama4j.models.chat.OllamaChatRequestBuilder;
+import io.github.ollama4j.models.chat.OllamaChatRequestModel;
+import io.github.ollama4j.models.generate.OllamaStreamHandler;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.ruoyi.chat.service.chat.IChatService;
+import org.ruoyi.chat.util.SSEUtil;
+import org.ruoyi.common.chat.entity.chat.Message;
+import org.ruoyi.common.chat.request.ChatRequest;
+import org.ruoyi.domain.vo.ChatModelVo;
+import org.ruoyi.service.IChatModelService;
+import org.springframework.beans.factory.annotation.Autowired;
+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;
+import java.util.concurrent.CompletableFuture;
+
+
+@Service
+@Slf4j
+public class OllamaServiceImpl implements IChatService {
+
+    @Autowired
+    private  IChatModelService chatModelService;
+
+    @Override
+    public SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter) {
+        ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
+        String host = chatModelVo.getApiHost();
+        List<Message> msgList = chatRequest.getMessages();
+
+        List<OllamaChatMessage> messages = new ArrayList<>();
+        for (Message message : msgList) {
+            OllamaChatMessage ollamaChatMessage = new OllamaChatMessage();
+            ollamaChatMessage.setRole(OllamaChatMessageRole.USER);
+            ollamaChatMessage.setContent(message.getContent().toString());
+            messages.add(ollamaChatMessage);
+        }
+        OllamaAPI api = new OllamaAPI(host);
+        api.setRequestTimeoutSeconds(100);
+        OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(chatRequest.getModel());
+
+        OllamaChatRequestModel requestModel = builder
+                .withMessages(messages)
+                .build();
+
+        // 异步执行 OllAma API 调用
+        CompletableFuture.runAsync(() -> {
+            try {
+                StringBuilder response = new StringBuilder();
+                OllamaStreamHandler streamHandler = (s) -> {
+                    String substr = s.substring(response.length());
+                    response.append(substr);
+                    System.out.println(substr);
+                    try {
+                        emitter.send(substr);
+                    } catch (IOException e) {
+                        SSEUtil.sendErrorEvent(emitter, e.getMessage());
+                    }
+                };
+                api.chat(requestModel, streamHandler);
+                emitter.complete();
+            } catch (Exception e) {
+                SSEUtil.sendErrorEvent(emitter, e.getMessage());
+            }
+        });
+
+        return emitter;
+    }
+}

+ 50 - 0
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java

@@ -0,0 +1,50 @@
+package org.ruoyi.chat.service.chat.impl;
+
+import lombok.extern.slf4j.Slf4j;
+import org.ruoyi.chat.config.ChatConfig;
+import org.ruoyi.chat.listener.SSEEventSourceListener;
+import org.ruoyi.chat.service.chat.IChatService;
+import org.ruoyi.common.chat.entity.chat.ChatCompletion;
+import org.ruoyi.common.chat.openai.OpenAiStreamClient;
+import org.ruoyi.common.chat.request.ChatRequest;
+import org.ruoyi.domain.vo.ChatModelVo;
+import org.ruoyi.service.IChatModelService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+@Service
+@Slf4j
+public class OpenAIServiceImpl implements IChatService {
+
+    @Autowired
+    private  IChatModelService chatModelService;
+    @Autowired
+    private ChatConfig chatConfig;
+    @Autowired
+    private OpenAiStreamClient openAiStreamClient;
+
+    @Override
+    public SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter) {
+
+        SSEEventSourceListener openAIEventSourceListener = new SSEEventSourceListener(emitter);
+        // 查询模型信息
+        ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
+
+        if(chatModelVo!=null){
+            // 建请求客户端
+            openAiStreamClient = chatConfig.createOpenAiStreamClient(chatModelVo.getApiHost(), chatModelVo.getApiKey());
+            // 设置默认提示词
+            chatRequest.setSysPrompt(chatModelVo.getSystemPrompt());
+        }
+        ChatCompletion completion = ChatCompletion
+                .builder()
+                .messages(chatRequest.getMessages())
+                .model(chatRequest.getModel())
+                .stream(chatRequest.getStream())
+                .build();
+        openAiStreamClient.streamChatCompletion(completion, openAIEventSourceListener);
+
+        return emitter;
+    }
+}

+ 47 - 127
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java

@@ -6,22 +6,17 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.protobuf.ServiceException;
 import com.zhipu.oapi.ClientV4;
 import com.zhipu.oapi.service.v4.tools.*;
-import io.github.ollama4j.OllamaAPI;
-import io.github.ollama4j.models.chat.OllamaChatMessage;
-import io.github.ollama4j.models.chat.OllamaChatMessageRole;
-import io.github.ollama4j.models.chat.OllamaChatRequestBuilder;
-import io.github.ollama4j.models.chat.OllamaChatRequestModel;
-import io.github.ollama4j.models.generate.OllamaStreamHandler;
 import jakarta.servlet.http.HttpServletRequest;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import okhttp3.*;
-import org.ruoyi.chat.config.ChatConfig;
-import org.ruoyi.chat.listener.SSEEventSourceListener;
 
 import org.ruoyi.chat.service.chat.IChatCostService;
+import org.ruoyi.chat.service.chat.IChatService;
 import org.ruoyi.chat.service.chat.ISseService;
+import org.ruoyi.chat.factory.SseServiceFactory;
 import org.ruoyi.chat.util.IpUtil;
+import org.ruoyi.chat.util.SSEUtil;
 import org.ruoyi.common.chat.request.ChatRequest;
 import org.ruoyi.common.chat.entity.Tts.TextToSpeech;
 import org.ruoyi.common.chat.entity.chat.ChatCompletion;
@@ -32,15 +27,14 @@ import org.ruoyi.common.chat.entity.files.UploadFileResponse;
 import org.ruoyi.common.chat.entity.whisper.WhisperResponse;
 import org.ruoyi.common.chat.openai.OpenAiStreamClient;
 import org.ruoyi.common.core.service.ConfigService;
+import org.ruoyi.common.core.utils.DateUtils;
 import org.ruoyi.common.core.utils.StringUtils;
 import org.ruoyi.common.core.utils.file.FileUtils;
 import org.ruoyi.common.core.utils.file.MimeTypeUtils;
 
 import org.ruoyi.common.redis.utils.RedisUtils;
 
-import org.ruoyi.domain.vo.ChatModelVo;
 import org.ruoyi.service.EmbeddingService;
-import org.ruoyi.service.IChatModelService;
 import org.ruoyi.service.VectorStoreService;
 import org.springframework.core.io.InputStreamResource;
 import org.springframework.core.io.Resource;
@@ -60,7 +54,6 @@ import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
@@ -72,10 +65,6 @@ public class SseServiceImpl implements ISseService {
 
     private final OpenAiStreamClient openAiStreamClient;
 
-    private final ChatConfig chatConfig;
-
-    private final IChatModelService chatModelService;
-
     private final EmbeddingService embeddingService;
 
     private final VectorStoreService vectorStore;
@@ -84,6 +73,8 @@ public class SseServiceImpl implements ISseService {
 
     private final IChatCostService chatCostService;
 
+    private final SseServiceFactory sseServiceFactory;
+
     private static final String requestIdTemplate = "company-%d";
 
     private static final ObjectMapper mapper = new ObjectMapper();
@@ -91,78 +82,64 @@ public class SseServiceImpl implements ISseService {
     @Override
     public SseEmitter sseChat(ChatRequest chatRequest, HttpServletRequest request) {
         SseEmitter sseEmitter = new SseEmitter(0L);
-        SSEEventSourceListener openAIEventSourceListener = new SSEEventSourceListener(sseEmitter);
-        // 获取对话消息列表
-        List<Message> messages = chatRequest.getMessages();
         try {
-            // 查询模型信息
-            ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
-
-            OpenAiStreamClient openAiModelStreamClient;
-            if(chatModelVo!=null){
-                // 建请求客户端
-                openAiModelStreamClient = chatConfig.createOpenAiStreamClient(chatModelVo.getApiHost(), chatModelVo.getApiKey());
-                // 设置默认提示词
-                chatRequest.setSysPrompt(chatModelVo.getSystemPrompt());
-            }else {
-                // 使用默认客户端
-                openAiModelStreamClient = openAiStreamClient;
-            }
             // 构建消息列表增加联网、知识库等内容
             buildChatMessageList(chatRequest);
-
             // 根据模型名称前缀调用不同的处理逻辑
-            switchModelAndHandle(chatRequest);
-
+            switchModelAndHandle(chatRequest,sseEmitter);
             // 未登录用户限制对话次数
-            if (!StpUtil.isLogin()) {
-                String clientIp = IpUtil.getClientIp(request);
-                // 访客每天默认只能对话5次
-                int timeWindowInSeconds = 5;
-                String redisKey = "clientIp:" + clientIp;
-                int count = 0;
-                if (RedisUtils.getCacheObject(redisKey) == null) {
-                    // 缓存有效时间1天
-                    RedisUtils.setCacheObject(redisKey, count, Duration.ofSeconds(86400));
-                }else {
-                    count = RedisUtils.getCacheObject(redisKey);
-                    if (count >= timeWindowInSeconds) {
-                        throw new ServiceException("当日免费次数已用完");
-                    }
-                    count++;
-                    RedisUtils.setCacheObject(redisKey, count);
-                }
-            }
-
-            ChatCompletion completion = ChatCompletion
-                    .builder()
-                    .messages(messages)
-                    .model(chatRequest.getModel())
-                    .stream(chatRequest.getStream())
-                    .build();
-            openAiModelStreamClient.streamChatCompletion(completion, openAIEventSourceListener);
-
+            checkUnauthenticatedUserChatLimit(request);
             // 保存消息记录 并扣除费用
             chatCostService.deductToken(chatRequest);
         } catch (Exception e) {
             String message = e.getMessage();
-            sendErrorEvent(sseEmitter, message);
+            SSEUtil.sendErrorEvent(sseEmitter, message);
             return sseEmitter;
         }
         return sseEmitter;
     }
 
+    /**
+     * 检查未登录用户是否超过当日对话次数限制
+     *
+     * @param request 当前请求
+     * @throws ServiceException 如果当日免费次数已用完
+     */
+    public void checkUnauthenticatedUserChatLimit(HttpServletRequest request) throws ServiceException {
+        // 未登录用户限制对话次数
+        if (!StpUtil.isLogin()) {
+            String clientIp = IpUtil.getClientIp(request);
+            // 访客每天默认只能对话5次
+            int timeWindowInSeconds = 5;
+            String redisKey = "clientIp:" + clientIp;
+            int count = 0;
+            // 检查Redis中的对话次数
+            if (RedisUtils.getCacheObject(redisKey) == null) {
+                // 缓存有效时间1天
+                RedisUtils.setCacheObject(redisKey, count, Duration.ofSeconds(86400));
+            } else {
+                count = RedisUtils.getCacheObject(redisKey);
+                if (count >= timeWindowInSeconds) {
+                    throw new ServiceException("当日免费次数已用完");
+                }
+                count++;
+                RedisUtils.setCacheObject(redisKey, count);
+            }
+        }
+    }
+
     /**
      *  根据模型名称前缀调用不同的处理逻辑
      */
-    private void switchModelAndHandle(ChatRequest chatRequest) {
+    private void switchModelAndHandle(ChatRequest chatRequest,SseEmitter emitter) {
         String model = chatRequest.getModel();
         // 如果模型名称以ollama开头,则调用ollama中部署的本地模型
         if (model.startsWith("ollama-")) {
             String[] parts = chatRequest.getModel().split("ollama-", 2); // 限制分割次数为2
             if (parts.length > 1) {
                 chatRequest.setModel(parts[1]);
-                ollamaChat(chatRequest);
+                IChatService chatService = sseServiceFactory.getSseService("ollama");
+                chatService.chat(chatRequest,emitter);
             } else {
                 throw new IllegalArgumentException("Invalid ollama model name: " + chatRequest.getModel());
             }
@@ -177,8 +154,13 @@ public class SseServiceImpl implements ISseService {
     private void buildChatMessageList(ChatRequest chatRequest){
         // 获取对话消息列表
         List<Message> messages = chatRequest.getMessages();
+        String sysPrompt = chatRequest.getSysPrompt();
+        if(StringUtils.isEmpty(sysPrompt)){
+            sysPrompt ="你是一个由RuoYI-AI开发的人工智能助手,名字叫熊猫助手。你擅长中英文对话,能够理解并处理各种问题,提供安全、有帮助、准确的回答。" +
+                    "当前时间:"+ DateUtils.getDate();
+        }
         // 设置系统默认提示词
-        Message sysMessage = Message.builder().content(chatRequest.getSysPrompt()).role(Message.Role.SYSTEM).build();
+        Message sysMessage = Message.builder().content(sysPrompt).role(Message.Role.SYSTEM).build();
         messages.add(0,sysMessage);
 
         // 查询向量库相关信息加入到上下文
@@ -216,23 +198,6 @@ public class SseServiceImpl implements ISseService {
         }
     }
 
-    /**
-     * 发送SSE错误事件的封装方法
-     *
-     * @param sseEmitter
-     * @param errorMessage
-     */
-    private void sendErrorEvent(SseEmitter sseEmitter, String errorMessage) {
-        SseEmitter.SseEventBuilder event = SseEmitter.event()
-                .name("error")
-                .data(errorMessage);
-        try {
-            sseEmitter.send(event);
-        } catch (IOException e) {
-            log.error("SSE发送失败: {}", e.getMessage());
-        }
-        sseEmitter.complete();
-    }
 
     /**
      * 文字转语音
@@ -323,51 +288,6 @@ public class SseServiceImpl implements ISseService {
         return file;
     }
 
-    @Override
-    public SseEmitter ollamaChat(ChatRequest chatRequest) {
-
-        ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
-        final SseEmitter emitter = new SseEmitter();
-        String host = chatModelVo.getApiHost();
-        List<Message> msgList = chatRequest.getMessages();
-
-        List<OllamaChatMessage> messages = new ArrayList<>();
-        for (Message message : msgList) {
-            OllamaChatMessage ollamaChatMessage = new OllamaChatMessage();
-            ollamaChatMessage.setRole(OllamaChatMessageRole.USER);
-            ollamaChatMessage.setContent(message.getContent().toString());
-            messages.add(ollamaChatMessage);
-        }
-        OllamaAPI api = new OllamaAPI(host);
-        api.setRequestTimeoutSeconds(100);
-        OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(chatRequest.getModel());
-
-        OllamaChatRequestModel requestModel = builder
-            .withMessages(messages)
-            .build();
-
-        // 异步执行 OllAma API 调用
-        CompletableFuture.runAsync(() -> {
-            try {
-                StringBuilder response = new StringBuilder();
-                OllamaStreamHandler streamHandler = (s) -> {
-                    String substr = s.substring(response.length());
-                    response.append(substr);
-                    System.out.println(substr);
-                    try {
-                        emitter.send(substr);
-                    } catch (IOException e) {
-                        sendErrorEvent(emitter, e.getMessage());
-                    }
-                };
-                api.chat(requestModel, streamHandler);
-                emitter.complete();
-            } catch (Exception e) {
-                sendErrorEvent(emitter, e.getMessage());
-            }
-        });
-        return emitter;
-    }
 
     @Override
     public String wxCpChat(String prompt) {

+ 33 - 0
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/util/SSEUtil.java

@@ -0,0 +1,33 @@
+package org.ruoyi.chat.util;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.io.IOException;
+
+/**
+ * sse工具类
+ *
+ * @author WangLe
+ */
+@Slf4j
+public class SSEUtil {
+
+    /**
+     * 发送SSE错误事件的封装方法
+     *
+     * @param sseEmitter sse事件对象
+     * @param errorMessage 错误信息
+     */
+    public static void sendErrorEvent(SseEmitter sseEmitter, String errorMessage) {
+        SseEmitter.SseEventBuilder event = SseEmitter.event()
+                .name("error")
+                .data(errorMessage);
+        try {
+            sseEmitter.send(event);
+        } catch (IOException e) {
+            log.error("SSE发送失败: {}", e.getMessage());
+        }
+        sseEmitter.complete();
+    }
+}