refactor: 重构聊天模块架构

- 删除废弃的ChatMessageDTO、ChatContext、AbstractChatMessageService等类
- 迁移ChatServiceFactory和IChatMessageService到ruoyi-chat模块
- 重构ChatHandler体系,移除DefaultChatHandler和ChatContextBuilder
- 优化SSE消息处理,新增SseEventDto
- 简化各AI服务提供商实现类代码
- 优化工作流节点消息处理逻辑

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ageerle
2026-03-20 01:20:41 +08:00
parent f582f38570
commit c84d6247b0
39 changed files with 933 additions and 1394 deletions

View File

@@ -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:

View File

@@ -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;
}
}

View File

@@ -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<ChatMessageDTO> 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<ChatMessage> chatMessages;
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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 "默认工作流大模型";
}
}

View File

@@ -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<String, SseEmitter> 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<String, SseEmitter> emitters = USER_TOKEN_EMITTERS.get(userId);
if (MapUtil.isNotEmpty(emitters)) {
for (Map.Entry<String, SseEmitter> 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);
}
}
/**
* 本机全用户会话发送消息
*

View File

@@ -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 事件数据传输对象
* <p>
* 标准的 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();
}
}

View File

@@ -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));
}
/**
* 是否开启
*/

View File

@@ -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());
}
}

View File

@@ -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<NodeIOData> 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> 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<ChatMessage> 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<AgentState> 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<NodeIOData> 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);
}

View File

@@ -46,13 +46,11 @@ public class LLMAnswerNode extends AbstractWfNode {
// 调用LLM
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
String modelName = nodeConfigObj.getModelName();
// 转换系统信息结构
List<SystemMessage> 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();
}
}

View File

@@ -67,13 +67,12 @@ public class KeywordExtractorNode extends AbstractWfNode {
// 调用 LLM 进行关键词提取
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
String modelName = config.getModelName();
List<SystemMessage> 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();
}

View File

@@ -151,7 +151,6 @@ public class KnowledgeRetrievalNode extends AbstractWfNode {
// 使用WorkflowUtil调用LLM流式
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
List<SystemMessage> 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,
""
);

View File

@@ -19,6 +19,11 @@
<artifactId>ruoyi-common-chat</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-sse</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-sensitive</artifactId>

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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<String, IChatService> chatServiceMap = new ConcurrentHashMap<>();
private final Map<String, AbstractChatService> chatServiceMap = new ConcurrentHashMap<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 初始化时收集所有IChatService的实现
Map<String, IChatService> serviceMap = applicationContext.getBeansOfType(IChatService.class);
for (IChatService service : serviceMap.values()) {
Map<String, AbstractChatService> 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);
}

View File

@@ -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();
}

View File

@@ -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<ChatMessageDTO> getMessagesBySessionId(Long sessionId);
List<ChatMessage> 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);
}

View File

@@ -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 深度思考处理器
* <p>
* 处理 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<Object> builtinTools = toolProviderFactory.getAllBuiltinToolObjects();
List<Object> 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 深度思考处理器
// * <p>
// * 处理 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<Object> builtinTools = toolProviderFactory.getAllBuiltinToolObjects();
// List<Object> 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 "";
// }
//
//}

View File

@@ -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;
/**
* 对话上下文构建器
* <p>
* 负责构建完整的对话上下文,包括:
* 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<ChatMessageDTO> 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<ChatMessageDTO> buildContextMessages(ChatRequest chatRequest) {
List<ChatMessageDTO> 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<String> 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;
}
}

View File

@@ -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;
/**
* 对话处理器接口
* <p>
* 使用策略模式,每种对话场景独立实现
* 通过 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;
}
}

View File

@@ -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;
/**
* 默认对话处理器
* <p>
* 处理普通对话场景,包含:
* 1. 历史记忆管理
* 2. 消息保存
* 3. 流式对话响应
*
* @author ageerle@163.com
* @date 2025/12/13
*/
@Slf4j
@Component
@Order(100)
public class DefaultChatHandler implements ChatHandler {
private final Map<String, AbstractStreamingChatService> chatServiceMap;
/**
* 默认保留的消息窗口大小
*/
private static final int DEFAULT_MAX_MESSAGES = 20;
/**
* 是否启用长期记忆
*/
private static final boolean ENABLE_PERSISTENT_MEMORY = true;
/**
* 内存实例缓存
*/
private static final Map<Object, MessageWindowChatMemory> MEMORY_CACHE = new ConcurrentHashMap<>();
/**
* 构造函数,注入所有聊天服务实现
*/
public DefaultChatHandler(List<AbstractStreamingChatService> 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<ChatMessage> 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<ChatMessage> buildMessagesWithMemory(org.ruoyi.common.chat.domain.dto.request.ChatRequest chatRequest) {
List<ChatMessage> messages = new ArrayList<>();
// 添加工作流对话消息
List<ChatMessage> 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<ChatMessage> 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);
}
}
};
}
}

