diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 2d6c173f..deaff830 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,26 +9,24 @@ assignees: '' ## 🐛 问题描述 -## 🔍 复现步骤 - +## 🖥️ 环境信息 +- 部署方式: 全模块部署 还是 单Server部署 +- 版本号: 例如 0.3.x + +## 🔍 告诉我们,应该怎么复现这个问题 + 1. 打开 '...' 2. 点击 '...' 3. 滚动到 '...' 4. 看到错误 -## 🤔 预期行为 +## 🤔 你原本希望是怎么样的 -## 😯 截图 +## 😯 提供一些截图 - -## 🖥️ 环境信息 -- 操作系统: [例如 Windows 10] -- 浏览器: [例如 Chrome 89] -- 项目版本: [例如 1.0.0] -- Python版本: [例如 3.9.13] -- Jdk版本:[例如 java 21.0.5 2024-10-15 LTS] -- Nodejs版本:[例如 v20.14.0] +1. 比如日志截图,越多越好 +2. 比如界面反应 ## 📋 其他信息 diff --git a/.gitignore b/.gitignore index e1ee3d0c..f4efaeaa 100644 --- a/.gitignore +++ b/.gitignore @@ -165,3 +165,4 @@ main/manager-api/.vscode main/manager-web/.webpack_cache/ main/xiaozhi-server/mysql uploadfile +.vscode diff --git a/README.md b/README.md index a783b3e9..b1bf6dae 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ [![Banners](docs/images/banner1.png)](https://github.com/xinnan-tech/xiaozhi-esp32-server) -

小智后端服务xiaozhi-esp32-server

@@ -12,11 +11,10 @@

English -· 简体中文 -· 更新日志部署文档反馈问题 · 常见问题反馈问题部署文档更新日志

@@ -132,17 +130,7 @@ 1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。 建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。 -2、本项目成立时间较短,还未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必在配置文件 -`config.yaml` 中开启防护: - -```yaml -server: - auth: - # 开启防护 - enabled: true -``` - -开启防护后,您需要根据实际情况校验机器的 token 或 mac 地址,详细请参见配置说明。 +2、本项目功能未完善,且未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必做好必要的防护。 --- @@ -168,11 +156,6 @@ server: OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/ ``` ---- - -## 常见问题 ❓ - -如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。 --- ## 功能清单 ✨ @@ -248,7 +231,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/ | 使用方式 | 支持平台 | 免费平台 | |:---:|:---:|:---:| | 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR | -| 接口调用 | DoubaoASR | - | +| 接口调用 | DoubaoASR、FunASRServer、TencentASR、AliyunASR | FunASRServer | --- diff --git a/docs/Deployment.md b/docs/Deployment.md index 03147198..01f0afeb 100644 --- a/docs/Deployment.md +++ b/docs/Deployment.md @@ -81,7 +81,7 @@ xiaozhi-server 下载完后,回到本教程继续往下。 -##### 1.2.3.2 下载 config.yaml +##### 1.2.3.2 创建 config.yaml 用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。 @@ -224,43 +224,36 @@ python app.py ## 配置项目 如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。 -如果你的`data`下面没有`.config.yaml`文件,你可以把`xiaozhi-server`目录下的`config.yaml`文件复制到`data`,并重命名为`.config.yaml` +如果你的`data`下面没有`.config.yaml`文件,有两个方式,任选一种: -修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。 +第一个方式:你可以把`xiaozhi-server`目录下的`config.yaml`文件复制到`data`,并重命名为`.config.yaml`。在此文件上修改 + +第二个方式:你也可以创建在`data`目录下手动创建`.config.yaml`空文件,然后在这个文件中增加必要的配置信息,系统会优先读取`.config.yaml`文件的配置,如果`.config.yaml`没有配置的,系统会自动去加载`xiaozhi-server`目录下的`config.yaml`的配置。推荐使用这种方式,这种方式是最简洁的方式。 - 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。 -配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。 -本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。 +以下是一个能正常跑起来的,最简单的`.config.yaml`配置示例 ``` +server: + websocket: ws://你的ip或者域名:端口号/xiaozhi/v1/ +prompt: | + 我是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。 + 我的男朋友是一个程序员,梦想是开发出一个机器人,能够帮助人们解决生活中的各种问题。 + 我是一个喜欢哈哈大笑的女孩,爱东说西说吹牛,不合逻辑的也照吹,就要逗别人开心。 + 请你像一个人一样说话,请勿返回配置xml及其他特殊字符。 + selected_module: - VAD: SileroVAD - ASR: FunASR - LLM: ChatGLMLLM - TTS: EdgeTTS - # 默认不开启记忆,如需开启请看配置文件里的描述 - Memory: nomem - # 默认不开启意图识别,如需开启请看配置文件里的描述 - Intent: nointent -``` + LLM: DoubaoLLM -比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。 -使用时,在`selected_module`修改成对应的如下LLM配置的名称: - -``` LLM: - DeepSeekLLM: - type: openai - ... ChatGLMLLM: - type: openai - ... - DifyLLM: - type: dify - ... + api_key: xxxxxxxxxxxxxxx.xxxxxx ``` +建议先将最简单的配置运行起来,然后再去`xiaozhi/config.yaml`阅读配置的使用说明。 +比如你要换更换模型,修改`selected_module`下的配置就行。 + ## 模型文件 本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt` diff --git a/docs/Deployment_all.md b/docs/Deployment_all.md index 0d372353..da984993 100644 --- a/docs/Deployment_all.md +++ b/docs/Deployment_all.md @@ -213,7 +213,7 @@ CREATE DATABASE xiaozhi_esp32_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unico 如果还没有MySQL,你可以通过docker安装mysql ``` -docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -d mysql:latest +docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -e TZ=Asia/Shanghai -d mysql:latest ``` ## 2.安装redis 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/common/constant/Constant.java b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java index 07d746b8..779fea83 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java +++ b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java @@ -177,5 +177,5 @@ public interface Constant { /** * 版本号 */ - public static final String VERSION = "0.3.13"; + public static final String VERSION = "0.3.14"; } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java b/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java index 5e5ec309..62487456 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java +++ b/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java @@ -103,4 +103,18 @@ public class RedisKeys { public static String getOtaDownloadCountKey(String uuid) { return "ota:download:count:" + uuid; } + + /** + * 获取字典数据的缓存key + */ + public static String getDictDataByTypeKey(String dictType) { + return "sys:dict:data:" + dictType; + } + + /** + * 获取智能体音频ID的缓存key + */ + public static String getAgentAudioIdKey(String uuid) { + return "agent:audio:id:" + uuid; + } } diff --git a/main/manager-api/src/main/java/xiaozhi/common/service/impl/BaseServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/common/service/impl/BaseServiceImpl.java index 8ebb4cd1..9d2ba5d5 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/service/impl/BaseServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/common/service/impl/BaseServiceImpl.java @@ -128,12 +128,10 @@ public abstract class BaseServiceImpl, T> implements Bas return SqlHelper.retBool(result); } - @SuppressWarnings("unchecked") protected Class currentMapperClass() { return (Class) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0); } - @SuppressWarnings("unchecked") @Override public Class currentModelClass() { return (Class) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1); diff --git a/main/manager-api/src/main/java/xiaozhi/common/service/impl/CrudServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/common/service/impl/CrudServiceImpl.java index 524a8cb2..9a1bf39e 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/service/impl/CrudServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/common/service/impl/CrudServiceImpl.java @@ -24,7 +24,6 @@ import xiaozhi.common.utils.ConvertUtils; public abstract class CrudServiceImpl, T, D> extends BaseServiceImpl implements CrudService { - @SuppressWarnings("unchecked") protected Class currentDtoClass() { return (Class) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentChatHistoryController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentChatHistoryController.java new file mode 100644 index 00000000..9d6542cc --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentChatHistoryController.java @@ -0,0 +1,36 @@ +package xiaozhi.modules.agent.controller; + +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.validation.Valid; +import lombok.RequiredArgsConstructor; +import xiaozhi.common.utils.Result; +import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO; +import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService; + +@Tag(name = "智能体聊天历史管理") +@RequiredArgsConstructor +@RestController +@RequestMapping("/agent/chat-history") +public class AgentChatHistoryController { + private final AgentChatHistoryBizService agentChatHistoryBizService; + + /** + * 小智服务聊天上报请求 + *

