From c84d6247b0c84ebdc09b980fa8ddca5fa66c40ee Mon Sep 17 00:00:00 2001 From: ageerle Date: Fri, 20 Mar 2026 01:20:41 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E6=A8=A1=E5=9D=97=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除废弃的ChatMessageDTO、ChatContext、AbstractChatMessageService等类 - 迁移ChatServiceFactory和IChatMessageService到ruoyi-chat模块 - 重构ChatHandler体系,移除DefaultChatHandler和ChatContextBuilder - 优化SSE消息处理,新增SseEventDto - 简化各AI服务提供商实现类代码 - 优化工作流节点消息处理逻辑 Co-Authored-By: Claude Opus 4.6 --- .../src/main/resources/application-dev.yml | 2 +- .../chat/domain/dto/ChatMessageDTO.java | 45 --- .../chat/domain/dto/request/ChatRequest.java | 56 ++- .../chat/domain/vo/chat/ChatMessageVo.java | 2 - .../ruoyi/common/chat/entity/BaseEntity.java | 2 + .../common/chat/entity/chat/ChatContext.java | 63 ---- .../chat/service/chat/IChatService.java | 14 +- .../AbstractChatMessageService.java | 58 --- .../common/sse/core/SseEmitterManager.java | 35 +- .../org/ruoyi/common/sse/dto/SseEventDto.java | 92 +++++ .../common/sse/utils/SseMessageUtils.java | 61 ++- .../workflow/util/WorkflowMessageUtil.java | 3 +- .../ruoyi/workflow/workflow/WorkflowUtil.java | 68 +--- .../workflow/node/answer/LLMAnswerNode.java | 4 +- .../KeywordExtractorNode.java | 3 +- .../KnowledgeRetrievalNode.java | 3 +- ruoyi-modules/ruoyi-chat/pom.xml | 5 + .../ruoyi/controller/chat/ChatController.java | 4 +- .../chat/ChatMessageController.java | 2 +- .../ruoyi}/factory/ChatServiceFactory.java | 13 +- .../service/chat/AbstractChatService.java | 28 ++ .../service/chat}/IChatMessageService.java | 17 +- .../chat/handler/AgentChatHandler.java | 290 +++++++-------- .../chat/handler/ChatContextBuilder.java | 136 ------- .../service/chat/handler/ChatHandler.java | 42 --- .../chat/handler/DefaultChatHandler.java | 247 ------------ .../chat/handler/ResumeChatHandler.java | 106 +++--- .../chat/handler/WorkflowChatHandler.java | 108 +++--- .../impl/AbstractStreamingChatService.java | 182 --------- .../chat/impl/ChatMessageServiceImpl.java | 57 ++- .../service/chat/impl/ChatServiceFacade.java | 352 +++++++++++++++++- .../memory/ChatMemoryProviderFactory.java | 64 ---- .../memory/PersistentChatMemoryStore.java | 35 +- .../impl/provider/DeepseekServiceImpl.java | 12 +- .../chat/impl/provider/OllamaServiceImpl.java | 18 +- .../chat/impl/provider/OpenAIServiceImpl.java | 15 +- .../chat/impl/provider/PPIOServiceImpl.java | 17 +- .../impl/provider/QianWenChatServiceImpl.java | 54 +-- .../impl/provider/ZhiPuChatServiceImpl.java | 12 +- 39 files changed, 933 insertions(+), 1394 deletions(-) delete mode 100644 ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/ChatMessageDTO.java delete mode 100644 ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/chat/ChatContext.java delete mode 100644 ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chatMessage/AbstractChatMessageService.java create mode 100644 ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/dto/SseEventDto.java rename {ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat => ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi}/factory/ChatServiceFactory.java (67%) create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/AbstractChatService.java rename {ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chatMessage => ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat}/IChatMessageService.java (80%) delete mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/ChatContextBuilder.java delete mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/ChatHandler.java delete mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/DefaultChatHandler.java delete mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/AbstractStreamingChatService.java delete mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/ChatMemoryProviderFactory.java diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 752bedc2..5168f7a0 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -58,7 +58,7 @@ spring: driverClassName: com.mysql.cj.jdbc.Driver # jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562 # rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题) - url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai-agent?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true + url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true username: root password: root # agent: diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/ChatMessageDTO.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/ChatMessageDTO.java deleted file mode 100644 index 633c90c8..00000000 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/ChatMessageDTO.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.ruoyi.common.chat.domain.dto; - -import lombok.Data; - -/** - * 聊天消息DTO - 用于上下文传递 - * - * @author ageerle@163.com - * @date 2025/12/13 - */ -@Data -public class ChatMessageDTO { - - /** - * 消息角色: system/user/assistant - */ - private String role; - - /** - * 消息内容 - */ - private String content; - - public static ChatMessageDTO system(String content) { - ChatMessageDTO msg = new ChatMessageDTO(); - msg.role = "system"; - msg.content = content; - return msg; - } - - public static ChatMessageDTO user(String content) { - ChatMessageDTO msg = new ChatMessageDTO(); - msg.role = "user"; - msg.content = content; - return msg; - } - - public static ChatMessageDTO assistant(String content) { - ChatMessageDTO msg = new ChatMessageDTO(); - msg.role = "assistant"; - msg.content = content; - return msg; - } -} - diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/request/ChatRequest.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/request/ChatRequest.java index 8d13339e..f14fb086 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/request/ChatRequest.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/request/ChatRequest.java @@ -1,11 +1,11 @@ package org.ruoyi.common.chat.domain.dto.request; -import dev.langchain4j.data.message.ChatMessage; +import com.alibaba.fastjson.annotation.JSONField; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import jakarta.validation.constraints.NotEmpty; import lombok.Data; -import org.ruoyi.common.chat.domain.dto.ChatMessageDTO; -import java.util.List; /** * 对话请求对象 @@ -16,11 +16,15 @@ import java.util.List; @Data public class ChatRequest { - @NotEmpty(message = "对话消息不能为空") - private List messages; @NotEmpty(message = "传入的模型不能为空") private String model; + /** + * 对话消息 + */ + @NotEmpty(message = "对话消息不能为空") + private String content; + /** * 工作流请求体 */ @@ -31,59 +35,49 @@ public class ChatRequest { */ private ReSumeRunner reSumeRunner; + /** + * 是否为人机交互用户继续输入 + */ + private Boolean isResume = false; + /** * 是否启用工作流 */ - private Boolean enableWorkFlow; + private Boolean enableWorkFlow = false; /** * 会话id */ + @JsonSerialize(using = ToStringSerializer.class) + @JSONField(serializeUsing = String.class) private Long sessionId; - /** - * 应用ID - */ - private String appId; - /** * 知识库id */ private String knowledgeId; /** - * 对话id(每个聊天窗口都不一样) + * 应用ID */ - private Long uuid; + private String appId; + /** - * 是否为人机交互用户继续输入 + * 对话id(每个聊天窗口都不一样) */ - private Boolean isResume; + @JsonSerialize(using = ToStringSerializer.class) + @JSONField(serializeUsing = String.class) + private Long uuid; /** * 是否启用深度思考 */ - private Boolean enableThinking; - - /** - * 是否自动切换模型 - */ - private Boolean autoSelectModel; + private Boolean enableThinking = false; /** * 是否支持联网 */ private Boolean enableInternet; - /** - * 会话令牌(为避免在非Web线程中获取Request,入口处注入) - */ - private String token; - - /** - * 原生对话对象 - */ - private List chatMessages; - } diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatMessageVo.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatMessageVo.java index bb5a9339..1981320b 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatMessageVo.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatMessageVo.java @@ -5,8 +5,6 @@ import cn.idev.excel.annotation.ExcelProperty; import io.github.linpeilie.annotations.AutoMapper; import lombok.Data; import org.ruoyi.common.chat.entity.chat.ChatMessage; -import org.ruoyi.common.excel.annotation.ExcelDictFormat; -import org.ruoyi.common.excel.convert.ExcelDictConvert; import java.io.Serial; import java.io.Serializable; diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/BaseEntity.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/BaseEntity.java index e9630ff2..61df1604 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/BaseEntity.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/BaseEntity.java @@ -6,12 +6,14 @@ import com.baomidou.mybatisplus.annotation.TableId; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.io.Serial; import java.io.Serializable; import java.time.LocalDateTime; @Data public class BaseEntity implements Serializable { + @Serial private static final long serialVersionUID = 1L; @TableId(type = IdType.AUTO) diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/chat/ChatContext.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/chat/ChatContext.java deleted file mode 100644 index d2ef6169..00000000 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/chat/ChatContext.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.ruoyi.common.chat.entity.chat; - -import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; -import jakarta.validation.constraints.NotNull; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.ruoyi.common.chat.domain.dto.request.ChatRequest; -import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; -import org.ruoyi.common.chat.service.chat.IChatService; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -/** - * 聊天对话上下文对象 - * - * @author zengxb - * @date 2026-02-14 - */ -@Data -@EqualsAndHashCode(callSuper = false) -@Builder -public class ChatContext { - - /** - * 模型管理视图对象 - */ - @NotNull(message = "模型管理视图对象不能为空") - private ChatModelVo chatModelVo; - - /** - * 对话请求对象 - */ - @NotNull(message = "对话请求对象不能为空") - private ChatRequest chatRequest; - - /** - * SSe连接对象 - */ - @NotNull(message = "SSe连接对象不能为空") - private SseEmitter emitter; - - /** - * 用户ID - */ - @NotNull(message = "用户ID不能为空") - private Long userId; - - /** - * Token - */ - @NotNull(message = "Token不能为空") - private String tokenValue; - - /** - * 响应处理器 - */ - private StreamingChatResponseHandler handler; - - /** - * 聊天服务实例 - */ - private IChatService chatService; -} diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chat/IChatService.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chat/IChatService.java index de9f13ca..155a582e 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chat/IChatService.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chat/IChatService.java @@ -1,7 +1,8 @@ package org.ruoyi.common.chat.service.chat; +import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import jakarta.validation.Valid; -import org.ruoyi.common.chat.entity.chat.ChatContext; +import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; /** @@ -12,10 +13,15 @@ public interface IChatService { /** * 客户端发送对话消息到服务端 */ - SseEmitter chat(@Valid ChatContext chatContext); + SseEmitter chat(@Valid ChatRequest chatRequest); /** - * 获取服务提供商名称 + * 支持外部 handler 的对话接口(跨模块调用) + * 同时发送到 SSE 和外部 handler + * + * @param chatRequest 聊天请求 + * @param externalHandler 外部响应处理器(可为 null) */ - String getProviderName(); + void chat(@Valid ChatRequest chatRequest, StreamingChatResponseHandler externalHandler); + } diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chatMessage/AbstractChatMessageService.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chatMessage/AbstractChatMessageService.java deleted file mode 100644 index d9cffb57..00000000 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chatMessage/AbstractChatMessageService.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.ruoyi.common.chat.service.chatMessage; - -import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo; -import org.ruoyi.common.chat.domain.dto.request.ChatRequest; -import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; - -/** - * 聊天信息抽象基类 - 保存聊天信息 - * - * @author Zengxb - * @date 2026-02-24 - */ -public abstract class AbstractChatMessageService { - - /** - * 创建日志对象 - */ - Logger log = LoggerFactory.getLogger(AbstractChatMessageService.class); - - @Autowired - private IChatMessageService chatMessageService; - - /** - * 保存聊天信息 - */ - public void saveChatMessage(ChatRequest chatRequest, Long userId, String content, String role, ChatModelVo chatModelVo){ - try { - // 验证必要的上下文信息 - if (chatRequest == null || userId == null) { - log.warn("缺少必要的聊天上下文信息,无法保存消息"); - return; - } - - // 创建ChatMessageBo对象 - ChatMessageBo messageBO = new ChatMessageBo(); - messageBO.setUserId(userId); - messageBO.setSessionId(chatRequest.getSessionId()); - messageBO.setContent(content); - messageBO.setRole(role); - messageBO.setModelName(chatRequest.getModel()); - messageBO.setRemark(null); - - chatMessageService.insertByBo(messageBO); - } catch (Exception e) { - log.error("保存{}聊天消息时出错: {}", getProviderName(), e.getMessage(), e); - } - } - - /** - * 获取服务提供商名称 - */ - protected String getProviderName(){ - return "默认工作流大模型"; - } -} diff --git a/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/core/SseEmitterManager.java b/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/core/SseEmitterManager.java index 4e7a5481..53086b20 100644 --- a/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/core/SseEmitterManager.java +++ b/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/core/SseEmitterManager.java @@ -2,9 +2,11 @@ package org.ruoyi.common.sse.core; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; +import cn.hutool.json.JSONUtil; import lombok.extern.slf4j.Slf4j; import org.ruoyi.common.core.utils.SpringUtils; import org.ruoyi.common.redis.utils.RedisUtils; +import org.ruoyi.common.sse.dto.SseEventDto; import org.ruoyi.common.sse.dto.SseMessageDto; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @@ -65,7 +67,7 @@ public class SseEmitterManager { emitter.onCompletion(() -> { SseEmitter remove = emitters.remove(token); if (remove != null) { -// remove.complete(); + remove.complete(); } }); emitter.onTimeout(() -> { @@ -174,9 +176,11 @@ public class SseEmitterManager { if (MapUtil.isNotEmpty(emitters)) { for (Map.Entry entry : emitters.entrySet()) { try { + // 格式化为标准SSE JSON格式 + SseEventDto eventDto = SseEventDto.content(message); entry.getValue().send(SseEmitter.event() .name("message") - .data(message)); + .data(JSONUtil.toJsonStr(eventDto))); } catch (Exception e) { SseEmitter remove = emitters.remove(entry.getKey()); if (remove != null) { @@ -189,6 +193,33 @@ public class SseEmitterManager { } } + /** + * 向指定的用户会话发送结构化事件 + * + * @param userId 要发送消息的用户id + * @param eventDto SSE事件对象 + */ + public void sendEvent(Long userId, SseEventDto eventDto) { + Map emitters = USER_TOKEN_EMITTERS.get(userId); + if (MapUtil.isNotEmpty(emitters)) { + for (Map.Entry entry : emitters.entrySet()) { + try { + entry.getValue().send(SseEmitter.event() + .name(eventDto.getEvent()) + .data(JSONUtil.toJsonStr(eventDto))); + } catch (Exception e) { + SseEmitter remove = emitters.remove(entry.getKey()); + if (remove != null) { + remove.complete(); + } + } + } + } else { + USER_TOKEN_EMITTERS.remove(userId); + } + } + + /** * 本机全用户会话发送消息 * diff --git a/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/dto/SseEventDto.java b/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/dto/SseEventDto.java new file mode 100644 index 00000000..68081926 --- /dev/null +++ b/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/dto/SseEventDto.java @@ -0,0 +1,92 @@ +package org.ruoyi.common.sse.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serial; +import java.io.Serializable; + +/** + * SSE 事件数据传输对象 + *