View File

@@ -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;
/**
* 人机交互恢复处理器
* <p>
* 处理 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;
//
///**
// * 人机交互恢复处理器
// * <p>
// * 处理 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();
// }
//}

View File

@@ -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;
/**
* 工作流对话处理器
* <p>
* 处理 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;
//
///**
// * 工作流对话处理器
// * <p>
// * 处理 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()
// );
// }
//}

View File

@@ -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;
/**
* 流式聊天服务抽象基类
* <p>
* 提供核心的流式对话能力:
* 1. 构建流式聊天模型
* 2. 创建响应处理器
* 3. 消息持久化
* <p>
* 设计原则:
* - 抽象层只依赖业务模型不依赖具体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<ChatMessage> 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<ChatMessage> convertToChatMessages(ChatRequest chatRequest) {
List<ChatMessage> 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<ChatMessage> 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);
}

View File

@@ -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<ChatMessageDTO> getMessagesBySessionId(Long sessionId) {
public List<dev.langchain4j.data.message.ChatMessage> getMessagesBySessionId(Long sessionId) {
if (sessionId == null) {
return new java.util.ArrayList<>();
}
List<dev.langchain4j.data.message.ChatMessage> chatMessageList = new ArrayList<>();
ChatMessageBo bo = new ChatMessageBo();
bo.setSessionId(sessionId);
List<ChatMessageVo> 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);
}
}
}

View File

@@ -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<ChatHandler> 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<Object, MessageWindowChatMemory> 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<ChatMessage> 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<ChatMessage> buildContextMessages(ChatRequest chatRequest) {
List<ChatMessage> 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<String> 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<ChatMessage> 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. 转发给外部 handlerWorkflow 等模块可处理)
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);
}
}
};
}
}

View File

@@ -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();
}
}

View File

@@ -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<ChatMessageDTO> 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<ChatMessage> convertToLangChainMessages(List<ChatMessageDTO> dtoList) {
List<ChatMessage> 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;
}
}

View File

@@ -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<ChatMessage> messagesWithMemory, StreamingChatResponseHandler handler) {
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest);
streamingChatModel.chat(messagesWithMemory, handler);
}
@Override
public String getProviderName() {

View File

@@ -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<ChatMessage> messagesWithMemory,StreamingChatResponseHandler handler) {
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest);
streamingChatModel.chat(messagesWithMemory, handler);
}
@Override
public String getProviderName() {
return ChatModeType.OLLAMA.getCode();

View File

@@ -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<ChatMessage> messagesWithMemory, StreamingChatResponseHandler handler) {
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest);
streamingChatModel.chat(messagesWithMemory, handler);
}
@Override
public String getProviderName() {
return ChatModeType.OPEN_AI.getCode();

View File

@@ -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<ChatMessage> messagesWithMemory, StreamingChatResponseHandler handler) {
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest);
streamingChatModel.chat(messagesWithMemory, handler);
}
@Override
public String getProviderName() {
return ChatModeType.PPIO.getCode();

View File

@@ -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<ChatMessage> messagesWithMemory,
StreamingChatResponseHandler handler) {
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo,chatRequest);
// 判断是否存在需要使用阿里千问的文档解析功能
List<ChatMessage> chatMessages = hasFileIdData(messagesWithMemory);
streamingChatModel.chat(chatMessages, handler);
}
/**
* 检查是否包含fileId数据
*/
private List<ChatMessage> hasFileIdData(List<ChatMessage> 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<ChatMessage> 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();

View File

@@ -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<ChatMessage> 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) {