diff --git a/main/manager-api/pom.xml b/main/manager-api/pom.xml index 9ad38043..d08e32f2 100644 --- a/main/manager-api/pom.xml +++ b/main/manager-api/pom.xml @@ -197,6 +197,10 @@ lombok true + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java index 123e10ba..7b717a3f 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java @@ -28,11 +28,14 @@ import xiaozhi.common.page.PageData; import xiaozhi.common.user.UserDetail; import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.Result; +import xiaozhi.modules.agent.dto.AgentChatHistoryDTO; +import xiaozhi.modules.agent.dto.AgentChatSessionDTO; import xiaozhi.modules.agent.dto.AgentCreateDTO; import xiaozhi.modules.agent.dto.AgentDTO; import xiaozhi.modules.agent.dto.AgentUpdateDTO; import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentTemplateEntity; +import xiaozhi.modules.agent.service.AgentChatHistoryService; import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.AgentTemplateService; import xiaozhi.modules.device.service.DeviceService; @@ -46,6 +49,7 @@ public class AgentController { private final AgentService agentService; private final AgentTemplateService agentTemplateService; private final DeviceService deviceService; + private final AgentChatHistoryService agentChatHistoryService; @GetMapping("/list") @Operation(summary = "获取用户智能体列表") @@ -192,4 +196,37 @@ public class AgentController { return new Result>().ok(list); } + @GetMapping("/{id}/sessions") + @Operation(summary = "获取智能体会话列表") + @RequiresPermissions("sys:role:normal") + @Parameters({ + @Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true), + @Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true), + }) + public Result> getAgentSessions( + @PathVariable("id") String id, + @Parameter(hidden = true) @RequestParam Map params) { + params.put("agentId", id); + PageData page = agentChatHistoryService.getSessionListByAgentId(params); + return new Result>().ok(page); + } + + @GetMapping("/{id}/chat-history/{sessionId}") + @Operation(summary = "获取智能体聊天记录") + @RequiresPermissions("sys:role:normal") + public Result> getAgentChatHistory( + @PathVariable("id") String id, + @PathVariable("sessionId") String sessionId) { + // 获取当前用户 + UserDetail user = SecurityUser.getUser(); + + // 检查权限 + if (!agentService.checkAgentPermission(id, user.getId())) { + return new Result>().error("没有权限查看该智能体的聊天记录"); + } + + // 查询聊天记录 + List result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId); + return new Result>().ok(result); + } } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryDTO.java new file mode 100644 index 00000000..a444917d --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryDTO.java @@ -0,0 +1,25 @@ +package xiaozhi.modules.agent.dto; + +import java.util.Date; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 智能体聊天记录DTO + */ +@Data +@Schema(description = "智能体聊天记录") +public class AgentChatHistoryDTO { + @Schema(description = "创建时间") + private Date createdAt; + + @Schema(description = "消息类型: 1-用户, 2-智能体") + private Byte chatType; + + @Schema(description = "聊天内容") + private String content; + + @Schema(description = "音频ID") + private String audioId; +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatSessionDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatSessionDTO.java new file mode 100644 index 00000000..c65f05c6 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatSessionDTO.java @@ -0,0 +1,26 @@ +package xiaozhi.modules.agent.dto; + +import java.time.LocalDateTime; + +import lombok.Data; + +/** + * 智能体会话列表DTO + */ +@Data +public class AgentChatSessionDTO { + /** + * 会话ID + */ + private String sessionId; + + /** + * 会话时间 + */ + private LocalDateTime createdAt; + + /** + * 聊天条数 + */ + private Integer chatCount; +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatHistoryEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatHistoryEntity.java index 96e82885..679c814a 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatHistoryEntity.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatHistoryEntity.java @@ -67,12 +67,6 @@ public class AgentChatHistoryEntity { @TableField(value = "audio_id") private String audioId; - /** - * 音频URL - */ - @TableField(value = "audio_url") - private String audioUrl; - /** * 创建时间 */ diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatHistoryService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatHistoryService.java index e852cae3..e729c6dd 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatHistoryService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatHistoryService.java @@ -1,6 +1,13 @@ package xiaozhi.modules.agent.service; +import java.util.List; +import java.util.Map; + import com.baomidou.mybatisplus.extension.service.IService; + +import xiaozhi.common.page.PageData; +import xiaozhi.modules.agent.dto.AgentChatHistoryDTO; +import xiaozhi.modules.agent.dto.AgentChatSessionDTO; import xiaozhi.modules.agent.entity.AgentChatHistoryEntity; /** @@ -11,4 +18,21 @@ import xiaozhi.modules.agent.entity.AgentChatHistoryEntity; * @since 1.0.0 */ public interface AgentChatHistoryService extends IService { + + /** + * 根据智能体ID获取会话列表 + * + * @param params 查询参数,包含agentId、page、limit + * @return 分页的会话列表 + */ + PageData getSessionListByAgentId(Map params); + + /** + * 根据会话ID获取聊天记录列表 + * + * @param agentId 智能体ID + * @param sessionId 会话ID + * @return 聊天记录列表 + */ + List getChatHistoryBySessionId(String agentId, String sessionId); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java index 48355a4b..2104d353 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java @@ -8,36 +8,56 @@ import xiaozhi.common.service.BaseService; import xiaozhi.modules.agent.dto.AgentDTO; import xiaozhi.modules.agent.entity.AgentEntity; +/** + * 智能体表处理service + * + * @author Goody + * @version 1.0, 2025/4/30 + * @since 1.0.0 + */ public interface AgentService extends BaseService { - /** - * 管理员获取所有智能体列表(分页) + * 获取管理员智能体列表 + * + * @param params 查询参数 + * @return 分页数据 */ PageData adminAgentList(Map params); /** - * 获取智能体详情 + * 根据ID获取智能体 + * + * @param id 智能体ID + * @return 智能体实体 */ AgentEntity getAgentById(String id); /** - * 删除这个用户的所有 - * - * @param userId + * 插入智能体 + * + * @param entity 智能体实体 + * @return 是否成功 + */ + boolean insert(AgentEntity entity); + + /** + * 根据用户ID删除智能体 + * + * @param userId 用户ID */ void deleteAgentByUserId(Long userId); /** * 获取用户智能体列表 - * - * @param userId - * @return + * + * @param userId 用户ID + * @return 智能体列表 */ List getUserAgents(Long userId); /** - * 获取智能体的设备数量 - * + * 根据智能体ID获取设备数量 + * * @param agentId 智能体ID * @return 设备数量 */ @@ -50,4 +70,13 @@ public interface AgentService extends BaseService { * @return 默认智能体信息,不存在时返回null */ AgentEntity getDefaultAgentByMacAddress(String macAddress); + + /** + * 检查用户是否有权限访问智能体 + * + * @param agentId 智能体ID + * @param userId 用户ID + * @return 是否有权限 + */ + boolean checkAgentPermission(String agentId, Long userId); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java index 95293a3f..18669a6c 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java @@ -1,19 +1,77 @@ -package xiaozhi.modules.agent.service.impl; - -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import org.springframework.stereotype.Service; -import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao; -import xiaozhi.modules.agent.entity.AgentChatHistoryEntity; -import xiaozhi.modules.agent.service.AgentChatHistoryService; - -/** - * 智能体聊天记录表处理service {@link AgentChatHistoryService} impl - * - * @author Goody - * @version 1.0, 2025/4/30 - * @since 1.0.0 - */ -@Service -public class AgentChatHistoryServiceImpl extends ServiceImpl implements AgentChatHistoryService { - -} +package xiaozhi.modules.agent.service.impl; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +import xiaozhi.common.constant.Constant; +import xiaozhi.common.page.PageData; +import xiaozhi.common.utils.ConvertUtils; +import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao; +import xiaozhi.modules.agent.dto.AgentChatHistoryDTO; +import xiaozhi.modules.agent.dto.AgentChatSessionDTO; +import xiaozhi.modules.agent.entity.AgentChatHistoryEntity; +import xiaozhi.modules.agent.service.AgentChatHistoryService; + +/** + * 智能体聊天记录表处理service {@link AgentChatHistoryService} impl + * + * @author Goody + * @version 1.0, 2025/4/30 + * @since 1.0.0 + */ +@Service +public class AgentChatHistoryServiceImpl extends ServiceImpl + implements AgentChatHistoryService { + + @Override + public PageData getSessionListByAgentId(Map params) { + String agentId = (String) params.get("agentId"); + int page = Integer.parseInt(params.get(Constant.PAGE).toString()); + int limit = Integer.parseInt(params.get(Constant.LIMIT).toString()); + + // 构建查询条件 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.select("session_id", "MAX(created_at) as created_at", "COUNT(*) as chat_count") + .eq("agent_id", agentId) + .groupBy("session_id") + .orderByDesc("created_at"); + + // 执行分页查询 + Page> pageParam = new Page<>(page, limit); + IPage> result = this.baseMapper.selectMapsPage(pageParam, wrapper); + + List records = result.getRecords().stream().map(map -> { + AgentChatSessionDTO dto = new AgentChatSessionDTO(); + dto.setSessionId((String) map.get("session_id")); + dto.setCreatedAt((LocalDateTime) map.get("created_at")); + dto.setChatCount(((Number) map.get("chat_count")).intValue()); + return dto; + }).collect(Collectors.toList()); + + return new PageData<>(records, result.getTotal()); + } + + @Override + public List getChatHistoryBySessionId(String agentId, String sessionId) { + // 构建查询条件 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("agent_id", agentId) + .eq("session_id", sessionId) + .orderByAsc("created_at"); + + // 查询聊天记录 + List historyList = list(wrapper); + + // 转换为DTO + return ConvertUtils.sourceToTarget(historyList, AgentChatHistoryDTO.class); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java index 4c529882..97722b59 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java @@ -6,13 +6,13 @@ import java.util.UUID; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.AllArgsConstructor; import xiaozhi.common.page.PageData; import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; @@ -22,24 +22,18 @@ import xiaozhi.modules.agent.dto.AgentDTO; import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.model.service.ModelConfigService; +import xiaozhi.modules.security.user.SecurityUser; +import xiaozhi.modules.sys.enums.SuperAdminEnum; import xiaozhi.modules.timbre.service.TimbreService; @Service +@AllArgsConstructor public class AgentServiceImpl extends BaseServiceImpl implements AgentService { + private final AgentDao agentDao; - - @Autowired - private TimbreService timbreModelService; - - @Autowired - private ModelConfigService modelConfigService; - - @Autowired - private RedisUtils redisUtils; - - public AgentServiceImpl(AgentDao agentDao) { - this.agentDao = agentDao; - } + private final TimbreService timbreModelService; + private final ModelConfigService modelConfigService; + private final RedisUtils redisUtils; @Override public PageData adminAgentList(Map params) { @@ -138,4 +132,21 @@ public class AgentServiceImpl extends BaseServiceImpl imp } return agentDao.getDefaultAgentByMacAddress(macAddress); } + + @Override + public boolean checkAgentPermission(String agentId, Long userId) { + // 获取智能体信息 + AgentEntity agent = getAgentById(agentId); + if (agent == null) { + return false; + } + + // 如果是超级管理员,直接返回true + if (SecurityUser.getUser().getSuperAdmin() == SuperAdminEnum.YES.value()) { + return true; + } + + // 检查是否是智能体的所有者 + return userId.equals(agent.getUserId()); + } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/config/WebMvcConfig.java b/main/manager-api/src/main/java/xiaozhi/modules/security/config/WebMvcConfig.java index d576bfb3..02538caf 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/config/WebMvcConfig.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/config/WebMvcConfig.java @@ -1,5 +1,6 @@ package xiaozhi.modules.security.config; +import java.text.SimpleDateFormat; import java.util.List; import java.util.TimeZone; @@ -18,6 +19,15 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; + +import xiaozhi.common.utils.DateUtils; @Configuration public class WebMvcConfig implements WebMvcConfigurer { @@ -53,10 +63,29 @@ public class WebMvcConfig implements WebMvcConfigurer { // 忽略未知属性 mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - // 日期格式转换 - // mapper.setDateFormat(new SimpleDateFormat(DateUtils.DATE_TIME_PATTERN)); + // 设置时区 mapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + // 配置Java8日期时间序列化 + JavaTimeModule javaTimeModule = new JavaTimeModule(); + javaTimeModule.addSerializer(java.time.LocalDateTime.class, new LocalDateTimeSerializer( + java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_TIME_PATTERN))); + javaTimeModule.addSerializer(java.time.LocalDate.class, new LocalDateSerializer( + java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_PATTERN))); + javaTimeModule.addSerializer(java.time.LocalTime.class, + new LocalTimeSerializer(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss"))); + javaTimeModule.addDeserializer(java.time.LocalDateTime.class, new LocalDateTimeDeserializer( + java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_TIME_PATTERN))); + javaTimeModule.addDeserializer(java.time.LocalDate.class, new LocalDateDeserializer( + java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_PATTERN))); + javaTimeModule.addDeserializer(java.time.LocalTime.class, + new LocalTimeDeserializer(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss"))); + mapper.registerModule(javaTimeModule); + + // 配置java.util.Date的序列化和反序列化 + SimpleDateFormat dateFormat = new SimpleDateFormat(DateUtils.DATE_TIME_PATTERN); + mapper.setDateFormat(dateFormat); + // Long类型转String类型 SimpleModule simpleModule = new SimpleModule(); simpleModule.addSerializer(Long.class, ToStringSerializer.instance); diff --git a/main/manager-api/src/main/resources/db/changelog/202505012207.sql b/main/manager-api/src/main/resources/db/changelog/202505022134.sql similarity index 82% rename from main/manager-api/src/main/resources/db/changelog/202505012207.sql rename to main/manager-api/src/main/resources/db/changelog/202505022134.sql index 5003edc7..ccbaaaee 100644 --- a/main/manager-api/src/main/resources/db/changelog/202505012207.sql +++ b/main/manager-api/src/main/resources/db/changelog/202505022134.sql @@ -1,5 +1,6 @@ -- 初始化智能体聊天记录 DROP TABLE IF EXISTS ai_chat_history; +DROP TABLE IF EXISTS ai_chat_message; DROP TABLE IF EXISTS ai_agent_chat_history; CREATE TABLE ai_agent_chat_history ( @@ -13,7 +14,9 @@ CREATE TABLE ai_agent_chat_history created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', INDEX idx_ai_agent_chat_history_mac (mac_address), - INDEX idx_ai_agent_chat_history_agent_id (agent_id) + INDEX idx_ai_agent_chat_history_session_id (session_id), + INDEX idx_ai_agent_chat_history_agent_id (agent_id), + INDEX idx_ai_agent_chat_history_agent_session_created (agent_id, session_id, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '智能体聊天记录表'; DROP TABLE IF EXISTS ai_agent_chat_audio; diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index aaef45bc..ffbef2ce 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -94,9 +94,9 @@ databaseChangeLog: encoding: utf8 path: classpath:db/changelog/202504301341.sql - changeSet: - id: 202505012207 + id: 202505022134 author: Goody changes: - sqlFile: encoding: utf8 - path: classpath:db/changelog/202505012207.sql \ No newline at end of file + path: classpath:db/changelog/202505022134.sql \ No newline at end of file diff --git a/main/manager-web/src/views/DeviceManagement.vue b/main/manager-web/src/views/DeviceManagement.vue index 34086960..14138bd8 100644 --- a/main/manager-web/src/views/DeviceManagement.vue +++ b/main/manager-web/src/views/DeviceManagement.vue @@ -294,20 +294,13 @@ export default { this.loading = false; if (data.code === 0) { this.deviceList = data.data.map(device => { - const bindDate = new Date(device.createDate); - const formattedBindTime = `${bindDate.getFullYear()}-${(bindDate.getMonth() + 1).toString().padStart(2, '0')}-${bindDate.getDate().toString().padStart(2, '0')} ${bindDate.getHours().toString().padStart(2, '0')}:${bindDate.getMinutes().toString().padStart(2, '0')}:${bindDate.getSeconds().toString().padStart(2, '0')}`; - let formattedLastConversation = ''; - if (device.lastConnectedAt) { - const lastConvoDate = new Date(device.lastConnectedAt); - formattedLastConversation = `${lastConvoDate.getFullYear()}-${(lastConvoDate.getMonth() + 1).toString().padStart(2, '0')}-${lastConvoDate.getDate().toString().padStart(2, '0')} ${lastConvoDate.getHours().toString().padStart(2, '0')}:${lastConvoDate.getMinutes().toString().padStart(2, '0')}:${lastConvoDate.getSeconds().toString().padStart(2, '0')}`; - } return { device_id: device.id, model: device.board, firmwareVersion: device.appVersion, macAddress: device.macAddress, - bindTime: formattedBindTime, - lastConversation: formattedLastConversation, + bindTime: device.createDate, + lastConversation: device.lastConnectedAt, remark: device.alias, isEdit: false, otaSwitch: device.autoUpdate === 1, diff --git a/main/manager-web/src/views/UserManagement.vue b/main/manager-web/src/views/UserManagement.vue index 87ce3747..34d60e74 100644 --- a/main/manager-web/src/views/UserManagement.vue +++ b/main/manager-web/src/views/UserManagement.vue @@ -26,11 +26,7 @@ - - - +