+ * 标准的 SSE 消息格式,支持不同事件类型 + * + * @author ageerle@163.com + * @date 2025/03/19 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SseEventDto implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 事件类型 + */ + private String event; + + /** + * 消息内容 + */ + private String content; + + /** + * 推理内容(深度思考模式) + */ + private String reasoningContent; + + /** + * 错误信息 + */ + private String error; + + /** + * 是否完成 + */ + private Boolean done; + + /** + * 创建内容事件 + */ + public static SseEventDto content(String content) { + return SseEventDto.builder() + .event("content") + .content(content) + .build(); + } + + /** + * 创建推理内容事件 + */ + public static SseEventDto reasoning(String reasoningContent) { + return SseEventDto.builder() + .event("reasoning") + .reasoningContent(reasoningContent) + .build(); + } + + /** + * 创建完成事件 + */ + public static SseEventDto done() { + return SseEventDto.builder() + .event("done") + .done(true) + .build(); + } + + /** + * 创建错误事件 + */ + public static SseEventDto error(String error) { + return SseEventDto.builder() + .event("error") + .error(error) + .build(); + } +} \ No newline at end of file diff --git a/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/utils/SseMessageUtils.java b/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/utils/SseMessageUtils.java index b4c224d5..49173343 100644 --- a/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/utils/SseMessageUtils.java +++ b/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/utils/SseMessageUtils.java @@ -1,10 +1,12 @@ package org.ruoyi.common.sse.utils; +import java.util.Collections; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.ruoyi.common.core.utils.SpringUtils; import org.ruoyi.common.sse.core.SseEmitterManager; +import org.ruoyi.common.sse.dto.SseEventDto; import org.ruoyi.common.sse.dto.SseMessageDto; /** @@ -27,6 +29,7 @@ public class SseMessageUtils { /** * 向指定的SSE会话发送消息 + * 通过 Redis Pub/Sub 广播,确保跨模块消息可达 * * @param userId 要发送消息的用户id * @param message 要发送的消息内容 @@ -35,7 +38,11 @@ public class SseMessageUtils { if (!isEnable()) { return; } - MANAGER.sendMessage(userId, message); + // 通过 Redis 广播,让所有模块的 SseTopicListener 接收并转发到本地 SSE 连接 + SseMessageDto dto = new SseMessageDto(); + dto.setMessage(message); + dto.setUserIds(Collections.singletonList(userId)); + MANAGER.publishMessage(dto); } /** @@ -86,6 +93,58 @@ public class SseMessageUtils { MANAGER.disconnect(userId, tokenValue); } + /** + * 向指定的SSE会话发送结构化事件 + * + * @param userId 要发送消息的用户id + * @param eventDto SSE事件对象 + */ + public static void sendEvent(Long userId, SseEventDto eventDto) { + if (!isEnable()) { + return; + } + MANAGER.sendEvent(userId, eventDto); + } + + /** + * 发送内容事件 + * + * @param userId 用户ID + * @param content 内容 + */ + public static void sendContent(Long userId, String content) { + sendEvent(userId, SseEventDto.content(content)); + } + + /** + * 发送推理内容事件 + * + * @param userId 用户ID + * @param reasoningContent 推理内容 + */ + public static void sendReasoning(Long userId, String reasoningContent) { + sendEvent(userId, SseEventDto.reasoning(reasoningContent)); + } + + /** + * 发送完成事件 + * + * @param userId 用户ID + */ + public static void sendDone(Long userId) { + sendEvent(userId, SseEventDto.done()); + } + + /** + * 发送错误事件 + * + * @param userId 用户ID + * @param error 错误信息 + */ + public static void sendError(Long userId, String error) { + sendEvent(userId, SseEventDto.error(error)); + } + /** * 是否开启 */ diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/util/WorkflowMessageUtil.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/util/WorkflowMessageUtil.java index 9fef1230..f94e4a4c 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/util/WorkflowMessageUtil.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/util/WorkflowMessageUtil.java @@ -64,7 +64,8 @@ public class WorkflowMessageUtil { ChatRequest chatRequest = new ChatRequest(); chatRequest.setSessionId(sessionId); WorkflowUtil workflowUtil = SpringUtils.getBean(WorkflowUtil.class); - workflowUtil.saveChatMessage(chatRequest, userId, message, RoleType.WORKFLOW.getName(), new ChatModelVo()); + // todo 保存消息 + //workflowUtil.saveChatMessage(chatRequest, userId, message, RoleType.WORKFLOW.getName(), new ChatModelVo()); } } diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowUtil.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowUtil.java index 53eb41cf..f1a944be 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowUtil.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowUtil.java @@ -4,23 +4,18 @@ import cn.hutool.core.collection.CollStreamUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import dev.langchain4j.data.message.ChatMessage; -import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.bsc.langgraph4j.langchain4j.generators.StreamingChatGenerator; import org.bsc.langgraph4j.state.AgentState; -import org.ruoyi.common.chat.enums.RoleType; import org.ruoyi.common.chat.service.chat.IChatModelService; import org.ruoyi.common.chat.service.chat.IChatService; -import org.ruoyi.common.chat.service.chatMessage.AbstractChatMessageService; import org.ruoyi.common.chat.service.image.IImageGenerationService; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; -import org.ruoyi.common.chat.entity.chat.ChatContext; import org.ruoyi.common.chat.entity.image.ImageContext; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; -import org.ruoyi.common.chat.factory.ChatServiceFactory; import org.ruoyi.common.chat.factory.ImageServiceFactory; import org.ruoyi.workflow.base.NodeInputConfigTypeHandler; import org.ruoyi.workflow.entity.WorkflowNode; @@ -29,9 +24,7 @@ import org.ruoyi.workflow.util.JsonUtil; import org.ruoyi.workflow.workflow.data.NodeIOData; import org.ruoyi.workflow.workflow.data.NodeIODataContent; import org.ruoyi.workflow.workflow.def.WfNodeParamRef; -import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.*; @@ -39,10 +32,7 @@ import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_O @Slf4j @Service -public class WorkflowUtil extends AbstractChatMessageService { - - @Resource - private ChatServiceFactory chatServiceFactory; +public class WorkflowUtil{ @Resource private ImageServiceFactory imageServiceFactory; @@ -50,6 +40,9 @@ public class WorkflowUtil extends AbstractChatMessageService { @Resource private IChatModelService chatModelService; + @Resource + private IChatService chatService; + public static String renderTemplate(String template, List values) { // 🔒 关键修复:如果 template 为 null,直接返回 null 或空字符串 if (template == null) { @@ -112,54 +105,23 @@ public class WorkflowUtil extends AbstractChatMessageService { } public void streamingInvokeLLM(WfState wfState, WfNodeState state, WorkflowNode node, String modelName, - List systemMessage, String nodeMessageTemplate) { + String prompt, String nodeMessageTemplate) { log.info("stream invoke, modelName: {}", modelName); - // 根据模型名称查询模型信息 - ChatModelVo chatModelVo = chatModelService.selectModelByName(modelName); - if (chatModelVo == null) { - throw new IllegalArgumentException("模型不存在: " + modelName); - } - - // 路由服务提供商 - String category = chatModelVo.getProviderCode(); - // 根据 category 获取对应的 ChatService(不使用计费代理,工作流场景单独计费) - IChatService chatService = chatServiceFactory.getOriginalService(category); - // 获取用户信息和Token以及SSe连接对象(对话接口需要使用) Long sessionId = wfState.getSessionId(); - Long userId = wfState.getUserId(); - String tokenValue = wfState.getTokenValue(); - SseEmitter sseEmitter = wfState.getSseEmitter(); - - // 构建 ruoyi-ai 的 ChatRequest - List chatMessages = new ArrayList<>(); - addUserMessage(node, state.getInputs(), chatMessages); - chatMessages.addAll(systemMessage); - // 定义模型调用对象 ChatRequest chatRequest = new ChatRequest(); - // 目前工作流深度思考成员变量只能写死 chatRequest.setSessionId(sessionId); chatRequest.setEnableThinking(false); chatRequest.setModel(modelName); - chatRequest.setChatMessages(chatMessages); + chatRequest.setContent(prompt); // 构建流式生成器 StreamingChatGenerator streamingGenerator = StreamingChatGenerator.builder() .mapResult(response -> { String responseTxt = response.aiMessage().text(); log.info("llm response:{}", responseTxt); - - // 会话ID不为空时插入数据库 - if (sessionId != null){ - // 获取模板消息拼接信息体 - String message = nodeMessageTemplate + responseTxt; - // 保存助手回复消息 - saveChatMessage(chatRequest, userId, message, RoleType.ASSISTANT.getName(), chatModelVo); - log.info("{}消息结束,已保存到数据库", getProviderName()); - } - // 传递所有输入数据 + 添加 LLM 输出 wfState.getNodeStateByNodeUuid(node.getUuid()).ifPresent(item -> { List outputs = new ArrayList<>(item.getInputs()); @@ -174,21 +136,13 @@ public class WorkflowUtil extends AbstractChatMessageService { .startingState(state) .build(); - // 构建流式回调响应器 - StreamingChatResponseHandler handler = streamingGenerator.handler(); + // 获取 StreamingChatGenerator 的 handler,用于处理流式响应 + StreamingChatResponseHandler workflowHandler = streamingGenerator.handler(); - //构建聊天对话上下文参数 - ChatContext chatContext = ChatContext.builder() - .chatModelVo(chatModelVo) - .chatRequest(chatRequest) - .emitter(sseEmitter) - .userId(userId) - .tokenValue(tokenValue) - .handler(handler) - .build(); + // 调用 Chat 服务,传入 workflow 的 handler + // 消息会同时发送到 SSE(前端)和 workflowHandler(工作流处理) + chatService.chat(chatRequest, workflowHandler); - // 使用工作流专用方法 - chatService.chat(chatContext); wfState.getNodeToStreamingGenerator().put(node.getUuid(), streamingGenerator); } diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNode.java index 2539226b..cddf7f74 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNode.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNode.java @@ -46,13 +46,11 @@ public class LLMAnswerNode extends AbstractWfNode { // 调用LLM WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class); String modelName = nodeConfigObj.getModelName(); - // 转换系统信息结构 - List systemMessage = List.of(new SystemMessage(prompt)); // 获取节点模板提示词信息 String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.LLM_RESPONSE.getValue()); // 发送SSE驱动事件消息 WorkflowMessageUtil.sendEmitterMessage(wfState.getSseEmitter(), node, nodeMessageTemplate); - workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, systemMessage, nodeMessageTemplate); + workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, prompt, nodeMessageTemplate); return new NodeProcessResult(); } } diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/keywordExtractor/KeywordExtractorNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/keywordExtractor/KeywordExtractorNode.java index a75c79f3..79585018 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/keywordExtractor/KeywordExtractorNode.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/keywordExtractor/KeywordExtractorNode.java @@ -67,13 +67,12 @@ public class KeywordExtractorNode extends AbstractWfNode { // 调用 LLM 进行关键词提取 WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class); String modelName = config.getModelName(); - List systemMessage = List.of(new SystemMessage(prompt)); // 获取节点模板提示词信息 String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.KEYWORD_EXTRACTOR.getValue()); // 发送SSE事件消息 WorkflowMessageUtil.sendEmitterMessage(wfState.getSseEmitter(), node, nodeMessageTemplate); // 使用流式调用 - workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, systemMessage, nodeMessageTemplate); + workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, prompt, nodeMessageTemplate); return new NodeProcessResult(); } diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/knowledgeRetrieval/KnowledgeRetrievalNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/knowledgeRetrieval/KnowledgeRetrievalNode.java index d934c79b..b7507f4d 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/knowledgeRetrieval/KnowledgeRetrievalNode.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/knowledgeRetrieval/KnowledgeRetrievalNode.java @@ -151,7 +151,6 @@ public class KnowledgeRetrievalNode extends AbstractWfNode { // 使用WorkflowUtil调用LLM(流式) WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class); - List systemMessage = List.of(new SystemMessage(prompt)); // 调用流式LLM String modelName = StringUtils.isNotBlank(config.getModelName()) ? config.getModelName() : "deepseek-chat"; @@ -161,7 +160,7 @@ public class KnowledgeRetrievalNode extends AbstractWfNode { tempState, tempNode, modelName, - systemMessage, + prompt, "" ); diff --git a/ruoyi-modules/ruoyi-chat/pom.xml b/ruoyi-modules/ruoyi-chat/pom.xml index f0a95fe5..ba2f10c0 100644 --- a/ruoyi-modules/ruoyi-chat/pom.xml +++ b/ruoyi-modules/ruoyi-chat/pom.xml @@ -19,6 +19,11 @@ ruoyi-common-chat + + org.ruoyi + ruoyi-common-sse + + org.ruoyi ruoyi-common-sensitive diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatController.java index 89566dee..d265206f 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatController.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatController.java @@ -30,8 +30,8 @@ public class ChatController { */ @PostMapping("/send") @ResponseBody - public SseEmitter sseChat(@RequestBody @Valid ChatRequest chatRequest, HttpServletRequest request) { - return chatService.sseChat(chatRequest,request); + public SseEmitter sseChat(@RequestBody @Valid ChatRequest chatRequest) { + return chatService.sseChat(chatRequest); } } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatMessageController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatMessageController.java index 7d85cdb1..2454d072 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatMessageController.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatMessageController.java @@ -8,7 +8,7 @@ import jakarta.validation.constraints.*; import cn.dev33.satoken.annotation.SaCheckPermission; import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo; import org.ruoyi.common.chat.domain.vo.chat.ChatMessageVo; -import org.ruoyi.common.chat.service.chatMessage.IChatMessageService; +import org.ruoyi.service.chat.IChatMessageService; import org.springframework.web.bind.annotation.*; import org.springframework.validation.annotation.Validated; import org.ruoyi.common.idempotent.annotation.RepeatSubmit; diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/factory/ChatServiceFactory.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/factory/ChatServiceFactory.java similarity index 67% rename from ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/factory/ChatServiceFactory.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/factory/ChatServiceFactory.java index 6e91254a..a52da9eb 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/factory/ChatServiceFactory.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/factory/ChatServiceFactory.java @@ -1,6 +1,7 @@ -package org.ruoyi.common.chat.factory; +package org.ruoyi.factory; import org.ruoyi.common.chat.service.chat.IChatService; +import org.ruoyi.service.chat.AbstractChatService; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -18,13 +19,13 @@ import java.util.concurrent.ConcurrentHashMap; @Component public class ChatServiceFactory implements ApplicationContextAware { - private final Map chatServiceMap = new ConcurrentHashMap<>(); + private final Map chatServiceMap = new ConcurrentHashMap<>(); @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { // 初始化时收集所有IChatService的实现 - Map serviceMap = applicationContext.getBeansOfType(IChatService.class); - for (IChatService service : serviceMap.values()) { + Map serviceMap = applicationContext.getBeansOfType(AbstractChatService.class); + for (AbstractChatService service : serviceMap.values()) { if (service != null ) { chatServiceMap.put(service.getProviderName(), service); } @@ -35,8 +36,8 @@ public class ChatServiceFactory implements ApplicationContextAware { /** * 获取原始服务(不包装代理) */ - public IChatService getOriginalService(String category) { - IChatService service = chatServiceMap.get(category); + public AbstractChatService getOriginalService(String category) { + AbstractChatService service = chatServiceMap.get(category); if (service == null) { throw new IllegalArgumentException("不支持的模型类别: " + category); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/AbstractChatService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/AbstractChatService.java new file mode 100644 index 00000000..8017fea2 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/AbstractChatService.java @@ -0,0 +1,28 @@ +package org.ruoyi.service.chat; + +import dev.langchain4j.model.chat.StreamingChatModel; +import org.ruoyi.common.chat.domain.dto.request.ChatRequest; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; + +/** + * 聊天消息Service接口 + * + * @author ageerle + * @date 2025-12-14 + */ +public interface AbstractChatService { + + /** + * 创建流式聊天模型 + * + * @param chatModelVo 模型配置 + * @param chatRequest 聊天请求 + * @return 流式聊天模型实例 + */ + StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest); + + /** + * 获取服务提供商名称 + */ + String getProviderName(); +} diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chatMessage/IChatMessageService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatMessageService.java similarity index 80% rename from ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chatMessage/IChatMessageService.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatMessageService.java index 98e31a40..790af133 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chatMessage/IChatMessageService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatMessageService.java @@ -1,7 +1,7 @@ -package org.ruoyi.common.chat.service.chatMessage; +package org.ruoyi.service.chat; +import dev.langchain4j.data.message.ChatMessage; import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo; -import org.ruoyi.common.chat.domain.dto.ChatMessageDTO; import org.ruoyi.common.chat.domain.vo.chat.ChatMessageVo; import org.ruoyi.common.mybatis.core.page.PageQuery; import org.ruoyi.common.mybatis.core.page.TableDataInfo; @@ -74,7 +74,7 @@ public interface IChatMessageService { * @param sessionId 会话ID * @return 消息DTO列表 */ - List getMessagesBySessionId(Long sessionId); + List getMessagesBySessionId(Long sessionId); /** * 根据会话ID删除所有消息 @@ -84,4 +84,15 @@ public interface IChatMessageService { * @return 是否删除成功 */ Boolean deleteBySessionId(Long sessionId); + + /** + * 保存聊天消息 + * + * @param userId 用户ID + * @param sessionId 会话ID + * @param content 消息内容 + * @param role 角色类型 + * @param modelName 模型名称 + */ + void saveChatMessage(Long userId, Long sessionId, String content, String role, String modelName); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/AgentChatHandler.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/AgentChatHandler.java index 98c0b3d1..ad0b06ca 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/AgentChatHandler.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/AgentChatHandler.java @@ -1,158 +1,132 @@ -package org.ruoyi.service.chat.handler; - -import dev.langchain4j.agentic.AgenticServices; -import dev.langchain4j.community.model.dashscope.QwenChatModel; -import dev.langchain4j.service.tool.ToolProvider; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.ruoyi.agent.McpAgent; -import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo; -import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; -import org.ruoyi.common.chat.entity.chat.ChatContext; -import org.ruoyi.common.chat.enums.RoleType; -import org.ruoyi.common.chat.service.chatMessage.IChatMessageService; -import org.ruoyi.common.sse.utils.SseMessageUtils; -import org.ruoyi.mcp.service.core.ToolProviderFactory; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -import java.util.ArrayList; -import java.util.List; - -/** - * Agent 深度思考处理器 - *

- * 处理 enableThinking=true 的场景,使用 Agent 进行深度思考和工具调用 - * - * @author ageerle@163.com - * @date 2025/12/13 - */ -@Slf4j -@Component -@Order(3) -@RequiredArgsConstructor -public class AgentChatHandler implements ChatHandler { - - private final ToolProviderFactory toolProviderFactory; - private final IChatMessageService chatMessageService; - - @Override - public boolean supports(ChatContext context) { - Boolean enableThinking = context.getChatRequest().getEnableThinking(); - return enableThinking != null && enableThinking; - } - - @Override - public SseEmitter handle(ChatContext context) { - log.info("处理 Agent 深度思考,用户: {}", context.getUserId()); - - Long userId = context.getUserId(); - String tokenValue = context.getTokenValue(); - ChatModelVo chatModelVo = context.getChatModelVo(); - - try { - // 1. 保存用户消息 - String content = extractUserContent(context); - saveChatMessage(context.getChatRequest(), userId, content, - RoleType.USER.getName(), chatModelVo); - - // 2. 执行 Agent 任务 - String result = doAgent(content, chatModelVo); - - // 3. 发送结果并保存 - SseMessageUtils.sendMessage(userId, result); - SseMessageUtils.completeConnection(userId, tokenValue); - saveChatMessage(context.getChatRequest(), userId, result, - RoleType.ASSISTANT.getName(), chatModelVo); - - } catch (Exception e) { - log.error("Agent 执行失败: {}", e.getMessage(), e); - SseMessageUtils.sendMessage(userId, "Agent 执行失败:" + e.getMessage()); - SseMessageUtils.completeConnection(userId, tokenValue); - } - - return context.getEmitter(); - } - - /** - * 执行 Agent 任务 - */ - private String doAgent(String userMessage, ChatModelVo chatModelVo) { - log.info("执行 Agent 任务,消息: {}", userMessage); - - try { - // 1. 加载 LLM 模型 - QwenChatModel qwenChatModel = QwenChatModel.builder() - .apiKey(chatModelVo.getApiKey()) - .modelName(chatModelVo.getModelName()) - .build(); - - // 2. 获取内置工具 - List builtinTools = toolProviderFactory.getAllBuiltinToolObjects(); - List allTools = new ArrayList<>(builtinTools); - log.debug("加载 {} 个内置工具", builtinTools.size()); - - // 3. 获取 MCP 工具提供者 - ToolProvider mcpToolProvider = toolProviderFactory.getAllEnabledMcpToolsProvider(); - - // 4. 创建 MCP Agent - var agentBuilder = AgenticServices.agentBuilder(McpAgent.class) - .chatModel(qwenChatModel); - - if (!allTools.isEmpty()) { - agentBuilder.tools(allTools.toArray(new Object[0])); - } - if (mcpToolProvider != null) { - agentBuilder.toolProvider(mcpToolProvider); - } - - McpAgent mcpAgent = agentBuilder.build(); - - // 5. 调用 Agent - String result = mcpAgent.callMcpTool(userMessage); - log.info("Agent 执行完成,结果长度: {}", result.length()); - return result; - - } catch (Exception e) { - log.error("Agent 模式执行失败: {}", e.getMessage(), e); - return "Agent 执行失败: " + e.getMessage(); - } - } - - /** - * 提取用户消息内容 - */ - private String extractUserContent(ChatContext context) { - var messages = context.getChatRequest().getMessages(); - if (messages != null && !messages.isEmpty()) { - return messages.get(0).getContent(); - } - return ""; - } - - /** - * 保存聊天消息 - */ - private void saveChatMessage(org.ruoyi.common.chat.domain.dto.request.ChatRequest chatRequest, - Long userId, String content, String role, ChatModelVo chatModelVo) { - try { - if (chatRequest == null || userId == null) { - log.warn("缺少必要的聊天上下文信息,无法保存消息"); - return; - } - - ChatMessageBo messageBO = new ChatMessageBo(); - messageBO.setUserId(userId); - messageBO.setSessionId(chatRequest.getSessionId()); - messageBO.setContent(content); - messageBO.setRole(role); - messageBO.setModelName(chatRequest.getModel()); - messageBO.setRemark(null); - - chatMessageService.insertByBo(messageBO); - } catch (Exception e) { - log.error("保存聊天消息时出错: {}", e.getMessage(), e); - } - } -} +//package org.ruoyi.service.chat.handler; +// +//import dev.langchain4j.agentic.AgenticServices; +//import dev.langchain4j.community.model.dashscope.QwenChatModel; +//import dev.langchain4j.service.tool.ToolProvider; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.ruoyi.agent.McpAgent; +//import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +//import org.ruoyi.common.chat.entity.chat.ChatContext; +//import org.ruoyi.common.chat.service.chatMessage.IChatMessageService; +//import org.ruoyi.common.sse.utils.SseMessageUtils; +//import org.ruoyi.mcp.service.core.ToolProviderFactory; +//import org.springframework.core.annotation.Order; +//import org.springframework.stereotype.Component; +//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +// +//import java.util.ArrayList; +//import java.util.List; +// +///** +// * Agent 深度思考处理器 +// *

+// * 处理 enableThinking=true 的场景,使用 Agent 进行深度思考和工具调用 +// * +// * @author ageerle@163.com +// * @date 2025/12/13 +// */ +//@Slf4j +//@Component +//@Order(3) +//@RequiredArgsConstructor +//public class AgentChatHandler implements ChatHandler { +// +// private final ToolProviderFactory toolProviderFactory; +// +// @Override +// public boolean supports(ChatContext context) { +// Boolean enableThinking = context.getChatRequest().getEnableThinking(); +// return enableThinking != null && enableThinking; +// } +// +// @Override +// public SseEmitter handle(ChatContext context) { +// log.info("处理 Agent 深度思考,用户: {}", context.getUserId()); +// +// Long userId = context.getUserId(); +// String tokenValue = context.getTokenValue(); +// ChatModelVo chatModelVo = context.getChatModelVo(); +// +// try { +// // 1. 保存用户消息 +// String content = extractUserContent(context); +//// saveChatMessage(context.getChatRequest(), userId, content, +//// RoleType.USER.getName(), chatModelVo); +// +// // 2. 执行 Agent 任务 +// String result = doAgent(content, chatModelVo); +// +// // 3. 发送结果并保存 +// SseMessageUtils.sendMessage(userId, result); +// SseMessageUtils.completeConnection(userId, tokenValue); +// +//// saveChatMessage(context.getChatRequest(), userId, result, +//// RoleType.ASSISTANT.getName(), chatModelVo); +// // todo 保存消息 +// } catch (Exception e) { +// log.error("Agent 执行失败: {}", e.getMessage(), e); +// SseMessageUtils.sendMessage(userId, "Agent 执行失败:" + e.getMessage()); +// SseMessageUtils.completeConnection(userId, tokenValue); +// } +// +// return context.getEmitter(); +// } +// +// /** +// * 执行 Agent 任务 +// */ +// private String doAgent(String userMessage, ChatModelVo chatModelVo) { +// log.info("执行 Agent 任务,消息: {}", userMessage); +// +// try { +// // 1. 加载 LLM 模型 +// QwenChatModel qwenChatModel = QwenChatModel.builder() +// .apiKey(chatModelVo.getApiKey()) +// .modelName(chatModelVo.getModelName()) +// .build(); +// +// // 2. 获取内置工具 +// List builtinTools = toolProviderFactory.getAllBuiltinToolObjects(); +// List allTools = new ArrayList<>(builtinTools); +// log.debug("加载 {} 个内置工具", builtinTools.size()); +// +// // 3. 获取 MCP 工具提供者 +// ToolProvider mcpToolProvider = toolProviderFactory.getAllEnabledMcpToolsProvider(); +// +// // 4. 创建 MCP Agent +// var agentBuilder = AgenticServices.agentBuilder(McpAgent.class) +// .chatModel(qwenChatModel); +// +// if (!allTools.isEmpty()) { +// agentBuilder.tools(allTools.toArray(new Object[0])); +// } +// if (mcpToolProvider != null) { +// agentBuilder.toolProvider(mcpToolProvider); +// } +// +// McpAgent mcpAgent = agentBuilder.build(); +// +// // 5. 调用 Agent +// String result = mcpAgent.callMcpTool(userMessage); +// log.info("Agent 执行完成,结果长度: {}", result.length()); +// return result; +// +// } catch (Exception e) { +// log.error("Agent 模式执行失败: {}", e.getMessage(), e); +// return "Agent 执行失败: " + e.getMessage(); +// } +// } +// +// /** +// * 提取用户消息内容 +// */ +// private String extractUserContent(ChatContext context) { +// var messages = context.getChatRequest().getMessages(); +// if (messages != null && !messages.isEmpty()) { +// return messages.get(0).getContent(); +// } +// return ""; +// } +// +//} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/ChatContextBuilder.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/ChatContextBuilder.java deleted file mode 100644 index 3799fd39..00000000 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/ChatContextBuilder.java +++ /dev/null @@ -1,136 +0,0 @@ -package org.ruoyi.service.chat.handler; - -import cn.dev33.satoken.stp.StpUtil; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.ruoyi.common.chat.domain.dto.ChatMessageDTO; -import org.ruoyi.common.chat.domain.dto.request.ChatRequest; -import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; -import org.ruoyi.common.chat.entity.chat.ChatContext; -import org.ruoyi.common.chat.factory.ChatServiceFactory; -import org.ruoyi.common.chat.service.chat.IChatModelService; -import org.ruoyi.common.chat.service.chat.IChatService; -import org.ruoyi.common.satoken.utils.LoginHelper; -import org.ruoyi.common.sse.core.SseEmitterManager; -import org.ruoyi.domain.bo.vector.QueryVectorBo; -import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo; -import org.ruoyi.service.knowledge.IKnowledgeInfoService; -import org.ruoyi.service.vector.VectorStoreService; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -import java.util.List; - -/** - * 对话上下文构建器 - *

- * 负责构建完整的对话上下文,包括: - * 1. 模型配置查询 - * 2. 知识库检索增强 - * 3. SSE连接创建 - * 4. 用户信息注入 - * - * @author ageerle@163.com - * @date 2025/12/13 - */ -@Slf4j -@Component -@RequiredArgsConstructor -public class ChatContextBuilder { - - private final IChatModelService chatModelService; - private final IKnowledgeInfoService knowledgeInfoService; - private final VectorStoreService vectorStoreService; - private final SseEmitterManager sseEmitterManager; - private final ChatServiceFactory chatServiceFactory; - - /** - * 构建对话上下文 - * - * @param chatRequest 对话请求 - * @return 完整的对话上下文 - */ - public ChatContext build(ChatRequest chatRequest) { - // 1. 查询模型配置 - ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel()); - if (chatModelVo == null) { - throw new IllegalArgumentException("模型不存在: " + chatRequest.getModel()); - } - - // 2. 构建上下文消息(知识库增强) - List contextMessages = buildContextMessages(chatRequest); - chatRequest.setMessages(contextMessages); - - // 3. 获取用户信息 - Long userId = LoginHelper.getUserId(); - String tokenValue = StpUtil.getTokenValue(); - - // 4. 创建SSE连接 - SseEmitter emitter = sseEmitterManager.connect(userId, tokenValue); - - // 5. 获取服务提供商 - String category = chatModelVo.getProviderCode(); - IChatService chatService = chatServiceFactory.getOriginalService(category); - log.info("路由到服务提供商: {}, 模型: {}", category, chatRequest.getModel()); - - // 6. 构建上下文对象 - return ChatContext.builder() - .chatModelVo(chatModelVo) - .chatRequest(chatRequest) - .emitter(emitter) - .userId(userId) - .tokenValue(tokenValue) - .chatService(chatService) - .build(); - } - - /** - * 构建上下文消息列表(知识库增强) - */ - private List buildContextMessages(ChatRequest chatRequest) { - List messages = chatRequest.getMessages(); - - // 从向量库查询相关历史消息 - if (chatRequest.getKnowledgeId() != null) { - KnowledgeInfoVo knowledgeInfoVo = knowledgeInfoService.queryById(Long.valueOf(chatRequest.getKnowledgeId())); - if (knowledgeInfoVo == null) { - log.warn("知识库信息不存在,kid: {}", chatRequest.getKnowledgeId()); - return messages; - } - - // 查询向量模型配置 - ChatModelVo chatModel = chatModelService.selectModelByName(knowledgeInfoVo.getEmbeddingModel()); - if (chatModel == null) { - log.warn("向量模型配置不存在,模型名称: {}", knowledgeInfoVo.getEmbeddingModel()); - return messages; - } - - // 构建向量查询参数并检索 - QueryVectorBo queryVectorBo = buildQueryVectorBo(chatRequest, knowledgeInfoVo, chatModel); - List nearestList = vectorStoreService.getQueryVector(queryVectorBo); - - // 知识库内容作为系统上下文添加 - for (String prompt : nearestList) { - messages.add(ChatMessageDTO.system(prompt)); - } - } - - return messages; - } - - /** - * 构建向量查询参数 - */ - private QueryVectorBo buildQueryVectorBo(ChatRequest chatRequest, KnowledgeInfoVo knowledgeInfoVo, - ChatModelVo chatModel) { - QueryVectorBo queryVectorBo = new QueryVectorBo(); - queryVectorBo.setQuery(chatRequest.getMessages().get(0).getContent()); - queryVectorBo.setKid(chatRequest.getKnowledgeId()); - queryVectorBo.setApiKey(chatModel.getApiKey()); - queryVectorBo.setBaseUrl(chatModel.getApiHost()); - queryVectorBo.setVectorModelName(knowledgeInfoVo.getVectorModel()); - queryVectorBo.setEmbeddingModelName(knowledgeInfoVo.getEmbeddingModel()); - queryVectorBo.setMaxResults(knowledgeInfoVo.getRetrieveLimit()); - return queryVectorBo; - } -} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/ChatHandler.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/ChatHandler.java deleted file mode 100644 index 3ce4f311..00000000 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/ChatHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.ruoyi.service.chat.handler; - -import org.ruoyi.common.chat.entity.chat.ChatContext; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -/** - * 对话处理器接口 - *

- * 使用策略模式,每种对话场景独立实现 - * 通过 Order 注解控制优先级 - * - * @author ageerle@163.com - * @date 2025/12/13 - */ -public interface ChatHandler { - - /** - * 是否支持处理该请求 - * - * @param context 对话上下文 - * @return true-支持处理,false-不支持 - */ - boolean supports(ChatContext context); - - /** - * 处理对话 - * - * @param context 对话上下文 - * @return SSE发射器 - */ - SseEmitter handle(ChatContext context); - - /** - * 优先级(越小越优先) - * 默认 100,数字越小优先级越高 - * - * @return 优先级数值 - */ - default int getOrder() { - return 100; - } -} \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/DefaultChatHandler.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/DefaultChatHandler.java deleted file mode 100644 index 610a1e93..00000000 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/DefaultChatHandler.java +++ /dev/null @@ -1,247 +0,0 @@ -package org.ruoyi.service.chat.handler; - -import dev.langchain4j.data.message.ChatMessage; -import dev.langchain4j.data.message.UserMessage; -import dev.langchain4j.memory.chat.MessageWindowChatMemory; -import dev.langchain4j.model.chat.StreamingChatModel; -import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; -import lombok.extern.slf4j.Slf4j; -import org.ruoyi.common.chat.entity.chat.ChatContext; -import org.ruoyi.common.chat.enums.RoleType; -import org.ruoyi.common.core.utils.StringUtils; -import org.ruoyi.common.sse.utils.SseMessageUtils; -import org.ruoyi.service.chat.impl.AbstractStreamingChatService; -import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; -import org.springframework.util.CollectionUtils; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import java.util.stream.Collectors; - -/** - * 默认对话处理器 - *

- * 处理普通对话场景,包含: - * 1. 历史记忆管理 - * 2. 消息保存 - * 3. 流式对话响应 - * - * @author ageerle@163.com - * @date 2025/12/13 - */ -@Slf4j -@Component -@Order(100) -public class DefaultChatHandler implements ChatHandler { - - private final Map chatServiceMap; - - /** - * 默认保留的消息窗口大小 - */ - private static final int DEFAULT_MAX_MESSAGES = 20; - - /** - * 是否启用长期记忆 - */ - private static final boolean ENABLE_PERSISTENT_MEMORY = true; - - /** - * 内存实例缓存 - */ - private static final Map MEMORY_CACHE = new ConcurrentHashMap<>(); - - /** - * 构造函数,注入所有聊天服务实现 - */ - public DefaultChatHandler(List chatServices) { - this.chatServiceMap = chatServices.stream() - .collect(Collectors.toMap( - AbstractStreamingChatService::getProviderName, - Function.identity() - )); - log.info("已加载 {} 个聊天服务: {}", chatServiceMap.size(), chatServiceMap.keySet()); - } - - /** - * 根据 providerCode 获取对应的聊天服务 - */ - private AbstractStreamingChatService getChatService(String providerCode) { - if (StringUtils.isBlank(providerCode)) { - // 默认使用千问服务 - return chatServiceMap.get("qianwen"); - } - AbstractStreamingChatService service = chatServiceMap.get(providerCode.toLowerCase()); - if (service == null) { - log.warn("未找到提供商 {} 对应的服务,使用默认千问服务", providerCode); - return chatServiceMap.get("qianwen"); - } - return service; - } - - @Override - public boolean supports(ChatContext context) { - // 默认处理器,始终支持 - return true; - } - - @Override - public SseEmitter handle(ChatContext context) { - log.info("处理默认对话,用户: {}, 会话: {}", - context.getUserId(), context.getChatRequest().getSessionId()); - - Long userId = context.getUserId(); - String tokenValue = context.getTokenValue(); - - // 根据 providerCode 获取对应的聊天服务 - String providerCode = context.getChatModelVo().getProviderCode(); - AbstractStreamingChatService chatService = getChatService(providerCode); - log.info("使用服务提供商: {}", chatService.getProviderName()); - - try { - // 1. 提取用户消息内容 - String content = extractUserContent(context); - - // 2. 保存用户消息 - chatService.saveChatMessage(context.getChatRequest(), userId, content, - RoleType.USER.getName(), context.getChatModelVo()); - - // 3. 构建包含历史记忆的消息列表 - List messagesWithMemory = buildMessagesWithMemory(context.getChatRequest()); - - // 4. 创建响应处理器 - StreamingChatResponseHandler handler = createResponseHandler( - context.getChatRequest(), userId, tokenValue, context.getChatModelVo(), chatService); - - // 5. 构建流式模型并执行对话 - StreamingChatModel streamingModel = chatService.buildStreamingChatModel( - context.getChatModelVo(), context.getChatRequest()); - streamingModel.chat(messagesWithMemory, handler); - - } catch (Exception e) { - log.error("对话处理失败: {}", e.getMessage(), e); - SseMessageUtils.sendMessage(userId, "对话出错:" + e.getMessage()); - SseMessageUtils.completeConnection(userId, tokenValue); - } - - return context.getEmitter(); - } - - /** - * 提取用户消息内容 - */ - private String extractUserContent(ChatContext context) { - return Optional.ofNullable(context.getChatRequest().getMessages()) - .filter(messages -> !messages.isEmpty()) - .map(messages -> messages.get(0).getContent()) - .filter(StringUtils::isNotBlank) - .orElseGet(() -> Optional.ofNullable(context.getChatRequest().getChatMessages()) - .orElse(List.of()).stream() - .filter(message -> message instanceof UserMessage) - .map(message -> ((UserMessage) message).singleText()) - .filter(StringUtils::isNotBlank) - .findFirst() - .orElse("")); - } - - /** - * 构建包含历史消息的消息列表 - */ - private List buildMessagesWithMemory(org.ruoyi.common.chat.domain.dto.request.ChatRequest chatRequest) { - List messages = new ArrayList<>(); - - // 添加工作流对话消息 - List chatMessages = chatRequest.getChatMessages(); - if (!CollectionUtils.isEmpty(chatMessages)) { - messages.addAll(chatMessages); - } - - // 添加历史记忆 - if (ENABLE_PERSISTENT_MEMORY && chatRequest.getSessionId() != null) { - MessageWindowChatMemory memory = createChatMemory(chatRequest.getSessionId()); - if (memory != null) { - List historicalMessages = memory.messages(); - if (historicalMessages != null && !historicalMessages.isEmpty()) { - messages.addAll(historicalMessages); - log.debug("已加载 {} 条历史消息用于会话 {}", historicalMessages.size(), chatRequest.getSessionId()); - } - } - } - - return messages; - } - - /** - * 创建或获取聊天内存实例 - */ - private MessageWindowChatMemory createChatMemory(Object memoryId) { - return MEMORY_CACHE.computeIfAbsent(memoryId, key -> { - try { - PersistentChatMemoryStore store = new PersistentChatMemoryStore(); - return MessageWindowChatMemory.builder() - .id(memoryId) - .maxMessages(DEFAULT_MAX_MESSAGES) - .chatMemoryStore(store) - .build(); - } catch (Exception e) { - log.warn("创建聊天内存失败: {}", e.getMessage()); - return null; - } - }); - } - - /** - * 创建响应处理器 - */ - private StreamingChatResponseHandler createResponseHandler( - org.ruoyi.common.chat.domain.dto.request.ChatRequest chatRequest, - Long userId, - String tokenValue, - org.ruoyi.common.chat.domain.vo.chat.ChatModelVo chatModelVo, - AbstractStreamingChatService chatService) { - - return new StreamingChatResponseHandler() { - private final StringBuilder messageBuffer = new StringBuilder(); - - @Override - public void onPartialResponse(String partialResponse) { - messageBuffer.append(partialResponse); - SseMessageUtils.sendMessage(userId, partialResponse); - log.debug("收到消息片段: {}", partialResponse); - } - - @Override - public void onCompleteResponse(dev.langchain4j.model.chat.response.ChatResponse completeResponse) { - try { - String fullMessage = messageBuffer.toString(); - if (!fullMessage.isEmpty()) { - chatService.saveChatMessage(chatRequest, userId, fullMessage, - RoleType.ASSISTANT.getName(), chatModelVo); - } - SseMessageUtils.completeConnection(userId, tokenValue); - log.info("消息结束,已保存到数据库"); - } catch (Exception e) { - log.error("完成响应时出错: {}", e.getMessage(), e); - } - } - - @Override - public void onError(Throwable error) { - log.error("流式响应错误: {}", error.getMessage(), error); - try { - SseMessageUtils.sendMessage(userId, "模型调用失败: " + error.getMessage()); - SseMessageUtils.completeConnection(userId, tokenValue); - } catch (Exception e) { - log.error("发送错误消息失败: {}", e.getMessage(), e); - } - } - }; - } -} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/ResumeChatHandler.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/ResumeChatHandler.java index bbda2ba8..3b017c1f 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/ResumeChatHandler.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/ResumeChatHandler.java @@ -1,53 +1,53 @@ -package org.ruoyi.service.chat.handler; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.ruoyi.common.chat.domain.dto.request.ReSumeRunner; -import org.ruoyi.common.chat.entity.chat.ChatContext; -import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService; -import org.ruoyi.common.core.utils.ObjectUtils; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -/** - * 人机交互恢复处理器 - *

- * 处理 isResume=true 的场景,恢复工作流的人机交互 - * - * @author ageerle@163.com - * @date 2025/12/13 - */ -@Slf4j -@Component -@Order(1) -@RequiredArgsConstructor -public class ResumeChatHandler implements ChatHandler { - - private final IWorkFlowStarterService workFlowStarterService; - - @Override - public boolean supports(ChatContext context) { - Boolean isResume = context.getChatRequest().getIsResume(); - return isResume != null && isResume; - } - - @Override - public SseEmitter handle(ChatContext context) { - log.info("处理人机交互恢复,用户: {}", context.getUserId()); - - ReSumeRunner reSumeRunner = context.getChatRequest().getReSumeRunner(); - if (ObjectUtils.isEmpty(reSumeRunner)) { - log.warn("人机交互恢复参数为空"); - return context.getEmitter(); - } - - workFlowStarterService.resumeFlow( - reSumeRunner.getRuntimeUuid(), - reSumeRunner.getFeedbackContent(), - context.getEmitter() - ); - - return context.getEmitter(); - } -} +//package org.ruoyi.service.chat.handler; +// +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.ruoyi.common.chat.domain.dto.request.ReSumeRunner; +//import org.ruoyi.common.chat.entity.chat.ChatContext; +//import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService; +//import org.ruoyi.common.core.utils.ObjectUtils; +//import org.springframework.core.annotation.Order; +//import org.springframework.stereotype.Component; +//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +// +///** +// * 人机交互恢复处理器 +// *

+// * 处理 isResume=true 的场景,恢复工作流的人机交互 +// * +// * @author ageerle@163.com +// * @date 2025/12/13 +// */ +//@Slf4j +//@Component +//@Order(1) +//@RequiredArgsConstructor +//public class ResumeChatHandler implements ChatHandler { +// +// private final IWorkFlowStarterService workFlowStarterService; +// +// @Override +// public boolean supports(ChatContext context) { +// Boolean isResume = context.getChatRequest().getIsResume(); +// return isResume != null && isResume; +// } +// +// @Override +// public SseEmitter handle(ChatContext context) { +// log.info("处理人机交互恢复,用户: {}", context.getUserId()); +// +// ReSumeRunner reSumeRunner = context.getChatRequest().getReSumeRunner(); +// if (ObjectUtils.isEmpty(reSumeRunner)) { +// log.warn("人机交互恢复参数为空"); +// return context.getEmitter(); +// } +// +// workFlowStarterService.resumeFlow( +// reSumeRunner.getRuntimeUuid(), +// reSumeRunner.getFeedbackContent(), +// context.getEmitter() +// ); +// +// return context.getEmitter(); +// } +//} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/WorkflowChatHandler.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/WorkflowChatHandler.java index f2a318db..322ffc06 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/WorkflowChatHandler.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/handler/WorkflowChatHandler.java @@ -1,54 +1,54 @@ -package org.ruoyi.service.chat.handler; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.ruoyi.common.chat.base.ThreadContext; -import org.ruoyi.common.chat.domain.dto.request.WorkFlowRunner; -import org.ruoyi.common.chat.entity.chat.ChatContext; -import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService; -import org.ruoyi.common.core.utils.ObjectUtils; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -/** - * 工作流对话处理器 - *

- * 处理 enableWorkFlow=true 的场景,启动工作流对话 - * - * @author ageerle@163.com - * @date 2025/12/13 - */ -@Slf4j -@Component -@Order(2) -@RequiredArgsConstructor -public class WorkflowChatHandler implements ChatHandler { - - private final IWorkFlowStarterService workFlowStarterService; - - @Override - public boolean supports(ChatContext context) { - Boolean enableWorkFlow = context.getChatRequest().getEnableWorkFlow(); - return enableWorkFlow != null && enableWorkFlow; - } - - @Override - public SseEmitter handle(ChatContext context) { - log.info("处理工作流对话,用户: {}, 会话: {}", - context.getUserId(), context.getChatRequest().getSessionId()); - - WorkFlowRunner runner = context.getChatRequest().getWorkFlowRunner(); - if (ObjectUtils.isEmpty(runner)) { - log.warn("工作流参数为空"); - return context.getEmitter(); - } - - return workFlowStarterService.streaming( - ThreadContext.getCurrentUser(), - runner.getUuid(), - runner.getInputs(), - context.getChatRequest().getSessionId() - ); - } -} +//package org.ruoyi.service.chat.handler; +// +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.ruoyi.common.chat.base.ThreadContext; +//import org.ruoyi.common.chat.domain.dto.request.WorkFlowRunner; +//import org.ruoyi.common.chat.entity.chat.ChatContext; +//import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService; +//import org.ruoyi.common.core.utils.ObjectUtils; +//import org.springframework.core.annotation.Order; +//import org.springframework.stereotype.Component; +//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +// +///** +// * 工作流对话处理器 +// *

+// * 处理 enableWorkFlow=true 的场景,启动工作流对话 +// * +// * @author ageerle@163.com +// * @date 2025/12/13 +// */ +//@Slf4j +//@Component +//@Order(2) +//@RequiredArgsConstructor +//public class WorkflowChatHandler implements ChatHandler { +// +// private final IWorkFlowStarterService workFlowStarterService; +// +// @Override +// public boolean supports(ChatContext context) { +// Boolean enableWorkFlow = context.getChatRequest().getEnableWorkFlow(); +// return enableWorkFlow != null && enableWorkFlow; +// } +// +// @Override +// public SseEmitter handle(ChatContext context) { +// log.info("处理工作流对话,用户: {}, 会话: {}", +// context.getUserId(), context.getChatRequest().getSessionId()); +// +// WorkFlowRunner runner = context.getChatRequest().getWorkFlowRunner(); +// if (ObjectUtils.isEmpty(runner)) { +// log.warn("工作流参数为空"); +// return context.getEmitter(); +// } +// +// return workFlowStarterService.streaming( +// ThreadContext.getCurrentUser(), +// runner.getUuid(), +// runner.getInputs(), +// context.getChatRequest().getSessionId() +// ); +// } +//} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/AbstractStreamingChatService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/AbstractStreamingChatService.java deleted file mode 100644 index 09643c5b..00000000 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/AbstractStreamingChatService.java +++ /dev/null @@ -1,182 +0,0 @@ -package org.ruoyi.service.chat.impl; - -import dev.langchain4j.data.message.ChatMessage; -import dev.langchain4j.data.message.UserMessage; -import dev.langchain4j.model.chat.StreamingChatModel; -import dev.langchain4j.model.chat.response.ChatResponse; -import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.ruoyi.common.chat.domain.dto.request.ChatRequest; -import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; -import org.ruoyi.common.chat.entity.chat.ChatContext; -import org.ruoyi.common.chat.enums.RoleType; -import org.ruoyi.common.chat.service.chat.IChatService; -import org.ruoyi.common.chat.service.chatMessage.AbstractChatMessageService; -import org.ruoyi.common.core.utils.StringUtils; -import org.ruoyi.common.sse.utils.SseMessageUtils; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -import java.util.List; -import java.util.Optional; - -/** - * 流式聊天服务抽象基类 - *

- * 提供核心的流式对话能力: - * 1. 构建流式聊天模型 - * 2. 创建响应处理器 - * 3. 消息持久化 - *

- * 设计原则: - * - 抽象层只依赖业务模型,不依赖具体SDK - * - 子类负责将业务模型转换为厂商SDK格式 - * - * @author ageerle@163.com - * @date 2025/12/13 - */ -@Slf4j -@Validated -public abstract class AbstractStreamingChatService extends AbstractChatMessageService implements IChatService { - - /** - * 定义聊天流程骨架 - * 注意:此方法已被 Handler 模式取代,保留是为了兼容旧调用 - */ - @Override - @Deprecated - public SseEmitter chat(ChatContext chatContext) { - ChatModelVo chatModelVo = chatContext.getChatModelVo(); - ChatRequest chatRequest = chatContext.getChatRequest(); - Long userId = chatContext.getUserId(); - String tokenValue = chatContext.getTokenValue(); - SseEmitter emitter = chatContext.getEmitter(); - - try { - // 提取用户消息内容 - String content = extractUserContent(chatRequest); - - // 保存用户消息 - saveChatMessage(chatRequest, userId, content, RoleType.USER.getName(), chatModelVo); - - // 构建消息列表(由 Handler 负责构建,这里简单处理) - List messages = convertToChatMessages(chatRequest); - - // 创建响应处理器 - StreamingChatResponseHandler handler = createResponseHandler( - chatRequest, userId, tokenValue, chatModelVo); - - // 调用具体实现的聊天方法 - doChat(chatModelVo, chatRequest, messages, handler); - - } catch (Exception e) { - SseMessageUtils.sendMessage(userId, "对话出错:" + e.getMessage()); - SseMessageUtils.completeConnection(userId, tokenValue); - log.error("{}请求失败:{}", getProviderName(), e.getMessage(), e); - } - - return emitter; - } - - /** - * 提取用户消息内容 - */ - private String extractUserContent(ChatRequest chatRequest) { - return Optional.ofNullable(chatRequest.getMessages()) - .filter(messages -> !messages.isEmpty()) - .map(messages -> messages.get(0).getContent()) - .filter(StringUtils::isNotBlank) - .orElseGet(() -> Optional.ofNullable(chatRequest.getChatMessages()) - .orElse(List.of()).stream() - .filter(message -> message instanceof UserMessage) - .map(message -> ((UserMessage) message).singleText()) - .filter(StringUtils::isNotBlank) - .findFirst() - .orElse("")); - } - - /** - * 转换消息格式 - */ - private List convertToChatMessages(ChatRequest chatRequest) { - List chatMessages = chatRequest.getChatMessages(); - return chatMessages != null ? chatMessages : List.of(); - } - - /** - * 执行聊天(钩子方法 - 子类必须实现) - * - * @param chatModelVo 模型配置 - * @param chatRequest 聊天请求 - * @param messagesWithMemory 消息列表 - * @param handler 响应处理器 - */ - protected abstract void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, - List messagesWithMemory, StreamingChatResponseHandler handler); - - /** - * 创建标准的响应处理器 - * - * @param chatRequest 聊天请求 - * @param userId 用户ID - * @param tokenValue 会话令牌 - * @param chatModelVo 模型配置 - * @return 流式响应处理器 - */ - protected StreamingChatResponseHandler createResponseHandler(ChatRequest chatRequest, Long userId, - String tokenValue, ChatModelVo chatModelVo) { - return new StreamingChatResponseHandler() { - private final StringBuilder messageBuffer = new StringBuilder(); - - @SneakyThrows - @Override - public void onPartialResponse(String partialResponse) { - messageBuffer.append(partialResponse); - SseMessageUtils.sendMessage(userId, partialResponse); - log.debug("收到{}消息片段: {}", getProviderName(), partialResponse); - } - - @Override - public void onCompleteResponse(ChatResponse completeResponse) { - try { - String fullMessage = messageBuffer.toString(); - if (!fullMessage.isEmpty()) { - saveChatMessage(chatRequest, userId, fullMessage, - RoleType.ASSISTANT.getName(), chatModelVo); - } - SseMessageUtils.completeConnection(userId, tokenValue); - log.info("{}消息结束,已保存到数据库", getProviderName()); - } catch (Exception e) { - log.error("{}完成响应时出错: {}", getProviderName(), e.getMessage(), e); - } - } - - @Override - public void onError(Throwable error) { - log.error("{}流式响应错误: {}", getProviderName(), error.getMessage(), error); - try { - String errorMessage = String.format("模型调用失败: %s", error.getMessage()); - SseMessageUtils.sendMessage(userId, errorMessage); - SseMessageUtils.completeConnection(userId, tokenValue); - } catch (Exception e) { - log.error("发送错误消息失败: {}", e.getMessage(), e); - } - } - }; - } - - /** - * 获取提供者名称(子类必须实现) - */ - public abstract String getProviderName(); - - /** - * 创建流式聊天模型(子类必须实现) - * - * @param chatModelVo 模型配置 - * @param chatRequest 聊天请求 - * @return 流式聊天模型实例 - */ - public abstract StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest); -} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatMessageServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatMessageServiceImpl.java index de2aee2b..8af56a81 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatMessageServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatMessageServiceImpl.java @@ -1,10 +1,10 @@ package org.ruoyi.service.chat.impl; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.UserMessage; import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo; -import org.ruoyi.common.chat.domain.dto.ChatMessageDTO; import org.ruoyi.common.chat.domain.vo.chat.ChatMessageVo; import org.ruoyi.common.chat.entity.chat.ChatMessage; -import org.ruoyi.common.chat.service.chatMessage.IChatMessageService; import org.ruoyi.common.core.utils.MapstructUtils; import org.ruoyi.common.core.utils.StringUtils; import org.ruoyi.common.mybatis.core.page.TableDataInfo; @@ -14,9 +14,11 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.ruoyi.service.chat.IChatMessageService; import org.springframework.stereotype.Service; import org.ruoyi.mapper.chat.ChatMessageMapper; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Collection; @@ -144,25 +146,27 @@ public class ChatMessageServiceImpl implements IChatMessageService { * @return 消息DTO列表 */ @Override - public List getMessagesBySessionId(Long sessionId) { + public List getMessagesBySessionId(Long sessionId) { if (sessionId == null) { return new java.util.ArrayList<>(); } + List chatMessageList = new ArrayList<>(); ChatMessageBo bo = new ChatMessageBo(); bo.setSessionId(sessionId); List voList = queryList(bo); - return voList.stream() - .map(vo -> { - ChatMessageDTO dto = new ChatMessageDTO(); - dto.setRole(vo.getRole()); - dto.setContent(vo.getContent()); - return dto; - }) - .collect(java.util.stream.Collectors.toList()); + for (ChatMessageVo chatMessageVo : voList) { + switch (chatMessageVo.getRole()) { + case "user" -> chatMessageList.add(UserMessage.from(chatMessageVo.getContent())); + case "assistant" -> chatMessageList.add(AiMessage.from(chatMessageVo.getContent())); + } + } + return chatMessageList; } + + /** * 根据会话ID删除所有消息 * 用于清理会话历史 @@ -180,4 +184,35 @@ public class ChatMessageServiceImpl implements IChatMessageService { lqw.eq(ChatMessage::getSessionId, sessionId); return baseMapper.delete(lqw) > 0; } + + /** + * 保存聊天消息 + * + * @param userId 用户ID + * @param sessionId 会话ID + * @param content 消息内容 + * @param role 角色类型 + * @param modelName 模型名称 + */ + @Override + public void saveChatMessage(Long userId, Long sessionId, String content, String role, String modelName) { + try { + if (userId == null) { + log.warn("缺少用户ID,无法保存消息"); + return; + } + + ChatMessageBo messageBo = new ChatMessageBo(); + messageBo.setUserId(userId); + messageBo.setSessionId(sessionId); + messageBo.setContent(content); + messageBo.setRole(role); + messageBo.setModelName(modelName); + + insertByBo(messageBo); + log.debug("保存聊天消息成功,角色: {}, 会话: {}", role, sessionId); + } catch (Exception e) { + log.error("保存聊天消息时出错: {}", e.getMessage(), e); + } + } } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java index e5994840..9c7b32d1 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java @@ -1,16 +1,37 @@ package org.ruoyi.service.chat.impl; -import jakarta.servlet.http.HttpServletRequest; +import cn.dev33.satoken.stp.StpUtil; +import dev.langchain4j.data.message.*; +import dev.langchain4j.memory.chat.MessageWindowChatMemory; +import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.model.chat.response.ChatResponse; +import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; -import org.ruoyi.common.chat.entity.chat.ChatContext; -import org.ruoyi.service.chat.handler.ChatContextBuilder; -import org.ruoyi.service.chat.handler.ChatHandler; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.ruoyi.common.chat.enums.RoleType; +import org.ruoyi.common.chat.service.chat.IChatModelService; +import org.ruoyi.common.chat.service.chat.IChatService; +import org.ruoyi.common.satoken.utils.LoginHelper; +import org.ruoyi.common.sse.core.SseEmitterManager; +import org.ruoyi.common.sse.utils.SseMessageUtils; +import org.ruoyi.domain.bo.vector.QueryVectorBo; +import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo; +import org.ruoyi.factory.ChatServiceFactory; +import org.ruoyi.service.chat.AbstractChatService; +import org.ruoyi.service.chat.IChatMessageService; +import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore; +import org.ruoyi.service.knowledge.IKnowledgeInfoService; +import org.ruoyi.service.vector.VectorStoreService; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * 聊天服务门面层 @@ -25,27 +46,324 @@ import java.util.List; @Service @Slf4j @RequiredArgsConstructor -public class ChatServiceFacade { +public class ChatServiceFacade implements IChatService { - private final ChatContextBuilder contextBuilder; - private final List handlers; + private static final Integer DEFAULT_MAX_MESSAGES = 20; + + private final IChatModelService chatModelService; + + private final ChatServiceFactory chatServiceFactory; + + private final IKnowledgeInfoService knowledgeInfoService; + + private final VectorStoreService vectorStoreService; + + private final SseEmitterManager sseEmitterManager; + + private final IChatMessageService chatMessageService; + + /** + * 内存实例缓存,避免同一会话重复创建 + * Key: sessionId, Value: MessageWindowChatMemory实例 + */ + private static final Map memoryCache = new ConcurrentHashMap<>(); /** * 统一聊天入口 - SSE流式响应 * * @param chatRequest 聊天请求 - * @param request HTTP请求对象 * @return SseEmitter */ - public SseEmitter sseChat(ChatRequest chatRequest, HttpServletRequest request) { - // 1. 构建对话上下文 - ChatContext context = contextBuilder.build(chatRequest); + public SseEmitter sseChat(ChatRequest chatRequest) { - // 2. 路由到对应的处理器 - return handlers.stream() - .filter(handler -> handler.supports(context)) - .findFirst() - .orElseThrow(() -> new IllegalStateException("无可用对话处理器")) - .handle(context); + // 1. 根据模型名称查询完整配置 + ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel()); + if (chatModelVo == null) { + throw new IllegalArgumentException("模型不存在: " + chatRequest.getModel()); + } + + // 2. 构建上下文消息列表 + List contextMessages = buildContextMessages(chatRequest); + + // 3. 路由服务提供商 + String providerCode = chatModelVo.getProviderCode(); + log.info("路由到服务提供商: {}, 模型: {}", providerCode, chatRequest.getModel()); + AbstractChatService chatService = chatServiceFactory.getOriginalService(providerCode); + // 4. 具体的服务实现 + Long userId = LoginHelper.getUserId(); + String tokenValue = StpUtil.getTokenValue(); + SseEmitter emitter = sseEmitterManager.connect(userId, tokenValue); + + StreamingChatResponseHandler handler = createResponseHandler(userId, tokenValue,chatRequest); + + // 保存用户消息 + chatMessageService.saveChatMessage(userId, chatRequest.getSessionId(), chatRequest.getContent(), RoleType.USER.getName(), chatRequest.getModel()); + + // 5. 发起对话 + StreamingChatModel streamingChatModel = chatService.buildStreamingChatModel(chatModelVo, chatRequest); + streamingChatModel.chat(contextMessages, handler); + return emitter; } + + /** + * 支持外部 handler 的对话接口(跨模块调用) + * 同时发送到 SSE 和外部 handler + * + * @param chatRequest 聊天请求 + * @param externalHandler 外部响应处理器(可为 null) + */ + @Override + public void chat(ChatRequest chatRequest, StreamingChatResponseHandler externalHandler) { + // 1. 根据模型名称查询完整配置 + ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel()); + if (chatModelVo == null) { + throw new IllegalArgumentException("模型不存在: " + chatRequest.getModel()); + } + + // 3. 路由服务提供商 + String providerCode = chatModelVo.getProviderCode(); + log.info("跨模块调用 - 路由到服务提供商: {}, 模型: {}", providerCode, chatRequest.getModel()); + AbstractChatService chatService = chatServiceFactory.getOriginalService(providerCode); + + // 4. 获取用户信息 + Long userId = LoginHelper.getUserId(); + String tokenValue = StpUtil.getTokenValue(); + + // 5. 建立 SSE 连接(用于前端监听) + sseEmitterManager.connect(userId, tokenValue); + + // 保存用户消息 + chatMessageService.saveChatMessage(userId, chatRequest.getSessionId(), chatRequest.getContent(), RoleType.USER.getName(), chatRequest.getModel()); + + // 6. 创建组合 handler:同时发送到 SSE 和外部 handler + StreamingChatResponseHandler combinedHandler = createCombinedHandler(userId, tokenValue, externalHandler); + + // 7. 发起对话 + StreamingChatModel streamingChatModel = chatService.buildStreamingChatModel(chatModelVo, chatRequest); + streamingChatModel.chat(chatRequest.getContent(), combinedHandler); + } + + /** + * 实现接口默认方法 - 不带 handler 的调用 + */ + @Override + public SseEmitter chat(ChatRequest chatRequest) { + return sseChat(chatRequest); + } + + + /** + * 创建或获取聊天内存实例(缓存机制) + * 同一个会话ID会返回同一个内存实例,避免重复创建和消息丢失 + * + * @param memoryId 内存ID(会话ID) + * @return MessageWindowChatMemory实例 + */ + private MessageWindowChatMemory createChatMemory(Object memoryId) { + // 先从缓存中获取 + return memoryCache.computeIfAbsent(memoryId, key -> { + try { + PersistentChatMemoryStore store = new PersistentChatMemoryStore(chatMessageService); + return MessageWindowChatMemory.builder() + .id(memoryId) + .maxMessages(DEFAULT_MAX_MESSAGES) + .chatMemoryStore(store) + .build(); + } catch (Exception e) { + log.warn("创建聊天内存失败: {}", e.getMessage()); + return null; + } + }); + } + + + /** + * 构建上下文消息列表 + * + * @param chatRequest 聊天请求 + * @return 上下文消息列表 + */ + private List buildContextMessages(ChatRequest chatRequest) { + List messages = new ArrayList<>(); + // 构建用户消息 + UserMessage userMessage = UserMessage.userMessage(chatRequest.getContent()); + messages.add(userMessage); + + // 从向量库查询相关历史消息 + if (chatRequest.getKnowledgeId() != null) { + // 查询知识库信息 + KnowledgeInfoVo knowledgeInfoVo = knowledgeInfoService.queryById(Long.valueOf(chatRequest.getKnowledgeId())); + if (knowledgeInfoVo == null) { + log.warn("知识库信息不存在,kid: {}", chatRequest.getKnowledgeId()); + return messages; + } + + // 查询向量模型配置信息 + ChatModelVo chatModel = chatModelService.selectModelByName(knowledgeInfoVo.getEmbeddingModel()); + if (chatModel == null) { + log.warn("向量模型配置不存在,模型名称: {}", knowledgeInfoVo.getEmbeddingModel()); + return messages; + } + + // 构建向量查询参数 + QueryVectorBo queryVectorBo = buildQueryVectorBo(chatRequest, knowledgeInfoVo, chatModel); + + // 获取向量查询结果 + List nearestList = vectorStoreService.getQueryVector(queryVectorBo); + for (String prompt : nearestList) { + // 知识库内容作为系统上下文添加 + messages.add( new AiMessage(prompt)); + } + } + + // 从数据库查询历史对话消息 + if (chatRequest.getSessionId() != null) { + MessageWindowChatMemory memory = createChatMemory(chatRequest.getSessionId()); + if (memory != null) { + List historicalMessages = memory.messages(); + if (historicalMessages != null && !historicalMessages.isEmpty()) { + messages.addAll(historicalMessages); + log.debug("已加载 {} 条历史消息用于会话 {}", historicalMessages.size(), chatRequest.getSessionId()); + } + } + } + + return messages; + } + + /** + * 构建向量查询参数 + */ + private QueryVectorBo buildQueryVectorBo(ChatRequest chatRequest, KnowledgeInfoVo knowledgeInfoVo, + ChatModelVo chatModel) { + QueryVectorBo queryVectorBo = new QueryVectorBo(); + queryVectorBo.setQuery(chatRequest.getContent()); + queryVectorBo.setKid(chatRequest.getKnowledgeId()); + queryVectorBo.setApiKey(chatModel.getApiKey()); + queryVectorBo.setBaseUrl(chatModel.getApiHost()); + queryVectorBo.setVectorModelName(knowledgeInfoVo.getVectorModel()); + queryVectorBo.setEmbeddingModelName(knowledgeInfoVo.getEmbeddingModel()); + queryVectorBo.setMaxResults(knowledgeInfoVo.getRetrieveLimit()); + return queryVectorBo; + } + + /** + * 创建标准的响应处理器 + * + * @param userId 用户ID + * @param tokenValue 会话令牌 + * @return 标准的流式响应处理器 + */ + protected StreamingChatResponseHandler createResponseHandler(Long userId, String tokenValue,ChatRequest chatRequest) { + return new StreamingChatResponseHandler() { + + private final StringBuilder messageBuffer = new StringBuilder(); + + @SneakyThrows + @Override + public void onPartialResponse(String partialResponse) { + // 将消息片段追加到缓冲区 + messageBuffer.append(partialResponse); + + // 实时发送内容事件到客户端 + SseMessageUtils.sendContent(userId, partialResponse); + log.debug("收到消息片段: {}", partialResponse); + } + + @Override + public void onCompleteResponse(ChatResponse completeResponse) { + try { + // 发送完成事件 + SseMessageUtils.sendDone(userId); + + // 消息流完成,保存消息到数据库和内存 + String fullMessage = messageBuffer.toString(); + + if (fullMessage.isEmpty()) { + log.warn("接收到空消息"); + } else { + // 保存助手回复消息 + chatMessageService.saveChatMessage(userId, chatRequest.getSessionId(), fullMessage, RoleType.ASSISTANT.getName(), chatRequest.getModel()); + } + + // 关闭SSE连接 + SseMessageUtils.completeConnection(userId, tokenValue); + log.info("消息结束,已保存到数据库"); + } catch (Exception e) { + log.error("完成响应时出错: {}", e.getMessage(), e); + } + } + + @Override + public void onError(Throwable error) { + // 发送错误事件 + SseMessageUtils.sendError(userId, error.getMessage()); + log.error("流式响应错误: {}", error.getMessage()); + } + }; + } + + /** + * 创建组合响应处理器 - 同时发送到 SSE 和外部 handler + * + * @param userId 用户ID + * @param tokenValue 会话令牌 + * @param externalHandler 外部响应处理器(可为 null) + * @return 组合的流式响应处理器 + */ + protected StreamingChatResponseHandler createCombinedHandler(Long userId, String tokenValue, + StreamingChatResponseHandler externalHandler) { + return new StreamingChatResponseHandler() { + + private final StringBuilder messageBuffer = new StringBuilder(); + + @SneakyThrows + @Override + public void onPartialResponse(String partialResponse) { + // 1. 追加到缓冲区 + messageBuffer.append(partialResponse); + + // 2. 发送内容事件到 SSE(前端可通过 SSE 监听) + SseMessageUtils.sendContent(userId, partialResponse); + + // 3. 转发给外部 handler(Workflow 等模块可处理) + if (externalHandler != null) { + externalHandler.onPartialResponse(partialResponse); + } + } + + @Override + public void onCompleteResponse(ChatResponse completeResponse) { + try { + // 1. 发送完成事件 + SseMessageUtils.sendDone(userId); + + // 2. 关闭 SSE 连接 + SseMessageUtils.completeConnection(userId, tokenValue); + + // 3. 转发给外部 handler + if (externalHandler != null) { + externalHandler.onCompleteResponse(completeResponse); + } + } catch (Exception e) { + log.error("完成响应时出错: {}", e.getMessage(), e); + } + } + + @Override + public void onError(Throwable error) { + // 发送错误事件 + SseMessageUtils.sendError(userId, error.getMessage()); + log.error("流式响应错误: {}", error.getMessage(), error); + + // 转发给外部 handler + if (externalHandler != null) { + externalHandler.onError(error); + } + } + }; + } + + } + diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/ChatMemoryProviderFactory.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/ChatMemoryProviderFactory.java deleted file mode 100644 index ffeea7c0..00000000 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/ChatMemoryProviderFactory.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.ruoyi.service.chat.impl.memory; - -import dev.langchain4j.memory.chat.ChatMemoryProvider; -import dev.langchain4j.memory.chat.MessageWindowChatMemory; -import lombok.extern.slf4j.Slf4j; - -/** - * 聊天记忆提供者工厂 - * 为每个会话创建独立的ChatMemoryProvider实例 - * 支持消息窗口滑动和持久化存储 - * - * @author ageerle@163.com - * @date 2025/01/10 - */ -@Slf4j -public class ChatMemoryProviderFactory { - - /** - * 默认保留的消息数量(不包括当前消息) - */ - private static final int DEFAULT_MAX_MESSAGES = 20; - - /** - * 创建聊天记忆提供者 - * - * @param maxMessages 最多保留的消息数量 - * @return ChatMemoryProvider实例 - */ - public static ChatMemoryProvider createChatMemoryProvider(int maxMessages) { - PersistentChatMemoryStore store = new PersistentChatMemoryStore(); - - return memoryId -> MessageWindowChatMemory.builder() - .id(memoryId) - .maxMessages(maxMessages) - .chatMemoryStore(store) - .build(); - } - - /** - * 使用默认消息数量创建聊天记忆提供者 - * - * @return ChatMemoryProvider实例 - */ - public static ChatMemoryProvider createChatMemoryProvider() { - return createChatMemoryProvider(DEFAULT_MAX_MESSAGES); - } - - /** - * 创建自定义的聊天记忆提供者 - * 允许更灵活的配置 - * - * @param maxMessages 最多保留的消息数量 - * @param chatMemoryStore 自定义的存储实现 - * @return ChatMemoryProvider实例 - */ - public static ChatMemoryProvider createChatMemoryProvider(int maxMessages, - dev.langchain4j.store.memory.chat.ChatMemoryStore chatMemoryStore) { - return memoryId -> MessageWindowChatMemory.builder() - .id(memoryId) - .maxMessages(maxMessages) - .chatMemoryStore(chatMemoryStore) - .build(); - } -} \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/PersistentChatMemoryStore.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/PersistentChatMemoryStore.java index a10ccfb6..b641acad 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/PersistentChatMemoryStore.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/PersistentChatMemoryStore.java @@ -2,10 +2,9 @@ package org.ruoyi.service.chat.impl.memory; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.store.memory.chat.ChatMemoryStore; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.ruoyi.common.chat.domain.dto.ChatMessageDTO; -import org.ruoyi.common.chat.service.chatMessage.IChatMessageService; -import org.ruoyi.common.core.utils.SpringUtils; +import org.ruoyi.service.chat.IChatMessageService; import java.util.ArrayList; import java.util.List; @@ -18,13 +17,11 @@ import java.util.List; * @date 2025/01/10 */ @Slf4j +@RequiredArgsConstructor public class PersistentChatMemoryStore implements ChatMemoryStore { private final IChatMessageService chatMessageService; - public PersistentChatMemoryStore() { - this.chatMessageService = SpringUtils.getBean(IChatMessageService.class); - } /** * 根据会话ID获取历史消息 @@ -38,16 +35,8 @@ public class PersistentChatMemoryStore implements ChatMemoryStore { } Long sessionId = Long.parseLong(memoryId.toString()); - // 从数据库获取该会话的所有消息 - List dtoList = chatMessageService.getMessagesBySessionId(sessionId); - - if (dtoList == null || dtoList.isEmpty()) { - return new ArrayList<>(); - } - - // 转换为LangChain4j格式 - return convertToLangChainMessages(dtoList); + return chatMessageService.getMessagesBySessionId(sessionId); } catch (Exception e) { log.error("获取会话 {} 的消息失败: {}", memoryId, e.getMessage(), e); return new ArrayList<>(); @@ -89,19 +78,5 @@ public class PersistentChatMemoryStore implements ChatMemoryStore { } } - /** - * 将ChatMessageDTO列表转换为LangChain4j的ChatMessage列表 - */ - private List convertToLangChainMessages(List dtoList) { - List messages = new ArrayList<>(); - for (ChatMessageDTO dto : dtoList) { - ChatMessage message = switch (dto.getRole()) { - case "system" -> dev.langchain4j.data.message.SystemMessage.from(dto.getContent()); - case "assistant" -> dev.langchain4j.data.message.AiMessage.from(dto.getContent()); - default -> dev.langchain4j.data.message.UserMessage.from(dto.getContent()); - }; - messages.add(message); - } - return messages; - } + } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/DeepseekServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/DeepseekServiceImpl.java index b79831b8..d5b1b1b9 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/DeepseekServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/DeepseekServiceImpl.java @@ -1,17 +1,14 @@ package org.ruoyi.service.chat.impl.provider; -import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.model.chat.StreamingChatModel; -import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import dev.langchain4j.model.openai.OpenAiStreamingChatModel; import lombok.extern.slf4j.Slf4j; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.enums.ChatModeType; -import org.ruoyi.service.chat.impl.AbstractStreamingChatService; +import org.ruoyi.service.chat.AbstractChatService; import org.springframework.stereotype.Service; -import java.util.List; /** * @Author: xiaoen @@ -20,7 +17,7 @@ import java.util.List; */ @Service @Slf4j -public class DeepseekServiceImpl extends AbstractStreamingChatService { +public class DeepseekServiceImpl implements AbstractChatService { @Override public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) { @@ -32,11 +29,6 @@ public class DeepseekServiceImpl extends AbstractStreamingChatService { .build(); } - @Override - public void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List messagesWithMemory, StreamingChatResponseHandler handler) { - StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest); - streamingChatModel.chat(messagesWithMemory, handler); - } @Override public String getProviderName() { diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OllamaServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OllamaServiceImpl.java index 1cbca8f9..c4de3427 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OllamaServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OllamaServiceImpl.java @@ -1,18 +1,14 @@ package org.ruoyi.service.chat.impl.provider; -import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.model.chat.StreamingChatModel; -import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import dev.langchain4j.model.ollama.OllamaStreamingChatModel; import lombok.extern.slf4j.Slf4j; import org.ruoyi.enums.ChatModeType; -import org.ruoyi.service.chat.impl.AbstractStreamingChatService; +import org.ruoyi.service.chat.AbstractChatService; import org.springframework.stereotype.Service; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; -import java.util.List; - /** * OllamaAI服务调用 * @@ -21,7 +17,7 @@ import java.util.List; */ @Service @Slf4j -public class OllamaServiceImpl extends AbstractStreamingChatService { +public class OllamaServiceImpl implements AbstractChatService { @Override public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) { @@ -31,16 +27,6 @@ public class OllamaServiceImpl extends AbstractStreamingChatService { .build(); } - - @Override - public void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List messagesWithMemory,StreamingChatResponseHandler handler) { - StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest); - streamingChatModel.chat(messagesWithMemory, handler); - } - - - - @Override public String getProviderName() { return ChatModeType.OLLAMA.getCode(); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OpenAIServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OpenAIServiceImpl.java index 90cac43c..e601fbcb 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OpenAIServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OpenAIServiceImpl.java @@ -1,19 +1,15 @@ package org.ruoyi.service.chat.impl.provider; -import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.model.chat.StreamingChatModel; -import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import dev.langchain4j.model.openai.OpenAiStreamingChatModel; import lombok.extern.slf4j.Slf4j; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.enums.ChatModeType; -import org.ruoyi.service.chat.impl.AbstractStreamingChatService; +import org.ruoyi.service.chat.AbstractChatService; import org.springframework.stereotype.Service; -import java.util.List; - /** * OPENAI服务调用 @@ -23,7 +19,7 @@ import java.util.List; */ @Service @Slf4j -public class OpenAIServiceImpl extends AbstractStreamingChatService { +public class OpenAIServiceImpl implements AbstractChatService { @Override public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) { @@ -35,13 +31,6 @@ public class OpenAIServiceImpl extends AbstractStreamingChatService { .build(); } - @Override - public void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List messagesWithMemory, StreamingChatResponseHandler handler) { - StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest); - streamingChatModel.chat(messagesWithMemory, handler); - } - - @Override public String getProviderName() { return ChatModeType.OPEN_AI.getCode(); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/PPIOServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/PPIOServiceImpl.java index 9c8c42ef..5b36fee9 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/PPIOServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/PPIOServiceImpl.java @@ -1,20 +1,14 @@ package org.ruoyi.service.chat.impl.provider; - -import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.model.chat.StreamingChatModel; -import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import dev.langchain4j.model.openai.OpenAiStreamingChatModel; import lombok.extern.slf4j.Slf4j; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.enums.ChatModeType; -import org.ruoyi.service.chat.impl.AbstractStreamingChatService; +import org.ruoyi.service.chat.AbstractChatService; import org.springframework.stereotype.Service; -import java.util.List; - - /** * OPENAI服务调用 * @@ -23,7 +17,7 @@ import java.util.List; */ @Service @Slf4j -public class PPIOServiceImpl extends AbstractStreamingChatService { +public class PPIOServiceImpl implements AbstractChatService { @Override public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) { @@ -35,13 +29,6 @@ public class PPIOServiceImpl extends AbstractStreamingChatService { .build(); } - @Override - public void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List messagesWithMemory, StreamingChatResponseHandler handler) { - StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest); - streamingChatModel.chat(messagesWithMemory, handler); - } - - @Override public String getProviderName() { return ChatModeType.PPIO.getCode(); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java index cc3d3184..bb7243c4 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java @@ -1,21 +1,14 @@ package org.ruoyi.service.chat.impl.provider; import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel; -import dev.langchain4j.data.message.ChatMessage; -import dev.langchain4j.data.message.SystemMessage; -import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.StreamingChatModel; -import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import lombok.extern.slf4j.Slf4j; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.enums.ChatModeType; -import org.ruoyi.service.chat.impl.AbstractStreamingChatService; +import org.ruoyi.service.chat.AbstractChatService; import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; -import java.util.ArrayList; -import java.util.List; /** * qianWenAI服务调用 @@ -25,11 +18,9 @@ import java.util.List; */ @Service @Slf4j -public class QianWenChatServiceImpl extends AbstractStreamingChatService { +public class QianWenChatServiceImpl implements AbstractChatService { // 添加文档解析的前缀字段 - private static final String UPLOAD_FILE_API_PREFIX = "fileid"; - @Override public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) { return QwenStreamingChatModel.builder() @@ -38,47 +29,6 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService { .build(); } - @Override - public void doChat(ChatModelVo chatModelVo,ChatRequest chatRequest,List messagesWithMemory, - StreamingChatResponseHandler handler) { - StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo,chatRequest); - // 判断是否存在需要使用阿里千问的文档解析功能 - List chatMessages = hasFileIdData(messagesWithMemory); - streamingChatModel.chat(chatMessages, handler); - } - - /** - * 检查是否包含fileId数据 - */ - private List hasFileIdData(List messagesWithMemory) { - if (CollectionUtils.isEmpty(messagesWithMemory)) { - return messagesWithMemory; - } - - // 找到包含阿里上传文件前缀的用户信息 - var foundUserMessage = messagesWithMemory.stream() - .filter(message -> message instanceof UserMessage) - .map(message -> (UserMessage) message) - .filter(userMessage -> - userMessage.singleText().toLowerCase().contains(UPLOAD_FILE_API_PREFIX.toLowerCase()) - ) - .findFirst(); - - // 找到原本SystemMessage - var systemMessage = messagesWithMemory.stream() - .filter(message -> message instanceof SystemMessage) - .map(message -> (SystemMessage) message) - .findFirst(); - - // 判断是否存在并重新构建信息体(符合千问文档解析格式) - return foundUserMessage.map(userMsg -> { - List messages = new ArrayList<>(); - messages.add(new SystemMessage(userMsg.singleText())); - systemMessage.ifPresent(sysMsg -> messages.add(new UserMessage(sysMsg.text()))); - return messages; - }).orElse(messagesWithMemory); - } - @Override public String getProviderName() { return ChatModeType.QIAN_WEN.getCode(); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/ZhiPuChatServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/ZhiPuChatServiceImpl.java index 06e35ad7..cf19a5a5 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/ZhiPuChatServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/ZhiPuChatServiceImpl.java @@ -1,17 +1,14 @@ package org.ruoyi.service.chat.impl.provider; import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel; -import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.model.chat.StreamingChatModel; -import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import lombok.extern.slf4j.Slf4j; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.enums.ChatModeType; -import org.ruoyi.service.chat.impl.AbstractStreamingChatService; +import org.ruoyi.service.chat.AbstractChatService; import org.springframework.stereotype.Service; -import java.util.List; /** * 智谱AI服务调用 @@ -21,12 +18,7 @@ import java.util.List; */ @Service @Slf4j -public class ZhiPuChatServiceImpl extends AbstractStreamingChatService { - @Override - public void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List messagesWithMemory, StreamingChatResponseHandler handler) { - StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo,chatRequest); - streamingChatModel.chat(messagesWithMemory, handler); - } +public class ZhiPuChatServiceImpl implements AbstractChatService { @Override public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {