diff --git a/main/manager-api/src/main/java/xiaozhi/common/config/SwaggerConfig.java b/main/manager-api/src/main/java/xiaozhi/common/config/SwaggerConfig.java index 8fb31641..25d1ad45 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/config/SwaggerConfig.java +++ b/main/manager-api/src/main/java/xiaozhi/common/config/SwaggerConfig.java @@ -43,7 +43,7 @@ public class SwaggerConfig { public GroupedOpenApi oatApi() { return GroupedOpenApi.builder() .group("ota") - .pathsToMatch("/ota/**") + .pathsToMatch("/ota/**", "/otaMag/**") .build(); } diff --git a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java index 172505a3..3f914e4c 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java +++ b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java @@ -243,5 +243,6 @@ public interface ErrorCode { // 设备工具相关错误码 int DEVICE_ID_NOT_NULL = 10193; // 设备ID不能为空 int DEVICE_NOT_EXIST = 10194; // 设备不存在 + int OTA_UPLOAD_COUNT_EXCEED = 10195; // OTA上传次数超过限制 } 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 527b9330..6b143930 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 @@ -170,10 +170,21 @@ public class RedisKeys { /** * OTA绑定设备 */ - public static String getOtaActivationCode(String activationCode) {return "ota:activation:code:" + activationCode;} + public static String getOtaActivationCode(String activationCode) { + return "ota:activation:code:" + activationCode; + } /** * OTA获取设备mac相关信息 */ - public static String getOtaDeviceActivationInfo(String deviceId) {return "ota:activation:data:" + deviceId;} + public static String getOtaDeviceActivationInfo(String deviceId) { + return "ota:activation:data:" + deviceId; + } + + /** + * OTA上传次数 + */ + public static String getOtaUploadCountKey(Long username) { + return "ota:upload:count:" + username; + } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java b/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java index ff593528..629ba80f 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java @@ -24,6 +24,7 @@ import xiaozhi.common.user.UserDetail; import xiaozhi.common.utils.Result; import xiaozhi.modules.device.dto.DeviceManualAddDTO; import xiaozhi.modules.device.dto.DeviceRegisterDTO; +import xiaozhi.modules.device.dto.DeviceToolsCallReqDTO; import xiaozhi.modules.device.dto.DeviceUnBindDTO; import xiaozhi.modules.device.dto.DeviceUpdateDTO; import xiaozhi.modules.device.entity.DeviceEntity; @@ -129,15 +130,10 @@ public class DeviceController { return new Result<>(); } - @PostMapping("/tools/list") + @PostMapping("/tools/list/{deviceId}") @Operation(summary = "获取设备工具列表") @RequiresPermissions("sys:role:normal") - public Result getDeviceTools(@RequestBody Map requestBody) { - String deviceId = requestBody.get("deviceId"); - if (StringUtils.isBlank(deviceId)) { - return new Result().error(ErrorCode.DEVICE_ID_NOT_NULL); - } - + public Result getDeviceTools(@PathVariable String deviceId) { Object toolsData = deviceService.getDeviceTools(deviceId); if (toolsData == null) { return new Result().error(ErrorCode.DEVICE_NOT_EXIST); @@ -145,4 +141,22 @@ public class DeviceController { return new Result().ok(toolsData); } + + @PostMapping("/tools/call/{deviceId}") + @Operation(summary = "调用设备工具") + @RequiresPermissions("sys:role:normal") + public Result callDeviceTool(@PathVariable String deviceId, + @Valid @RequestBody DeviceToolsCallReqDTO request) { + String toolName = request.getName(); + Map arguments = request.getArguments(); + + Object result = deviceService.callDeviceTool(deviceId, toolName, arguments); + if (result == null) { + return new Result().error(ErrorCode.DEVICE_NOT_EXIST); + } + + Result response = new Result(); + response.setMsg("Tools called successfully"); + return response.ok(result); + } } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/controller/OTAMagController.java b/main/manager-api/src/main/java/xiaozhi/modules/device/controller/OTAMagController.java index 128ba195..cfdf3913 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/controller/OTAMagController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/controller/OTAMagController.java @@ -38,6 +38,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import xiaozhi.common.constant.Constant; +import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.page.PageData; import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; @@ -45,8 +46,11 @@ import xiaozhi.common.utils.Result; import xiaozhi.common.validator.ValidatorUtils; import xiaozhi.modules.device.entity.OtaEntity; import xiaozhi.modules.device.service.OtaService; +import xiaozhi.modules.security.user.SecurityUser; +import xiaozhi.modules.sys.enums.SuperAdminEnum; +import xiaozhi.modules.sys.service.SysParamsService; -@Tag(name = "设备管理", description = "OTA 相关接口") +@Tag(name = "固件升级管理", description = "OTA 相关接口") @Slf4j @RestController @RequiredArgsConstructor @@ -55,6 +59,7 @@ public class OTAMagController { private static final Logger logger = LoggerFactory.getLogger(OTAController.class); private final OtaService otaService; private final RedisUtils redisUtils; + private final SysParamsService sysParamsService; @GetMapping @Operation(summary = "分页查询 OTA 固件信息") @@ -160,7 +165,17 @@ public class OTAMagController { try { // 获取固件信息 - OtaEntity otaEntity = otaService.selectById(id); + OtaEntity otaEntity = null; + if (id.indexOf("file:") == 0) { + id = id.substring(5); + otaEntity = new OtaEntity(); + otaEntity.setFirmwarePath(id); + otaEntity.setType("assets"); + otaEntity.setVersion("1.0.0"); + } else { + otaEntity = otaService.selectById(id); + } + if (otaEntity == null || StringUtils.isBlank(otaEntity.getFirmwarePath())) { logger.warn("Firmware not found or path is empty for ID: {}", id); return ResponseEntity.notFound().build(); @@ -168,6 +183,7 @@ public class OTAMagController { // 获取文件路径 - 确保路径是绝对路径或正确的相对路径 String firmwarePath = otaEntity.getFirmwarePath(); + String originalFilename = otaEntity.getType() + "_" + otaEntity.getVersion(); Path path; // 检查是否是绝对路径 @@ -201,7 +217,7 @@ public class OTAMagController { byte[] fileContent = Files.readAllBytes(path); // 设置响应头 - String originalFilename = otaEntity.getType() + "_" + otaEntity.getVersion(); + if (firmwarePath.contains(".")) { String extension = firmwarePath.substring(firmwarePath.lastIndexOf(".")); originalFilename += extension; @@ -277,6 +293,42 @@ public class OTAMagController { } } + @PostMapping("/uploadAssetsBin") + @Operation(summary = "上传资源固件文件") + @RequiresPermissions("sys:role:normal") + public Result uploadAssetsBin(@RequestParam("file") MultipartFile file) { + String otaUrl = sysParamsService.getValue(Constant.SERVER_OTA, true); + if (StringUtils.isBlank(otaUrl) || otaUrl.equals("null")) { + return new Result().error(ErrorCode.OTA_URL_EMPTY); + } + logger.info("username:{},uploadAssetsBin size: {}", SecurityUser.getUser().getUsername(), file.getSize()); + // 验证文件大小 (资源固件最大20MB) + if (file.getSize() > 20 * 1024 * 1024) { + return new Result().error(ErrorCode.VOICE_CLONE_AUDIO_TOO_LARGE); + } + // 普通用户只能每天上传50次 + if (SecurityUser.getUser().getSuperAdmin() == SuperAdminEnum.NO.value()) { + String uploadCountKey = RedisKeys.getOtaUploadCountKey(SecurityUser.getUser().getId()); + Integer uploadCount = (Integer) Optional.ofNullable(redisUtils.get(uploadCountKey)).orElse(0); + if (uploadCount >= 50) { + return new Result().error(ErrorCode.OTA_UPLOAD_COUNT_EXCEED); + } + // 增加上传次数 + redisUtils.increment(RedisKeys.getOtaUploadCountKey(SecurityUser.getUser().getId()), + RedisUtils.DEFAULT_EXPIRE); + } + Result result = uploadFirmware(file); + + // 生成资源文件路径 + if (StringUtils.isNotBlank(result.getData())) { + String uuid = UUID.randomUUID().toString(); + redisUtils.set(RedisKeys.getOtaIdKey(uuid), "file:" + result.getData()); + String downloadUrl = otaUrl.replace("/ota/", "/otaMag/download/") + uuid; + result.setData(downloadUrl); + } + return result; + } + private String calculateMD5(MultipartFile file) throws IOException, NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(file.getBytes()); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceToolsCallReqDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceToolsCallReqDTO.java new file mode 100644 index 00000000..777bee03 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceToolsCallReqDTO.java @@ -0,0 +1,15 @@ +package xiaozhi.modules.device.dto; + +import java.util.Map; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class DeviceToolsCallReqDTO { + + @NotBlank(message = "工具名称不能为空") + private String name; + + private Map arguments; +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java index b34a0c4a..ecbf719c 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java @@ -2,6 +2,7 @@ package xiaozhi.modules.device.service; import java.util.Date; import java.util.List; +import java.util.Map; import xiaozhi.common.page.PageData; import xiaozhi.common.service.BaseService; @@ -127,4 +128,9 @@ public interface DeviceService extends BaseService { */ Object getDeviceTools(String deviceId); + /** + * 调用设备工具 + */ + Object callDeviceTool(String deviceId, String toolName, Map arguments); + } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java index 856b627b..09ab63bd 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java @@ -735,4 +735,83 @@ public class DeviceServiceImpl extends BaseServiceImpl return null; } + + @Override + public Object callDeviceTool(String deviceId, String toolName, Map arguments) { + // 从系统参数中获取MQTT网关地址 + String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true); + if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) { + return null; + } + + // 获取设备信息 + DeviceEntity device = baseDao.selectById(deviceId); + if (device == null) { + return null; + } + + // 检查设备是否属于当前用户 + UserDetail user = SecurityUser.getUser(); + if (!device.getUserId().equals(user.getId())) { + return null; + } + + // 构建clientId + String macAddress = Optional.ofNullable(device.getMacAddress()).orElse("unknown").replace(":", "_"); + String groupId = Optional.ofNullable(device.getBoard()).orElse("GID_default").replace(":", "_"); + String clientId = StrUtil.format("{}@@@{}@@@{}", groupId, macAddress, macAddress); + + // 构建完整的URL + String url = StrUtil.format("http://{}/api/commands/{}", mqttGatewayUrl, clientId); + + // 构建请求体 + Map params = MapUtil + .builder(new HashMap()) + .put("name", toolName) + .put("arguments", arguments) + .build(); + + Map payload = MapUtil + .builder(new HashMap()) + .put("jsonrpc", "2.0") + .put("id", 2) + .put("method", "tools/call") + .put("params", params) + .build(); + + Map requestBody = MapUtil + .builder(new HashMap()) + .put("type", "mcp") + .put("payload", payload) + .build(); + + // 发送请求 + String resultMessage = HttpRequest.post(url) + .header(Header.CONTENT_TYPE, ContentType.JSON.getValue()) + .header(Header.AUTHORIZATION, "Bearer " + generateBearerToken()) + .body(JSONUtil.toJsonStr(requestBody)) + .timeout(10000) // 超时,毫秒 + .execute().body(); + + // 解析响应 + if (StringUtils.isNotBlank(resultMessage)) { + cn.hutool.json.JSONObject jsonObject = JSONUtil.parseObj(resultMessage); + if (jsonObject.getBool("success", false)) { + cn.hutool.json.JSONObject data = jsonObject.getJSONObject("data"); + if (data != null) { + cn.hutool.json.JSONArray content = data.getJSONArray("content"); + if (content != null && content.size() > 0) { + cn.hutool.json.JSONObject firstContent = content.getJSONObject(0); + if (firstContent != null && "text".equals(firstContent.getStr("type"))) { + String text = firstContent.getStr("text"); + if (StringUtils.isNotBlank(text)) { + return JSONUtil.parseObj(text); + } + } + } + } + } + } + return null; + } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/controller/VoiceCloneController.java b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/controller/VoiceCloneController.java index a85dd34a..21370a46 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/controller/VoiceCloneController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/controller/VoiceCloneController.java @@ -83,6 +83,13 @@ public class VoiceCloneController { return new Result().error(ErrorCode.VOICE_CLONE_NOT_AUDIO_FILE); } + // 加强验证文件扩展名 + String originalFilename = voiceFile.getOriginalFilename(); + String extension = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase(); + if (!extension.equals(".mp3") && !extension.equals(".wav")) { + return new Result().error("只允许上传.mp3和.wav格式的文件"); + } + // 验证文件大小 (最大10MB) if (voiceFile.getSize() > 10 * 1024 * 1024) { return new Result().error(ErrorCode.VOICE_CLONE_AUDIO_TOO_LARGE); diff --git a/main/manager-api/src/main/resources/i18n/messages.properties b/main/manager-api/src/main/resources/i18n/messages.properties index 324fc4fd..25cd7021 100644 --- a/main/manager-api/src/main/resources/i18n/messages.properties +++ b/main/manager-api/src/main/resources/i18n/messages.properties @@ -200,4 +200,5 @@ 10191=\u9002\u914D\u5668\u7F13\u5B58\u9519\u8BEF 10192=\u9002\u914D\u5668\u7C7B\u578B\u672A\u627E\u5230 10193=\u8BBE\u5907ID\u4E0D\u80FD\u4E3A\u7A7A -10194=\u8BBE\u5907\u4E0D\u5B58\u5728 \ No newline at end of file +10194=\u8BBE\u5907\u4E0D\u5B58\u5728\u6216\u4E0D\u5728\u7EBF +10195=OTA\u4E0A\u4F20\u6B21\u6570\u8D85\u8FC7\u9650\u5236 \ No newline at end of file diff --git a/main/manager-api/src/main/resources/i18n/messages_de_DE.properties b/main/manager-api/src/main/resources/i18n/messages_de_DE.properties index 1e3b1f44..0b9ed96d 100644 --- a/main/manager-api/src/main/resources/i18n/messages_de_DE.properties +++ b/main/manager-api/src/main/resources/i18n/messages_de_DE.properties @@ -200,4 +200,5 @@ 10191=Adapter-Cache-Fehler 10192=Adaptertyp nicht gefunden 10193=Adapter-ID darf nicht leer sein -10194=Adapter nicht gefunden \ No newline at end of file +10194=Adapter nicht gefunden oder nicht online +10195=OTA-Upload-Anzahl \u00FCberschreitet das Limit \ No newline at end of file diff --git a/main/manager-api/src/main/resources/i18n/messages_en_US.properties b/main/manager-api/src/main/resources/i18n/messages_en_US.properties index 71f98a76..17b1b961 100644 --- a/main/manager-api/src/main/resources/i18n/messages_en_US.properties +++ b/main/manager-api/src/main/resources/i18n/messages_en_US.properties @@ -200,4 +200,5 @@ 10191=Adapter cache error 10192=Adapter type not found 10193=Device ID cannot be empty -10194=Device not found \ No newline at end of file +10194=Device not found or not online +10195=OTA upload times exceed the limit \ No newline at end of file diff --git a/main/manager-api/src/main/resources/i18n/messages_vi_VN.properties b/main/manager-api/src/main/resources/i18n/messages_vi_VN.properties index 91e5cfff..3b80a6dc 100644 --- a/main/manager-api/src/main/resources/i18n/messages_vi_VN.properties +++ b/main/manager-api/src/main/resources/i18n/messages_vi_VN.properties @@ -200,4 +200,5 @@ 10191=L\u1ED7i b\u1ED9 nh\u1EDB \u0111\u1EC7m b\u1ED9 chuy\u1EC3n \u0111\u1ED5i 10192=Kh\u00F4ng t\u00ECm th\u1EA5y lo\u1EA1i b\u1ED9 chuy\u1EC3n \u0111\u1ED5i 10193=ID thi\u1EBFt b\u1ECB kh\u00F4ng th\u1EC3 tr\u1ED1ng -10194=Kh\u00F4ng t\u00ECm th\u1EA5y thi\u1EBFt b\u1ECB \ No newline at end of file +10194=Kh\u00F4ng t\u00ECm th\u1EA5y thi\u1EBFt b\u1ECB ho\u1EB7c thi\u1EBFt b\u1ECB kh\u00F4ng tr\u1EF1c tuy\u1EBFn +10195=S\u1ED1 l\u1EA7n t\u1EA3i l\u00EAn OTA v\u01B0\u1EE3t qu\u00E1 gi\u1EDBi h\u1EA1n \ No newline at end of file diff --git a/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties b/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties index 324fc4fd..25cd7021 100644 --- a/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties +++ b/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties @@ -200,4 +200,5 @@ 10191=\u9002\u914D\u5668\u7F13\u5B58\u9519\u8BEF 10192=\u9002\u914D\u5668\u7C7B\u578B\u672A\u627E\u5230 10193=\u8BBE\u5907ID\u4E0D\u80FD\u4E3A\u7A7A -10194=\u8BBE\u5907\u4E0D\u5B58\u5728 \ No newline at end of file +10194=\u8BBE\u5907\u4E0D\u5B58\u5728\u6216\u4E0D\u5728\u7EBF +10195=OTA\u4E0A\u4F20\u6B21\u6570\u8D85\u8FC7\u9650\u5236 \ No newline at end of file diff --git a/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties b/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties index 4c4f9b48..f6913eea 100644 --- a/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties +++ b/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties @@ -200,4 +200,5 @@ 10191=\u9002\u914D\u5668\u7F13\u5B58\u932F\u8AA4 10192=\u9002\u914D\u5668\u985E\u578B\u672A\u627E\u5230 10193=\u8A2D\u5099ID\u4E0D\u80FD\u4E3A\u7A7A -10194=\u8A2D\u5099\u4E0D\u5B58\u5728 +10194=\u8A2D\u5099\u4E0D\u5B58\u5728\u6216\u672A\u5728\u7DDA +10195=OTA\u4E0A\u4F20\u6B21\u6578\u8D85\u904E\u9650\u5236 \ No newline at end of file