mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
add:上传自定义表情固件接口
This commit is contained in:
@@ -43,7 +43,7 @@ public class SwaggerConfig {
|
||||
public GroupedOpenApi oatApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("ota")
|
||||
.pathsToMatch("/ota/**")
|
||||
.pathsToMatch("/ota/**", "/otaMag/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -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上传次数超过限制
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+21
-7
@@ -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<Object> getDeviceTools(@RequestBody Map<String, String> requestBody) {
|
||||
String deviceId = requestBody.get("deviceId");
|
||||
if (StringUtils.isBlank(deviceId)) {
|
||||
return new Result<Object>().error(ErrorCode.DEVICE_ID_NOT_NULL);
|
||||
}
|
||||
|
||||
public Result<Object> getDeviceTools(@PathVariable String deviceId) {
|
||||
Object toolsData = deviceService.getDeviceTools(deviceId);
|
||||
if (toolsData == null) {
|
||||
return new Result<Object>().error(ErrorCode.DEVICE_NOT_EXIST);
|
||||
@@ -145,4 +141,22 @@ public class DeviceController {
|
||||
|
||||
return new Result<Object>().ok(toolsData);
|
||||
}
|
||||
|
||||
@PostMapping("/tools/call/{deviceId}")
|
||||
@Operation(summary = "调用设备工具")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Object> callDeviceTool(@PathVariable String deviceId,
|
||||
@Valid @RequestBody DeviceToolsCallReqDTO request) {
|
||||
String toolName = request.getName();
|
||||
Map<String, Object> arguments = request.getArguments();
|
||||
|
||||
Object result = deviceService.callDeviceTool(deviceId, toolName, arguments);
|
||||
if (result == null) {
|
||||
return new Result<Object>().error(ErrorCode.DEVICE_NOT_EXIST);
|
||||
}
|
||||
|
||||
Result<Object> response = new Result<Object>();
|
||||
response.setMsg("Tools called successfully");
|
||||
return response.ok(result);
|
||||
}
|
||||
}
|
||||
+55
-3
@@ -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<String> uploadAssetsBin(@RequestParam("file") MultipartFile file) {
|
||||
String otaUrl = sysParamsService.getValue(Constant.SERVER_OTA, true);
|
||||
if (StringUtils.isBlank(otaUrl) || otaUrl.equals("null")) {
|
||||
return new Result<String>().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<String>().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<String>().error(ErrorCode.OTA_UPLOAD_COUNT_EXCEED);
|
||||
}
|
||||
// 增加上传次数
|
||||
redisUtils.increment(RedisKeys.getOtaUploadCountKey(SecurityUser.getUser().getId()),
|
||||
RedisUtils.DEFAULT_EXPIRE);
|
||||
}
|
||||
Result<String> 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());
|
||||
|
||||
@@ -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<String, Object> arguments;
|
||||
}
|
||||
@@ -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<DeviceEntity> {
|
||||
*/
|
||||
Object getDeviceTools(String deviceId);
|
||||
|
||||
/**
|
||||
* 调用设备工具
|
||||
*/
|
||||
Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments);
|
||||
|
||||
}
|
||||
+79
@@ -735,4 +735,83 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object callDeviceTool(String deviceId, String toolName, Map<String, Object> 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<String, Object> params = MapUtil
|
||||
.builder(new HashMap<String, Object>())
|
||||
.put("name", toolName)
|
||||
.put("arguments", arguments)
|
||||
.build();
|
||||
|
||||
Map<String, Object> payload = MapUtil
|
||||
.builder(new HashMap<String, Object>())
|
||||
.put("jsonrpc", "2.0")
|
||||
.put("id", 2)
|
||||
.put("method", "tools/call")
|
||||
.put("params", params)
|
||||
.build();
|
||||
|
||||
Map<String, Object> requestBody = MapUtil
|
||||
.builder(new HashMap<String, Object>())
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -83,6 +83,13 @@ public class VoiceCloneController {
|
||||
return new Result<String>().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<String>().error("只允许上传.mp3和.wav格式的文件");
|
||||
}
|
||||
|
||||
// 验证文件大小 (最大10MB)
|
||||
if (voiceFile.getSize() > 10 * 1024 * 1024) {
|
||||
return new Result<String>().error(ErrorCode.VOICE_CLONE_AUDIO_TOO_LARGE);
|
||||
|
||||
@@ -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
|
||||
10194=\u8BBE\u5907\u4E0D\u5B58\u5728\u6216\u4E0D\u5728\u7EBF
|
||||
10195=OTA\u4E0A\u4F20\u6B21\u6570\u8D85\u8FC7\u9650\u5236
|
||||
@@ -200,4 +200,5 @@
|
||||
10191=Adapter-Cache-Fehler
|
||||
10192=Adaptertyp nicht gefunden
|
||||
10193=Adapter-ID darf nicht leer sein
|
||||
10194=Adapter nicht gefunden
|
||||
10194=Adapter nicht gefunden oder nicht online
|
||||
10195=OTA-Upload-Anzahl \u00FCberschreitet das Limit
|
||||
@@ -200,4 +200,5 @@
|
||||
10191=Adapter cache error
|
||||
10192=Adapter type not found
|
||||
10193=Device ID cannot be empty
|
||||
10194=Device not found
|
||||
10194=Device not found or not online
|
||||
10195=OTA upload times exceed the limit
|
||||
@@ -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
|
||||
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
|
||||
@@ -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
|
||||
10194=\u8BBE\u5907\u4E0D\u5B58\u5728\u6216\u4E0D\u5728\u7EBF
|
||||
10195=OTA\u4E0A\u4F20\u6B21\u6570\u8D85\u8FC7\u9650\u5236
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user