+ * 小智服务聊天上报请求,包含Base64编码的音频数据和相关信息。 + * + * @param request 包含上传文件及相关信息的请求对象 + */ + @Operation(summary = "小智服务聊天上报请求") + @PostMapping("/report") + public Result uploadFile(@Valid @RequestBody AgentChatHistoryReportDTO request) { + Boolean result = agentChatHistoryBizService.report(request); + return new Result().ok(result); + } +} 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..9368cf6b 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 @@ -3,8 +3,13 @@ package xiaozhi.modules.agent.controller; 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.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -25,14 +30,20 @@ import jakarta.validation.Valid; import lombok.AllArgsConstructor; import xiaozhi.common.constant.Constant; import xiaozhi.common.page.PageData; +import xiaozhi.common.redis.RedisKeys; +import xiaozhi.common.redis.RedisUtils; 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.AgentChatAudioService; +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 +57,9 @@ public class AgentController { private final AgentService agentService; private final AgentTemplateService agentTemplateService; private final DeviceService deviceService; + private final AgentChatHistoryService agentChatHistoryService; + private final AgentChatAudioService agentChatAudioService; + private final RedisUtils redisUtils; @GetMapping("/list") @Operation(summary = "获取用户智能体列表") @@ -80,7 +94,7 @@ public class AgentController { @PostMapping @Operation(summary = "创建智能体") @RequiresPermissions("sys:role:normal") - public Result save(@RequestBody @Valid AgentCreateDTO dto) { + public Result save(@RequestBody @Valid AgentCreateDTO dto) { AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class); // 获取默认模板 @@ -108,7 +122,7 @@ public class AgentController { // ID、智能体编码和排序会在Service层自动生成 agentService.insert(entity); - return new Result<>(); + return new Result().ok(entity.getId()); } @PutMapping("/{id}") @@ -178,6 +192,8 @@ public class AgentController { public Result delete(@PathVariable String id) { // 先删除关联的设备 deviceService.deleteByAgentId(id); + // 删除关联的聊天记录 + agentChatHistoryService.deleteByAgentId(id); // 再删除智能体 agentService.deleteById(id); return new Result<>(); @@ -192,4 +208,71 @@ 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); + } + + @PostMapping("/audio/{audioId}") + @Operation(summary = "获取音频下载ID") + @RequiresPermissions("sys:role:normal") + public Result getAudioId(@PathVariable("audioId") String audioId) { + byte[] audioData = agentChatAudioService.getAudio(audioId); + if (audioData == null) { + return new Result().error("音频不存在"); + } + String uuid = UUID.randomUUID().toString(); + redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId); + return new Result().ok(uuid); + } + + @GetMapping("/play/{uuid}") + @Operation(summary = "播放音频") + public ResponseEntity playAudio(@PathVariable("uuid") String uuid) { + + String audioId = (String) redisUtils.get(RedisKeys.getAgentAudioIdKey(uuid)); + if (StringUtils.isBlank(audioId)) { + return ResponseEntity.notFound().build(); + } + + byte[] audioData = agentChatAudioService.getAudio(audioId); + if (audioData == null) { + return ResponseEntity.notFound().build(); + } + redisUtils.delete(RedisKeys.getAgentAudioIdKey(uuid)); + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"play.wav\"") + .body(audioData); + } + } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java index 8c80d6cc..3b4be452 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java @@ -3,6 +3,7 @@ package xiaozhi.modules.agent.dao; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import xiaozhi.common.dao.BaseDao; import xiaozhi.modules.agent.entity.AgentEntity; @@ -15,4 +16,16 @@ public interface AgentDao extends BaseDao { * @return 设备数量 */ Integer getDeviceCountByAgentId(@Param("agentId") String agentId); -} \ No newline at end of file + + /** + * 根据设备MAC地址查询对应设备的默认智能体信息 + * + * @param macAddress 设备MAC地址 + * @return 默认智能体信息 + */ + @Select(" SELECT a.* FROM ai_device d " + + " LEFT JOIN ai_agent a ON d.agent_id = a.id " + + " WHERE d.mac_address = #{macAddress} " + + " ORDER BY d.id DESC LIMIT 1") + AgentEntity getDefaultAgentByMacAddress(@Param("macAddress") String macAddress); +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatAudioDao.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatAudioDao.java new file mode 100644 index 00000000..355b25ac --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatAudioDao.java @@ -0,0 +1,18 @@ +package xiaozhi.modules.agent.dao; + +import org.apache.ibatis.annotations.Mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import xiaozhi.modules.agent.entity.AgentChatAudioEntity; + +/** + * {@link AgentChatAudioEntity} 智能体聊天音频数据Dao对象 + * + * @author Goody + * @version 1.0, 2025/5/8 + * @since 1.0.0 + */ +@Mapper +public interface AiAgentChatAudioDao extends BaseMapper { +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatHistoryDao.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatHistoryDao.java new file mode 100644 index 00000000..43a5f1ce --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatHistoryDao.java @@ -0,0 +1,31 @@ +package xiaozhi.modules.agent.dao; + +import org.apache.ibatis.annotations.Mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import xiaozhi.modules.agent.entity.AgentChatHistoryEntity; + +/** + * {@link AgentChatHistoryEntity} 智能体聊天历史记录Dao对象 + * + * @author Goody + * @version 1.0, 2025/4/30 + * @since 1.0.0 + */ +@Mapper +public interface AiAgentChatHistoryDao extends BaseMapper { + /** + * 根据智能体ID删除音频 + * + * @param agentId 智能体ID + */ + void deleteAudioByAgentId(String agentId); + + /** + * 根据智能体ID删除聊天历史记录 + * + * @param agentId 智能体ID + */ + void deleteHistoryByAgentId(String agentId); +} 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..fd49e52b --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryDTO.java @@ -0,0 +1,28 @@ +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; + + @Schema(description = "MAC地址") + private String macAddress; +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java new file mode 100644 index 00000000..bdaed950 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java @@ -0,0 +1,31 @@ +package xiaozhi.modules.agent.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 小智设备聊天上报请求 + * + * @author Haotian + * @version 1.0, 2025/5/8 + */ +@Data +@Schema(description = "小智设备聊天上报请求") +public class AgentChatHistoryReportDTO { + @Schema(description = "MAC地址", example = "00:11:22:33:44:55") + @NotBlank + private String macAddress; + @Schema(description = "会话ID", example = "79578c31-f1fb-426a-900e-1e934215f05a") + @NotBlank + private String sessionId; + @Schema(description = "消息类型: 1-用户, 2-智能体", example = "1") + @NotNull + private Byte chatType; + @Schema(description = "聊天内容", example = "你好呀") + @NotBlank + private String content; + @Schema(description = "base64编码的opus音频数据", example = "") + private String audioBase64; +} 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/AgentChatAudioEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatAudioEntity.java new file mode 100644 index 00000000..ac30d9d4 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatAudioEntity.java @@ -0,0 +1,29 @@ +package xiaozhi.modules.agent.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import lombok.Data; + +/** + * 智能体聊天音频数据表 + * + * @author Goody + * @version 1.0, 2025/5/8 + * @since 1.0.0 + */ +@Data +@TableName("ai_agent_chat_audio") +public class AgentChatAudioEntity { + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_UUID) + private String id; + + /** + * 音频opus数据 + */ + private byte[] audio; +} \ 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 new file mode 100644 index 00000000..679c814a --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatHistoryEntity.java @@ -0,0 +1,81 @@ +package xiaozhi.modules.agent.entity; + +import java.util.Date; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 智能体聊天记录表 + * + * @author Goody + * @version 1.0, 2025/4/30 + * @since 1.0.0 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@TableName(value = "ai_agent_chat_history") +public class AgentChatHistoryEntity { + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * MAC地址 + */ + @TableField(value = "mac_address") + private String macAddress; + + /** + * 智能体id + */ + @TableField(value = "agent_id") + private String agentId; + + /** + * 会话ID + */ + @TableField(value = "session_id") + private String sessionId; + + /** + * 消息类型: 1-用户, 2-智能体 + */ + @TableField(value = "chat_type") + private Byte chatType; + + /** + * 聊天内容 + */ + @TableField(value = "content") + private String content; + + /** + * 音频base64数据 + */ + @TableField(value = "audio_id") + private String audioId; + + /** + * 创建时间 + */ + @TableField(value = "created_at") + private Date createdAt; + + /** + * 更新时间 + */ + @TableField(value = "updated_at") + private Date updatedAt; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatAudioService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatAudioService.java new file mode 100644 index 00000000..e284b69f --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatAudioService.java @@ -0,0 +1,30 @@ +package xiaozhi.modules.agent.service; + +import com.baomidou.mybatisplus.extension.service.IService; + +import xiaozhi.modules.agent.entity.AgentChatAudioEntity; + +/** + * 智能体聊天音频数据表处理service + * + * @author Goody + * @version 1.0, 2025/5/8 + * @since 1.0.0 + */ +public interface AgentChatAudioService extends IService { + /** + * 保存音频数据 + * + * @param audioData 音频数据 + * @return 音频ID + */ + String saveAudio(byte[] audioData); + + /** + * 获取音频数据 + * + * @param audioId 音频ID + * @return 音频数据 + */ + byte[] getAudio(String audioId); +} \ No newline at end of file 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 new file mode 100644 index 00000000..889f64cd --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatHistoryService.java @@ -0,0 +1,45 @@ +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; + +/** + * 智能体聊天记录表处理service + * + * @author Goody + * @version 1.0, 2025/4/30 + * @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); + + /** + * 根据智能体ID删除聊天记录 + * + * @param agentId 智能体ID + */ + void deleteByAgentId(String agentId); +} 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 8d0b2959..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,38 +8,75 @@ 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 设备数量 */ Integer getDeviceCountByAgentId(String agentId); -} \ No newline at end of file + + /** + * 根据设备MAC地址查询对应设备的默认智能体信息 + * + * @param macAddress 设备MAC地址 + * @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/biz/AgentChatHistoryBizService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/AgentChatHistoryBizService.java new file mode 100644 index 00000000..191b231b --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/AgentChatHistoryBizService.java @@ -0,0 +1,22 @@ +package xiaozhi.modules.agent.service.biz; + +import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO; + +/** + * 智能体聊天历史业务逻辑层 + * + * @author Goody + * @version 1.0, 2025/4/30 + * @since 1.0.0 + */ +public interface AgentChatHistoryBizService { + + /** + * 聊天上报方法 + * + * @param agentChatHistoryReportDTO 包含聊天上报所需信息的输入对象 + * 例如:设备MAC地址、文件类型、内容等 + * @return 上传结果,true表示成功,false表示失败 + */ + Boolean report(AgentChatHistoryReportDTO agentChatHistoryReportDTO); +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java new file mode 100644 index 00000000..b839e9e4 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java @@ -0,0 +1,82 @@ +package xiaozhi.modules.agent.service.biz.impl; + +import java.util.Base64; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO; +import xiaozhi.modules.agent.entity.AgentChatHistoryEntity; +import xiaozhi.modules.agent.entity.AgentEntity; +import xiaozhi.modules.agent.service.AgentChatAudioService; +import xiaozhi.modules.agent.service.AgentChatHistoryService; +import xiaozhi.modules.agent.service.AgentService; +import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService; + +/** + * {@link AgentChatHistoryBizService} impl + * + * @author Goody + * @version 1.0, 2025/4/30 + * @since 1.0.0 + */ +@Service +@Slf4j +@RequiredArgsConstructor +public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizService { + private final AgentService agentService; + private final AgentChatHistoryService agentChatHistoryService; + private final AgentChatAudioService agentChatAudioService; + + /** + * 处理聊天记录上报,包括文件上传和相关信息记录 + * + * @param report 包含聊天上报所需信息的输入对象 + * @return 上传结果,true表示成功,false表示失败 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean report(AgentChatHistoryReportDTO report) { + String macAddress = report.getMacAddress(); + Byte chatType = report.getChatType(); + log.info("小智设备聊天上报请求: macAddress={}, type={}", macAddress, chatType); + + // 1. base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表 + String audioId = null; + if (report.getAudioBase64() != null && !report.getAudioBase64().isEmpty()) { + try { + byte[] audioData = Base64.getDecoder().decode(report.getAudioBase64()); + audioId = agentChatAudioService.saveAudio(audioData); + log.info("音频数据保存成功,audioId={}", audioId); + } catch (Exception e) { + log.error("音频数据保存失败", e); + return false; + } + } + + // 2. 组装上报数据 + // 2.1 根据设备MAC地址查询对应的默认智能体,判断是否需要上报 + AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress); + if (agentEntity == null) { + return false; + } + String agentId = agentEntity.getId(); + log.info("设备 {} 对应智能体 {} 上报", macAddress, agentEntity.getId()); + + // 2.2 构建聊天记录实体 + AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder() + .macAddress(macAddress) + .agentId(agentId) + .sessionId(report.getSessionId()) + .chatType(report.getChatType()) + .content(report.getContent()) + .audioId(audioId) + .build(); + + // 3. 保存数据 + agentChatHistoryService.save(entity); + return Boolean.TRUE; + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatAudioServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatAudioServiceImpl.java new file mode 100644 index 00000000..cf804678 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatAudioServiceImpl.java @@ -0,0 +1,34 @@ +package xiaozhi.modules.agent.service.impl; + +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +import xiaozhi.modules.agent.dao.AiAgentChatAudioDao; +import xiaozhi.modules.agent.entity.AgentChatAudioEntity; +import xiaozhi.modules.agent.service.AgentChatAudioService; + +/** + * 智能体聊天音频数据表处理service {@link AgentChatAudioService} impl + * + * @author Goody + * @version 1.0, 2025/5/8 + * @since 1.0.0 + */ +@Service +public class AgentChatAudioServiceImpl extends ServiceImpl + implements AgentChatAudioService { + @Override + public String saveAudio(byte[] audioData) { + AgentChatAudioEntity entity = new AgentChatAudioEntity(); + entity.setAudio(audioData); + save(entity); + return entity.getId(); + } + + @Override + public byte[] getAudio(String audioId) { + AgentChatAudioEntity entity = getById(audioId); + return entity != null ? entity.getAudio() : null; + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..cc7236ae --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java @@ -0,0 +1,85 @@ +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 org.springframework.transaction.annotation.Transactional; + +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); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByAgentId(String agentId) { + baseMapper.deleteAudioByAgentId(agentId); + baseMapper.deleteHistoryByAgentId(agentId); + } +} 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 c731c2fa..8fb85f06 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; @@ -23,27 +23,17 @@ import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.device.service.DeviceService; 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 DeviceService deviceService; - - @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) { @@ -136,4 +126,29 @@ public class AgentServiceImpl extends BaseServiceImpl imp return deviceCount != null ? deviceCount : 0; } -} \ No newline at end of file + + @Override + public AgentEntity getDefaultAgentByMacAddress(String macAddress) { + if (StringUtils.isEmpty(macAddress)) { + return null; + } + 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/config/controller/ConfigController.java b/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java index bdbd20ed..f83efb3a 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java @@ -1,6 +1,5 @@ package xiaozhi.modules.config.controller; -import org.apache.commons.lang3.StringUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -8,15 +7,12 @@ import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.AllArgsConstructor; -import xiaozhi.common.constant.Constant; -import xiaozhi.common.exception.RenException; import xiaozhi.common.utils.Result; import xiaozhi.common.validator.ValidatorUtils; import xiaozhi.modules.config.dto.AgentModelsDTO; import xiaozhi.modules.config.service.ConfigService; -import xiaozhi.modules.sys.dto.ConfigSecretDTO; -import xiaozhi.modules.sys.service.SysParamsService; /** * xiaozhi-server 配置获取 @@ -29,33 +25,20 @@ import xiaozhi.modules.sys.service.SysParamsService; @AllArgsConstructor public class ConfigController { private final ConfigService configService; - private final SysParamsService sysParamsService; @PostMapping("server-base") @Operation(summary = "获取配置") - public Result getConfig(@RequestBody ConfigSecretDTO dto) { - // 效验数据 - ValidatorUtils.validateEntity(dto); - checkSecret(dto.getSecret()); + public Result getConfig() { Object config = configService.getConfig(true); return new Result().ok(config); } @PostMapping("agent-models") @Operation(summary = "获取智能体模型") - public Result getAgentModels(@RequestBody AgentModelsDTO dto) { + public Result getAgentModels(@Valid @RequestBody AgentModelsDTO dto) { // 效验数据 ValidatorUtils.validateEntity(dto); - checkSecret(dto.getSecret()); Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule()); return new Result().ok(models); } - - private void checkSecret(String secret) { - String secretParam = sysParamsService.getValue(Constant.SERVER_SECRET, true); - // 验证密钥 - if (StringUtils.isBlank(secret) || !secret.equals(secretParam)) { - throw new RenException("密钥错误"); - } - } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/dto/AgentModelsDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/config/dto/AgentModelsDTO.java index 4a07a743..47c08a81 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/dto/AgentModelsDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/dto/AgentModelsDTO.java @@ -10,9 +10,6 @@ import lombok.Data; @Data @Schema(description = "获取智能体模型配置DTO") public class AgentModelsDTO { - @NotBlank(message = "密钥不能为空") - @Schema(description = "密钥") - private String secret; @NotBlank(message = "设备MAC地址不能为空") @Schema(description = "设备MAC地址") diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java index e4914029..a15df002 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java @@ -61,15 +61,15 @@ public class ConfigServiceImpl implements ConfigService { // 构建模块配置 buildModuleConfig( - agent.getAgentName(), + null, null, null, agent.getVadModelId(), agent.getAsrModelId(), - agent.getLlmModelId(), - agent.getTtsModelId(), - agent.getMemModelId(), - agent.getIntentModelId(), + null, + null, + null, + null, result, isCache); @@ -117,18 +117,6 @@ public class ConfigServiceImpl implements ConfigService { if (alreadySelectedAsrModelId != null && alreadySelectedAsrModelId.equals(agent.getAsrModelId())) { agent.setAsrModelId(null); } - String alreadySelectedLlmModelId = (String) selectedModule.get("LLM"); - if (alreadySelectedLlmModelId != null && alreadySelectedLlmModelId.equals(agent.getLlmModelId())) { - agent.setLlmModelId(null); - } - String alreadySelectedMemModelId = (String) selectedModule.get("Memory"); - if (alreadySelectedMemModelId != null && alreadySelectedMemModelId.equals(agent.getMemModelId())) { - agent.setMemModelId(null); - } - String alreadySelectedIntentModelId = (String) selectedModule.get("Intent"); - if (alreadySelectedIntentModelId != null && alreadySelectedIntentModelId.equals(agent.getIntentModelId())) { - agent.setIntentModelId(null); - } // 构建模块配置 buildModuleConfig( @@ -153,7 +141,6 @@ public class ConfigServiceImpl implements ConfigService { * @param paramsList 系统参数列表 * @return 配置信息 */ - @SuppressWarnings("unchecked") private Object buildConfig(Map config) { // 查询所有系统参数 diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java index 25b03034..1f006d93 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java @@ -21,9 +21,7 @@ import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.utils.ConvertUtils; import xiaozhi.modules.agent.dao.AgentDao; -import xiaozhi.modules.agent.dao.AgentTemplateDao; import xiaozhi.modules.agent.entity.AgentEntity; -import xiaozhi.modules.agent.service.AgentTemplateService; import xiaozhi.modules.model.dao.ModelConfigDao; import xiaozhi.modules.model.dto.ModelBasicInfoDTO; import xiaozhi.modules.model.dto.ModelConfigBodyDTO; @@ -41,8 +39,6 @@ public class ModelConfigServiceImpl extends BaseServiceImpl filters = new HashMap<>(); + // oauth过滤 filters.put("oauth2", new Oauth2Filter()); + // 服务密钥过滤 + filters.put("server", new ServerSecretFilter(sysParamsService)); shiroFilter.setFilters(filters); // 添加Shiro的内置过滤器 @@ -79,8 +83,10 @@ public class ShiroConfig { filterMap.put("/user/login", "anon"); filterMap.put("/user/pub-config", "anon"); filterMap.put("/user/register", "anon"); - filterMap.put("/config/server-base", "anon"); - filterMap.put("/config/agent-models", "anon"); + // 将config路径使用server服务过滤器 + filterMap.put("/config/**", "server"); + filterMap.put("/agent/chat-history/report", "server"); + filterMap.put("/agent/play/**", "anon"); filterMap.put("/**", "oauth2"); shiroFilter.setFilterChainDefinitionMap(filterMap); @@ -98,4 +104,4 @@ public class ShiroConfig { advisor.setSecurityManager(securityManager); return advisor; } -} \ No newline at end of file +} 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/java/xiaozhi/modules/security/secret/ServerSecretFilter.java b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java new file mode 100644 index 00000000..b2a18547 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java @@ -0,0 +1,105 @@ +package xiaozhi.modules.security.secret; + +import java.io.IOException; + +import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.web.filter.authc.AuthenticatingFilter; +import org.springframework.web.bind.annotation.RequestMethod; + +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import xiaozhi.common.constant.Constant; +import xiaozhi.common.exception.ErrorCode; +import xiaozhi.common.utils.HttpContextUtils; +import xiaozhi.common.utils.JsonUtils; +import xiaozhi.common.utils.Result; +import xiaozhi.modules.sys.service.SysParamsService; + +/** + * Config API 过滤器 + */ +@Slf4j +@RequiredArgsConstructor +public class ServerSecretFilter extends AuthenticatingFilter { + private final SysParamsService sysParamsService; + + @Override + protected ServerSecretToken createToken(ServletRequest request, ServletResponse response) { + // 获取请求token + String token = getRequestToken((HttpServletRequest) request); + + if (StringUtils.isBlank(token)) { + log.warn("createToken:token is empty"); + return null; + } + + return new ServerSecretToken(token); + } + + @Override + protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { + // 对OPTIONS请求放行 + if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) { + return true; + } + return false; + } + + @Override + protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { + // 获取token并校验 + String token = getRequestToken((HttpServletRequest) servletRequest); + if (StringUtils.isBlank(token)) { + // token为空,返回401 + this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "服务器密钥不能为空"); + return false; + } + + // 验证token是否匹配 + String serverSecret = getServerSecret(); + if (StringUtils.isBlank(serverSecret) || !serverSecret.equals(token)) { + // token无效,返回401 + this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "无效的服务器密钥"); + return false; + } + + return true; + } + + /** + * 发送未授权响应 + */ + private void sendUnauthorizedResponse(HttpServletResponse response, String message) { + response.setContentType("application/json;charset=utf-8"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); + + try { + String json = JsonUtils.toJsonString(new Result().error(ErrorCode.UNAUTHORIZED, message)); + response.getWriter().print(json); + } catch (IOException e) { + log.error("响应输出失败", e); + } + } + + /** + * 获取请求的token + */ + private String getRequestToken(HttpServletRequest httpRequest) { + String token = null; + // 从header中获取token + String authorization = httpRequest.getHeader("Authorization"); + if (StringUtils.isNotBlank(authorization) && authorization.startsWith("Bearer ")) { + token = authorization.replace("Bearer ", ""); + } + return token; + } + + private String getServerSecret() { + return sysParamsService.getValue(Constant.SERVER_SECRET, true); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretToken.java b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretToken.java new file mode 100644 index 00000000..9072eaf8 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretToken.java @@ -0,0 +1,25 @@ +package xiaozhi.modules.security.secret; + +import org.apache.shiro.authc.AuthenticationToken; + +/** + * Config API Token + */ +public class ServerSecretToken implements AuthenticationToken { + private static final long serialVersionUID = 1L; + private final String token; + + public ServerSecretToken(String token) { + this.token = token; + } + + @Override + public Object getPrincipal() { + return token; + } + + @Override + public Object getCredentials() { + return token; + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/AdminController.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/AdminController.java index e202e1e4..2ccc1e6a 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/AdminController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/AdminController.java @@ -58,7 +58,6 @@ public class AdminController { dto.setLimit((String) params.get(Constant.LIMIT)); dto.setPage((String) params.get(Constant.PAGE)); ValidatorUtils.validateEntity(dto); - ValidatorUtils.validateEntity(dto); PageData page = sysUserService.page(dto); return new Result>().ok(page); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictDataController.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictDataController.java new file mode 100644 index 00000000..30187fe8 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictDataController.java @@ -0,0 +1,105 @@ +package xiaozhi.modules.sys.controller; + +import java.util.List; +import java.util.Map; + +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.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import xiaozhi.common.constant.Constant; +import xiaozhi.common.page.PageData; +import xiaozhi.common.utils.Result; +import xiaozhi.common.validator.ValidatorUtils; +import xiaozhi.modules.sys.dto.SysDictDataDTO; +import xiaozhi.modules.sys.service.SysDictDataService; +import xiaozhi.modules.sys.vo.SysDictDataItem; +import xiaozhi.modules.sys.vo.SysDictDataVO; + +/** + * 字典数据管理 + * + * @author czc + * @since 2025-04-30 + */ +@AllArgsConstructor +@RestController +@RequestMapping("/admin/dict/data") +@Tag(name = "字典数据管理") +public class SysDictDataController { + private final SysDictDataService sysDictDataService; + + @GetMapping("/page") + @Operation(summary = "分页查询字典数据") + @RequiresPermissions("sys:role:superAdmin") + @Parameters({ @Parameter(name = "dictTypeId", description = "字典类型ID", required = true), + @Parameter(name = "dictLabel", description = "数据标签"), @Parameter(name = "dictValue", description = "数据值"), + @Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true), + @Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true) }) + public Result> page(@Parameter(hidden = true) @RequestParam Map params) { + ValidatorUtils.validateEntity(params); + // 强制校验dictTypeId是否存在 + if (!params.containsKey("dictTypeId") || StringUtils.isEmpty(String.valueOf(params.get("dictTypeId")))) { + return new Result>().error("dictTypeId不能为空"); + } + + PageData page = sysDictDataService.page(params); + return new Result>().ok(page); + } + + @GetMapping("/{id}") + @Operation(summary = "获取字典数据详情") + @RequiresPermissions("sys:role:superAdmin") + public Result get(@PathVariable("id") Long id) { + SysDictDataVO vo = sysDictDataService.get(id); + return new Result().ok(vo); + } + + @PostMapping("/save") + @Operation(summary = "新增字典数据") + @RequiresPermissions("sys:role:superAdmin") + public Result save(@RequestBody SysDictDataDTO dto) { + ValidatorUtils.validateEntity(dto); + sysDictDataService.save(dto); + return new Result<>(); + } + + @PutMapping("/update") + @Operation(summary = "修改字典数据") + @RequiresPermissions("sys:role:superAdmin") + public Result update(@RequestBody SysDictDataDTO dto) { + ValidatorUtils.validateEntity(dto); + sysDictDataService.update(dto); + return new Result<>(); + } + + @PostMapping("/delete") + @Operation(summary = "删除字典数据") + @RequiresPermissions("sys:role:superAdmin") + @Parameter(name = "ids", description = "ID数组", required = true) + public Result delete(@RequestBody Long[] ids) { + sysDictDataService.delete(ids); + return new Result<>(); + } + + @GetMapping("/type/{dictType}") + @Operation(summary = "获取字典数据列表") + @RequiresPermissions("sys:role:normal") + public Result> getDictDataByType(@PathVariable("dictType") String dictType) { + List list = sysDictDataService.getDictDataByType(dictType); + return new Result>().ok(list); + } + +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictTypeController.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictTypeController.java new file mode 100644 index 00000000..f16409b0 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictTypeController.java @@ -0,0 +1,92 @@ +package xiaozhi.modules.sys.controller; + +import java.util.Map; + +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.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import xiaozhi.common.constant.Constant; +import xiaozhi.common.page.PageData; +import xiaozhi.common.utils.Result; +import xiaozhi.common.validator.ValidatorUtils; +import xiaozhi.modules.sys.dto.SysDictTypeDTO; +import xiaozhi.modules.sys.service.SysDictTypeService; +import xiaozhi.modules.sys.vo.SysDictTypeVO; + +/** + * 字典类型管理 + * + * @author czc + * @since 2025-04-30 + */ +@AllArgsConstructor +@RestController +@RequestMapping("/admin/dict/type") +@Tag(name = "字典类型管理") +public class SysDictTypeController { + private final SysDictTypeService sysDictTypeService; + + @GetMapping("/page") + @Operation(summary = "分页查询字典类型") + @RequiresPermissions("sys:role:superAdmin") + @Parameters({ @Parameter(name = "dictType", description = "字典类型编码"), + @Parameter(name = "dictName", description = "字典类型名称"), + @Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true), + @Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true) }) + public Result> page(@Parameter(hidden = true) @RequestParam Map params) { + ValidatorUtils.validateEntity(params); + PageData page = sysDictTypeService.page(params); + return new Result>().ok(page); + } + + @GetMapping("/{id}") + @Operation(summary = "获取字典类型详情") + @RequiresPermissions("sys:role:superAdmin") + public Result get(@PathVariable("id") Long id) { + SysDictTypeVO vo = sysDictTypeService.get(id); + return new Result().ok(vo); + } + + @PostMapping("/save") + @Operation(summary = "保存字典类型") + @RequiresPermissions("sys:role:superAdmin") + public Result save(@RequestBody SysDictTypeDTO dto) { + // 参数校验 + ValidatorUtils.validateEntity(dto); + + sysDictTypeService.save(dto); + return new Result<>(); + } + + @PutMapping("/update") + @Operation(summary = "修改字典类型") + @RequiresPermissions("sys:role:superAdmin") + public Result update(@RequestBody SysDictTypeDTO dto) { + // 参数校验 + ValidatorUtils.validateEntity(dto); + + sysDictTypeService.update(dto); + return new Result<>(); + } + + @PostMapping("/delete") + @Operation(summary = "删除字典类型") + @RequiresPermissions("sys:role:superAdmin") + @Parameter(name = "ids", description = "ID数组", required = true) + public Result delete(@RequestBody Long[] ids) { + sysDictTypeService.delete(ids); + return new Result<>(); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictDataDao.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictDataDao.java index 9dd124fe..e5c4c7bb 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictDataDao.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictDataDao.java @@ -5,9 +5,8 @@ import java.util.List; import org.apache.ibatis.annotations.Mapper; import xiaozhi.common.dao.BaseDao; -import xiaozhi.modules.sys.dto.SysDictDataDTO; -import xiaozhi.modules.sys.entity.DictData; import xiaozhi.modules.sys.entity.SysDictDataEntity; +import xiaozhi.modules.sys.vo.SysDictDataItem; /** * 字典数据 @@ -15,10 +14,13 @@ import xiaozhi.modules.sys.entity.SysDictDataEntity; @Mapper public interface SysDictDataDao extends BaseDao { - /** - * 字典数据列表 - */ - List getDictDataList(); + List getDictDataByType(String dictType); - List getDataByTypeCode(String dictType); + /** + * 根据字典类型ID获取字典类型编码 + * + * @param dictTypeId 字典类型ID + * @return 字典类型编码 + */ + String getTypeByTypeId(Long dictTypeId); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictTypeDao.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictTypeDao.java index 5a9d5a27..6c271e31 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictTypeDao.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictTypeDao.java @@ -1,11 +1,8 @@ package xiaozhi.modules.sys.dao; -import java.util.List; - import org.apache.ibatis.annotations.Mapper; import xiaozhi.common.dao.BaseDao; -import xiaozhi.modules.sys.entity.DictType; import xiaozhi.modules.sys.entity.SysDictTypeEntity; /** @@ -14,9 +11,4 @@ import xiaozhi.modules.sys.entity.SysDictTypeEntity; @Mapper public interface SysDictTypeDao extends BaseDao { - /** - * 字典类型列表 - */ - List getDictTypeList(); - } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ConfigSecretDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ConfigSecretDTO.java deleted file mode 100644 index 8e4978f9..00000000 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ConfigSecretDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package xiaozhi.modules.sys.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -@Data -@Schema(description = "配置密钥DTO") -public class ConfigSecretDTO { - @Schema(description = "密钥") - @NotBlank(message = "密钥不能为空") - private String secret; -} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/SysDictTypeDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/SysDictTypeDTO.java index 01d69795..912c817c 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/SysDictTypeDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/SysDictTypeDTO.java @@ -51,5 +51,6 @@ public class SysDictTypeDTO implements Serializable { @Schema(description = "更新时间") @JsonProperty(access = JsonProperty.Access.READ_ONLY) + @JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN) private Date updateDate; } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/DictData.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/DictData.java deleted file mode 100644 index c8deac81..00000000 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/DictData.java +++ /dev/null @@ -1,16 +0,0 @@ -package xiaozhi.modules.sys.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import lombok.Data; - -/** - * 字典数据 - */ -@Data -public class DictData { - @JsonIgnore - private Long dictTypeId; - private String dictLabel; - private String dictValue; -} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/DictType.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/DictType.java deleted file mode 100644 index 4a1e2ca5..00000000 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/DictType.java +++ /dev/null @@ -1,19 +0,0 @@ -package xiaozhi.modules.sys.entity; - -import java.util.ArrayList; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import lombok.Data; - -/** - * 字典类型 - */ -@Data -public class DictType { - @JsonIgnore - private Long id; - private String dictType; - private List dataList = new ArrayList<>(); -} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/SysDictTypeEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/SysDictTypeEntity.java index fa372055..350d857b 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/SysDictTypeEntity.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/SysDictTypeEntity.java @@ -18,7 +18,7 @@ import xiaozhi.common.entity.BaseEntity; @TableName("sys_dict_type") public class SysDictTypeEntity extends BaseEntity { /** - * 字典类型 + * 字典类型编码 */ private String dictType; /** diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictDataService.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictDataService.java index a33df029..ea898447 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictDataService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictDataService.java @@ -1,25 +1,70 @@ package xiaozhi.modules.sys.service; +import java.util.List; import java.util.Map; import xiaozhi.common.page.PageData; import xiaozhi.common.service.BaseService; import xiaozhi.modules.sys.dto.SysDictDataDTO; import xiaozhi.modules.sys.entity.SysDictDataEntity; +import xiaozhi.modules.sys.vo.SysDictDataItem; +import xiaozhi.modules.sys.vo.SysDictDataVO; /** * 数据字典 */ public interface SysDictDataService extends BaseService { - PageData page(Map params); + /** + * 分页查询数据字典信息 + * + * @param params 查询参数,包含分页信息和查询条件 + * @return 返回数据字典的分页查询结果 + */ + PageData page(Map params); - SysDictDataDTO get(Long id); + /** + * 根据ID获取数据字典实体 + * + * @param id 数据字典实体的唯一标识 + * @return 返回数据字典实体的详细信息 + */ + SysDictDataVO get(Long id); + /** + * 保存新的数据字典项 + * + * @param dto 数据字典项的保存数据传输对象 + */ void save(SysDictDataDTO dto); + /** + * 更新数据字典项 + * + * @param dto 数据字典项的更新数据传输对象 + */ void update(SysDictDataDTO dto); + /** + * 删除数据字典项 + * + * @param ids 要删除的数据字典项的ID数组 + */ void delete(Long[] ids); + /** + * 根据字典类型ID删除对应的字典数据 + * + * @param dictTypeId 字典类型ID + */ + void deleteByTypeId(Long dictTypeId); + + /** + * 根据字典类型获取字典数据列表 + * + * @param dictType 字典类型 + * @return 返回字典数据列表 + */ + List getDictDataByType(String dictType); + } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictTypeService.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictTypeService.java index a18e086f..697b2d10 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictTypeService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictTypeService.java @@ -6,25 +6,55 @@ import java.util.Map; import xiaozhi.common.page.PageData; import xiaozhi.common.service.BaseService; import xiaozhi.modules.sys.dto.SysDictTypeDTO; -import xiaozhi.modules.sys.entity.DictType; import xiaozhi.modules.sys.entity.SysDictTypeEntity; +import xiaozhi.modules.sys.vo.SysDictTypeVO; /** * 数据字典 */ public interface SysDictTypeService extends BaseService { - PageData page(Map params); + /** + * 分页查询字典类型信息 + * + * @param params 查询参数,包含分页信息和查询条件 + * @return 返回分页的字典类型数据 + */ + PageData page(Map params); - SysDictTypeDTO get(Long id); + /** + * 根据ID获取字典类型信息 + * + * @param id 字典类型ID + * @return 返回字典类型对象 + */ + SysDictTypeVO get(Long id); + /** + * 保存字典类型信息 + * + * @param dto 字典类型数据传输对象 + */ void save(SysDictTypeDTO dto); + /** + * 更新字典类型信息 + * + * @param dto 字典类型数据传输对象 + */ void update(SysDictTypeDTO dto); + /** + * 删除字典类型信息 + * + * @param ids 要删除的字典类型ID数组 + */ void delete(Long[] ids); - List getAllList(); - - List getDictTypeList(); + /** + * 列出所有字典类型信息 + * + * @return 返回字典类型列表 + */ + List list(Map params); } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java index 20530e6b..ffe88986 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java @@ -1,37 +1,55 @@ package xiaozhi.modules.sys.service.impl; -import java.util.Arrays; +import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.AllArgsConstructor; +import xiaozhi.common.exception.RenException; import xiaozhi.common.page.PageData; +import xiaozhi.common.redis.RedisKeys; +import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.utils.ConvertUtils; import xiaozhi.modules.sys.dao.SysDictDataDao; +import xiaozhi.modules.sys.dao.SysUserDao; import xiaozhi.modules.sys.dto.SysDictDataDTO; import xiaozhi.modules.sys.entity.SysDictDataEntity; +import xiaozhi.modules.sys.entity.SysUserEntity; import xiaozhi.modules.sys.service.SysDictDataService; +import xiaozhi.modules.sys.vo.SysDictDataItem; +import xiaozhi.modules.sys.vo.SysDictDataVO; /** * 字典类型 */ @Service +@AllArgsConstructor public class SysDictDataServiceImpl extends BaseServiceImpl implements SysDictDataService { + private final SysUserDao sysUserDao; + private final RedisUtils redisUtils; @Override - public PageData page(Map params) { - IPage page = baseDao.selectPage( - getPage(params, "sort", true), - getWrapper(params)); + public PageData page(Map params) { + IPage page = baseDao.selectPage(getPage(params, "sort", true), getWrapper(params)); - return getPageData(page, SysDictDataDTO.class); + PageData pageData = getPageData(page, SysDictDataVO.class); + + setUserName(pageData.getList()); + + return pageData; } private QueryWrapper getWrapper(Map params) { @@ -48,32 +66,118 @@ public class SysDictDataServiceImpl extends BaseServiceImpl wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SysDictDataEntity::getDictTypeId, dictTypeId); + baseDao.delete(wrapper); + } + + /** + * 设置用户名 + * + * @param sysDictDataList 字典类型集合 + */ + private void setUserName(List sysDictDataList) { + // 收集所有用户 ID + Set userIds = sysDictDataList.stream().flatMap(vo -> Stream.of(vo.getCreator(), vo.getUpdater())) + .filter(Objects::nonNull).collect(Collectors.toSet()); + + // 设置更新者和创建者名称 + if (!userIds.isEmpty()) { + List sysUserEntities = sysUserDao.selectBatchIds(userIds); + // 把List转成Map,Map + Map userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId, + SysUserEntity::getUsername, (existing, replacement) -> existing)); + + sysDictDataList.forEach(vo -> { + vo.setCreatorName(userNameMap.get(vo.getCreator())); + vo.setUpdaterName(userNameMap.get(vo.getUpdater())); + }); + } + } + + private void checkDictValueUnique(Long dictTypeId, String dictValue, String excludeId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysDictDataEntity::getDictTypeId, dictTypeId).eq(SysDictDataEntity::getDictLabel, dictValue); + if (StringUtils.isNotBlank(excludeId)) { + queryWrapper.ne(SysDictDataEntity::getId, excludeId); + } + boolean exists = baseDao.exists(queryWrapper); + if (exists) { + throw new RenException("字典标签重复"); + } + } + + @Override + public List getDictDataByType(String dictType) { + if (StringUtils.isBlank(dictType)) { + return null; + } + + // 先从Redis获取缓存 + String key = RedisKeys.getDictDataByTypeKey(dictType); + List cachedData = (List) redisUtils.get(key); + if (cachedData != null) { + return cachedData; + } + + // 如果缓存中没有,则从数据库获取 + List data = baseDao.getDictDataByType(dictType); + + // 存入Redis缓存 + if (data != null) { + redisUtils.set(key, data); + } + + return data; } } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictTypeServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictTypeServiceImpl.java index ce2e423e..91522482 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictTypeServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictTypeServiceImpl.java @@ -3,42 +3,52 @@ package xiaozhi.modules.sys.service.impl; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import lombok.AllArgsConstructor; +import xiaozhi.common.exception.RenException; import xiaozhi.common.page.PageData; import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.utils.ConvertUtils; -import xiaozhi.modules.sys.dao.SysDictDataDao; import xiaozhi.modules.sys.dao.SysDictTypeDao; +import xiaozhi.modules.sys.dao.SysUserDao; import xiaozhi.modules.sys.dto.SysDictTypeDTO; -import xiaozhi.modules.sys.entity.DictData; -import xiaozhi.modules.sys.entity.DictType; import xiaozhi.modules.sys.entity.SysDictTypeEntity; +import xiaozhi.modules.sys.entity.SysUserEntity; +import xiaozhi.modules.sys.service.SysDictDataService; import xiaozhi.modules.sys.service.SysDictTypeService; +import xiaozhi.modules.sys.vo.SysDictTypeVO; /** * 字典类型 */ -@AllArgsConstructor @Service +@AllArgsConstructor public class SysDictTypeServiceImpl extends BaseServiceImpl implements SysDictTypeService { - private final SysDictDataDao sysDictDataDao; + private final SysUserDao sysUserDao; + private final SysDictDataService sysDictDataService; @Override - public PageData page(Map params) { - IPage page = baseDao.selectPage( - getPage(params, "sort", true), - getWrapper(params)); + public PageData page(Map params) { + IPage page = baseDao.selectPage(getPage(params, "sort", true), getWrapper(params)); - return getPageData(page, SysDictTypeDTO.class); + PageData pageData = getPageData(page, SysDictTypeVO.class); + + setUserName(pageData.getList()); + + return pageData; } private QueryWrapper getWrapper(Map params) { @@ -53,15 +63,21 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl getAllList() { - List typeList = baseDao.getDictTypeList(); - List dataList = sysDictDataDao.getDictDataList(); - for (DictType type : typeList) { - for (DictData data : dataList) { - if (type.getId().equals(data.getDictTypeId())) { - type.getDataList().add(data); - } - } + public List list(Map params) { + List entityList = baseDao.selectList(getWrapper(params)); + List sysDictTypeVOList = ConvertUtils.sourceToTarget(entityList, SysDictTypeVO.class); + // 设置用户名 + setUserName(sysDictTypeVOList); + + return sysDictTypeVOList; + } + + /** + * 设置用户名 + * + * @param sysDictTypeList 字典类型集合 + */ + private void setUserName(List sysDictTypeList) { + // 收集所有用户 ID + Set userIds = sysDictTypeList.stream().flatMap(vo -> Stream.of(vo.getCreator(), vo.getUpdater())) + .filter(Objects::nonNull).collect(Collectors.toSet()); + + // 设置更新者和创建者名称 + if (!userIds.isEmpty()) { + List sysUserEntities = sysUserDao.selectBatchIds(userIds); + // 把List转成Map,Map + Map userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId, + SysUserEntity::getUsername, (existing, replacement) -> existing)); + + sysDictTypeList.forEach(vo -> { + vo.setCreatorName(userNameMap.get(vo.getCreator())); + vo.setUpdaterName(userNameMap.get(vo.getUpdater())); + }); } - return typeList; } - @Override - public List getDictTypeList() { - return baseDao.getDictTypeList(); + private void checkDictTypeUnique(String dictType, String excludeId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysDictTypeEntity::getDictType, dictType); + if (StringUtils.isNotBlank(excludeId)) { + queryWrapper.ne(SysDictTypeEntity::getId, excludeId); + } + boolean exists = baseDao.exists(queryWrapper); + if (exists) { + throw new RenException("字典类型编码重复"); + } } - } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserServiceImpl.java index 30976229..97944ba9 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserServiceImpl.java @@ -169,6 +169,7 @@ public class SysUserServiceImpl extends BaseServiceImpl(list, page.getTotal()); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserUtilServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserUtilServiceImpl.java index 8fd0351a..f4fe601a 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserUtilServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserUtilServiceImpl.java @@ -20,7 +20,9 @@ public class SysUserUtilServiceImpl extends BaseServiceImpl setter) { String userIdKey = RedisKeys.getUserIdKey(userId); - String username = redisUtils.get(userIdKey).toString(); + + Object value = redisUtils.get(userIdKey); + String username = (value != null) ? value.toString() : null; if(username != null){ setter.accept(username); }else { diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/utils/WebSocketValidator.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/utils/WebSocketValidator.java index d9ff280b..39bbe01f 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/utils/WebSocketValidator.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/utils/WebSocketValidator.java @@ -8,6 +8,7 @@ import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.web.socket.WebSocketHttpHeaders; import org.springframework.web.socket.client.WebSocketClient; import org.springframework.web.socket.client.standard.StandardWebSocketClient; @@ -45,8 +46,9 @@ public class WebSocketValidator { try { WebSocketClient client = new StandardWebSocketClient(); CompletableFuture future = new CompletableFuture<>(); + WebSocketHttpHeaders headers = new WebSocketHttpHeaders(); - client.doHandshake(new WebSocketTestHandler(future), null, URI.create(url)); + client.execute(new WebSocketTestHandler(future), headers, URI.create(url)); // 等待最多5秒获取连接结果 return future.get(5, TimeUnit.SECONDS); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/AdminPageUserVO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/AdminPageUserVO.java index a406848f..9f832372 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/AdminPageUserVO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/AdminPageUserVO.java @@ -1,5 +1,7 @@ package xiaozhi.modules.sys.vo; +import java.util.Date; + import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -23,4 +25,7 @@ public class AdminPageUserVO { @Schema(description = "用户id") private String userid; + + @Schema(description = "注册时间") + private Date createDate; } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictDataItem.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictDataItem.java new file mode 100644 index 00000000..f6c81500 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictDataItem.java @@ -0,0 +1,20 @@ +package xiaozhi.modules.sys.vo; + +import java.io.Serializable; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 字典数据VO + */ +@Data +@Schema(description = "字典数据项") +public class SysDictDataItem implements Serializable { + + @Schema(description = "字典标签") + private String name; + + @Schema(description = "字典值") + private String key; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictDataVO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictDataVO.java new file mode 100644 index 00000000..40025b70 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictDataVO.java @@ -0,0 +1,50 @@ +package xiaozhi.modules.sys.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 字典数据VO + */ +@Data +@Schema(description = "字典数据VO") +public class SysDictDataVO implements Serializable { + @Schema(description = "主键") + private Long id; + + @Schema(description = "字典类型ID") + private Long dictTypeId; + + @Schema(description = "字典标签") + private String dictLabel; + + @Schema(description = "字典值") + private String dictValue; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "排序") + private Integer sort; + + @Schema(description = "创建者") + private Long creator; + + @Schema(description = "创建者名称") + private String creatorName; + + @Schema(description = "创建时间") + private Date createDate; + + @Schema(description = "更新者") + private Long updater; + + @Schema(description = "更新者名称") + private String updaterName; + + @Schema(description = "更新时间") + private Date updateDate; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictTypeVO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictTypeVO.java new file mode 100644 index 00000000..5e5844f3 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictTypeVO.java @@ -0,0 +1,47 @@ +package xiaozhi.modules.sys.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 字典类型VO + */ +@Data +@Schema(description = "字典类型VO") +public class SysDictTypeVO implements Serializable { + @Schema(description = "主键") + private Long id; + + @Schema(description = "字典类型") + private String dictType; + + @Schema(description = "字典名称") + private String dictName; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "排序") + private Integer sort; + + @Schema(description = "创建者") + private Long creator; + + @Schema(description = "创建者名称") + private String creatorName; + + @Schema(description = "创建时间") + private Date createDate; + + @Schema(description = "更新者") + private Long updater; + + @Schema(description = "更新者名称") + private String updaterName; + + @Schema(description = "更新时间") + private Date updateDate; +} diff --git a/main/manager-api/src/main/resources/db/changelog/202504291043.sql b/main/manager-api/src/main/resources/db/changelog/202504291043.sql new file mode 100644 index 00000000..555b8cf4 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202504291043.sql @@ -0,0 +1,504 @@ +-- 增加FunASR服务语音识别模型供应器和配置 +DELETE FROM `ai_model_provider` WHERE `id` = 'SYSTEM_ASR_FunASRServer'; +INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES +('SYSTEM_ASR_FunASRServer', 'ASR', 'fun_server', 'FunASR服务语音识别', '[{"key":"host","label":"服务地址","type":"string"},{"key":"port","label":"端口号","type":"number"}]', 4, 1, NOW(), 1, NOW()); + +DELETE FROM `ai_model_config` WHERE `id` = 'ASR_FunASRServer'; +INSERT INTO `ai_model_config` VALUES ('ASR_FunASRServer', 'ASR', 'FunASRServer', 'FunASR服务语音识别', 0, 1, '{\"type\": \"fun_server\", \"host\": \"127.0.0.1\", \"port\": 10096}', NULL, NULL, 5, NULL, NULL, NULL, NULL); + +-- 修改ai_model_config表的remark字段类型为TEXT +ALTER TABLE `ai_model_config` MODIFY COLUMN `remark` TEXT COMMENT '备注'; + +-- 更新ASR模型配置的说明文档 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md', +`remark` = '独立部署FunASR,使用FunASR的API服务,只需要五句话 +第一句:mkdir -p ./funasr-runtime-resources/models +第二句:sudo docker run -d -p 10096:10095 --privileged=true -v $PWD/funasr-runtime-resources/models:/workspace/models registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12 +上一句话执行后会进入到容器,继续第三句:cd FunASR/runtime +不要退出容器,继续在容器中执行第四句:nohup bash run_server_2pass.sh --download-model-dir /workspace/models --vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx --model-dir damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-onnx --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx --lm-dir damo/speech_ngram_lm_zh-cn-ai-wesp-fst --itn-dir thuduj12/fst_itn_zh --hotword /workspace/models/hotwords.txt > log.txt 2>&1 & +上一句话执行后会进入到容器,继续第五句:tail -f log.txt +第五句话执行完后,会看到模型下载日志,下载完后就可以连接使用了 +以上是使用CPU推理,如果有GPU,详细参考:https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md' WHERE `id` = 'ASR_FunASRServer'; + +-- 更新FunASR本地模型配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/modelscope/FunASR', +`remark` = 'FunASR本地模型配置说明: +1. 需要下载模型文件到xiaozhi-server/models/SenseVoiceSmall目录 +2. 支持中日韩粤语音识别 +3. 本地推理,无需网络连接 +4. 待识别文件保存在tmp/目录' WHERE `id` = 'ASR_FunASR'; + +-- 更新SherpaASR配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/k2-fsa/sherpa-onnx', +`remark` = 'SherpaASR配置说明: +1. 运行时自动下载模型文件到models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17目录 +2. 支持中文、英文、日语、韩语、粤语等多种语言 +3. 本地推理,无需网络连接 +4. 输出文件保存在tmp/目录' WHERE `id` = 'ASR_SherpaASR'; + +-- 更新豆包ASR配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://console.volcengine.com/speech/app', +`remark` = '豆包ASR配置说明: +1. 需要在火山引擎控制台创建应用并获取appid和access_token +2. 支持中文语音识别 +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://console.volcengine.com/speech/app +2. 创建新应用 +3. 获取appid和access_token +4. 填入配置文件中' WHERE `id` = 'ASR_DoubaoASR'; + +-- 更新腾讯ASR配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://console.cloud.tencent.com/cam/capi', +`remark` = '腾讯ASR配置说明: +1. 需要在腾讯云控制台创建应用并获取appid、secret_id和secret_key +2. 支持中文语音识别 +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://console.cloud.tencent.com/cam/capi 获取密钥 +2. 访问 https://console.cloud.tencent.com/asr/resourcebundle 领取免费资源 +3. 获取appid、secret_id和secret_key +4. 填入配置文件中' WHERE `id` = 'ASR_TencentASR'; + +-- 更新TTS模型配置说明 +-- EdgeTTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/rany2/edge-tts', +`remark` = 'EdgeTTS配置说明: +1. 使用微软Edge TTS服务 +2. 支持多种语言和音色 +3. 免费使用,无需注册 +4. 需要网络连接 +5. 输出文件保存在tmp/目录' WHERE `id` = 'TTS_EdgeTTS'; + +-- 豆包TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://console.volcengine.com/speech/service/8', +`remark` = '豆包TTS配置说明: +1. 访问 https://console.volcengine.com/speech/service/8 +2. 需要在火山引擎控制台创建应用并获取appid和access_token +3. 山引擎语音一定要购买花钱,起步价30元,就有100并发了。如果用免费的只有2个并发,会经常报tts错误 +4. 购买服务后,购买免费的音色后,可能要等半小时左右,才能使用。 +5. 填入配置文件中' WHERE `id` = 'TTS_DoubaoTTS'; + +-- 硅基流动TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://cloud.siliconflow.cn/account/ak', +`remark` = '硅基流动TTS配置说明: +1. 访问 https://cloud.siliconflow.cn/account/ak +2. 注册并获取API密钥 +3. 填入配置文件中' WHERE `id` = 'TTS_CosyVoiceSiliconflow'; + +-- Coze中文TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://www.coze.cn/open/oauth/pats', +`remark` = 'Coze中文TTS配置说明: +1. 访问 https://www.coze.cn/open/oauth/pats +2. 获取个人令牌 +3. 填入配置文件中' WHERE `id` = 'TTS_CozeCnTTS'; + +-- FishSpeech配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/fishaudio/fish-speech', +`remark` = 'FishSpeech配置说明: +1. 需要本地部署FishSpeech服务 +2. 支持自定义音色 +3. 本地推理,无需网络连接 +4. 输出文件保存在tmp/目录 +5. 运行服务示例命令:python -m tools.api_server --listen 0.0.0.0:8080 --llama-checkpoint-path "checkpoints/fish-speech-1.5" --decoder-checkpoint-path "checkpoints/fish-speech-1.5/firefly-gan-vq-fsq-8x1024-21hz-generator.pth" --decoder-config-name firefly_gan_vq --compile' WHERE `id` = 'TTS_FishSpeech'; + +-- GPT-SoVITS V2配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/RVC-Boss/GPT-SoVITS', +`remark` = 'GPT-SoVITS V2配置说明: +1. 需要本地部署GPT-SoVITS服务 +2. 支持自定义音色克隆 +3. 本地推理,无需网络连接 +4. 输出文件保存在tmp/目录 +部署步骤: +1. 运行服务示例命令:python api_v2.py -a 127.0.0.1 -p 9880 -c GPT_SoVITS/configs/demo.yaml' WHERE `id` = 'TTS_GPT_SOVITS_V2'; + +-- GPT-SoVITS V3配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/RVC-Boss/GPT-SoVITS', +`remark` = 'GPT-SoVITS V3配置说明: +1. 需要本地部署GPT-SoVITS V3服务 +2. 支持自定义音色克隆 +3. 本地推理,无需网络连接 +4. 输出文件保存在tmp/目录' WHERE `id` = 'TTS_GPT_SOVITS_V3'; + +-- MiniMax TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://platform.minimaxi.com/', +`remark` = 'MiniMax TTS配置说明: +1. 需要在MiniMax平台创建账户并充值 +2. 支持多种音色,当前配置使用female-shaonv +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://platform.minimaxi.com/ 注册账号 +2. 访问 https://platform.minimaxi.com/user-center/payment/balance 充值 +3. 访问 https://platform.minimaxi.com/user-center/basic-information 获取group_id +4. 访问 https://platform.minimaxi.com/user-center/basic-information/interface-key 获取api_key +5. 填入配置文件中' WHERE `id` = 'TTS_MinimaxTTS'; + +-- 阿里云TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://nls-portal.console.aliyun.com/', +`remark` = '阿里云TTS配置说明: +1. 需要在阿里云平台开通智能语音交互服务 +2. 支持多种音色,当前配置使用xiaoyun +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://nls-portal.console.aliyun.com/ 开通服务 +2. 访问 https://nls-portal.console.aliyun.com/applist 获取appkey +3. 访问 https://nls-portal.console.aliyun.com/overview 获取token +4. 填入配置文件中 +注意:token是临时的24小时有效,长期使用需要配置access_key_id和access_key_secret' WHERE `id` = 'TTS_AliyunTTS'; + +-- 腾讯TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://console.cloud.tencent.com/cam/capi', +`remark` = '腾讯TTS配置说明: +1. 需要在腾讯云平台开通智能语音交互服务 +2. 支持多种音色,当前配置使用101001 +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://console.cloud.tencent.com/cam/capi 获取密钥 +2. 访问 https://console.cloud.tencent.com/tts/resourcebundle 领取免费资源 +3. 创建新应用 +4. 获取appid、secret_id和secret_key +5. 填入配置文件中' WHERE `id` = 'TTS_TencentTTS'; + +-- 302AI TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://dash.302.ai/', +`remark` = '302AI TTS配置说明: +1. 需要在302平台创建账户并获取API密钥 +2. 支持多种音色,当前配置使用湾湾小何音色 +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://dash.302.ai/ 注册账号 +2. 访问 https://dash.302.ai/apis/list 获取API密钥 +3. 填入配置文件中 +价格:$35/百万字符' WHERE `id` = 'TTS_TTS302AI'; + +-- 机智云TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://agentrouter.gizwitsapi.com/panel/token', +`remark` = '机智云TTS配置说明: +1. 需要在机智云平台获取API密钥 +2. 支持多种音色,当前配置使用湾湾小何音色 +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://agentrouter.gizwitsapi.com/panel/token 获取API密钥 +2. 填入配置文件中 +注意:前一万名注册的用户,将送5元体验金额' WHERE `id` = 'TTS_GizwitsTTS'; + +-- ACGN TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://acgn.ttson.cn/', +`remark` = 'ACGN TTS配置说明: +1. 需要在ttson平台购买token +2. 支持多种角色音色,当前配置使用角色ID:1695 +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://acgn.ttson.cn/ 查看角色列表 +2. 访问 www.ttson.cn 购买token +3. 填入配置文件中 +开发相关疑问请提交至网站上的qq' WHERE `id` = 'TTS_ACGNTTS'; + +-- OpenAI TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://platform.openai.com/api-keys', +`remark` = 'OpenAI TTS配置说明: +1. 需要在OpenAI平台获取API密钥 +2. 支持多种音色,当前配置使用onyx +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://platform.openai.com/api-keys 获取API密钥 +2. 填入配置文件中 +注意:国内需要使用代理访问' WHERE `id` = 'TTS_OpenAITTS'; + +-- 自定义TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = NULL, +`remark` = '自定义TTS配置说明: +1. 支持自定义TTS接口服务 +2. 使用GET方式请求 +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +配置说明: +1. 在params中配置请求参数 +2. 在headers中配置请求头 +3. 设置返回音频格式' WHERE `id` = 'TTS_CustomTTS'; + +-- 火山引擎边缘大模型网关TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://console.volcengine.com/vei/aigateway/', +`remark` = '火山引擎边缘大模型网关TTS配置说明: +1. 访问 https://console.volcengine.com/vei/aigateway/ +2. 创建网关访问密钥,搜索并勾选 Doubao-语音合成 +3. 如果需要使用LLM,一并勾选 Doubao-pro-32k-functioncall +4. 访问 https://console.volcengine.com/vei/aigateway/tokens-list 获取密钥 +5. 填入配置文件中 +音色列表参考:https://www.volcengine.com/docs/6561/1257544' WHERE `id` = 'TTS_VolcesAiGatewayTTS'; + +-- 更新LLM模型配置说明 +-- ChatGLM配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://bigmodel.cn/usercenter/proj-mgmt/apikeys', +`remark` = 'ChatGLM配置说明: +1. 访问 https://bigmodel.cn/usercenter/proj-mgmt/apikeys +2. 注册并获取API密钥 +3. 填入配置文件中' WHERE `id` = 'LLM_ChatGLMLLM'; + +-- Ollama配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://ollama.com/', +`remark` = 'Ollama配置说明: +1. 安装Ollama服务 +2. 运行命令:ollama pull qwen2.5 +3. 确保服务运行在http://localhost:11434' WHERE `id` = 'LLM_OllamaLLM'; + +-- 通义千问配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://bailian.console.aliyun.com/?apiKey=1#/api-key', +`remark` = '通义千问配置说明: +1. 访问 https://bailian.console.aliyun.com/?apiKey=1#/api-key +2. 获取API密钥 +3. 填入配置文件中,当前配置使用qwen-turbo模型 +4. 支持自定义参数:temperature=0.7, max_tokens=500, top_p=1, top_k=50' WHERE `id` = 'LLM_AliLLM'; + +-- 通义百炼配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://bailian.console.aliyun.com/?apiKey=1#/api-key', +`remark` = '通义百炼配置说明: +1. 访问 https://bailian.console.aliyun.com/?apiKey=1#/api-key +2. 获取app_id和api_key +3. 填入配置文件中' WHERE `id` = 'LLM_AliAppLLM'; + +-- 豆包大模型配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement', +`remark` = '豆包大模型配置说明: +1. 访问 https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement +2. 开通Doubao-1.5-pro服务 +3. 访问 https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey 获取API密钥 +4. 填入配置文件中 +5. 当前建议使用doubao-1-5-pro-32k-250115 +注意:有免费额度500000token' WHERE `id` = 'LLM_DoubaoLLM'; + +-- DeepSeek配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://platform.deepseek.com/', +`remark` = 'DeepSeek配置说明: +1. 访问 https://platform.deepseek.com/ +2. 注册并获取API密钥 +3. 填入配置文件中' WHERE `id` = 'LLM_DeepSeekLLM'; + +-- Dify配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://cloud.dify.ai/', +`remark` = 'Dify配置说明: +1. 访问 https://cloud.dify.ai/ +2. 注册并获取API密钥 +3. 填入配置文件中 +4. 支持多种对话模式:workflows/run, chat-messages, completion-messages +5. 平台设置的角色定义会失效,需要在Dify控制台设置 +注意:建议使用本地部署的Dify接口,国内部分区域访问公有云接口可能受限' WHERE `id` = 'LLM_DifyLLM'; + +-- Gemini配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://aistudio.google.com/apikey', +`remark` = 'Gemini配置说明: +1. 使用谷歌Gemini API服务 +2. 当前配置使用gemini-2.0-flash模型 +3. 需要网络连接 +4. 支持配置代理 +申请步骤: +1. 访问 https://aistudio.google.com/apikey +2. 创建API密钥 +3. 填入配置文件中 +注意:若在中国境内使用,请遵守《生成式人工智能服务管理暂行办法》' WHERE `id` = 'LLM_GeminiLLM'; + +-- Coze配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://www.coze.cn/open/oauth/pats', +`remark` = 'Coze配置说明: +1. 使用Coze平台服务 +2. 需要bot_id、user_id和个人令牌 +3. 需要网络连接 +申请步骤: +1. 访问 https://www.coze.cn/open/oauth/pats +2. 获取个人令牌 +3. 手动计算bot_id和user_id +4. 填入配置文件中' WHERE `id` = 'LLM_CozeLLM'; + +-- LM Studio配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://lmstudio.ai/', +`remark` = 'LM Studio配置说明: +1. 使用本地部署的LM Studio服务 +2. 当前配置使用deepseek-r1-distill-llama-8b@q4_k_m模型 +3. 本地推理,无需网络连接 +4. 需要预先下载模型 +部署步骤: +1. 安装LM Studio +2. 从社区下载模型 +3. 确保服务运行在http://localhost:1234/v1' WHERE `id` = 'LLM_LMStudioLLM'; + +-- FastGPT配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://cloud.tryfastgpt.ai/account/apikey', +`remark` = 'FastGPT配置说明: +1. 使用FastGPT平台服务 +2. 需要网络连接 +3. 配置文件中的prompt无效,需要在FastGPT控制台设置 +4. 支持自定义变量 +申请步骤: +1. 访问 https://cloud.tryfastgpt.ai/account/apikey +2. 获取API密钥 +3. 填入配置文件中' WHERE `id` = 'LLM_FastgptLLM'; + +-- Xinference配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/xorbitsai/inference', +`remark` = 'Xinference配置说明: +1. 使用本地部署的Xinference服务 +2. 当前配置使用qwen2.5:72b-AWQ模型 +3. 本地推理,无需网络连接 +4. 需要预先启动对应模型 +部署步骤: +1. 安装Xinference +2. 启动服务并加载模型 +3. 确保服务运行在http://localhost:9997' WHERE `id` = 'LLM_XinferenceLLM'; + +-- Xinference小模型配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/xorbitsai/inference', +`remark` = 'Xinference小模型配置说明: +1. 使用本地部署的Xinference服务 +2. 当前配置使用qwen2.5:3b-AWQ模型 +3. 本地推理,无需网络连接 +4. 用于意图识别 +部署步骤: +1. 安装Xinference +2. 启动服务并加载模型 +3. 确保服务运行在http://localhost:9997' WHERE `id` = 'LLM_XinferenceSmallLLM'; + +-- 火山引擎边缘大模型网关LLM配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://console.volcengine.com/vei/aigateway/', +`remark` = '火山引擎边缘大模型网关LLM配置说明: +1. 使用火山引擎边缘大模型网关服务 +2. 需要网关访问密钥 +3. 需要网络连接 +4. 支持function_call功能 +申请步骤: +1. 访问 https://console.volcengine.com/vei/aigateway/ +2. 创建网关访问密钥,搜索并勾选 Doubao-pro-32k-functioncall +3. 如果需要使用语音合成,一并勾选 Doubao-语音合成 +4. 访问 https://console.volcengine.com/vei/aigateway/tokens-list 获取密钥 +5. 填入配置文件中' WHERE `id` = 'LLM_VolcesAiGatewayLLM'; + +-- 更新Memory模型配置说明 +-- 无记忆配置说明 +UPDATE `ai_model_config` SET +`doc_link` = NULL, +`remark` = '无记忆配置说明: +1. 不保存对话历史 +2. 每次对话都是独立的 +3. 无需额外配置 +4. 适合对隐私要求高的场景' WHERE `id` = 'Memory_nomem'; + +-- 本地短期记忆配置说明 +UPDATE `ai_model_config` SET +`doc_link` = NULL, +`remark` = '本地短期记忆配置说明: +1. 使用本地存储保存对话历史 +2. 通过selected_module的llm总结对话内容 +3. 数据保存在本地,不会上传到服务器 +4. 适合注重隐私的场景 +5. 无需额外配置' WHERE `id` = 'Memory_mem_local_short'; + +-- Mem0AI记忆配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://app.mem0.ai/dashboard/api-keys', +`remark` = 'Mem0AI记忆配置说明: +1. 使用Mem0AI服务保存对话历史 +2. 需要API密钥 +3. 需要网络连接 +4. 每月有1000次免费调用 +申请步骤: +1. 访问 https://app.mem0.ai/dashboard/api-keys +2. 获取API密钥 +3. 填入配置文件中' WHERE `id` = 'Memory_mem0ai'; + +-- 更新Intent模型配置说明 +-- 无意图识别配置说明 +UPDATE `ai_model_config` SET +`doc_link` = NULL, +`remark` = '无意图识别配置说明: +1. 不进行意图识别 +2. 所有对话直接传递给LLM处理 +3. 无需额外配置 +4. 适合简单对话场景' WHERE `id` = 'Intent_nointent'; + +-- LLM意图识别配置说明 +UPDATE `ai_model_config` SET +`doc_link` = NULL, +`remark` = 'LLM意图识别配置说明: +1. 使用独立的LLM进行意图识别 +2. 默认使用selected_module.LLM的模型 +3. 可以配置使用独立的LLM(如免费的ChatGLMLLM) +4. 通用性强,但会增加处理时间 +5. 不支持控制音量大小等iot操作 +配置说明: +1. 在llm字段中指定使用的LLM模型 +2. 如果不指定,则使用selected_module.LLM的模型' WHERE `id` = 'Intent_intent_llm'; + +-- 函数调用意图识别配置说明 +UPDATE `ai_model_config` SET +`doc_link` = NULL, +`remark` = '函数调用意图识别配置说明: +1. 使用LLM的function_call功能进行意图识别 +2. 需要所选择的LLM支持function_call +3. 按需调用工具,处理速度快 +4. 支持所有iot指令 +5. 默认已加载以下功能: + - handle_exit_intent(退出识别) + - play_music(音乐播放) + - change_role(角色切换) + - get_weather(天气查询) + - get_news(新闻查询) +配置说明: +1. 在functions字段中配置需要加载的功能模块 +2. 系统默认已加载基础功能,无需重复配置 +3. 可以添加自定义功能模块' WHERE `id` = 'Intent_function_call'; + +-- 更新VAD模型配置说明 +-- SileroVAD配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/snakers4/silero-vad', +`remark` = 'SileroVAD配置说明: +1. 使用SileroVAD模型进行语音活动检测 +2. 本地推理,无需网络连接 +3. 需要下载模型文件到models/snakers4_silero-vad目录 +4. 可配置参数: + - threshold: 0.5(语音检测阈值) + - min_silence_duration_ms: 700(最小静音持续时间,单位毫秒) +5. 如果说话停顿比较长,可以适当增加min_silence_duration_ms的值' WHERE `id` = 'VAD_SileroVAD'; diff --git a/main/manager-api/src/main/resources/db/changelog/202504301341.sql b/main/manager-api/src/main/resources/db/changelog/202504301341.sql new file mode 100644 index 00000000..a88ee783 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202504301341.sql @@ -0,0 +1,85 @@ +update `ai_model_provider` set `fields` = +'[{"key": "api_url","label": "API地址","type": "string"},{"key": "voice","label": "音色","type": "string"},{"key": "output_dir","label": "输出目录","type": "string"},{"key": "authorization","label": "授权","type": "string"},{"key": "appid","label": "应用ID","type": "string"},{"key": "access_token","label": "访问令牌","type": "string"},{"key": "cluster","label": "集群","type": "string"},{"key": "speed_ratio","label": "语速","type": "number"},{"key": "volume_ratio","label": "音量","type": "number"},{"key": "pitch_ratio","label": "音高","type": "number"}]' +where `id` = 'SYSTEM_TTS_doubao'; + +-- 添加阿里云ASR供应器 +delete from `ai_model_provider` where `id` = 'SYSTEM_ASR_AliyunASR'; +INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES +('SYSTEM_ASR_AliyunASR', 'ASR', 'aliyun', '阿里云语音识别', '[{"key":"appkey","label":"应用AppKey","type":"string"},{"key":"token","label":"临时Token","type":"string"},{"key":"access_key_id","label":"AccessKey ID","type":"string"},{"key":"access_key_secret","label":"AccessKey Secret","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 5, 1, NOW(), 1, NOW()); + +-- 添加阿里云ASR模型配置 +delete from `ai_model_config` where `id` = 'ASR_AliyunASR'; +INSERT INTO `ai_model_config` VALUES ('ASR_AliyunASR', 'ASR', 'AliyunASR', '阿里云语音识别', 0, 1, '{\"type\": \"aliyun\", \"appkey\": \"\", \"token\": \"\", \"access_key_id\": \"\", \"access_key_secret\": \"\", \"output_dir\": \"tmp/\"}', NULL, NULL, 6, NULL, NULL, NULL, NULL); + +-- 更新阿里云ASR模型配置的说明文档 +UPDATE `ai_model_config` SET +`doc_link` = 'https://nls-portal.console.aliyun.com/', +`remark` = '阿里云ASR配置说明: +1. 访问 https://nls-portal.console.aliyun.com/ 开通服务 +2. 访问 https://nls-portal.console.aliyun.com/applist 获取appkey +3. 访问 https://nls-portal.console.aliyun.com/overview 获取token +4. 获取access_key_id和access_key_secret +5. 填入配置文件中' WHERE `id` = 'ASR_AliyunASR'; + +-- 插入固件类型字典类型 +delete from `sys_dict_type` where `id` = 101; +INSERT INTO `sys_dict_type` (`id`, `dict_type`, `dict_name`, `remark`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES +(101, 'FIRMWARE_TYPE', '固件类型', '固件类型字典', 0, 1, NOW(), 1, NOW()); + +-- 插入固件类型字典数据 +delete from `sys_dict_data` where `dict_type_id` = 101; +INSERT INTO `sys_dict_data` (`id`, `dict_type_id`, `dict_label`, `dict_value`, `remark`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES +(101001, 101, '面包板新版接线(WiFi)', 'bread-compact-wifi', '面包板新版接线(WiFi)', 1, 1, NOW(), 1, NOW()), +(101002, 101, '面包板新版接线(WiFi)+ LCD', 'bread-compact-wifi-lcd', '面包板新版接线(WiFi)+ LCD', 2, 1, NOW(), 1, NOW()), +(101003, 101, '面包板新版接线(ML307 AT)', 'bread-compact-ml307', '面包板新版接线(ML307 AT)', 3, 1, NOW(), 1, NOW()), +(101004, 101, '面包板(WiFi) ESP32 DevKit', 'bread-compact-esp32', '面包板(WiFi) ESP32 DevKit', 4, 1, NOW(), 1, NOW()), +(101005, 101, '面包板(WiFi+ LCD) ESP32 DevKit', 'bread-compact-esp32-lcd', '面包板(WiFi+ LCD) ESP32 DevKit', 5, 1, NOW(), 1, NOW()), +(101006, 101, 'DFRobot 行空板 k10', 'df-k10', 'DFRobot 行空板 k10', 6, 1, NOW(), 1, NOW()), +(101007, 101, 'ESP32 CGC', 'esp32-cgc', 'ESP32 CGC', 7, 1, NOW(), 1, NOW()), +(101008, 101, 'ESP BOX 3', 'esp-box-3', 'ESP BOX 3', 8, 1, NOW(), 1, NOW()), +(101009, 101, 'ESP BOX', 'esp-box', 'ESP BOX', 9, 1, NOW(), 1, NOW()), +(101010, 101, 'ESP BOX Lite', 'esp-box-lite', 'ESP BOX Lite', 10, 1, NOW(), 1, NOW()), +(101011, 101, 'Kevin Box 1', 'kevin-box-1', 'Kevin Box 1', 11, 1, NOW(), 1, NOW()), +(101012, 101, 'Kevin Box 2', 'kevin-box-2', 'Kevin Box 2', 12, 1, NOW(), 1, NOW()), +(101013, 101, 'Kevin C3', 'kevin-c3', 'Kevin C3', 13, 1, NOW(), 1, NOW()), +(101014, 101, 'Kevin SP V3开发板', 'kevin-sp-v3-dev', 'Kevin SP V3开发板', 14, 1, NOW(), 1, NOW()), +(101015, 101, 'Kevin SP V4开发板', 'kevin-sp-v4-dev', 'Kevin SP V4开发板', 15, 1, NOW(), 1, NOW()), +(101016, 101, '鱼鹰科技3.13LCD开发板', 'kevin-yuying-313lcd', '鱼鹰科技3.13LCD开发板', 16, 1, NOW(), 1, NOW()), +(101017, 101, '立创·实战派ESP32-S3开发板', 'lichuang-dev', '立创·实战派ESP32-S3开发板', 17, 1, NOW(), 1, NOW()), +(101018, 101, '立创·实战派ESP32-C3开发板', 'lichuang-c3-dev', '立创·实战派ESP32-C3开发板', 18, 1, NOW(), 1, NOW()), +(101019, 101, '神奇按钮 Magiclick_2.4', 'magiclick-2p4', '神奇按钮 Magiclick_2.4', 19, 1, NOW(), 1, NOW()), +(101020, 101, '神奇按钮 Magiclick_2.5', 'magiclick-2p5', '神奇按钮 Magiclick_2.5', 20, 1, NOW(), 1, NOW()), +(101021, 101, '神奇按钮 Magiclick_C3', 'magiclick-c3', '神奇按钮 Magiclick_C3', 21, 1, NOW(), 1, NOW()), +(101022, 101, '神奇按钮 Magiclick_C3_v2', 'magiclick-c3-v2', '神奇按钮 Magiclick_C3_v2', 22, 1, NOW(), 1, NOW()), +(101023, 101, 'M5Stack CoreS3', 'm5stack-core-s3', 'M5Stack CoreS3', 23, 1, NOW(), 1, NOW()), +(101024, 101, 'AtomS3 + Echo Base', 'atoms3-echo-base', 'AtomS3 + Echo Base', 24, 1, NOW(), 1, NOW()), +(101025, 101, 'AtomS3R + Echo Base', 'atoms3r-echo-base', 'AtomS3R + Echo Base', 25, 1, NOW(), 1, NOW()), +(101026, 101, 'AtomS3R CAM/M12 + Echo Base', 'atoms3r-cam-m12-echo-base', 'AtomS3R CAM/M12 + Echo Base', 26, 1, NOW(), 1, NOW()), +(101027, 101, 'AtomMatrix + Echo Base', 'atommatrix-echo-base', 'AtomMatrix + Echo Base', 27, 1, NOW(), 1, NOW()), +(101028, 101, '虾哥 Mini C3', 'xmini-c3', '虾哥 Mini C3', 28, 1, NOW(), 1, NOW()), +(101029, 101, 'ESP32S3_KORVO2_V3开发板', 'esp32s3-korvo2-v3', 'ESP32S3_KORVO2_V3开发板', 29, 1, NOW(), 1, NOW()), +(101030, 101, 'ESP-SparkBot开发板', 'esp-sparkbot', 'ESP-SparkBot开发板', 30, 1, NOW(), 1, NOW()), +(101031, 101, 'ESP-Spot-S3', 'esp-spot-s3', 'ESP-Spot-S3', 31, 1, NOW(), 1, NOW()), +(101032, 101, 'Waveshare ESP32-S3-Touch-AMOLED-1.8', 'esp32-s3-touch-amoled-1.8', 'Waveshare ESP32-S3-Touch-AMOLED-1.8', 32, 1, NOW(), 1, NOW()), +(101033, 101, 'Waveshare ESP32-S3-Touch-LCD-1.85C', 'esp32-s3-touch-lcd-1.85c', 'Waveshare ESP32-S3-Touch-LCD-1.85C', 33, 1, NOW(), 1, NOW()), +(101034, 101, 'Waveshare ESP32-S3-Touch-LCD-1.85', 'esp32-s3-touch-lcd-1.85', 'Waveshare ESP32-S3-Touch-LCD-1.85', 34, 1, NOW(), 1, NOW()), +(101035, 101, 'Waveshare ESP32-S3-Touch-LCD-1.46', 'esp32-s3-touch-lcd-1.46', 'Waveshare ESP32-S3-Touch-LCD-1.46', 35, 1, NOW(), 1, NOW()), +(101036, 101, 'Waveshare ESP32-S3-Touch-LCD-3.5', 'esp32-s3-touch-lcd-3.5', 'Waveshare ESP32-S3-Touch-LCD-3.5', 36, 1, NOW(), 1, NOW()), +(101037, 101, '土豆子', 'tudouzi', '土豆子', 37, 1, NOW(), 1, NOW()), +(101038, 101, 'LILYGO T-Circle-S3', 'lilygo-t-circle-s3', 'LILYGO T-Circle-S3', 38, 1, NOW(), 1, NOW()), +(101039, 101, 'LILYGO T-CameraPlus-S3', 'lilygo-t-cameraplus-s3', 'LILYGO T-CameraPlus-S3', 39, 1, NOW(), 1, NOW()), +(101040, 101, 'Movecall Moji 小智AI衍生版', 'movecall-moji-esp32s3', 'Movecall Moji 小智AI衍生版', 40, 1, NOW(), 1, NOW()), +(101041, 101, 'Movecall CuiCan 璀璨·AI吊坠', 'movecall-cuican-esp32s3', 'Movecall CuiCan 璀璨·AI吊坠', 41, 1, NOW(), 1, NOW()), +(101042, 101, '正点原子DNESP32S3开发板', 'atk-dnesp32s3', '正点原子DNESP32S3开发板', 42, 1, NOW(), 1, NOW()), +(101043, 101, '正点原子DNESP32S3-BOX', 'atk-dnesp32s3-box', '正点原子DNESP32S3-BOX', 43, 1, NOW(), 1, NOW()), +(101044, 101, '嘟嘟开发板CHATX(wifi)', 'du-chatx', '嘟嘟开发板CHATX(wifi)', 44, 1, NOW(), 1, NOW()), +(101045, 101, '太极小派esp32s3', 'taiji-pi-s3', '太极小派esp32s3', 45, 1, NOW(), 1, NOW()), +(101046, 101, '无名科技星智0.85(WIFI)', 'xingzhi-cube-0.85tft-wifi', '无名科技星智0.85(WIFI)', 46, 1, NOW(), 1, NOW()), +(101047, 101, '无名科技星智0.85(ML307)', 'xingzhi-cube-0.85tft-ml307', '无名科技星智0.85(ML307)', 47, 1, NOW(), 1, NOW()), +(101048, 101, '无名科技星智0.96(WIFI)', 'xingzhi-cube-0.96oled-wifi', '无名科技星智0.96(WIFI)', 48, 1, NOW(), 1, NOW()), +(101049, 101, '无名科技星智0.96(ML307)', 'xingzhi-cube-0.96oled-ml307', '无名科技星智0.96(ML307)', 49, 1, NOW(), 1, NOW()), +(101050, 101, '无名科技星智1.54(WIFI)', 'xingzhi-cube-1.54tft-wifi', '无名科技星智1.54(WIFI)', 50, 1, NOW(), 1, NOW()), +(101051, 101, '无名科技星智1.54(ML307)', 'xingzhi-cube-1.54tft-ml307', '无名科技星智1.54(ML307)', 51, 1, NOW(), 1, NOW()), +(101052, 101, 'SenseCAP Watcher', 'sensecap-watcher', 'SenseCAP Watcher', 52, 1, NOW(), 1, NOW()), +(101053, 101, '四博智联AI陪伴盒子', 'doit-s3-aibox', '四博智联AI陪伴盒子', 53, 1, NOW(), 1, NOW()), +(101054, 101, '元控·青春', 'mixgo-nova', '元控·青春', 54, 1, NOW(), 1, NOW()); diff --git a/main/manager-api/src/main/resources/db/changelog/202505022134.sql b/main/manager-api/src/main/resources/db/changelog/202505022134.sql new file mode 100644 index 00000000..ccbaaaee --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202505022134.sql @@ -0,0 +1,27 @@ +-- 初始化智能体聊天记录 +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 +( + id BIGINT AUTO_INCREMENT COMMENT '主键ID' PRIMARY KEY, + mac_address VARCHAR(50) COMMENT 'MAC地址', + agent_id VARCHAR(32) COMMENT '智能体id', + session_id VARCHAR(50) COMMENT '会话ID', + chat_type TINYINT(3) COMMENT '消息类型: 1-用户, 2-智能体', + content VARCHAR(1024) COMMENT '聊天内容', + audio_id VARCHAR(32) COMMENT '音频ID', + 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_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; +CREATE TABLE ai_agent_chat_audio +( + id VARCHAR(32) COMMENT '主键ID' PRIMARY KEY, + audio LONGBLOB COMMENT '音频opus数据' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '智能体聊天音频数据表'; \ No newline at end of file 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 1bbddb75..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 @@ -78,4 +78,25 @@ databaseChangeLog: changes: - sqlFile: encoding: utf8 - path: classpath:db/changelog/202504251422.sql \ No newline at end of file + path: classpath:db/changelog/202504251422.sql + - changeSet: + id: 202504291043 + author: jiangkunyin + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202504291043.sql + - changeSet: + id: 202504301341 + author: Goody + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202504301341.sql + - changeSet: + id: 202505022134 + author: Goody + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202505022134.sql \ No newline at end of file diff --git a/main/manager-api/src/main/resources/mapper/agent/AiAgentChatHistoryDao.xml b/main/manager-api/src/main/resources/mapper/agent/AiAgentChatHistoryDao.xml new file mode 100644 index 00000000..24a669c1 --- /dev/null +++ b/main/manager-api/src/main/resources/mapper/agent/AiAgentChatHistoryDao.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + id, mac_address, agent_id, session_id, sort, chat_type, content, audio, audio_url, + created_at, updated_at + + + + DELETE FROM ai_agent_chat_audio + WHERE id IN ( + SELECT audio_id + FROM ai_agent_chat_history + WHERE agent_id = #{agentId} + ); + + + + DELETE FROM ai_agent_chat_history + WHERE agent_id = #{agentId}; + + diff --git a/main/manager-api/src/main/resources/mapper/sys/SysDictDataDao.xml b/main/manager-api/src/main/resources/mapper/sys/SysDictDataDao.xml index 7ea5e429..39848831 100644 --- a/main/manager-api/src/main/resources/mapper/sys/SysDictDataDao.xml +++ b/main/manager-api/src/main/resources/mapper/sys/SysDictDataDao.xml @@ -2,12 +2,17 @@ - - + SELECT d.dict_label AS `name`, d.dict_value AS `key` + FROM sys_dict_data d + LEFT JOIN sys_dict_type t ON d.dict_type_id = t.id + WHERE t.dict_type = #{dictType} + ORDER BY d.sort ASC - + SELECT dict_type + FROM sys_dict_type + WHERE id = #{dictTypeId} \ No newline at end of file diff --git a/main/manager-api/src/main/resources/mapper/sys/SysDictTypeDao.xml b/main/manager-api/src/main/resources/mapper/sys/SysDictTypeDao.xml index 11887552..825db02b 100644 --- a/main/manager-api/src/main/resources/mapper/sys/SysDictTypeDao.xml +++ b/main/manager-api/src/main/resources/mapper/sys/SysDictTypeDao.xml @@ -3,8 +3,4 @@ - - \ No newline at end of file diff --git a/main/manager-web/src/apis/api.js b/main/manager-web/src/apis/api.js index 1b9f0a9e..0a3e8d18 100755 --- a/main/manager-web/src/apis/api.js +++ b/main/manager-web/src/apis/api.js @@ -2,11 +2,11 @@ import admin from './module/admin.js' import agent from './module/agent.js' import device from './module/device.js' +import dict from './module/dict.js' import model from './module/model.js' +import ota from './module/ota.js' import timbre from "./module/timbre.js" import user from './module/user.js' -import ota from './module/ota.js' - /** * 接口地址 * 开发时自动读取使用.env.development文件 @@ -32,5 +32,6 @@ export default { device, model, timbre, - ota + ota, + dict } diff --git a/main/manager-web/src/apis/module/agent.js b/main/manager-web/src/apis/module/agent.js index 87323f9c..8aa89072 100644 --- a/main/manager-web/src/apis/module/agent.js +++ b/main/manager-web/src/apis/module/agent.js @@ -97,4 +97,50 @@ export default { }); }).send(); }, + // 获取智能体会话列表 + getAgentSessions(agentId, params, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/agent/${agentId}/sessions`) + .method('GET') + .data(params) + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .fail(() => { + RequestService.reAjaxFun(() => { + this.getAgentSessions(agentId, params, callback); + }); + }).send(); + }, + // 获取智能体聊天记录 + getAgentChatHistory(agentId, sessionId, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/agent/${agentId}/chat-history/${sessionId}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .fail(() => { + RequestService.reAjaxFun(() => { + this.getAgentChatHistory(agentId, sessionId, callback); + }); + }).send(); + }, + // 获取音频下载ID + getAudioId(audioId, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/agent/audio/${audioId}`) + .method('POST') + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .fail(() => { + RequestService.reAjaxFun(() => { + this.getAudioId(audioId, callback); + }); + }).send(); + }, } diff --git a/main/manager-web/src/apis/module/dict.js b/main/manager-web/src/apis/module/dict.js new file mode 100644 index 00000000..caf78279 --- /dev/null +++ b/main/manager-web/src/apis/module/dict.js @@ -0,0 +1,227 @@ +import { getServiceUrl } from '../api'; +import RequestService from '../httpRequest'; + +export default { + // 获取字典类型列表 + getDictTypeList(params, callback) { + const queryParams = new URLSearchParams({ + dictType: params.dictType || '', + dictName: params.dictName || '', + page: params.page || 1, + limit: params.limit || 10 + }).toString(); + + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/type/page?${queryParams}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('获取字典类型列表失败:', err) + this.$message.error(err.msg || '获取字典类型列表失败') + RequestService.reAjaxFun(() => { + this.getDictTypeList(params, callback) + }) + }).send() + }, + + // 获取字典类型详情 + getDictTypeDetail(id, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/type/${id}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('获取字典类型详情失败:', err) + this.$message.error(err.msg || '获取字典类型详情失败') + RequestService.reAjaxFun(() => { + this.getDictTypeDetail(id, callback) + }) + }).send() + }, + + // 新增字典类型 + addDictType(data, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/type/save`) + .method('POST') + .data(data) + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('新增字典类型失败:', err) + this.$message.error(err.msg || '新增字典类型失败') + RequestService.reAjaxFun(() => { + this.addDictType(data, callback) + }) + }).send() + }, + + // 更新字典类型 + updateDictType(data, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/type/update`) + .method('PUT') + .data(data) + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('更新字典类型失败:', err) + this.$message.error(err.msg || '更新字典类型失败') + RequestService.reAjaxFun(() => { + this.updateDictType(data, callback) + }) + }).send() + }, + + // 删除字典类型 + deleteDictType(ids, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/type/delete`) + .method('POST') + .data(ids) + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('删除字典类型失败:', err) + this.$message.error(err.msg || '删除字典类型失败') + RequestService.reAjaxFun(() => { + this.deleteDictType(ids, callback) + }) + }).send() + }, + + // 获取字典数据列表 + getDictDataList(params, callback) { + const queryParams = new URLSearchParams({ + dictTypeId: params.dictTypeId, + dictLabel: params.dictLabel || '', + dictValue: params.dictValue || '', + page: params.page || 1, + limit: params.limit || 10 + }).toString(); + + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/data/page?${queryParams}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('获取字典数据列表失败:', err) + this.$message.error(err.msg || '获取字典数据列表失败') + RequestService.reAjaxFun(() => { + this.getDictDataList(params, callback) + }) + }).send() + }, + + // 获取字典数据详情 + getDictDataDetail(id, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/data/${id}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('获取字典数据详情失败:', err) + this.$message.error(err.msg || '获取字典数据详情失败') + RequestService.reAjaxFun(() => { + this.getDictDataDetail(id, callback) + }) + }).send() + }, + + // 新增字典数据 + addDictData(data, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/data/save`) + .method('POST') + .data(data) + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('新增字典数据失败:', err) + this.$message.error(err.msg || '新增字典数据失败') + RequestService.reAjaxFun(() => { + this.addDictData(data, callback) + }) + }).send() + }, + + // 更新字典数据 + updateDictData(data, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/data/update`) + .method('PUT') + .data(data) + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('更新字典数据失败:', err) + this.$message.error(err.msg || '更新字典数据失败') + RequestService.reAjaxFun(() => { + this.updateDictData(data, callback) + }) + }).send() + }, + + // 删除字典数据 + deleteDictData(ids, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/data/delete`) + .method('POST') + .data(ids) + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('删除字典数据失败:', err) + this.$message.error(err.msg || '删除字典数据失败') + RequestService.reAjaxFun(() => { + this.deleteDictData(ids, callback) + }) + }).send() + }, + + // 获取字典数据列表 + getDictDataByType(dictType) { + return new Promise((resolve, reject) => { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/data/type/${dictType}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime() + if (res.data && res.data.code === 0) { + resolve(res.data) + } else { + reject(new Error(res.data?.msg || '获取字典数据列表失败')) + } + }) + .fail((err) => { + console.error('获取字典数据列表失败:', err) + reject(err) + }).send() + }) + } + +} \ No newline at end of file diff --git a/main/manager-web/src/assets/user-avatar1.png b/main/manager-web/src/assets/user-avatar1.png new file mode 100644 index 00000000..d835d2f7 Binary files /dev/null and b/main/manager-web/src/assets/user-avatar1.png differ diff --git a/main/manager-web/src/assets/user-avatar2.png b/main/manager-web/src/assets/user-avatar2.png new file mode 100644 index 00000000..71d5fd1b Binary files /dev/null and b/main/manager-web/src/assets/user-avatar2.png differ diff --git a/main/manager-web/src/assets/user-avatar3.png b/main/manager-web/src/assets/user-avatar3.png new file mode 100644 index 00000000..159e6886 Binary files /dev/null and b/main/manager-web/src/assets/user-avatar3.png differ diff --git a/main/manager-web/src/assets/user-avatar4.png b/main/manager-web/src/assets/user-avatar4.png new file mode 100644 index 00000000..0d078144 Binary files /dev/null and b/main/manager-web/src/assets/user-avatar4.png differ diff --git a/main/manager-web/src/assets/user-avatar5.png b/main/manager-web/src/assets/user-avatar5.png new file mode 100644 index 00000000..c9960475 Binary files /dev/null and b/main/manager-web/src/assets/user-avatar5.png differ diff --git a/main/manager-web/src/components/ChatHistoryDialog.vue b/main/manager-web/src/components/ChatHistoryDialog.vue new file mode 100644 index 00000000..33350208 --- /dev/null +++ b/main/manager-web/src/components/ChatHistoryDialog.vue @@ -0,0 +1,447 @@ + + + + + + + \ No newline at end of file diff --git a/main/manager-web/src/components/DeviceItem.vue b/main/manager-web/src/components/DeviceItem.vue index 2ac0bbac..fa81a04d 100644 --- a/main/manager-web/src/components/DeviceItem.vue +++ b/main/manager-web/src/components/DeviceItem.vue @@ -26,6 +26,9 @@
设备管理({{ device.deviceCount }})
+
+ 聊天记录 +
最近对话:{{ device.lastConnectedAt }}
@@ -51,6 +54,9 @@ export default { }, handleDeviceManage() { this.$router.push({ path: '/device-management', query: { agentId: this.device.agentId } }); + }, + handleChatHistory() { + this.$emit('chat-history', { agentId: this.device.agentId, agentName: this.device.agentName }) } } } diff --git a/main/manager-web/src/components/DictDataDialog.vue b/main/manager-web/src/components/DictDataDialog.vue new file mode 100644 index 00000000..7934602b --- /dev/null +++ b/main/manager-web/src/components/DictDataDialog.vue @@ -0,0 +1,105 @@ + + + + + \ No newline at end of file diff --git a/main/manager-web/src/components/DictTypeDialog.vue b/main/manager-web/src/components/DictTypeDialog.vue new file mode 100644 index 00000000..a9a7aa07 --- /dev/null +++ b/main/manager-web/src/components/DictTypeDialog.vue @@ -0,0 +1,86 @@ + + + + + \ No newline at end of file diff --git a/main/manager-web/src/components/FirmwareDialog.vue b/main/manager-web/src/components/FirmwareDialog.vue index fab99a57..940fbbeb 100644 --- a/main/manager-web/src/components/FirmwareDialog.vue +++ b/main/manager-web/src/components/FirmwareDialog.vue @@ -38,7 +38,6 @@ + + \ No newline at end of file diff --git a/main/manager-web/src/views/OtaManagement.vue b/main/manager-web/src/views/OtaManagement.vue index 106e12fb..300e83ad 100644 --- a/main/manager-web/src/views/OtaManagement.vue +++ b/main/manager-web/src/views/OtaManagement.vue @@ -96,8 +96,8 @@
- + @@ -109,7 +109,6 @@ import Api from "@/apis/api"; import FirmwareDialog from "@/components/FirmwareDialog.vue"; import HeaderBar from "@/components/HeaderBar.vue"; import VersionFooter from "@/components/VersionFooter.vue"; -import { FIRMWARE_TYPES } from "@/utils"; import { formatDate, formatFileSize } from "@/utils/format"; export default { @@ -136,10 +135,12 @@ export default { remark: "", firmwarePath: "" }, + firmwareTypes: [], }; }, created() { this.fetchFirmwareList(); + this.getFirmwareTypes(); }, computed: { @@ -387,9 +388,18 @@ export default { }, formatDate, formatFileSize, + async getFirmwareTypes() { + try { + const res = await Api.dict.getDictDataByType('FIRMWARE_TYPE') + this.firmwareTypes = res.data + } catch (error) { + console.error('获取固件类型失败:', error) + this.$message.error(error.message || '获取固件类型失败') + } + }, getFirmwareTypeName(type) { - const firmwareType = FIRMWARE_TYPES.find(item => item.key === type); - return firmwareType ? firmwareType.name : type; + const firmwareType = this.firmwareTypes.find(item => item.key === type) + return firmwareType ? firmwareType.name : type }, }, }; diff --git a/main/manager-web/src/views/ParamsManagement.vue b/main/manager-web/src/views/ParamsManagement.vue index 3301bbb5..afb0dc1d 100644 --- a/main/manager-web/src/views/ParamsManagement.vue +++ b/main/manager-web/src/views/ParamsManagement.vue @@ -25,8 +25,19 @@ - + + +