From d27d6c3ac7de2e888ea35115d2fe9685da8a8d25 Mon Sep 17 00:00:00 2001 From: liqupan Date: Sat, 8 Nov 2025 21:02:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E5=8D=A1=EF=BC=8C=E6=99=BA=E8=83=BD=E4=BD=93=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=EF=BC=8C=E5=8A=A0=E8=BD=BD=EF=BC=8C=E5=8E=86=E5=8F=B2=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=EF=BC=8C=E6=B8=85=E7=A9=BA=E7=AD=89=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- db/add_background_image_to_sys_role.sql | 3 + .../controller/FileUploadController.java | 13 +- .../controller/TemplateController.java | 14 + .../java/com/xiaozhi/dao/MessageMapper.java | 12 +- .../dialogue/llm/memory/ChatMemory.java | 4 +- .../llm/memory/DatabaseChatMemory.java | 13 +- .../llm/memory/MessageWindowConversation.java | 22 +- src/main/java/com/xiaozhi/entity/SysRole.java | 5 + .../java/com/xiaozhi/mapper/MessageMapper.xml | 22 ++ .../java/com/xiaozhi/mapper/RoleMapper.xml | 7 +- .../com/xiaozhi/mapper/TemplateMapper.xml | 5 +- .../app/controller/AppMessageController.java | 34 +- .../app/controller/AppTemplateController.java | 2 +- .../xiaozhi/service/SysMessageService.java | 10 +- .../service/impl/SysMessageServiceImpl.java | 13 +- web/src/services/api.js | 1 + web/src/views/page/Role.vue | 318 ++++++++++++++++-- 18 files changed, 441 insertions(+), 59 deletions(-) create mode 100644 db/add_background_image_to_sys_role.sql diff --git a/.gitignore b/.gitignore index dab4b34..e3faa8c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ gen/ dist/ avatar/ # 忽略特定格式的文件 -uploads/ +upload/ audio/ vosk-model* *.zip diff --git a/db/add_background_image_to_sys_role.sql b/db/add_background_image_to_sys_role.sql new file mode 100644 index 0000000..cd53c53 --- /dev/null +++ b/db/add_background_image_to_sys_role.sql @@ -0,0 +1,3 @@ +-- 为 sys_role 表添加 backgroundImage 字段 +-- 用于存放角色卡的背景照片 +ALTER TABLE sys_role ADD COLUMN backgroundImage VARCHAR(255) DEFAULT NULL COMMENT '角色背景照片' AFTER avatar; diff --git a/src/main/java/com/xiaozhi/controller/FileUploadController.java b/src/main/java/com/xiaozhi/controller/FileUploadController.java index 4e322f8..fceb7a8 100644 --- a/src/main/java/com/xiaozhi/controller/FileUploadController.java +++ b/src/main/java/com/xiaozhi/controller/FileUploadController.java @@ -1,10 +1,11 @@ package com.xiaozhi.controller; import com.xiaozhi.common.web.AjaxResult; +import com.xiaozhi.plus.config.properties.LocalFileProperties; import com.xiaozhi.utils.FileUploadUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -22,8 +23,8 @@ import java.util.UUID; public class FileUploadController { private static final Logger logger = LoggerFactory.getLogger(FileUploadController.class); - @Value("${xiaozhi.upload-path:uploads}") - private String uploadPath; + @Autowired + private LocalFileProperties localFileProperties; /** * 通用文件上传方法 @@ -49,7 +50,7 @@ public class FileUploadController { // 使用FileUploadUtils进行智能上传(根据配置自动选择上传到本地或腾讯云COS) try { - String fileUrl = FileUploadUtils.smartUpload(uploadPath, relativePath, fileName, file); + String fileUrl = FileUploadUtils.smartUpload(localFileProperties.getUploadPath(), relativePath, fileName, file); logger.info("文件上传成功: {}", fileUrl); // 判断是否是COS URL @@ -62,8 +63,8 @@ public class FileUploadController { // 如果是本地URL,需要调整格式 if (!isCosUrl) { - // 将本地路径转换为访问URL格式 - String accessUrl = "uploads/" + relativePath + "/" + fileName; + // 将本地路径转换为访问URL格式,使用正确的访问路径 + String accessUrl = "/file/" + relativePath + "/" + fileName; result.put("url", accessUrl); } diff --git a/src/main/java/com/xiaozhi/controller/TemplateController.java b/src/main/java/com/xiaozhi/controller/TemplateController.java index 8ed344b..f8a6cd8 100644 --- a/src/main/java/com/xiaozhi/controller/TemplateController.java +++ b/src/main/java/com/xiaozhi/controller/TemplateController.java @@ -67,4 +67,18 @@ public class TemplateController { } } + /** + * 删除模板 + */ + @PostMapping("/delete") + @ResponseBody + public AjaxResult delete(@RequestParam("templateId") Integer templateId) { + try { + int rows = templateService.delete(templateId); + return rows > 0 ? AjaxResult.success() : AjaxResult.error("删除模板失败"); + } catch (Exception e) { + return AjaxResult.error(e.getMessage()); + } + } + } \ No newline at end of file diff --git a/src/main/java/com/xiaozhi/dao/MessageMapper.java b/src/main/java/com/xiaozhi/dao/MessageMapper.java index 4e444dc..4beb987 100644 --- a/src/main/java/com/xiaozhi/dao/MessageMapper.java +++ b/src/main/java/com/xiaozhi/dao/MessageMapper.java @@ -3,12 +3,13 @@ package com.xiaozhi.dao; import java.util.List; import com.xiaozhi.entity.SysMessage; +import org.apache.ibatis.annotations.Param; /** * 聊天记录 数据层 - * + * * @author Joey - * + * */ public interface MessageMapper { @@ -17,4 +18,11 @@ public interface MessageMapper { int delete(SysMessage message); List query(SysMessage message); + + /** + * 根据sessionId查询历史消息 + * @param sessionId 会话ID + * @return 历史消息列表 + */ + List queryBySessionId(@Param("sessionId") String sessionId); } \ No newline at end of file diff --git a/src/main/java/com/xiaozhi/dialogue/llm/memory/ChatMemory.java b/src/main/java/com/xiaozhi/dialogue/llm/memory/ChatMemory.java index 0cff481..914a960 100644 --- a/src/main/java/com/xiaozhi/dialogue/llm/memory/ChatMemory.java +++ b/src/main/java/com/xiaozhi/dialogue/llm/memory/ChatMemory.java @@ -3,6 +3,7 @@ package com.xiaozhi.dialogue.llm.memory; import com.xiaozhi.entity.SysMessage; import java.util.List; +import java.util.concurrent.CompletableFuture; /** * 聊天记忆接口,全局对象,不针对单个会话,而是负责全局记忆的存储策略及针对不同类型数据库的适配。。 @@ -17,8 +18,9 @@ public interface ChatMemory { /** * 添加消息 * TODO 参数太多,后续考虑如何简化一些 + * @return CompletableFuture,用于等待消息插入完成 */ - void addMessage(String deviceId, String sessionId, String sender, String content, Integer roleId, String messageType, Long timeMillis); + CompletableFuture addMessage(String deviceId, String sessionId, String sender, String content, Integer roleId, String messageType, Long timeMillis); /** * 获取历史对话消息列表 diff --git a/src/main/java/com/xiaozhi/dialogue/llm/memory/DatabaseChatMemory.java b/src/main/java/com/xiaozhi/dialogue/llm/memory/DatabaseChatMemory.java index 102bec8..cf851f4 100644 --- a/src/main/java/com/xiaozhi/dialogue/llm/memory/DatabaseChatMemory.java +++ b/src/main/java/com/xiaozhi/dialogue/llm/memory/DatabaseChatMemory.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.List; +import java.util.concurrent.CompletableFuture; /** * 基于数据库的聊天记忆实现 @@ -34,7 +35,10 @@ public class DatabaseChatMemory implements ChatMemory { } @Override - public void addMessage(String deviceId, String sessionId, String sender, String content, Integer roleId, String messageType, Long timeMillis) { + public CompletableFuture addMessage(String deviceId, String sessionId, String sender, String content, Integer roleId, String messageType, Long timeMillis) { + // 返回CompletableFuture,用于等待消息插入完成,确保插入顺序 + CompletableFuture future = new CompletableFuture<>(); + // 异步虚拟线程处理持久化。 Thread.startVirtualThread(() -> { try { @@ -54,11 +58,18 @@ public class DatabaseChatMemory implements ChatMemory { messageService.add(message); logger.debug("消息保存成功: deviceId={}, sessionId={}, sender={}, timeMillis={}", deviceId, sessionId, sender, actualTimeMillis); + + // 插入成功,完成Future + future.complete(null); } catch (Exception e) { logger.error("保存消息时出错: deviceId={}, sessionId={}, sender={}, error={}", deviceId, sessionId, sender, e.getMessage(), e); + // 插入失败,完成Future并传递异常 + future.completeExceptionally(e); } }); + + return future; } @Override diff --git a/src/main/java/com/xiaozhi/dialogue/llm/memory/MessageWindowConversation.java b/src/main/java/com/xiaozhi/dialogue/llm/memory/MessageWindowConversation.java index 40b18c7..fe516dc 100644 --- a/src/main/java/com/xiaozhi/dialogue/llm/memory/MessageWindowConversation.java +++ b/src/main/java/com/xiaozhi/dialogue/llm/memory/MessageWindowConversation.java @@ -8,6 +8,7 @@ import org.springframework.ai.chat.messages.*; import org.springframework.util.StringUtils; import java.util.*; +import java.util.concurrent.CompletableFuture; /** * 限定消息条数(消息窗口)的Conversation实现。根据不同的策略,可实现聊天会话的持久化、加载、清除等功能。 @@ -110,15 +111,20 @@ public class MessageWindowConversation extends Conversation { String messageType = hasToolCalls ? SysMessage.MESSAGE_TYPE_FUNCTION_CALL : SysMessage.MESSAGE_TYPE_NORMAL; String deviceId = device().getDeviceId(); int roleId = role().getRoleId(); + + // 先插入user消息,获取返回的CompletableFuture // 如果本轮对话是function_call或mcp调用(最后一条信息的类型),把用户的消息类型也修正为同样类型 - chatMemory.addMessage(deviceId, sessionId(), userMessage.getMessageType().getValue(), userMessage.getText(), - roleId, messageType, userTimeMillis); - - String response = assistantMessage.getText(); - if (StringUtils.hasText(response)) { - chatMemory.addMessage(deviceId, sessionId(), assistantMessage.getMessageType().getValue(), response, - roleId, messageType, assistantTimeMillis); - } + CompletableFuture userFuture = chatMemory.addMessage(deviceId, sessionId(), userMessage.getMessageType().getValue(), + userMessage.getText(), roleId, messageType, userTimeMillis); + + // 等待user消息插入完成后再插入assistant消息,确保数据库中的顺序 + userFuture.thenRun(() -> { + String response = assistantMessage.getText(); + if (StringUtils.hasText(response)) { + chatMemory.addMessage(deviceId, sessionId(), assistantMessage.getMessageType().getValue(), + response, roleId, messageType, assistantTimeMillis); + } + }); } @Override diff --git a/src/main/java/com/xiaozhi/entity/SysRole.java b/src/main/java/com/xiaozhi/entity/SysRole.java index 792efa5..324fd7f 100644 --- a/src/main/java/com/xiaozhi/entity/SysRole.java +++ b/src/main/java/com/xiaozhi/entity/SysRole.java @@ -26,6 +26,11 @@ public class SysRole extends Base { */ private String avatar; + /** + * 角色背景照片 + */ + private String backgroundImage; + /** * 角色名称 */ diff --git a/src/main/java/com/xiaozhi/mapper/MessageMapper.xml b/src/main/java/com/xiaozhi/mapper/MessageMapper.xml index e2eab06..47a3ea1 100644 --- a/src/main/java/com/xiaozhi/mapper/MessageMapper.xml +++ b/src/main/java/com/xiaozhi/mapper/MessageMapper.xml @@ -63,4 +63,26 @@ + + + \ No newline at end of file diff --git a/src/main/java/com/xiaozhi/mapper/RoleMapper.xml b/src/main/java/com/xiaozhi/mapper/RoleMapper.xml index 3509d62..23d404d 100644 --- a/src/main/java/com/xiaozhi/mapper/RoleMapper.xml +++ b/src/main/java/com/xiaozhi/mapper/RoleMapper.xml @@ -3,7 +3,7 @@ - sys_role.roleId, sys_role.avatar, sys_role.roleName, sys_role.roleDesc, sys_role.voiceName, + sys_role.roleId, sys_role.avatar, sys_role.backgroundImage, sys_role.roleName, sys_role.roleDesc, sys_role.voiceName, sys_role.modelId, sys_role.templateId, sys_role.sttId, sys_role.ttsId, sys_role.vadSpeechTh, sys_role.vadSilenceTh, sys_role.vadEnergyTh, sys_role.vadSilenceMs, sys_role.userId, sys_role.state, sys_role.isDefault, sys_role.createTime @@ -46,6 +46,8 @@ sys_role avatar = #{avatar}, + backgroundImage = #{backgroundImage}, + roleName = #{roleName}, roleDesc = #{roleDesc}, voiceName = #{voiceName}, isDefault = #{isDefault}, @@ -80,8 +82,9 @@ - INSERT INTO sys_role ( avatar, roleName, roleDesc, voiceName, modelId, templateId, ttsId, sttId, userId, isDefault ) VALUES ( + INSERT INTO sys_role ( avatar, backgroundImage, roleName, roleDesc, voiceName, modelId, templateId, ttsId, sttId, userId, isDefault ) VALUES ( #{avatar}, + #{backgroundImage}, #{roleName}, #{roleDesc}, #{voiceName}, diff --git a/src/main/java/com/xiaozhi/mapper/TemplateMapper.xml b/src/main/java/com/xiaozhi/mapper/TemplateMapper.xml index d1ff443..d621112 100644 --- a/src/main/java/com/xiaozhi/mapper/TemplateMapper.xml +++ b/src/main/java/com/xiaozhi/mapper/TemplateMapper.xml @@ -72,5 +72,8 @@ UPDATE sys_template SET isDefault = '0' WHERE isDefault = 1 AND userId = #{userId} - + + DELETE FROM sys_template WHERE templateId = #{templateId} + + \ No newline at end of file diff --git a/src/main/java/com/xiaozhi/plus/app/controller/AppMessageController.java b/src/main/java/com/xiaozhi/plus/app/controller/AppMessageController.java index b4cdc50..71055b5 100644 --- a/src/main/java/com/xiaozhi/plus/app/controller/AppMessageController.java +++ b/src/main/java/com/xiaozhi/plus/app/controller/AppMessageController.java @@ -58,9 +58,39 @@ public class AppMessageController extends BaseController { } } + /** + * 根据sessionId查询历史消息(全部) + * + * @param sessionId 会话ID + * @return 历史消息列表 + */ + @GetMapping("/history") + @ResponseBody + public AjaxResult getHistoryBySessionId(String sessionId) { + try { + if (sessionId == null || sessionId.trim().isEmpty()) { + return AjaxResult.error("sessionId不能为空"); + } + + logger.info("查询历史消息,sessionId: {}", sessionId); + + // 使用新的queryBySessionId方法,直接根据sessionId查询 + List messageList = sysMessageService.queryBySessionId(sessionId); + + logger.info("查询到历史消息数量: {}", messageList.size()); + + AjaxResult result = AjaxResult.success(); + result.put("data", messageList); + return result; + } catch (Exception e) { + logger.error("查询历史消息失败, sessionId: {}, error: {}", sessionId, e.getMessage(), e); + return AjaxResult.error("查询历史消息失败: " + e.getMessage()); + } + } + /** * 删除聊天记录 - * + * * @param message * @return */ @@ -81,5 +111,5 @@ public class AppMessageController extends BaseController { return AjaxResult.error(); } } - + } \ No newline at end of file diff --git a/src/main/java/com/xiaozhi/plus/app/controller/AppTemplateController.java b/src/main/java/com/xiaozhi/plus/app/controller/AppTemplateController.java index 2581180..8184d45 100644 --- a/src/main/java/com/xiaozhi/plus/app/controller/AppTemplateController.java +++ b/src/main/java/com/xiaozhi/plus/app/controller/AppTemplateController.java @@ -21,7 +21,7 @@ import java.util.List; */ @RestController @RequestMapping("/app/template") -public class AppTemplateController { +public class AppTemplateController { @Resource private SysTemplateService templateService; diff --git a/src/main/java/com/xiaozhi/service/SysMessageService.java b/src/main/java/com/xiaozhi/service/SysMessageService.java index 5ecab0c..af587a3 100644 --- a/src/main/java/com/xiaozhi/service/SysMessageService.java +++ b/src/main/java/com/xiaozhi/service/SysMessageService.java @@ -31,10 +31,18 @@ public interface SysMessageService { /** * 删除记忆 - * + * * @param message * @return */ int delete(SysMessage message); + /** + * 根据sessionId查询历史消息 + * + * @param sessionId 会话ID + * @return 历史消息列表(按时间升序) + */ + List queryBySessionId(String sessionId); + } \ No newline at end of file diff --git a/src/main/java/com/xiaozhi/service/impl/SysMessageServiceImpl.java b/src/main/java/com/xiaozhi/service/impl/SysMessageServiceImpl.java index d5f81f3..e07c72f 100644 --- a/src/main/java/com/xiaozhi/service/impl/SysMessageServiceImpl.java +++ b/src/main/java/com/xiaozhi/service/impl/SysMessageServiceImpl.java @@ -52,7 +52,7 @@ public class SysMessageServiceImpl extends BaseServiceImpl implements SysMessage /** * 删除记忆 - * + * * @param message * @return */ @@ -62,4 +62,15 @@ public class SysMessageServiceImpl extends BaseServiceImpl implements SysMessage return messageMapper.delete(message); } + /** + * 根据sessionId查询历史消息 + * + * @param sessionId 会话ID + * @return 历史消息列表(按时间升序) + */ + @Override + public List queryBySessionId(String sessionId) { + return messageMapper.queryBySessionId(sessionId); + } + } \ No newline at end of file diff --git a/web/src/services/api.js b/web/src/services/api.js index 0943871..488c533 100644 --- a/web/src/services/api.js +++ b/web/src/services/api.js @@ -32,6 +32,7 @@ export default { query: "/api/template/query", add: "/api/template/add", update: "/api/template/update", + delete: "/api/template/delete", }, message: { query: "/api/message/query", diff --git a/web/src/views/page/Role.vue b/web/src/views/page/Role.vue index 5c5badb..26fe354 100644 --- a/web/src/views/page/Role.vue +++ b/web/src/views/page/Role.vue @@ -109,7 +109,7 @@

点击上传

- +
@@ -117,24 +117,69 @@
- + - 移除头像 - +
支持JPG、PNG、GIF格式,不超过2MB
+ + +
+ + +
+ + 角色背景 + +
+ +

点击上传背景

+
+ + +
+ +

{{ backgroundImageUrl ? '更换背景' : '上传背景' }}

+
+
+
+ + + + 移除背景 + + +
+ 支持JPG、PNG、GIF格式,不超过5MB +
+
+
+