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