diff --git a/main/manager-api/pom.xml b/main/manager-api/pom.xml index eacbdff9..0857ec3c 100644 --- a/main/manager-api/pom.xml +++ b/main/manager-api/pom.xml @@ -168,6 +168,12 @@ lombok true + + + org.apache.logging.log4j + log4j-slf4j-impl + 2.20.0 + diff --git a/main/manager-api/src/main/java/xiaozhi/common/controller/BaseController.java b/main/manager-api/src/main/java/xiaozhi/common/controller/BaseController.java new file mode 100644 index 00000000..8cda64c0 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/common/controller/BaseController.java @@ -0,0 +1,46 @@ +package xiaozhi.common.controller; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Enumeration; +import java.util.HashMap; + +public class BaseController { + + @Autowired + protected HttpServletRequest request; + + /** + * 获取请求头 + * + * @return + */ + protected HashMap getRequestHeaders() { + HashMap requestHeaders = new HashMap(); + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + String headerValue = request.getHeader(headerName); + requestHeaders.put(headerName, headerValue); + } + return requestHeaders; + } + + /** + * 获取请求参数 + * + * @return + */ + protected HashMap getRequestParams() { + HashMap requestParams = new HashMap(); + Enumeration paramNames = request.getParameterNames(); + while (paramNames.hasMoreElements()) { + String paramName = paramNames.nextElement(); + String paramValue = request.getParameter(paramName); + requestParams.put(paramName, paramValue); + } + return requestParams; + } + +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/UserAgentController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/UserAgentController.java new file mode 100644 index 00000000..0e13fac2 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/UserAgentController.java @@ -0,0 +1,195 @@ +package xiaozhi.modules.agent.controller; + +import cn.hutool.json.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.ObjectUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.beanutils.BeanUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.*; +import xiaozhi.common.controller.BaseController; +import xiaozhi.common.redis.RedisUtils; +import xiaozhi.common.user.UserDetail; +import xiaozhi.common.utils.Result; +import xiaozhi.modules.agent.domain.Agent; +import xiaozhi.modules.agent.domain.AgentTemplate; +import xiaozhi.modules.agent.service.AgentService; +import xiaozhi.modules.agent.service.AgentTemplateService; +import xiaozhi.modules.agent.vo.AgentConfigVO; +import xiaozhi.modules.agent.vo.AgentVO; +import xiaozhi.modules.device.domain.Device; +import xiaozhi.modules.device.service.DeviceService; +import xiaozhi.modules.model.domain.ModelConfig; +import xiaozhi.modules.model.domain.TtsVoice; +import xiaozhi.modules.model.service.ModelConfigService; +import xiaozhi.modules.model.service.TtsVoiceService; +import xiaozhi.modules.security.user.SecurityUser; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Tag(name = "智能体管理") +@AllArgsConstructor +@RestController +@RequestMapping("/user/agent") +public class UserAgentController extends BaseController { + private final AgentService agentService; + private final AgentTemplateService agentTemplateService; + private final ModelConfigService modelConfigService; + private final TtsVoiceService ttsVoiceService; + private final DeviceService deviceService; + private final RedisUtils redisUtils; + + @PostMapping + @Operation(summary = "添加智能体") + public Result addAgent(@RequestBody Agent agent) { + UserDetail user = SecurityUser.getUser(); + if(StringUtils.isBlank(agent.getAgentName())){ + log.error("智能体名称不能为空"); + } + Agent oldAgent = agentService.getOne(new QueryWrapper().eq("agent_name", agent.getAgentName())); + if(ObjectUtils.isNull(oldAgent)){ + AgentTemplate agentTemplate = agentTemplateService.getOne(new QueryWrapper().eq("is_default", 1)); + if(ObjectUtils.isNull(agentTemplate)){ + + }else{ + try { + oldAgent = new Agent(); + BeanUtils.copyProperties(oldAgent, agentTemplate); + oldAgent.setId(UUID.randomUUID().toString().replace("-","")); + oldAgent.setAgentName(agent.getAgentName()); + oldAgent.setUserId(user.getId()); + oldAgent.setCreator(user.getId()); + oldAgent.setCreatedAt(new Date()); + agentService.save(oldAgent); + } catch (Exception e) { + log.error("对象赋值异常", e); + } + } + }else{ + + } + return new Result().ok(agent); + } + + @GetMapping + @Operation(summary = "智能体列表") + public Result> agentList() { + UserDetail user = SecurityUser.getUser(); + List agents = agentService.list(new QueryWrapper().eq("user_id",user.getId())); + List list = new ArrayList<>(); + this.convertAgetVOList(list, agents); + return new Result>().ok(list); + } + + @GetMapping("/loadAgentConfig/{deviceId}") + @Operation(summary = "下载智能体配置") +// public Result loadAgentConfig(@PathVariable String deviceId){ + public Result loadAgentConfig(@PathVariable String deviceId){ + Device device = deviceService.getOne(new QueryWrapper().eq("mac_address", deviceId.toUpperCase())); + if(ObjectUtils.isNull(device)){ + return new Result().error("设备不存在"); + } + Agent agent = agentService.getOne(new QueryWrapper().eq("id", device.getAgentId())); + if(ObjectUtils.isNull(agent)){ + return new Result().error("智能体不存在"); + } + AgentConfigVO agentConfigVO = new AgentConfigVO(); +// return new Result().ok(agentConfigVO); + String json = "{\n" + + " \"ASR\": {\n" + + " \"FunASR\": {\n" + + " \"model_dir\": \"models/SenseVoiceSmall\",\n" + + " \"output_dir\": \"tmp/\",\n" + + " \"type\": \"fun_local\"\n" + + " }\n" + + " },\n" + + " \"LLM\": {\n" + + " \"ChatGLMLLM\": {\n" + + " \"api_key\": \"0415dad4014847babc3e3f03024c50a3.qH7FgTy5Yawc85fl\",\n" + + " \"model_name\": \"glm-4-flash\",\n" + + " \"type\": \"openai\",\n" + + " \"url\": \"https://open.bigmodel.cn/api/paas/v4/\"\n" + + " }\n" + + " },\n" + + " \"TTS\": {\n" + + " \"DoubaoTTS\": {\n" + + " \"access_token\": \"hrnx22F9WutWBm7YJzE62r_Z1myUmHEL\",\n" + + " \"api_url\": \"https://openspeech.bytedance.com/api/v1/tts\",\n" + + " \"appid\": \"6295576095\",\n" + + " \"authorization\": \"Bearer;\",\n" + + " \"cluster\": \"volcano_tts\",\n" + + " \"output_dir\": \"tmp/\",\n" + + " \"type\": \"doubao\",\n" + + " \"voice\": \"BV034_streaming\"\n" + + " }\n" + + " },\n" + + " \"VAD\": {\n" + + " \"SileroVAD\": {\n" + + " \"min_silence_duration_ms\": 700,\n" + + " \"model_dir\": \"models/snakers4_silero-vad\",\n" + + " \"threshold\": 0.5\n" + + " }\n" + + " },\n" + + " \"auth_code\": \"642365\",\n" + + " \"prompt\": \"你是一个叫小优的女孩,来自优享生活公司的AI智能体,声音好听,习惯简短表达,爱用网络梗。\\n请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。\\n现在我正在和你进行语音聊天,我们开始吧。\\n如果用户希望结束对话,请在最后说“拜拜”或“再见”。\\n\",\n" + + " \"selected_module\": {\n" + + " \"ASR\": \"FunASR\",\n" + + " \"Intent\": \"function_call\",\n" + + " \"LLM\": \"ChatGLMLLM\",\n" + + " \"Memory\": \"mem0ai\",\n" + + " \"TTS\": \"DoubaoTTS\",\n" + + " \"VAD\": \"SileroVAD\"\n" + + " },\n" + + " \"owner\":\"18600806164\"\n" + + "}"; + + return new Result().ok(new JSONObject(json)); + } + + /** + * 将Agent对象列表转换为AgentVO对象列表 + * 此方法遍历Agent对象列表,将每个Agent对象转换为AgentVO对象,并添加到AgentVO列表中 + * + * @param agentVOList 转换后的AgentVO对象列表 + * @param agentList 原始的Agent对象列表 + */ + private void convertAgetVOList(List agentVOList, List agentList){ + // 遍历Agent对象列表 + for(Agent agent : agentList) { + // 创建一个新的AgentVO对象 + AgentVO agentVO = new AgentVO(); + // 将当前Agent对象的属性值转换并设置到AgentVO对象中 + this.convertAgentVO(agentVO, agent); + // 将转换后的AgentVO对象添加到AgentVO列表中 + agentVOList.add(agentVO); + } + } + private void convertAgentVO(AgentVO agentVO, Agent agent){ + try { + BeanUtils.copyProperties(agentVO, agent); + agentVO.setTtsModelName("未知"); + TtsVoice ttsVoice = ttsVoiceService.getOne(new QueryWrapper().eq("id", agent.getTtsVoiceId())); + if(ObjectUtils.isNotNull(ttsVoice)){ + agentVO.setTtsModelName(ttsVoice.getName()); + } + agentVO.setLlmModelName("未知"); + ModelConfig modelConfig = modelConfigService.getOne(new QueryWrapper().eq("id", agent.getLlmModelId())); + if(ObjectUtils.isNotNull(modelConfig)){ + agentVO.setLlmModelName(modelConfig.getModelName()); + } + agentVO.setLastConnectedAt("今天"); + agentVO.setDeviceCount(0L); + long deviceCount = deviceService.count(new QueryWrapper().eq("agent_id", agent.getId())); + agentVO.setDeviceCount(deviceCount); + } catch (Exception e) { + log.error("[convertAgentVO]对象转换报错",e); + } + } +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/domain/Agent.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/domain/Agent.java new file mode 100644 index 00000000..74f75491 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/domain/Agent.java @@ -0,0 +1,116 @@ +package xiaozhi.modules.agent.domain; + +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 java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * 智能体配置表 + * @TableName ai_agent + */ +@TableName(value ="ai_agent") +@Data +public class Agent implements Serializable { + /** + * 智能体唯一标识 + */ + @TableId + private String id; + + /** + * 所属用户 ID + */ + private Long userId; + + /** + * 智能体编码 + */ + private String agentCode; + + /** + * 智能体名称 + */ + private String agentName; + + /** + * 语音识别模型标识 + */ + private String asrModelId; + + /** + * 语音活动检测标识 + */ + private String vadModelId; + + /** + * 大语言模型标识 + */ + private String llmModelId; + + /** + * 语音合成模型标识 + */ + private String ttsModelId; + + /** + * 音色标识 + */ + private String ttsVoiceId; + + /** + * 记忆模型标识 + */ + private String memModelId; + + /** + * 意图模型标识 + */ + private String intentModelId; + + /** + * 角色设定参数 + */ + private String systemPrompt; + + /** + * 语言编码 + */ + private String langCode; + + /** + * 交互语种 + */ + private String language; + + /** + * 排序权重 + */ + private Integer sort; + + /** + * 创建者 ID + */ + private Long creator; + + /** + * 创建时间 + */ + private Date createdAt; + + /** + * 更新者 ID + */ + private Long updater; + + /** + * 更新时间 + */ + private Date updatedAt; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/domain/AgentTemplate.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/domain/AgentTemplate.java new file mode 100644 index 00000000..b25dc407 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/domain/AgentTemplate.java @@ -0,0 +1,116 @@ +package xiaozhi.modules.agent.domain; + +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 java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * 智能体配置模板表 + * @TableName ai_agent_template + */ +@TableName(value ="ai_agent_template") +@Data +public class AgentTemplate implements Serializable { + /** + * 智能体唯一标识 + */ + @TableId + private String id; + + /** + * 智能体编码 + */ + private String agentCode; + + /** + * 智能体名称 + */ + private String agentName; + + /** + * 语音识别模型标识 + */ + private String asrModelId; + + /** + * 语音活动检测标识 + */ + private String vadModelId; + + /** + * 大语言模型标识 + */ + private String llmModelId; + + /** + * 语音合成模型标识 + */ + private String ttsModelId; + + /** + * 音色标识 + */ + private String ttsVoiceId; + + /** + * 记忆模型标识 + */ + private String memModelId; + + /** + * 意图模型标识 + */ + private String intentModelId; + + /** + * 角色设定参数 + */ + private String systemPrompt; + + /** + * 语言编码 + */ + private String langCode; + + /** + * 交互语种 + */ + private String language; + + /** + * 排序权重 + */ + private Integer sort; + + /** + * 是否默认模板:1:是,0:不是 + */ + private Integer isDefault; + + /** + * 创建者 ID + */ + private Long creator; + + /** + * 创建时间 + */ + private Date createdAt; + + /** + * 更新者 ID + */ + private Long updater; + + /** + * 更新时间 + */ + private Date updatedAt; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/mapper/AgentMapper.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/mapper/AgentMapper.java new file mode 100644 index 00000000..7c849280 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/mapper/AgentMapper.java @@ -0,0 +1,20 @@ +package xiaozhi.modules.agent.mapper; + +import org.apache.ibatis.annotations.Mapper; +import xiaozhi.modules.agent.domain.Agent; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author chenerlei +* @description 针对表【ai_agent(智能体配置表)】的数据库操作Mapper +* @createDate 2025-03-22 11:48:18 +* @Entity xiaozhi.modules.agent.domain.Agent +*/ +@Mapper +public interface AgentMapper extends BaseMapper { + +} + + + + diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/mapper/AgentTemplateMapper.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/mapper/AgentTemplateMapper.java new file mode 100644 index 00000000..c3222cc4 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/mapper/AgentTemplateMapper.java @@ -0,0 +1,20 @@ +package xiaozhi.modules.agent.mapper; + +import org.apache.ibatis.annotations.Mapper; +import xiaozhi.modules.agent.domain.AgentTemplate; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author chenerlei +* @description 针对表【ai_agent_template(智能体配置模板表)】的数据库操作Mapper +* @createDate 2025-03-22 11:48:18 +* @Entity xiaozhi.modules.agent.domain.AgentTemplate +*/ +@Mapper +public interface AgentTemplateMapper extends BaseMapper { + +} + + + + 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 new file mode 100644 index 00000000..b7a7705b --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java @@ -0,0 +1,13 @@ +package xiaozhi.modules.agent.service; + +import xiaozhi.modules.agent.domain.Agent; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author chenerlei +* @description 针对表【ai_agent(智能体配置表)】的数据库操作Service +* @createDate 2025-03-22 11:48:18 +*/ +public interface AgentService extends IService { + +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentTemplateService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentTemplateService.java new file mode 100644 index 00000000..18849526 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentTemplateService.java @@ -0,0 +1,13 @@ +package xiaozhi.modules.agent.service; + +import xiaozhi.modules.agent.domain.AgentTemplate; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author chenerlei +* @description 针对表【ai_agent_template(智能体配置模板表)】的数据库操作Service +* @createDate 2025-03-22 11:48:18 +*/ +public interface AgentTemplateService extends IService { + +} 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 new file mode 100644 index 00000000..dfa3400d --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java @@ -0,0 +1,22 @@ +package xiaozhi.modules.agent.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import xiaozhi.modules.agent.domain.Agent; +import xiaozhi.modules.agent.service.AgentService; +import xiaozhi.modules.agent.mapper.AgentMapper; +import org.springframework.stereotype.Service; + +/** +* @author chenerlei +* @description 针对表【ai_agent(智能体配置表)】的数据库操作Service实现 +* @createDate 2025-03-22 11:48:18 +*/ +@Service +public class AgentServiceImpl extends ServiceImpl + implements AgentService{ + +} + + + + diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentTemplateServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentTemplateServiceImpl.java new file mode 100644 index 00000000..112b65c0 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentTemplateServiceImpl.java @@ -0,0 +1,22 @@ +package xiaozhi.modules.agent.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import xiaozhi.modules.agent.domain.AgentTemplate; +import xiaozhi.modules.agent.service.AgentTemplateService; +import xiaozhi.modules.agent.mapper.AgentTemplateMapper; +import org.springframework.stereotype.Service; + +/** +* @author chenerlei +* @description 针对表【ai_agent_template(智能体配置模板表)】的数据库操作Service实现 +* @createDate 2025-03-22 11:48:18 +*/ +@Service +public class AgentTemplateServiceImpl extends ServiceImpl + implements AgentTemplateService{ + +} + + + + diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentConfigVO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentConfigVO.java new file mode 100644 index 00000000..ba1ececc --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentConfigVO.java @@ -0,0 +1,17 @@ +package xiaozhi.modules.agent.vo; + +import cn.hutool.json.JSONObject; +import lombok.Data; + +@Data +public class AgentConfigVO { + private JSONObject selected_module; + private String prompt; + private JSONObject LLM; + private JSONObject TTS; + private JSONObject ASR; + private JSONObject VAD; + private JSONObject Memory; + private JSONObject Intent; + private String owner; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentVO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentVO.java new file mode 100644 index 00000000..3ffddbd7 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentVO.java @@ -0,0 +1,19 @@ +package xiaozhi.modules.agent.vo; + +import lombok.Data; +import xiaozhi.modules.agent.domain.Agent; + +@Data +public class AgentVO extends Agent { + //角色音色 + private String ttsModelName; + + //角色明星 + private String llmModelName; + + //最近对话 + private String lastConnectedAt; + + //设备数量 + private Long deviceCount; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/constant/DeviceConstant.java b/main/manager-api/src/main/java/xiaozhi/modules/device/constant/DeviceConstant.java new file mode 100644 index 00000000..5d6fb39e --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/constant/DeviceConstant.java @@ -0,0 +1,7 @@ +package xiaozhi.modules.device.constant; + +public class DeviceConstant { + + public static final String REDIS_KEY_PREFIX_DEVICE_ACTIVATION_CODE = "device:activation:code"; + public static final String REDIS_KEY_PREFIX_DEVICE_ACTIVATION_MAC = "device:activation:mac"; +} 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 deleted file mode 100644 index d458e9f9..00000000 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java +++ /dev/null @@ -1,82 +0,0 @@ -package xiaozhi.modules.device.controller; - -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 org.apache.commons.lang3.StringUtils; -import org.apache.shiro.authz.annotation.RequiresPermissions; -import org.springframework.web.bind.annotation.*; -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; -import xiaozhi.common.user.UserDetail; -import xiaozhi.common.utils.JsonUtils; -import xiaozhi.common.utils.Result; -import xiaozhi.modules.device.dto.DeviceHeaderDTO; -import xiaozhi.modules.device.dto.DeviceUnBindDTO; -import xiaozhi.modules.device.entity.DeviceEntity; -import xiaozhi.modules.device.service.DeviceService; -import xiaozhi.modules.security.user.SecurityUser; - -import java.util.List; -import java.util.Map; - -@Tag(name = "设备管理") -@AllArgsConstructor -@RestController -@RequestMapping("/device") -public class DeviceController { - private final DeviceService deviceService; - private final RedisUtils redisUtils; - - - @PostMapping("/bind/{deviceCode}") - @Operation(summary = "绑定设备") - @RequiresPermissions("sys:role:normal") - public Result bindDevice(@PathVariable String deviceCode) { - UserDetail user = SecurityUser.getUser(); - - String deviceHeaders = (String) redisUtils.get(RedisKeys.getDeviceCaptchaKey(deviceCode)); - if (StringUtils.isBlank(deviceHeaders)) { - return new Result().error(ErrorCode.DEVICE_CAPTCHA_ERROR); - } - DeviceHeaderDTO deviceHeader = JsonUtils.parseObject(deviceHeaders.getBytes(), DeviceHeaderDTO.class); - DeviceEntity device = deviceService.bindDevice(user.getId(), deviceHeader); - return new Result().ok(device); - } - - @GetMapping("/bind") - @Operation(summary = "获取已绑定设备") - @RequiresPermissions("sys:role:normal") - public Result> getUserDevices() { - UserDetail user = SecurityUser.getUser(); - List devices = deviceService.getUserDevices(user.getId()); - return new Result>().ok(devices); - } - - @PostMapping("/unbind") - @Operation(summary = "解绑设备") - @RequiresPermissions("sys:role:normal") - public Result unbindDevice(@RequestBody DeviceUnBindDTO unDeviveBind) { - UserDetail user = SecurityUser.getUser(); - deviceService.unbindDevice(user.getId(), unDeviveBind.getDeviceId()); - return new Result(); - } - - @GetMapping("/all") - @Operation(summary = "设备列表(管理员)") - @RequiresPermissions("sys:role:superAdmin") - @Parameters({ - @Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true), - @Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true), - }) - public Result> adminDeviceList( - @Parameter(hidden = true) @RequestParam Map params) { - PageData page = deviceService.adminDeviceList(params); - return new Result>().ok(page); - } -} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/controller/OtaController.java b/main/manager-api/src/main/java/xiaozhi/modules/device/controller/OtaController.java new file mode 100644 index 00000000..c222a264 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/controller/OtaController.java @@ -0,0 +1,81 @@ +package xiaozhi.modules.device.controller; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.ObjectUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.*; +import xiaozhi.common.controller.BaseController; +import xiaozhi.common.redis.RedisUtils; +import xiaozhi.modules.device.constant.DeviceConstant; +import xiaozhi.modules.device.domain.Device; +import xiaozhi.modules.device.service.DeviceService; +import xiaozhi.modules.device.utils.CodeGeneratorUtil; +import xiaozhi.modules.device.vo.DeviceOtaVO; +import xiaozhi.modules.sys.service.SysParamsService; + +import java.util.Date; + +@Slf4j +@Tag(name = "设备OTA管理") +@AllArgsConstructor +@RestController +@RequestMapping("/ota") +public class OtaController extends BaseController { + private final DeviceService deviceService; + private final SysParamsService sysParamsService; + @Resource + private RedisUtils redisUtils; + private final String OTA_PARAM_CODE = "OTA"; + + @CrossOrigin(originPatterns = "*", methods = {RequestMethod.GET, RequestMethod.POST}) + @RequestMapping(path = "/", method = {RequestMethod.POST, RequestMethod.GET}, produces = "application/json") + @Operation(summary = "设备OTA升级") + public String ota(@RequestHeader(required = false) HttpHeaders headers, @RequestBody(required = false) JSONObject jsonObject) { + log.info("OTA升级请求:header:{},body:{}", headers, jsonObject); + if (ObjectUtils.isNull(headers) || StringUtils.isBlank(headers.getFirst("device-id"))) { + log.error("设备ID不能为空"); + return "{\"error\": \"Device ID is required\"}"; + } + + DeviceOtaVO otaVO = new DeviceOtaVO(); + Device device = deviceService.getOne(new UpdateWrapper().eq("mac_address", headers.getFirst("device-id").toUpperCase()).eq("id", headers.getFirst("client-Id").replace("-", ""))); + if (ObjectUtils.isNull(device)) { + // 从 Redis 中获取设备信息 + Object redisValue = redisUtils.hGet(DeviceConstant.REDIS_KEY_PREFIX_DEVICE_ACTIVATION_MAC, headers.getFirst("device-id").toUpperCase()); + if (ObjectUtils.isNull(redisValue)) { + String code = CodeGeneratorUtil.generateCode(6); + log.info("[gen]授权码已广播:{}", code); + otaVO.setActivation(new DeviceOtaVO.Activation(code, "youxlife.com\n" + code)); + redisUtils.hSet(DeviceConstant.REDIS_KEY_PREFIX_DEVICE_ACTIVATION_MAC, headers.getFirst("device-id").toUpperCase(), code, RedisUtils.HOUR_ONE_EXPIRE); + redisUtils.hSet(DeviceConstant.REDIS_KEY_PREFIX_DEVICE_ACTIVATION_CODE, code, jsonObject, RedisUtils.HOUR_ONE_EXPIRE); + } else { + log.warn("[get]授权码已广播:{}", redisValue); + otaVO.setActivation(new DeviceOtaVO.Activation(redisValue.toString(), "youxlife.com\n" + redisValue)); + } + } + + + if (ObjectUtils.isNull(device) || device.getAutoUpdate() == 1) { + //{"version":"1.0.0","url":"http://https://youxlife.oss-cn-zhangjiakou.aliyuncs.com/v1.4.5.bin"} + String value = sysParamsService.getValue(OTA_PARAM_CODE); + if (StringUtils.isBlank(value)) { + log.warn("OTA升级信息未配置"); + } else { + JSONObject object = new JSONObject(value); + otaVO.setFirmware(new DeviceOtaVO.Firmware(object.getStr("version"), object.getStr("url"))); + } + } + + otaVO.setServer_time(new DeviceOtaVO.ServerTime(new Date().getTime(), 8 * 60)); + return JSONUtil.toJsonStr(otaVO); + } +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/controller/UserDeviceController.java b/main/manager-api/src/main/java/xiaozhi/modules/device/controller/UserDeviceController.java new file mode 100644 index 00000000..b5900faa --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/controller/UserDeviceController.java @@ -0,0 +1,127 @@ +package xiaozhi.modules.device.controller; + +import cn.hutool.json.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.ObjectUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.*; +import xiaozhi.common.controller.BaseController; +import xiaozhi.common.redis.RedisUtils; +import xiaozhi.common.user.UserDetail; +import xiaozhi.common.utils.Result; +import xiaozhi.modules.agent.domain.Agent; +import xiaozhi.modules.agent.service.AgentService; +import xiaozhi.modules.device.constant.DeviceConstant; +import xiaozhi.modules.device.domain.Device; +import xiaozhi.modules.device.service.DeviceService; +import xiaozhi.modules.device.vo.DeviceCodeVO; +import xiaozhi.modules.security.user.SecurityUser; + +import java.util.Date; + +@Slf4j +@Tag(name = "设备管理") +@AllArgsConstructor +@RestController +@RequestMapping("/user/agent/device") +public class UserDeviceController extends BaseController { + private final AgentService agentService; + private final DeviceService deviceService; + private final RedisUtils redisUtils; + + @GetMapping("/bind/{agentId}") + @Operation(summary = "已绑定设备列表") + public Result> bindDeviceList(@PathVariable String agentId, @RequestParam Integer pageNo, @RequestParam Integer pageSize) { + if (StringUtils.isBlank(agentId)) { + log.error("智能体ID不能为空"); + return new Result().error("智能体ID不能为空"); + } + + UserDetail user = SecurityUser.getUser(); + Agent agent = agentService.getById(agentId); + if (ObjectUtils.isNull(agent)) { + log.error("智能体不存在"); + return new Result().error("智能体不存在"); + } + Page page = new Page(pageNo, pageSize); + page = deviceService.page(page, new QueryWrapper().eq("user_id", user.getId()).eq("agent_id", agentId)); + return new Result>().ok(page); + } + + + @PostMapping("/bind/{agentId}") + @Operation(summary = "绑定设备") + public Result bindDevice(@PathVariable String agentId, @RequestBody DeviceCodeVO deviceCodeVO) { + if (ObjectUtils.isNull(deviceCodeVO) || StringUtils.isBlank(deviceCodeVO.getDeviceCode())) { + log.error("授权码不能为空"); + return new Result().error("授权码不能为空"); + } + if (StringUtils.isBlank(agentId)) { + log.error("智能体ID不能为空"); + return new Result().error("智能体ID不能为空"); + } + + UserDetail user = SecurityUser.getUser(); + + // 从 Redis 中获取设备信息 + Object redisValue = redisUtils.hGet(DeviceConstant.REDIS_KEY_PREFIX_DEVICE_ACTIVATION_CODE, deviceCodeVO.getDeviceCode()); + if (ObjectUtils.isNull(redisValue)) { + log.error("授权码不存在:{}", deviceCodeVO.getDeviceCode()); + return new Result().error("授权码不存在"); + } + JSONObject jsonObject = (JSONObject) redisValue; + try { + redisUtils.hDel(DeviceConstant.REDIS_KEY_PREFIX_DEVICE_ACTIVATION_CODE, deviceCodeVO.getDeviceCode()); + redisUtils.hDel(DeviceConstant.REDIS_KEY_PREFIX_DEVICE_ACTIVATION_MAC, jsonObject.getStr("mac_address").toUpperCase()); + } catch (Exception e) { + log.warn("授权缓存删除失败", e); + } + JSONObject board = jsonObject.getJSONObject("board"); + JSONObject application = jsonObject.getJSONObject("application"); + if (ObjectUtils.isNull(board) || ObjectUtils.isNull(application)) { + log.error("设备码绑定信息无效,{}:{}", jsonObject.getStr("mac_address").toUpperCase(), deviceCodeVO.getDeviceCode()); + return new Result().error("设备码绑定信息无效"); + } + + Agent agent = agentService.getById(agentId); + if (ObjectUtils.isNull(agent)) { + log.error("智能体不存在"); + return new Result().error("智能体不存在"); + } + Device device = new Device(); + device.setId(jsonObject.getStr("uuid").replace("-", "")); + device.setUserId(user.getId()); + device.setMacAddress(jsonObject.getStr("mac_address").toUpperCase()); + device.setBoard(board.getStr("type")); + device.setAppVersion(application.getStr("version")); + device.setAgentId(agentId); + device.setCreator(user.getId()); + device.setCreateDate(new Date()); + device.setAutoUpdate(1); + boolean bool = deviceService.save(device); + if (!bool) { + return new Result().error("绑定失败"); + } + return new Result().ok(device); + } + + @PutMapping("/unbind/{deviceId}") + @Operation(summary = "解绑设备") + public Result bindDevice(@PathVariable String deviceId) { + if (StringUtils.isBlank(deviceId)) { + log.error("设备ID不能为空"); + return new Result().error("设备ID不能为空"); + } + UserDetail user = SecurityUser.getUser(); + boolean bool = deviceService.removeById(deviceId); + if (!bool) { + return new Result().error("解绑失败"); + } + return new Result().ok(null); + } +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/dao/DeviceDao.java b/main/manager-api/src/main/java/xiaozhi/modules/device/dao/DeviceDao.java deleted file mode 100644 index 9e4a8cd4..00000000 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/dao/DeviceDao.java +++ /dev/null @@ -1,9 +0,0 @@ -package xiaozhi.modules.device.dao; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import org.apache.ibatis.annotations.Mapper; -import xiaozhi.modules.device.entity.DeviceEntity; - -@Mapper -public interface DeviceDao extends BaseMapper { -} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/domain/Device.java b/main/manager-api/src/main/java/xiaozhi/modules/device/domain/Device.java new file mode 100644 index 00000000..ae662dde --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/domain/Device.java @@ -0,0 +1,91 @@ +package xiaozhi.modules.device.domain; + +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 java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * 设备信息表 + * @TableName ai_device + */ +@TableName(value ="ai_device") +@Data +public class Device implements Serializable { + /** + * 设备唯一标识 + */ + @TableId + private String id; + + /** + * 关联用户 ID + */ + private Long userId; + + /** + * MAC 地址 + */ + private String macAddress; + + /** + * 最后连接时间 + */ + private Date lastConnectedAt; + + /** + * 自动更新开关(0 关闭/1 开启) + */ + private Integer autoUpdate; + + /** + * 设备硬件型号 + */ + private String board; + + /** + * 设备别名 + */ + private String alias; + + /** + * 智能体 ID + */ + private String agentId; + + /** + * 固件版本号 + */ + private String appVersion; + + /** + * 排序 + */ + private Integer sort; + + /** + * 创建者 + */ + private Long creator; + + /** + * 创建时间 + */ + private Date createDate; + + /** + * 更新者 + */ + private Long updater; + + /** + * 更新时间 + */ + private Date updateDate; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceHeaderDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceHeaderDTO.java deleted file mode 100644 index 83327fef..00000000 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceHeaderDTO.java +++ /dev/null @@ -1,19 +0,0 @@ -package xiaozhi.modules.device.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Data -@Schema(description = "设备连接头信息") -public class DeviceHeaderDTO { - - @Schema(description = "设备ID") - private String deviceId; - - @Schema(description = "协议版本号") - private Long protocolVersion; - - @Schema(description = "认证信息") - private String authorization; - -} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceUnBindDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceUnBindDTO.java deleted file mode 100644 index 830a51ae..00000000 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceUnBindDTO.java +++ /dev/null @@ -1,19 +0,0 @@ -package xiaozhi.modules.device.dto; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -import java.io.Serializable; - -/** - * 设备解绑表单 - */ -@Data -@Schema(description = "设备解绑表单") -public class DeviceUnBindDTO implements Serializable { - - @Schema(description = "设备ID") - @NotBlank(message = "设备ID不能为空") - private Long deviceId; - -} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/entity/DeviceEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/device/entity/DeviceEntity.java deleted file mode 100644 index ccbe4683..00000000 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/entity/DeviceEntity.java +++ /dev/null @@ -1,54 +0,0 @@ -package xiaozhi.modules.device.entity; - -import com.baomidou.mybatisplus.annotation.TableName; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.util.Date; - -@Data -@TableName("ai_device") -@Schema(description = "设备信息") -public class DeviceEntity { - @Schema(description = "设备ID") - private Long id; - - @Schema(description = "关联用户ID") - private Long userId; - - @Schema(description = "MAC地址") - private String macAddress; - - @Schema(description = "最后连接时间") - private Date lastConnectedAt; - - @Schema(description = "自动更新开关(0关闭/1开启)") - private Integer autoUpdate; - - @Schema(description = "设备硬件型号") - private String board; - - @Schema(description = "设备别名") - private String alias; - - @Schema(description = "智能体ID") - private String agentId; - - @Schema(description = "固件版本号") - private String appVersion; - - @Schema(description = "排序") - private Integer sort; - - @Schema(description = "创建者") - private Long creator; - - @Schema(description = "创建时间") - private Date createDate; - - @Schema(description = "更新者") - private Long updater; - - @Schema(description = "更新时间") - private Date updateDate; -} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/mapper/DeviceMapper.java b/main/manager-api/src/main/java/xiaozhi/modules/device/mapper/DeviceMapper.java new file mode 100644 index 00000000..b43847a5 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/mapper/DeviceMapper.java @@ -0,0 +1,20 @@ +package xiaozhi.modules.device.mapper; + +import org.apache.ibatis.annotations.Mapper; +import xiaozhi.modules.device.domain.Device; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author chenerlei +* @description 针对表【ai_device(设备信息表)】的数据库操作Mapper +* @createDate 2025-03-22 13:26:35 +* @Entity xiaozhi.modules.device.domain.Device +*/ +@Mapper +public interface DeviceMapper extends BaseMapper { + +} + + + + 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 16ad44fe..ec3efe56 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 @@ -1,18 +1,13 @@ -package xiaozhi.modules.device.service; - -import xiaozhi.common.page.PageData; -import xiaozhi.modules.device.dto.DeviceHeaderDTO; -import xiaozhi.modules.device.entity.DeviceEntity; - -import java.util.List; -import java.util.Map; - -public interface DeviceService { - DeviceEntity bindDevice(Long userId, DeviceHeaderDTO deviceHeader); - - List getUserDevices(Long userId); - - void unbindDevice(Long userId, Long deviceId); - - PageData adminDeviceList(Map params); -} \ No newline at end of file +package xiaozhi.modules.device.service; + +import xiaozhi.modules.device.domain.Device; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author chenerlei +* @description 针对表【ai_device(设备信息表)】的数据库操作Service +* @createDate 2025-03-22 13:26:35 +*/ +public interface DeviceService extends IService { + +} 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 279a9f2e..4c317a64 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 @@ -1,57 +1,22 @@ -package xiaozhi.modules.device.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.core.metadata.IPage; -import org.springframework.stereotype.Service; -import xiaozhi.common.page.PageData; -import xiaozhi.common.service.impl.BaseServiceImpl; -import xiaozhi.modules.device.dao.DeviceDao; -import xiaozhi.modules.device.dto.DeviceHeaderDTO; -import xiaozhi.modules.device.entity.DeviceEntity; -import xiaozhi.modules.device.service.DeviceService; - -import java.util.Date; -import java.util.List; -import java.util.Map; - -@Service -public class DeviceServiceImpl extends BaseServiceImpl implements DeviceService { - private final DeviceDao deviceDao; - - // 添加构造函数来初始化 deviceMapper - public DeviceServiceImpl(DeviceDao deviceDao) { - this.deviceDao = deviceDao; - } - - @Override - public DeviceEntity bindDevice(Long userId, DeviceHeaderDTO deviceHeader) { - DeviceEntity device = new DeviceEntity(); - device.setUserId(userId); - device.setMacAddress(deviceHeader.getDeviceId()); - device.setCreateDate(new Date()); - deviceDao.insert(device); - return device; - } - - @Override - public List getUserDevices(Long userId) { - QueryWrapper wrapper = new QueryWrapper<>(); - wrapper.eq("user_id", userId); - return deviceDao.selectList(wrapper); - } - - @Override - public void unbindDevice(Long userId, Long deviceId) { - deviceDao.deleteById(deviceId); - } - - @Override - public PageData adminDeviceList(Map params) { - IPage page = deviceDao.selectPage( - getPage(params, "sort", true), - new QueryWrapper<>() - ); - return new PageData<>(page.getRecords(), page.getTotal()); - } - -} \ No newline at end of file +package xiaozhi.modules.device.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import xiaozhi.modules.device.domain.Device; +import xiaozhi.modules.device.service.DeviceService; +import xiaozhi.modules.device.mapper.DeviceMapper; +import org.springframework.stereotype.Service; + +/** +* @author chenerlei +* @description 针对表【ai_device(设备信息表)】的数据库操作Service实现 +* @createDate 2025-03-22 13:26:35 +*/ +@Service +public class DeviceServiceImpl extends ServiceImpl + implements DeviceService{ + +} + + + + diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/utils/CodeGeneratorUtil.java b/main/manager-api/src/main/java/xiaozhi/modules/device/utils/CodeGeneratorUtil.java new file mode 100644 index 00000000..3a9ca2e6 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/utils/CodeGeneratorUtil.java @@ -0,0 +1,68 @@ +package xiaozhi.modules.device.utils; + +import java.util.Random; + +public class CodeGeneratorUtil { + + private static long lastTime = 0; + private static int counter = 0; + + /** + * 生成6位数字码,首位不为0,短期内不会重复。 + * @return 6位数字码 + */ + public static synchronized String generateCode() { + long currentTime = System.currentTimeMillis(); + + // 如果当前时间与上次相同,增加计数器以避免重复 + if (currentTime == lastTime) { + counter++; + } else { + counter = 0; // 时间变化时重置计数器 + } + lastTime = currentTime; + + // 使用当前时间和计数器生成随机种子 + long seed = currentTime + counter; + Random random = new Random(seed); + + // 确保首位不为0 + int firstDigit = random.nextInt(9) + 1; + int remainingDigits = random.nextInt(100000); // 剩余5位 + + // 拼接生成的6位数字码 + return String.format("%d%05d", firstDigit, remainingDigits); + } + + /** + * 生成指定位数的数字码,首位不为0,短期内不会重复。 + * @param digits 生成的数字码位数 + * @return 指定位数的数字码 + */ + public static synchronized String generateCode(int digits) { + if (digits < 2) { + throw new IllegalArgumentException("数字码位数必须大于等于2"); + } + + long currentTime = System.currentTimeMillis(); + + // 如果当前时间与上次相同,增加计数器以避免重复 + if (currentTime == lastTime) { + counter++; + } else { + counter = 0; // 时间变化时重置计数器 + } + lastTime = currentTime; + + // 使用当前时间和计数器生成随机种子 + long seed = currentTime + counter; + Random random = new Random(seed); + + // 确保首位不为0 + int firstDigit = random.nextInt(9) + 1; + int remainingDigits = random.nextInt((int) Math.pow(10, digits - 1)); // 剩余位数 + + // 拼接生成的动态位数数字码 + return String.format("%d%0" + (digits - 1) + "d", firstDigit, remainingDigits); + } +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/vo/DeviceCodeVO.java b/main/manager-api/src/main/java/xiaozhi/modules/device/vo/DeviceCodeVO.java new file mode 100644 index 00000000..f5817dc1 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/vo/DeviceCodeVO.java @@ -0,0 +1,10 @@ +package xiaozhi.modules.device.vo; + +import lombok.Data; + +@Data +public class DeviceCodeVO { + private String deviceId; + private String deviceCode; + private String token; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/vo/DeviceOtaVO.java b/main/manager-api/src/main/java/xiaozhi/modules/device/vo/DeviceOtaVO.java new file mode 100644 index 00000000..c5af75af --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/vo/DeviceOtaVO.java @@ -0,0 +1,55 @@ +package xiaozhi.modules.device.vo; + +import lombok.Data; + +@Data +public class DeviceOtaVO { + private Activation activation; + private Mqtt mqtt; + private ServerTime server_time; + private Firmware firmware; + private String error; + + @Data + public static class Activation { + private String code; + private String message; + + public Activation() { + } + public Activation(String code, String message) { + this.code = code; + this.message = message; + } + } + + @Data + public class Mqtt { + + } + + @Data + public static class ServerTime { + private Long timestamp; + private Integer timezone_offset; + + public ServerTime() { + } + public ServerTime(Long timestamp, Integer timezone_offset) { + this.timestamp = timestamp; + this.timezone_offset = timezone_offset; + } + } + + @Data + public static class Firmware { + private String version; + private String url; + public Firmware() { + } + public Firmware(String version, String url) { + this.version = version; + this.url = url; + } + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/domain/ModelConfig.java b/main/manager-api/src/main/java/xiaozhi/modules/model/domain/ModelConfig.java new file mode 100644 index 00000000..b9967900 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/domain/ModelConfig.java @@ -0,0 +1,91 @@ +package xiaozhi.modules.model.domain; + +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 java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * 模型配置表 + * @TableName ai_model_config + */ +@TableName(value ="ai_model_config") +@Data +public class ModelConfig implements Serializable { + /** + * 主键 + */ + @TableId + private String id; + + /** + * 模型类型(Memory/ASR/VAD/LLM/TTS) + */ + private String modelType; + + /** + * 模型编码(如AliLLM、DoubaoTTS) + */ + private String modelCode; + + /** + * 模型名称 + */ + private String modelName; + + /** + * 是否默认配置(0否 1是) + */ + private Integer isDefault; + + /** + * 是否启用 + */ + private Integer isEnabled; + + /** + * 模型配置(JSON格式) + */ + private Object configJson; + + /** + * 官方文档链接 + */ + private String docLink; + + /** + * 备注 + */ + private String remark; + + /** + * 排序 + */ + private Integer sort; + + /** + * 创建者 + */ + private Long creator; + + /** + * 创建时间 + */ + private Date createDate; + + /** + * 更新者 + */ + private Long updater; + + /** + * 更新时间 + */ + private Date updateDate; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/domain/TtsVoice.java b/main/manager-api/src/main/java/xiaozhi/modules/model/domain/TtsVoice.java new file mode 100644 index 00000000..744bd6e8 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/domain/TtsVoice.java @@ -0,0 +1,81 @@ +package xiaozhi.modules.model.domain; + +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 java.io.Serializable; +import java.util.Date; +import lombok.Data; + +/** + * TTS 音色表 + * @TableName ai_tts_voice + */ +@TableName(value ="ai_tts_voice") +@Data +public class TtsVoice implements Serializable { + /** + * 主键 + */ + @TableId + private String id; + + /** + * 对应 TTS 模型主键 + */ + private String ttsModelId; + + /** + * 音色名称 + */ + private String name; + + /** + * 音色编码 + */ + private String ttsVoice; + + /** + * 语言 + */ + private String languages; + + /** + * 音色 Demo + */ + private String voiceDemo; + + /** + * 备注 + */ + private String remark; + + /** + * 排序 + */ + private Integer sort; + + /** + * 创建者 + */ + private Long creator; + + /** + * 创建时间 + */ + private Date createDate; + + /** + * 更新者 + */ + private Long updater; + + /** + * 更新时间 + */ + private Date updateDate; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/mapper/ModelConfigMapper.java b/main/manager-api/src/main/java/xiaozhi/modules/model/mapper/ModelConfigMapper.java new file mode 100644 index 00000000..0bbf4ca8 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/mapper/ModelConfigMapper.java @@ -0,0 +1,20 @@ +package xiaozhi.modules.model.mapper; + +import org.apache.ibatis.annotations.Mapper; +import xiaozhi.modules.model.domain.ModelConfig; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author chenerlei +* @description 针对表【ai_model_config(模型配置表)】的数据库操作Mapper +* @createDate 2025-03-22 15:31:57 +* @Entity xiaozhi.modules.model.domain.ModelConfig +*/ +@Mapper +public interface ModelConfigMapper extends BaseMapper { + +} + + + + diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/mapper/TtsVoiceMapper.java b/main/manager-api/src/main/java/xiaozhi/modules/model/mapper/TtsVoiceMapper.java new file mode 100644 index 00000000..c4ec5089 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/mapper/TtsVoiceMapper.java @@ -0,0 +1,20 @@ +package xiaozhi.modules.model.mapper; + +import org.apache.ibatis.annotations.Mapper; +import xiaozhi.modules.model.domain.TtsVoice; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author chenerlei +* @description 针对表【ai_tts_voice(TTS 音色表)】的数据库操作Mapper +* @createDate 2025-03-22 15:31:57 +* @Entity xiaozhi.modules.model.domain.TtsVoice +*/ +@Mapper +public interface TtsVoiceMapper extends BaseMapper { + +} + + + + diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelConfigService.java b/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelConfigService.java new file mode 100644 index 00000000..a82905eb --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelConfigService.java @@ -0,0 +1,13 @@ +package xiaozhi.modules.model.service; + +import xiaozhi.modules.model.domain.ModelConfig; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author chenerlei +* @description 针对表【ai_model_config(模型配置表)】的数据库操作Service +* @createDate 2025-03-22 15:31:57 +*/ +public interface ModelConfigService extends IService { + +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/service/TtsVoiceService.java b/main/manager-api/src/main/java/xiaozhi/modules/model/service/TtsVoiceService.java new file mode 100644 index 00000000..5432d715 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/TtsVoiceService.java @@ -0,0 +1,13 @@ +package xiaozhi.modules.model.service; + +import xiaozhi.modules.model.domain.TtsVoice; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author chenerlei +* @description 针对表【ai_tts_voice(TTS 音色表)】的数据库操作Service +* @createDate 2025-03-22 15:31:57 +*/ +public interface TtsVoiceService extends IService { + +} 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 new file mode 100644 index 00000000..f057e270 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java @@ -0,0 +1,22 @@ +package xiaozhi.modules.model.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import xiaozhi.modules.model.domain.ModelConfig; +import xiaozhi.modules.model.service.ModelConfigService; +import xiaozhi.modules.model.mapper.ModelConfigMapper; +import org.springframework.stereotype.Service; + +/** +* @author chenerlei +* @description 针对表【ai_model_config(模型配置表)】的数据库操作Service实现 +* @createDate 2025-03-22 15:31:57 +*/ +@Service +public class ModelConfigServiceImpl extends ServiceImpl + implements ModelConfigService{ + +} + + + + diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/TtsVoiceServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/TtsVoiceServiceImpl.java new file mode 100644 index 00000000..cb27eaa4 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/TtsVoiceServiceImpl.java @@ -0,0 +1,22 @@ +package xiaozhi.modules.model.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import xiaozhi.modules.model.domain.TtsVoice; +import xiaozhi.modules.model.service.TtsVoiceService; +import xiaozhi.modules.model.mapper.TtsVoiceMapper; +import org.springframework.stereotype.Service; + +/** +* @author chenerlei +* @description 针对表【ai_tts_voice(TTS 音色表)】的数据库操作Service实现 +* @createDate 2025-03-22 15:31:57 +*/ +@Service +public class TtsVoiceServiceImpl extends ServiceImpl + implements TtsVoiceService{ + +} + + + + diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java index de25d80e..74b85933 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java @@ -73,6 +73,8 @@ public class ShiroConfig { filterMap.put("/user/captcha", "anon"); filterMap.put("/user/login", "anon"); filterMap.put("/user/register", "anon"); + filterMap.put("/ota/**", "anon"); + filterMap.put("/user/agent/loadAgentConfig/**", "anon"); filterMap.put("/**", "oauth2"); shiroFilter.setFilterChainDefinitionMap(filterMap); diff --git a/main/manager-api/src/main/resources/application-dev.yml b/main/manager-api/src/main/resources/application-dev.yml index a8fdf21f..90a62b6a 100644 --- a/main/manager-api/src/main/resources/application-dev.yml +++ b/main/manager-api/src/main/resources/application-dev.yml @@ -41,4 +41,22 @@ spring: merge-sql: false wall: config: - multi-statement-allow: true \ No newline at end of file + multi-statement-allow: true + redis: + host: localhost # Redis服务器地址 + port: 6379 # Redis服务器连接端口 + password: # Redis服务器连接密码(默认为空) + database: 0 # Redis数据库索引(默认为0) +# jedis: +# pool: +# max-active: 8 # 连接池最大连接数(使用负值表示没有限制) +# max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制) +# max-idle: 8 # 连接池中的最大空闲连接 +# min-idle: 0 # 连接池中的最小空闲连接 + timeout: 10000ms # 连接超时时间(毫秒) + lettuce: + pool: + max-active: 8 # 连接池最大连接数(使用负值表示没有限制) + max-idle: 8 # 连接池中的最大空闲连接 + min-idle: 0 # 连接池中的最小空闲连接 + shutdown-timeout: 100ms # 客户端优雅关闭的等待时间 \ No newline at end of file diff --git a/main/manager-api/src/main/resources/application.yml b/main/manager-api/src/main/resources/application.yml index 967f610e..74dfb0e6 100644 --- a/main/manager-api/src/main/resources/application.yml +++ b/main/manager-api/src/main/resources/application.yml @@ -24,19 +24,6 @@ spring: max-file-size: 100MB max-request-size: 100MB enabled: true - data: - redis: - database: 0 - host: 127.0.0.1 - port: 6379 - password: # 密码(默认为空) - timeout: 6000ms # 连接超时时长(毫秒) - lettuce: - pool: - max-active: 1000 # 连接池最大连接数(使用负值表示没有限制) - max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制) - max-idle: 10 # 连接池中的最大空闲连接 - min-idle: 5 # 连接池中的最小空闲连接 main: allow-bean-definition-overriding: true @@ -51,7 +38,7 @@ knife4j: renren: redis: - open: false + open: true xss: enabled: true exclude-urls: @@ -60,7 +47,7 @@ renren: mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml #实体扫描,多个package用逗号或者分号分隔 - typeAliasesPackage: xiaozhi.modules.*.entity + typeAliasesPackage: xiaozhi.modules.*.entity;xiaozhi.modules.*.domain global-config: #数据库相关配置 db-config: diff --git a/main/manager-api/src/main/resources/mapper/agent/AgentMapper.xml b/main/manager-api/src/main/resources/mapper/agent/AgentMapper.xml new file mode 100644 index 00000000..7d8ddc74 --- /dev/null +++ b/main/manager-api/src/main/resources/mapper/agent/AgentMapper.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + id,user_id,agent_code,agent_name,asr_model_id,vad_model_id, + llm_model_id,tts_model_id,tts_voice_id,mem_model_id,intent_model_id, + system_prompt,lang_code,language,sort,creator, + created_at,updater,updated_at + + diff --git a/main/manager-api/src/main/resources/mapper/agent/AgentTemplateMapper.xml b/main/manager-api/src/main/resources/mapper/agent/AgentTemplateMapper.xml new file mode 100644 index 00000000..833bcd0e --- /dev/null +++ b/main/manager-api/src/main/resources/mapper/agent/AgentTemplateMapper.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + id,agent_code,agent_name,asr_model_id,vad_model_id,llm_model_id, + tts_model_id,tts_voice_id,mem_model_id,intent_model_id,system_prompt, + lang_code,language,sort,is_default,creator, + created_at,updater,updated_at + + diff --git a/main/manager-api/src/main/resources/mapper/device/DeviceMapper.xml b/main/manager-api/src/main/resources/mapper/device/DeviceMapper.xml new file mode 100644 index 00000000..743003f7 --- /dev/null +++ b/main/manager-api/src/main/resources/mapper/device/DeviceMapper.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + id,user_id,mac_address,last_connected_at,auto_update,board, + alias,agent_id,app_version,sort,creator, + create_date,updater,update_date + + diff --git a/main/manager-api/src/main/resources/mapper/model/ModelConfigMapper.xml b/main/manager-api/src/main/resources/mapper/model/ModelConfigMapper.xml new file mode 100644 index 00000000..90cf097b --- /dev/null +++ b/main/manager-api/src/main/resources/mapper/model/ModelConfigMapper.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + id,model_type,model_code,model_name,is_default,is_enabled, + config_json,doc_link,remark,sort,creator, + create_date,updater,update_date + + diff --git a/main/manager-api/src/main/resources/mapper/model/TtsVoiceMapper.xml b/main/manager-api/src/main/resources/mapper/model/TtsVoiceMapper.xml new file mode 100644 index 00000000..008cc4ed --- /dev/null +++ b/main/manager-api/src/main/resources/mapper/model/TtsVoiceMapper.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + id,tts_model_id,name,tts_voice,languages,voice_demo, + remark,sort,creator,create_date,updater, + update_date + + diff --git a/main/manager-web/src/apis/api.js b/main/manager-web/src/apis/api.js index 38c2f86b..03a5b5a2 100755 --- a/main/manager-web/src/apis/api.js +++ b/main/manager-web/src/apis/api.js @@ -9,9 +9,9 @@ import user from './module/user.js' * 如果你想调用8002端口,就用'/xiaozhi-esp32-api/api/v1',请与vue.config.js的devServer配置相结合,方便跨域请求 * */ -const DEV_API_SERVICE = 'https://m1.apifoxmock.com/m1/5931378-5618560-default' +// const DEV_API_SERVICE = 'https://m1.apifoxmock.com/m1/5931378-5618560-default' // 8002开发完成完成后使用这个 -// const DEV_API_SERVICE = '/xiaozhi-esp32-api' +const DEV_API_SERVICE = '/xiaozhi-esp32-api' /** * 根据开发环境返回接口url diff --git a/main/manager-web/src/apis/module/agent.js b/main/manager-web/src/apis/module/agent.js index 721f075c..e3c2c538 100755 --- a/main/manager-web/src/apis/module/agent.js +++ b/main/manager-web/src/apis/module/agent.js @@ -16,20 +16,17 @@ export default { }).send() }, //添加智能体 - addAgent(name, callback) { + addAgent(agentName, callback) { RequestService.sendRequest() .url(`${getServiceUrl()}/api/v1/user/agent`) .method('POST') - .data({name}) + .data({agentName}) .success((res) => { RequestService.clearRequestTime(); callback(res); }) .fail((err) => { console.error('添加智能体失败:', err); - RequestService.reAjaxFun(() => { - this.addAgent(name, callback); - }); }).send(); }, //获取智能体配置 diff --git a/main/manager-web/src/apis/module/device.js b/main/manager-web/src/apis/module/device.js index cd568745..e16bec25 100755 --- a/main/manager-web/src/apis/module/device.js +++ b/main/manager-web/src/apis/module/device.js @@ -4,9 +4,10 @@ import {getServiceUrl} from '../api' export default { //设备列表 - getDeviceList(agent_id, callback) { + getDeviceList(agent_id, page, callback) { RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/agent/device/bind/${agent_id}`) .method('GET') + .data(page) .success((res) => { RequestService.clearRequestTime() callback(res) @@ -16,11 +17,11 @@ export default { }).send() }, //绑定设备 - bindDevice(agent_id, code, callback) { + bindDevice(agentId, deviceCode, callback) { RequestService.sendRequest() - .url(`${getServiceUrl()}/api/v1/user/agent/device/bind/${agent_id}`) + .url(`${getServiceUrl()}/api/v1/user/agent/device/bind/${agentId}`) .method('POST') - .data({code}) + .data({deviceCode}) .success((res) => { RequestService.clearRequestTime(); callback(res); @@ -28,14 +29,14 @@ export default { .fail((err) => { console.error('绑定设备失败:', err); RequestService.reAjaxFun(() => { - this.addDevice(name, callback); + this.addDevice(agentId, deviceCode, callback); }); }).send(); }, // 解绑设备 - unbindDevice(device_id, callback) { + unbindDevice(deviceId, callback) { RequestService.sendRequest() - .url(`${getServiceUrl()}/api/v1/user/device/unbind/${device_id}`) + .url(`${getServiceUrl()}/api/v1/user/agent/device/unbind/${deviceId}`) .method('PUT') .success((res) => { RequestService.clearRequestTime(); @@ -43,7 +44,7 @@ export default { }) .fail(() => { RequestService.reAjaxFun(() => { - this.unbindDevice(device_id, callback); + this.unbindDevice(deviceId, callback); }); }).send() }, diff --git a/main/manager-web/src/components/AddAgentDialog.vue b/main/manager-web/src/components/AddAgentDialog.vue index 026b646e..e805897f 100644 --- a/main/manager-web/src/components/AddAgentDialog.vue +++ b/main/manager-web/src/components/AddAgentDialog.vue @@ -46,12 +46,13 @@ export default { this.$message.error('请输入智能体名称'); return; } - userApi.addAgent(this.agentName, (res) => { - this.$message.success('添加成功'); - this.$emit('confirm', res); + this.$emit('confirm',this.agentName) + // userApi.addAgent(this.agentName, (res) => { + // this.$message.success('添加成功'); + // this.$emit('confirm', res); this.$emit('update:visible', false); this.agentName = ""; - }); + // }); }, cancel() { this.$emit('update:visible', false) diff --git a/main/manager-web/src/components/AddDeviceDialog.vue b/main/manager-web/src/components/AddDeviceDialog.vue index f98ff7d9..5b94090e 100644 --- a/main/manager-web/src/components/AddDeviceDialog.vue +++ b/main/manager-web/src/components/AddDeviceDialog.vue @@ -40,8 +40,12 @@ export default { }, methods: { confirm() { + if (!this.deviceCode.trim()) { + this.$message.error('请输入设备验证码'); + return; + } + this.$emit('confirm',this.deviceCode) this.$emit('update:visible', false) - this.$emit('added', this.deviceCode) this.deviceCode = "" }, cancel() { diff --git a/main/manager-web/src/utils/date.js b/main/manager-web/src/utils/date.js index c6210bc0..afc8a339 100755 --- a/main/manager-web/src/utils/date.js +++ b/main/manager-web/src/utils/date.js @@ -39,10 +39,13 @@ function formatDateTool(date, fmt) { const o = { 'M+': date.getMonth() + 1, 'd+': date.getDate(), - 'h+': date.getHours(), + 'H+': date.getHours(), + 'h+': date.getHours() > 12 ? date.getHours() - 12 : date.getHours(), 'm+': date.getMinutes(), 's+': date.getSeconds() } + // 12小时制 + const is_12Hours = fmt.indexOf('hh') > -1; for (const k in o) { if (new RegExp(`(${k})`).test(fmt)) { const str = o[k] + '' @@ -52,6 +55,8 @@ function formatDateTool(date, fmt) { ) } } + // 12小时制 + fmt = is_12Hours ? date.getHours() > 12 ? fmt + " PM" : fmt + " AM" : fmt return fmt } diff --git a/main/manager-web/src/views/device.vue b/main/manager-web/src/views/device.vue index bbfe41fc..4a60bdde 100644 --- a/main/manager-web/src/views/device.vue +++ b/main/manager-web/src/views/device.vue @@ -18,50 +18,53 @@ + 添加设备 - - - - + + + + + - +