package xiaozhi.modules.agent.controller; import java.io.IOException; import java.io.OutputStream; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import xiaozhi.common.constant.Constant; import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.RenException; import xiaozhi.common.page.PageData; import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.user.UserDetail; import xiaozhi.common.utils.DateUtils; import xiaozhi.common.utils.MessageUtils; import xiaozhi.common.utils.Result; import xiaozhi.modules.agent.dto.AgentChatHistoryDTO; import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO; import xiaozhi.modules.agent.dto.AgentChatSessionDTO; import xiaozhi.modules.agent.service.AgentChatHistoryService; import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService; import xiaozhi.modules.security.user.SecurityUser; @Tag(name = "智能体聊天历史管理") @RequiredArgsConstructor @RestController @RequestMapping("/agent/chat-history") public class AgentChatHistoryController { private final AgentChatHistoryBizService agentChatHistoryBizService; private final AgentChatHistoryService agentChatHistoryService; private final AgentService agentService; private final RedisUtils redisUtils; /** * 小智服务聊天上报请求 *

* 小智服务聊天上报请求,包含Base64编码的音频数据和相关信息。 * * @param request 包含上传文件及相关信息的请求对象 */ @Operation(summary = "小智服务聊天上报请求") @PostMapping("/report") public Result uploadFile(@Valid @RequestBody AgentChatHistoryReportDTO request) { Boolean result = agentChatHistoryBizService.report(request); return new Result().ok(result); } /** * 获取聊天记录下载链接 * * @param agentId 智能体ID * @param sessionId 会话ID * @return UUID作为下载标识 */ @Operation(summary = "获取聊天记录下载链接") @RequiresPermissions("sys:role:normal") @PostMapping("/getDownloadUrl/{agentId}/{sessionId}") public Result getDownloadUrl(@PathVariable("agentId") String agentId, @PathVariable("sessionId") String sessionId) { // 获取当前用户 UserDetail user = SecurityUser.getUser(); // 检查权限 if (!agentService.checkAgentPermission(agentId, user.getId())) { throw new RenException(ErrorCode.CHAT_HISTORY_NO_PERMISSION); } // 生成UUID String uuid = UUID.randomUUID().toString(); // 存储agentId和sessionId到Redis,格式为agentId:sessionId redisUtils.set(RedisKeys.getChatHistoryKey(uuid), agentId + ":" + sessionId); return new Result().ok(uuid); } /** * 下载本会话聊天记录 * * @param uuid 下载标识 * @param response HTTP响应 */ @Operation(summary = "下载本会话聊天记录") @GetMapping("/download/{uuid}/current") public void downloadCurrentSession(@PathVariable("uuid") String uuid, HttpServletResponse response) { // 从Redis获取agentId和sessionId String agentSessionInfo = (String) redisUtils.get(RedisKeys.getChatHistoryKey(uuid)); if (StringUtils.isBlank(agentSessionInfo)) { throw new RenException(ErrorCode.DOWNLOAD_LINK_EXPIRED); } try { // 解析agentId和sessionId String[] parts = agentSessionInfo.split(":"); if (parts.length != 2) { throw new RenException(ErrorCode.DOWNLOAD_LINK_INVALID); } String agentId = parts[0]; String sessionId = parts[1]; // 执行下载 downloadChatHistory(agentId, List.of(sessionId), response); } finally { // 下载完成后删除UUID,防止盗刷 redisUtils.delete(RedisKeys.getChatHistoryKey(uuid)); } } /** * 下载本会话及前20条会话聊天记录 * * @param uuid 下载标识 * @param response HTTP响应 */ @Operation(summary = "下载本会话及前20条会话聊天记录") @GetMapping("/download/{uuid}/previous") public void downloadCurrentSessionWithPrevious(@PathVariable("uuid") String uuid, HttpServletResponse response) { // 从Redis获取agentId和sessionId String agentSessionInfo = (String) redisUtils.get(RedisKeys.getChatHistoryKey(uuid)); if (StringUtils.isBlank(agentSessionInfo)) { throw new RenException(ErrorCode.DOWNLOAD_LINK_EXPIRED); } try { // 解析agentId和sessionId String[] parts = agentSessionInfo.split(":"); if (parts.length != 2) { throw new RenException(ErrorCode.DOWNLOAD_LINK_INVALID); } String agentId = parts[0]; String sessionId = parts[1]; // 获取所有会话列表 Map params = Map.of( "agentId", agentId, Constant.PAGE, 1, Constant.LIMIT, 1000 // 获取足够多的会话 ); PageData sessionPage = agentChatHistoryService.getSessionListByAgentId(params); List allSessions = sessionPage.getList(); // 查找当前会话在列表中的位置 int currentIndex = -1; for (int i = 0; i < allSessions.size(); i++) { if (allSessions.get(i).getSessionId().equals(sessionId)) { currentIndex = i; break; } } // 如果找到了当前会话,收集当前会话及前20条会话ID List sessionIdsToDownload = new ArrayList<>(); if (currentIndex != -1) { // 从当前会话开始,向后(数组后面)取最多20条会话(包括当前会话) int endIndex = Math.min(allSessions.size() - 1, currentIndex + 20); // 确保不越界 for (int i = currentIndex; i <= endIndex; i++) { sessionIdsToDownload.add(allSessions.get(i).getSessionId()); } } // 如果没有找到当前会话,至少下载当前会话 if (sessionIdsToDownload.isEmpty()) { sessionIdsToDownload.add(sessionId); } downloadChatHistory(agentId, sessionIdsToDownload, response); } finally { // 下载完成后删除UUID,防止盗刷 redisUtils.delete(RedisKeys.getChatHistoryKey(uuid)); } } /** * 下载指定会话的聊天记录 * * @param agentId 智能体ID * @param sessionIds 会话ID列表 * @param response HTTP响应 */ private void downloadChatHistory(String agentId, List sessionIds, HttpServletResponse response) { try { // 设置响应头 response.setContentType("text/plain;charset=UTF-8"); String fileName = URLEncoder.encode("history.txt", StandardCharsets.UTF_8.toString()); response.setHeader("Content-Disposition", "attachment;filename=" + fileName); // 获取聊天记录并写入响应流 try (OutputStream out = response.getOutputStream()) { // 为每个会话生成聊天记录 for (String sessionId : sessionIds) { // 获取该会话的所有聊天记录 List chatHistoryList = agentChatHistoryService .getChatHistoryBySessionId(agentId, sessionId); // 从聊天记录中获取第一条消息的创建时间作为会话时间 if (!chatHistoryList.isEmpty()) { Date firstMessageTime = chatHistoryList.get(0).getCreatedAt(); String sessionTimeStr = DateUtils.format(firstMessageTime, DateUtils.DATE_TIME_PATTERN); out.write((sessionTimeStr + "\n").getBytes(StandardCharsets.UTF_8)); } for (AgentChatHistoryDTO message : chatHistoryList) { String role = message.getChatType() == 1 ? MessageUtils.getMessage(ErrorCode.CHAT_ROLE_USER) : MessageUtils.getMessage(ErrorCode.CHAT_ROLE_AGENT); String direction = message.getChatType() == 1 ? ">>" : "<<"; Date messageTime = message.getCreatedAt(); String messageTimeStr = DateUtils.format(messageTime, DateUtils.DATE_TIME_PATTERN); String content = message.getContent(); String line = "[" + role + "]-[" + messageTimeStr + "]" + direction + ":" + content + "\n"; out.write(line.getBytes(StandardCharsets.UTF_8)); } // 会话之间添加空行分隔 if (sessionIds.indexOf(sessionId) < sessionIds.size() - 1) { out.write("\n".getBytes(StandardCharsets.UTF_8)); } } out.flush(); } } catch (IOException e) { e.printStackTrace(); } } }