This commit is contained in:
lcj
2025-11-05 11:46:48 +08:00
parent 05569d0471
commit fae046885e
9 changed files with 224 additions and 20 deletions

View File

@ -6,7 +6,6 @@ import org.dromara.ai.advisor.CustomLoggerAdvisor;
import org.dromara.ai.chatmemory.FileBasedChatMemory; import org.dromara.ai.chatmemory.FileBasedChatMemory;
import org.dromara.ai.domain.AIChatMemory; import org.dromara.ai.domain.AIChatMemory;
import org.dromara.ai.service.IAIChatMemoryService; import org.dromara.ai.service.IAIChatMemoryService;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; 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 org.springframework.stereotype.Component;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
/** /**
@ -57,18 +55,12 @@ public class DashScopeChat {
* @param chatId 会话id * @param chatId 会话id
* @return 流式输出结果 * @return 流式输出结果
*/ */
public Flux<String> doChatStream(String message, String chatId) { public Flux<String> doChatStream(String message, String chatId, Boolean isFirst) {
Long userId = LoginHelper.getUserId(); Long userId = LoginHelper.getUserId();
boolean isFirst = StringUtils.isBlank(chatId);
if (StringUtils.isBlank(chatId)) {
// 构建新的会话id
chatId = UUID.randomUUID().toString();
}
String finalChatId = chatId;
return chatClient return chatClient
.prompt() .prompt()
.user(message) .user(message)
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, finalChatId)) .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId))
.stream() .stream()
.content()// 收集所有 token生成完整回复 .content()// 收集所有 token生成完整回复
.collectList() .collectList()
@ -76,7 +68,7 @@ public class DashScopeChat {
String aiResponse = String.join("", tokens); String aiResponse = String.join("", tokens);
if (isFirst) { if (isFirst) {
// 异步生成标题 // 异步生成标题
generateChatTitleAsync(finalChatId, message, aiResponse, userId); generateChatTitleAsync(chatId, message, aiResponse, userId);
} }
// 返回完整的流结果 // 返回完整的流结果
return Flux.fromIterable(tokens); return Flux.fromIterable(tokens);
@ -94,6 +86,14 @@ public class DashScopeChat {
private void generateChatTitleAsync(String chatId, String userMessage, String aiResponse, Long userId) { private void generateChatTitleAsync(String chatId, String userMessage, String aiResponse, Long userId) {
CompletableFuture.runAsync(() -> { CompletableFuture.runAsync(() -> {
try { try {
// 先判断一下当前聊天是否存在
Long count = chatMemoryService.lambdaQuery()
.eq(AIChatMemory::getUserId, userId)
.eq(AIChatMemory::getFileName, chatId)
.count();
if (count > 0) {
return;
}
// 构建生成标题的提示词 // 构建生成标题的提示词
String prompt = String.format(""" String prompt = String.format("""
请为下面这段用户与AI的对话生成一个简短的标题不超过10个字 请为下面这段用户与AI的对话生成一个简短的标题不超过10个字

View File

@ -48,7 +48,9 @@ public class FileBasedChatMemory implements ChatMemory {
@Override @Override
public List<Message> get(String conversationId) { public List<Message> get(String conversationId) {
return getOrCreateConversation(conversationId); List<Message> messages = getOrCreateConversation(conversationId);
log.info("获取对话:{}", messages);
return messages;
} }
@Override @Override

View File

@ -5,8 +5,8 @@ import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.chat.DashScopeChat; import org.dromara.ai.chat.DashScopeChat;
import org.dromara.ai.chatmemory.FileBasedChatMemory; import org.dromara.ai.chatmemory.FileBasedChatMemory;
import org.dromara.ai.domain.dto.AIChatReq;
import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.R;
import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.Message;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@ -33,9 +33,9 @@ public class AIChatController {
* ChatClient 流式调用 * ChatClient 流式调用
*/ */
@GetMapping("/stream") @GetMapping("/stream")
public Flux<String> streamChat(String query, String chatId, HttpServletResponse response) { public Flux<String> streamChat(@Validated AIChatReq req, HttpServletResponse response) {
response.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8");
return dashScopeChat.doChatStream(query, chatId); return dashScopeChat.doChatStream(req.getQuery(), req.getChatId(), req.getIsFirst());
} }
/** /**

View File

@ -6,10 +6,12 @@ import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.dromara.ai.domain.dto.AIChatMemoryQueryReq; 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.domain.vo.AIChatMemoryVo;
import org.dromara.ai.service.IAIChatMemoryService; import org.dromara.ai.service.IAIChatMemoryService;
import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil; 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.annotation.Log;
import org.dromara.common.log.enums.BusinessType; import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.PageQuery;
@ -66,6 +68,17 @@ public class AIChatMemoryController extends BaseController {
return R.ok(aiChatMemoryService.queryById(id)); return R.ok(aiChatMemoryService.queryById(id));
} }
/**
* 修改 AI 对话记录信息
*/
@SaCheckPermission("ai:chatMemory:edit")
@Log(title = "AI 对话记录信息", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> updateAIChatMemory(@Validated AIChatMemoryUpdateReq req) {
return toAjax(aiChatMemoryService.updateByReq(req));
}
/** /**
* 删除AI 对话记录信息 * 删除AI 对话记录信息
* *

View File

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

View File

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

View File

@ -3,6 +3,7 @@ package org.dromara.ai.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import org.dromara.ai.domain.AIChatMemory; import org.dromara.ai.domain.AIChatMemory;
import org.dromara.ai.domain.dto.AIChatMemoryQueryReq; 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.domain.vo.AIChatMemoryVo;
import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
@ -43,6 +44,14 @@ public interface IAIChatMemoryService extends IService<AIChatMemory>{
*/ */
List<AIChatMemoryVo> queryList(AIChatMemoryQueryReq bo); List<AIChatMemoryVo> queryList(AIChatMemoryQueryReq bo);
/**
* 修改 AI 对话记录信息
*
* @param req 修改参数
* @return 是否修改成功
*/
Boolean updateByReq(AIChatMemoryUpdateReq req);
/** /**
* 校验并批量删除AI 对话记录信息信息 * 校验并批量删除AI 对话记录信息信息
* *

View File

@ -6,12 +6,16 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.dromara.ai.domain.AIChatMemory; import org.dromara.ai.domain.AIChatMemory;
import org.dromara.ai.domain.dto.AIChatMemoryQueryReq; 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.domain.vo.AIChatMemoryVo;
import org.dromara.ai.mapper.AIChatMemoryMapper; import org.dromara.ai.mapper.AIChatMemoryMapper;
import org.dromara.ai.service.IAIChatMemoryService; 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.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Collection; import java.util.Collection;
@ -64,6 +68,23 @@ public class AIChatMemoryServiceImpl extends ServiceImpl<AIChatMemoryMapper, AIC
return baseMapper.selectVoList(lqw); return baseMapper.selectVoList(lqw);
} }
/**
* 修改 AI 对话记录信息
*
* @param req 修改参数
* @return 是否修改成功
*/
@Override
public Boolean updateByReq(AIChatMemoryUpdateReq req) {
AIChatMemory oldChatMemory = this.getById(req.getId());
if (oldChatMemory == null) {
throw new ServiceException("数据不存在", HttpStatus.NOT_FOUND);
}
AIChatMemory update = new AIChatMemory();
BeanUtils.copyProperties(req, update);
return this.updateById(update);
}
private LambdaQueryWrapper<AIChatMemory> buildQueryWrapper(AIChatMemoryQueryReq req) { private LambdaQueryWrapper<AIChatMemory> buildQueryWrapper(AIChatMemoryQueryReq req) {
LambdaQueryWrapper<AIChatMemory> lqw = Wrappers.lambdaQuery(); LambdaQueryWrapper<AIChatMemory> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(AIChatMemory::getId); lqw.orderByDesc(AIChatMemory::getId);

View File

@ -5,7 +5,18 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 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 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.BusBwlBo;
import org.dromara.bigscreen.domain.dto.TaskInfoDto; import org.dromara.bigscreen.domain.dto.TaskInfoDto;
import org.dromara.bigscreen.domain.vo.BusBwlVo; 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.domain.dto.UserDTO;
import org.dromara.common.core.enums.BusinessStatusEnum; import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.core.utils.StreamUtils; 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.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper; 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.bo.FlowTaskBo;
import org.dromara.workflow.domain.vo.FlowTaskVo; import org.dromara.workflow.domain.vo.FlowTaskVo;
import org.dromara.workflow.mapper.FlwTaskMapper; import org.dromara.workflow.mapper.FlwTaskMapper;
import org.springframework.ai.chat.messages.Message;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMapping; import reactor.core.publisher.Flux;
import org.springframework.web.bind.annotation.RestController;
import java.time.DayOfWeek; import java.time.DayOfWeek;
import java.time.LocalDate; import java.time.LocalDate;
@ -63,8 +77,82 @@ public class PersonalHomeController extends BaseController {
private final IBusBwlService busBwlService; private final IBusBwlService busBwlService;
@Resource
private DashScopeChat dashScopeChat;
@Resource
private IAIChatMemoryService aiChatMemoryService;
// region AI 模块
/**
* 获取新 AI 对话聊天id
*/
@GetMapping("/ai/chat/new")
public R<String> getNewAIChat() {
return R.ok(UUID.randomUUID().toString());
}
/**
* AI 对话流式调用
*/
@GetMapping("/ai/chat/stream")
public Flux<String> 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<List<Message>> 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<AIChatMemoryVo> listAIChatMemory(AIChatMemoryQueryReq req, PageQuery pageQuery) {
return aiChatMemoryService.queryPageList(req, pageQuery);
}
/**
* 获取 AI 对话记录信息详细信息
*
* @param id 主键
*/
@GetMapping("/ai/chatMemory/{id}")
public R<AIChatMemoryVo> 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<Void> updateAIChatMemory(@Validated AIChatMemoryUpdateReq req) {
return toAjax(aiChatMemoryService.updateByReq(req));
}
/**
* 删除 AI 对话记录信息
*
* @param ids 主键串
*/
@Log(title = "AI 对话记录信息", businessType = BusinessType.DELETE)
@DeleteMapping("/ai/chatMemory/{ids}")
public R<Void> removeAIChatMemory(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(aiChatMemoryService.deleteWithValidByIds(List.of(ids), true));
}
// endregion
/** /**
* 查询派单列表 * 查询派单列表