From fae046885e034fe9d6d570421bd5f704769e71b2 Mon Sep 17 00:00:00 2001 From: lcj <2331845269@qq.com> Date: Wed, 5 Nov 2025 11:46:48 +0800 Subject: [PATCH] ai --- .../org/dromara/ai/chat/DashScopeChat.java | 22 ++--- .../ai/chatmemory/FileBasedChatMemory.java | 4 +- .../ai/controller/AIChatController.java | 6 +- .../ai/controller/AIChatMemoryController.java | 13 +++ .../ai/domain/dto/AIChatMemoryUpdateReq.java | 34 +++++++ .../org/dromara/ai/domain/dto/AIChatReq.java | 37 +++++++ .../ai/service/IAIChatMemoryService.java | 11 ++- .../service/impl/AIChatMemoryServiceImpl.java | 21 ++++ .../controller/PersonalHomeController.java | 96 ++++++++++++++++++- 9 files changed, 224 insertions(+), 20 deletions(-) create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/domain/dto/AIChatMemoryUpdateReq.java create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/domain/dto/AIChatReq.java diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/chat/DashScopeChat.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/chat/DashScopeChat.java index b7ea1383..bef84732 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/chat/DashScopeChat.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/chat/DashScopeChat.java @@ -6,7 +6,6 @@ import org.dromara.ai.advisor.CustomLoggerAdvisor; import org.dromara.ai.chatmemory.FileBasedChatMemory; import org.dromara.ai.domain.AIChatMemory; import org.dromara.ai.service.IAIChatMemoryService; -import org.dromara.common.core.utils.StringUtils; import org.dromara.common.satoken.utils.LoginHelper; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; @@ -15,7 +14,6 @@ import org.springframework.ai.chat.model.ChatModel; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; -import java.util.UUID; import java.util.concurrent.CompletableFuture; /** @@ -57,18 +55,12 @@ public class DashScopeChat { * @param chatId 会话id * @return 流式输出结果 */ - public Flux doChatStream(String message, String chatId) { + public Flux doChatStream(String message, String chatId, Boolean isFirst) { Long userId = LoginHelper.getUserId(); - boolean isFirst = StringUtils.isBlank(chatId); - if (StringUtils.isBlank(chatId)) { - // 构建新的会话id - chatId = UUID.randomUUID().toString(); - } - String finalChatId = chatId; return chatClient .prompt() .user(message) - .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, finalChatId)) + .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId)) .stream() .content()// 收集所有 token,生成完整回复 .collectList() @@ -76,7 +68,7 @@ public class DashScopeChat { String aiResponse = String.join("", tokens); if (isFirst) { // 异步生成标题 - generateChatTitleAsync(finalChatId, message, aiResponse, userId); + generateChatTitleAsync(chatId, message, aiResponse, userId); } // 返回完整的流结果 return Flux.fromIterable(tokens); @@ -94,6 +86,14 @@ public class DashScopeChat { private void generateChatTitleAsync(String chatId, String userMessage, String aiResponse, Long userId) { CompletableFuture.runAsync(() -> { try { + // 先判断一下当前聊天是否存在 + Long count = chatMemoryService.lambdaQuery() + .eq(AIChatMemory::getUserId, userId) + .eq(AIChatMemory::getFileName, chatId) + .count(); + if (count > 0) { + return; + } // 构建生成标题的提示词 String prompt = String.format(""" 请为下面这段用户与AI的对话生成一个简短的标题(不超过10个字): diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/chatmemory/FileBasedChatMemory.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/chatmemory/FileBasedChatMemory.java index 82368afd..8db66960 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/chatmemory/FileBasedChatMemory.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/chatmemory/FileBasedChatMemory.java @@ -48,7 +48,9 @@ public class FileBasedChatMemory implements ChatMemory { @Override public List get(String conversationId) { - return getOrCreateConversation(conversationId); + List messages = getOrCreateConversation(conversationId); + log.info("获取对话:{}", messages); + return messages; } @Override diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/controller/AIChatController.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/controller/AIChatController.java index 79396579..a6676565 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/controller/AIChatController.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/controller/AIChatController.java @@ -5,8 +5,8 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.dromara.ai.chat.DashScopeChat; import org.dromara.ai.chatmemory.FileBasedChatMemory; +import org.dromara.ai.domain.dto.AIChatReq; import org.dromara.common.core.domain.R; -import org.dromara.common.satoken.utils.LoginHelper; import org.springframework.ai.chat.messages.Message; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; @@ -33,9 +33,9 @@ public class AIChatController { * ChatClient 流式调用 */ @GetMapping("/stream") - public Flux streamChat(String query, String chatId, HttpServletResponse response) { + public Flux streamChat(@Validated AIChatReq req, HttpServletResponse response) { response.setCharacterEncoding("UTF-8"); - return dashScopeChat.doChatStream(query, chatId); + return dashScopeChat.doChatStream(req.getQuery(), req.getChatId(), req.getIsFirst()); } /** diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/controller/AIChatMemoryController.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/controller/AIChatMemoryController.java index 1aebef79..014b5cca 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/controller/AIChatMemoryController.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/controller/AIChatMemoryController.java @@ -6,10 +6,12 @@ import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.dromara.ai.domain.dto.AIChatMemoryQueryReq; +import org.dromara.ai.domain.dto.AIChatMemoryUpdateReq; import org.dromara.ai.domain.vo.AIChatMemoryVo; import org.dromara.ai.service.IAIChatMemoryService; import org.dromara.common.core.domain.R; import org.dromara.common.excel.utils.ExcelUtil; +import org.dromara.common.idempotent.annotation.RepeatSubmit; import org.dromara.common.log.annotation.Log; import org.dromara.common.log.enums.BusinessType; import org.dromara.common.mybatis.core.page.PageQuery; @@ -66,6 +68,17 @@ public class AIChatMemoryController extends BaseController { return R.ok(aiChatMemoryService.queryById(id)); } + /** + * 修改 AI 对话记录信息 + */ + @SaCheckPermission("ai:chatMemory:edit") + @Log(title = "AI 对话记录信息", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping() + public R updateAIChatMemory(@Validated AIChatMemoryUpdateReq req) { + return toAjax(aiChatMemoryService.updateByReq(req)); + } + /** * 删除AI 对话记录信息 * diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/domain/dto/AIChatMemoryUpdateReq.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/domain/dto/AIChatMemoryUpdateReq.java new file mode 100644 index 00000000..97d0b859 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/domain/dto/AIChatMemoryUpdateReq.java @@ -0,0 +1,34 @@ +package org.dromara.ai.domain.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * @author lilemy + * @date 2025-11-05 11:28 + */ +@Data +public class AIChatMemoryUpdateReq implements Serializable { + + @Serial + private static final long serialVersionUID = 6541297164616819137L; + + /** + * 主键 + */ + @NotNull(message = "主键不能为空") + private Long id; + + /** + * 第一条问题 + */ + private String firstQuestion; + + /** + * 备注 + */ + private String remark; +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/domain/dto/AIChatReq.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/domain/dto/AIChatReq.java new file mode 100644 index 00000000..756ce282 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/domain/dto/AIChatReq.java @@ -0,0 +1,37 @@ +package org.dromara.ai.domain.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * @author lilemy + * @date 2025-11-05 09:31 + */ +@Data +public class AIChatReq implements Serializable { + + @Serial + private static final long serialVersionUID = -4669223630531267889L; + + /** + * 聊天内容 + */ + @NotBlank(message = "请输入内容") + private String query; + + /** + * 会话id + */ + @NotBlank(message = "请输入会话id") + private String chatId; + + /** + * 是否首次对话 + */ + @NotNull(message = "请选择是否首次对话") + private Boolean isFirst; +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/service/IAIChatMemoryService.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/service/IAIChatMemoryService.java index 9c513751..1e0e3ec6 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/service/IAIChatMemoryService.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/service/IAIChatMemoryService.java @@ -3,6 +3,7 @@ package org.dromara.ai.service; import com.baomidou.mybatisplus.extension.service.IService; import org.dromara.ai.domain.AIChatMemory; import org.dromara.ai.domain.dto.AIChatMemoryQueryReq; +import org.dromara.ai.domain.dto.AIChatMemoryUpdateReq; import org.dromara.ai.domain.vo.AIChatMemoryVo; import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.TableDataInfo; @@ -16,7 +17,7 @@ import java.util.List; * @author lilemy * @date 2025-11-04 */ -public interface IAIChatMemoryService extends IService{ +public interface IAIChatMemoryService extends IService { /** * 查询AI 对话记录信息 @@ -43,6 +44,14 @@ public interface IAIChatMemoryService extends IService{ */ List queryList(AIChatMemoryQueryReq bo); + /** + * 修改 AI 对话记录信息 + * + * @param req 修改参数 + * @return 是否修改成功 + */ + Boolean updateByReq(AIChatMemoryUpdateReq req); + /** * 校验并批量删除AI 对话记录信息信息 * diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/service/impl/AIChatMemoryServiceImpl.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/service/impl/AIChatMemoryServiceImpl.java index 1b0cdaa1..577aa700 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/service/impl/AIChatMemoryServiceImpl.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/service/impl/AIChatMemoryServiceImpl.java @@ -6,12 +6,16 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.dromara.ai.domain.AIChatMemory; import org.dromara.ai.domain.dto.AIChatMemoryQueryReq; +import org.dromara.ai.domain.dto.AIChatMemoryUpdateReq; import org.dromara.ai.domain.vo.AIChatMemoryVo; import org.dromara.ai.mapper.AIChatMemoryMapper; import org.dromara.ai.service.IAIChatMemoryService; +import org.dromara.common.core.constant.HttpStatus; +import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import java.util.Collection; @@ -64,6 +68,23 @@ public class AIChatMemoryServiceImpl extends ServiceImpl buildQueryWrapper(AIChatMemoryQueryReq req) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.orderByDesc(AIChatMemory::getId); diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/bigscreen/controller/PersonalHomeController.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/bigscreen/controller/PersonalHomeController.java index edea0978..ce15a777 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/bigscreen/controller/PersonalHomeController.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/bigscreen/controller/PersonalHomeController.java @@ -5,7 +5,18 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; +import org.dromara.ai.chat.DashScopeChat; +import org.dromara.ai.chatmemory.FileBasedChatMemory; +import org.dromara.ai.domain.dto.AIChatMemoryQueryReq; +import org.dromara.ai.domain.dto.AIChatMemoryUpdateReq; +import org.dromara.ai.domain.dto.AIChatReq; +import org.dromara.ai.domain.vo.AIChatMemoryVo; +import org.dromara.ai.service.IAIChatMemoryService; import org.dromara.bigscreen.domain.dto.BusBwlBo; import org.dromara.bigscreen.domain.dto.TaskInfoDto; import org.dromara.bigscreen.domain.vo.BusBwlVo; @@ -14,6 +25,9 @@ import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.dto.UserDTO; import org.dromara.common.core.enums.BusinessStatusEnum; import org.dromara.common.core.utils.StreamUtils; +import org.dromara.common.idempotent.annotation.RepeatSubmit; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.log.enums.BusinessType; import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.satoken.utils.LoginHelper; @@ -30,11 +44,11 @@ import org.dromara.warm.flow.orm.mapper.FlowDefinitionMapper; import org.dromara.workflow.domain.bo.FlowTaskBo; import org.dromara.workflow.domain.vo.FlowTaskVo; import org.dromara.workflow.mapper.FlwTaskMapper; +import org.springframework.ai.chat.messages.Message; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; import java.time.DayOfWeek; import java.time.LocalDate; @@ -63,8 +77,82 @@ public class PersonalHomeController extends BaseController { private final IBusBwlService busBwlService; + @Resource + private DashScopeChat dashScopeChat; + @Resource + private IAIChatMemoryService aiChatMemoryService; + // region AI 模块 + + /** + * 获取新 AI 对话聊天id + */ + @GetMapping("/ai/chat/new") + public R getNewAIChat() { + return R.ok(UUID.randomUUID().toString()); + } + + /** + * AI 对话流式调用 + */ + @GetMapping("/ai/chat/stream") + public Flux streamChat(@Validated AIChatReq req, HttpServletResponse response) { + response.setCharacterEncoding("UTF-8"); + return dashScopeChat.doChatStream(req.getQuery(), req.getChatId(), req.getIsFirst()); + } + + /** + * 获取 AI 对话记录 + */ + @GetMapping("/ai/chat/history") + public R> getChatHistory(String chatId) { + FileBasedChatMemory memory = new FileBasedChatMemory(System.getProperty("user.dir") + "/chat-memory"); + return R.ok(memory.get(chatId)); + } + + /** + * 查询 AI 对话记录信息列表 + */ + @GetMapping("/ai/chatMemory/list") + public TableDataInfo listAIChatMemory(AIChatMemoryQueryReq req, PageQuery pageQuery) { + return aiChatMemoryService.queryPageList(req, pageQuery); + } + + /** + * 获取 AI 对话记录信息详细信息 + * + * @param id 主键 + */ + @GetMapping("/ai/chatMemory/{id}") + public R getAIChatMemoryInfo(@NotNull(message = "主键不能为空") + @PathVariable Long id) { + return R.ok(aiChatMemoryService.queryById(id)); + } + + /** + * 修改 AI 对话记录信息 + */ + @Log(title = "AI 对话记录信息", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping("/ai/chatMemory") + public R updateAIChatMemory(@Validated AIChatMemoryUpdateReq req) { + return toAjax(aiChatMemoryService.updateByReq(req)); + } + + /** + * 删除 AI 对话记录信息 + * + * @param ids 主键串 + */ + @Log(title = "AI 对话记录信息", businessType = BusinessType.DELETE) + @DeleteMapping("/ai/chatMemory/{ids}") + public R removeAIChatMemory(@NotEmpty(message = "主键不能为空") + @PathVariable Long[] ids) { + return toAjax(aiChatMemoryService.deleteWithValidByIds(List.of(ids), true)); + } + + // endregion /** * 查询派单列表 @@ -91,7 +179,7 @@ public class PersonalHomeController extends BaseController { QueryWrapper queryWrapper = new QueryWrapper<>(); List definitionIds = new ArrayList<>(); - if (!"0".equals(projectId)){ + if (!"0".equals(projectId)) { List flowDefinitions = flowDefinitionMapper.selectList(new LambdaQueryWrapper() .select(FlowDefinition::getId) .like(FlowDefinition::getFlowCode, projectId));