diff --git a/docs/firmware-setting.md b/docs/firmware-setting.md index ae66b6e9..0ed350a0 100644 --- a/docs/firmware-setting.md +++ b/docs/firmware-setting.md @@ -1,7 +1,7 @@ # 基于虾哥编译好的固件配置自定义服务器 ## 第1步 确认版本 -烧录虾哥已经编译好的1.6.1版本固件 +烧录虾哥已经编译好的[1.6.1版本以上固件](https://github.com/78/xiaozhi-esp32/releases) ## 第2步 准备你的ota地址 如果你按照教程使用的是全模块部署,就应该会有ota地址。 @@ -29,6 +29,7 @@ wss://2662r3426b.vicp.fun/xiaozhi/v1/ ## 第3步 进入配网模式 进入机器的配网模式,在页面顶部,点击“高级选项”,在里面输入你服务器的`ota`地址,点击保存。重启设备 +![请参考-OTA地址设置](../docs/images/firmware-setting-ota.png) ## 第4步 唤醒小智,查看日志输出 diff --git a/docs/fish-speech-integration.md b/docs/fish-speech-integration.md new file mode 100644 index 00000000..b916e619 --- /dev/null +++ b/docs/fish-speech-integration.md @@ -0,0 +1,72 @@ +登录AutoDL,租赁镜像 +选择镜像: +``` +PyTorch / 2.1.0 / 3.10(ubuntu22.04) / cuda 12.1 +``` + +机器开机后,设置学术加速 +``` +source /etc/network_turbo +``` + +进入工作目录 +``` +cd autodl-tmp/ +``` + +拉取项目 +``` +git clone https://gitclone.com/github.com/fishaudio/fish-speech.git ; cd fish-speech +``` + +安装依赖 +``` +pip install -e. +``` + +如果报错,安装portaudio +``` +apt-get install portaudio19-dev -y +``` + +安装后执行 +``` +pip install torch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 --index-url https://download.pytorch.org/whl/cu121 +``` + +下载模型 +``` +cd tools +python download_models.py +``` + +下载完模型后运行接口 +``` +python -m tools.api_server --listen 0.0.0.0:6006 +``` + +然后用浏览器去到aotodl实例页面 +``` +https://autodl.com/console/instance/list +``` + +如下图点击你刚才机器的`自定义服务`按钮,开启端口转发服务 +![自定义服务](images/fishspeech/autodl-01.png) + +端口转发服务设置完成后,你本地电脑打开网址`http://localhost:6006/`,就可以访问fish-speech的接口了 +![服务预览](images/fishspeech/autodl-02.png) + + +如果你是单模块部署,核心配置如下 +``` +selected_module: + TTS: FishSpeech +TTS: + FishSpeech: + reference_audio: ["config/assets/wakeup_words.wav",] + reference_text: ["哈啰啊,我是小智啦,声音好听的台湾女孩一枚,超开心认识你耶,最近在忙啥,别忘了给我来点有趣的料哦,我超爱听八卦的啦",] + api_key: "123" + api_url: "http://127.0.0.1:6006/v1/tts" +``` + +然后重启服务 \ No newline at end of file diff --git a/docs/images/firmware-setting-ota.png b/docs/images/firmware-setting-ota.png new file mode 100644 index 00000000..c151fd93 Binary files /dev/null and b/docs/images/firmware-setting-ota.png differ diff --git a/docs/images/fishspeech/autodl-01.png b/docs/images/fishspeech/autodl-01.png new file mode 100644 index 00000000..695b94b8 Binary files /dev/null and b/docs/images/fishspeech/autodl-01.png differ diff --git a/docs/images/fishspeech/autodl-02.png b/docs/images/fishspeech/autodl-02.png new file mode 100644 index 00000000..e3f72afe Binary files /dev/null and b/docs/images/fishspeech/autodl-02.png differ diff --git a/main/manager-api/src/main/java/xiaozhi/common/config/AsyncConfig.java b/main/manager-api/src/main/java/xiaozhi/common/config/AsyncConfig.java new file mode 100644 index 00000000..d0cc2dfc --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/common/config/AsyncConfig.java @@ -0,0 +1,40 @@ +package xiaozhi.common.config; + +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@Configuration +@EnableAsync +@EnableAspectJAutoProxy(exposeProxy = true) +public class AsyncConfig { + + @Bean(name = "taskExecutor") + public Executor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(4); + executor.setQueueCapacity(1000); + executor.setThreadNamePrefix("AsyncThread-"); + // 设置拒绝策略:由调用线程执行 + executor.setRejectedExecutionHandler(new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + try { + // 如果线程池已满,则由调用线程执行 + r.run(); + } catch (Exception e) { + throw new RuntimeException("执行异步任务失败", e); + } + } + }); + executor.initialize(); + return executor; + } +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java index 9c0c5be7..c6ec6761 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java +++ b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java @@ -1,5 +1,7 @@ package xiaozhi.common.constant; +import lombok.Getter; + /** * 常量 * Copyright (c) 人人开源 All rights reserved. @@ -109,6 +111,11 @@ public interface Constant { */ String FILE_EXTENSION_SEG = "."; + /** + * 无记忆 + */ + String MEMORY_NO_MEM = "Memory_nomem"; + enum SysBaseParam { /** * 系统全称 @@ -214,8 +221,28 @@ public interface Constant { } } + @Getter + enum ChatHistoryConfEnum { + IGNORE(0, "不记录"), + RECORD_TEXT(1, "记录文本"), + RECORD_TEXT_AUDIO(2, "文本音频都记录"); + + private final int code; + private final String name; + + ChatHistoryConfEnum(int code, String name) { + this.code = code; + this.name = name; + } + } + /** * 版本号 */ - public static final String VERSION = "0.4.2"; + public static final String VERSION = "0.4.4"; + + /** + * 无效固件URL + */ + String INVALID_FIRMWARE_URL = "http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL"; } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/common/exception/RenExceptionHandler.java b/main/manager-api/src/main/java/xiaozhi/common/exception/RenExceptionHandler.java index 0d0aa118..df9023ae 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/exception/RenExceptionHandler.java +++ b/main/manager-api/src/main/java/xiaozhi/common/exception/RenExceptionHandler.java @@ -1,7 +1,10 @@ package xiaozhi.common.exception; import org.apache.shiro.authz.UnauthorizedException; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.dao.DuplicateKeyException; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.resource.NoResourceFoundException; @@ -10,6 +13,8 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import xiaozhi.common.utils.Result; +import java.util.List; +import java.util.Objects; /** * 异常处理器 * Copyright (c) 人人开源 All rights reserved. @@ -60,4 +65,16 @@ public class RenExceptionHandler { return new Result().error(404, "资源不存在"); } + @ExceptionHandler(MethodArgumentNotValidException.class) + public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { + StringBuilder sb = new StringBuilder(); + List allErrors = ex.getBindingResult().getAllErrors(); + String errorMsg = allErrors.stream() + .filter(Objects::nonNull) + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .findFirst() + .orElse(""); + return new Result().error(400, errorMsg); + } + } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/common/service/impl/BaseServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/common/service/impl/BaseServiceImpl.java index 9d2ba5d5..9d06c09a 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/service/impl/BaseServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/common/service/impl/BaseServiceImpl.java @@ -1,6 +1,7 @@ package xiaozhi.common.service.impl; import java.io.Serializable; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; @@ -18,6 +19,7 @@ import com.baomidou.mybatisplus.core.enums.SqlMethod; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.baomidou.mybatisplus.core.toolkit.Constants; import com.baomidou.mybatisplus.core.toolkit.ReflectionKit; import com.baomidou.mybatisplus.core.toolkit.StringUtils; @@ -45,6 +47,12 @@ public abstract class BaseServiceImpl, T> implements Bas * @param params 分页查询参数 * @param defaultOrderField 默认排序字段 * @param isAsc 排序方式 + * @see xiaozhi.common.constant.Constant + * params.put(Constant.PAGE, "1"); + * params.put(Constant.LIMIT, "10"); + * params.put(Constant.ORDER_FIELD, "field"); // 单个字段 + * params.put(Constant.ORDER_FIELD, List.of("field1", "field2")); // 多个字段 + * params.put(Constant.ORDER, "asc"); */ protected IPage getPage(Map params, String defaultOrderField, boolean isAsc) { // 分页参数 @@ -65,28 +73,34 @@ public abstract class BaseServiceImpl, T> implements Bas params.put(Constant.PAGE, page); // 排序字段 - String orderField = (String) params.get(Constant.ORDER_FIELD); + Object orderField = params.get(Constant.ORDER_FIELD); String order = (String) params.get(Constant.ORDER); - // 前端字段排序 - if (StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)) { - if (Constant.ASC.equalsIgnoreCase(order)) { - return page.addOrder(OrderItem.asc(orderField)); + List orderFields = new ArrayList<>(); + + // 处理排序字段 + if (orderField instanceof String) { + orderFields.add((String) orderField); + } else if (orderField instanceof List) { + orderFields.addAll((List) orderField); + } + + // 有排序字段则排序 + if (CollectionUtils.isNotEmpty(orderFields)) { + if (StringUtils.isNotBlank(order) && Constant.ASC.equalsIgnoreCase(order)) { + return page.addOrder(OrderItem.ascs(orderFields.toArray(new String[0]))); } else { - return page.addOrder(OrderItem.desc(orderField)); + return page.addOrder(OrderItem.descs(orderFields.toArray(new String[0]))); } } - // 没有排序字段,则不排序 - if (StringUtils.isBlank(defaultOrderField)) { - return page; - } - - // 默认排序 - if (isAsc) { - page.addOrder(OrderItem.asc(defaultOrderField)); - } else { - page.addOrder(OrderItem.desc(defaultOrderField)); + // 没有排序字段,使用默认排序 + if (StringUtils.isNotBlank(defaultOrderField)) { + if (isAsc) { + page.addOrder(OrderItem.asc(defaultOrderField)); + } else { + page.addOrder(OrderItem.desc(defaultOrderField)); + } } return page; diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java index 9368cf6b..54f3fa8c 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java @@ -39,6 +39,7 @@ import xiaozhi.modules.agent.dto.AgentChatHistoryDTO; import xiaozhi.modules.agent.dto.AgentChatSessionDTO; import xiaozhi.modules.agent.dto.AgentCreateDTO; import xiaozhi.modules.agent.dto.AgentDTO; +import xiaozhi.modules.agent.dto.AgentMemoryDTO; import xiaozhi.modules.agent.dto.AgentUpdateDTO; import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentTemplateEntity; @@ -46,6 +47,7 @@ import xiaozhi.modules.agent.service.AgentChatAudioService; import xiaozhi.modules.agent.service.AgentChatHistoryService; import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.AgentTemplateService; +import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.security.user.SecurityUser; @@ -109,6 +111,8 @@ public class AgentController { entity.setMemModelId(template.getMemModelId()); entity.setIntentModelId(template.getIntentModelId()); entity.setSystemPrompt(template.getSystemPrompt()); + entity.setSummaryMemory(template.getSummaryMemory()); + entity.setChatHistoryConf(template.getChatHistoryConf()); entity.setLangCode(template.getLangCode()); entity.setLanguage(template.getLanguage()); } @@ -125,10 +129,26 @@ public class AgentController { return new Result().ok(entity.getId()); } + @PutMapping("/saveMemory/{macAddress}") + @Operation(summary = "根据设备id更新智能体") + public Result updateByDeviceId(@PathVariable String macAddress, @RequestBody @Valid AgentMemoryDTO dto) { + DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress); + if (device == null) { + return new Result<>(); + } + AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO(); + agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory()); + return updateAgentById(device.getAgentId(), agentUpdateDTO); + } + @PutMapping("/{id}") @Operation(summary = "更新智能体") @RequiresPermissions("sys:role:normal") public Result update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) { + return updateAgentById(id, dto); + } + + private Result updateAgentById(String id, AgentUpdateDTO dto) { // 先查询现有实体 AgentEntity existingEntity = agentService.getAgentById(id); if (existingEntity == null) { @@ -166,6 +186,12 @@ public class AgentController { if (dto.getSystemPrompt() != null) { existingEntity.setSystemPrompt(dto.getSystemPrompt()); } + if (dto.getSummaryMemory() != null) { + existingEntity.setSummaryMemory(dto.getSummaryMemory()); + } + if (dto.getChatHistoryConf() != null) { + existingEntity.setChatHistoryConf(dto.getChatHistoryConf()); + } if (dto.getLangCode() != null) { existingEntity.setLangCode(dto.getLangCode()); } @@ -181,8 +207,16 @@ public class AgentController { existingEntity.setUpdater(user.getId()); existingEntity.setUpdatedAt(new Date()); + // 更新记忆策略 + if (existingEntity.getMemModelId() == null || existingEntity.getMemModelId().equals(Constant.MEMORY_NO_MEM)) { + // 删除所有记录 + agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, true); + existingEntity.setSummaryMemory(""); + } else if (existingEntity.getChatHistoryConf() != null && existingEntity.getChatHistoryConf() == 1) { + // 删除音频数据 + agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false); + } agentService.updateById(existingEntity); - return new Result<>(); } @@ -193,7 +227,7 @@ public class AgentController { // 先删除关联的设备 deviceService.deleteByAgentId(id); // 删除关联的聊天记录 - agentChatHistoryService.deleteByAgentId(id); + agentChatHistoryService.deleteByAgentId(id, true, true); // 再删除智能体 agentService.deleteById(id); return new Result<>(); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatHistoryDao.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatHistoryDao.java index 43a5f1ce..b75312d2 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatHistoryDao.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatHistoryDao.java @@ -28,4 +28,11 @@ public interface AiAgentChatHistoryDao extends BaseMapper imp @Override public AgentEntity getAgentById(String id) { - return agentDao.selectById(id); + AgentEntity agent = agentDao.selectById(id); + if (agent != null && agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) { + agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode()); + } else if (agent != null && agent.getMemModelId() != null + && !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM) + && agent.getChatHistoryConf() == null) { + agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode()); + } + return agent; } @Override @@ -93,6 +102,9 @@ public class AgentServiceImpl extends BaseServiceImpl imp // 获取 LLM 模型名称 dto.setLlmModelName(modelConfigService.getModelNameById(agent.getLlmModelId())); + // 获取记忆模型名称 + dto.setMemModelId(agent.getMemModelId()); + // 获取 TTS 音色名称 dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId())); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java index ee42c934..592b3c1d 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java @@ -9,6 +9,7 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import lombok.AllArgsConstructor; +import xiaozhi.common.constant.Constant; import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.RenException; import xiaozhi.common.redis.RedisKeys; @@ -64,6 +65,7 @@ public class ConfigServiceImpl implements ConfigService { null, null, null, + null, agent.getVadModelId(), agent.getAsrModelId(), null, @@ -108,6 +110,17 @@ public class ConfigServiceImpl implements ConfigService { // 获取单台设备每天最多输出字数 String deviceMaxOutputSize = sysParamsService.getValue("device_max_output_size", true); result.put("device_max_output_size", deviceMaxOutputSize); + + // 获取聊天记录配置 + Integer chatHistoryConf = agent.getChatHistoryConf(); + if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) { + chatHistoryConf = Constant.ChatHistoryConfEnum.IGNORE.getCode(); + } else if (agent.getMemModelId() != null + && !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM) + && agent.getChatHistoryConf() == null) { + chatHistoryConf = Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode(); + } + result.put("chat_history_conf", chatHistoryConf); // 如果客户端已实例化模型,则不返回 String alreadySelectedVadModelId = (String) selectedModule.get("VAD"); if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) { @@ -122,6 +135,7 @@ public class ConfigServiceImpl implements ConfigService { buildModuleConfig( agent.getAgentName(), agent.getSystemPrompt(), + agent.getSummaryMemory(), voice, agent.getVadModelId(), agent.getAsrModelId(), @@ -222,6 +236,7 @@ public class ConfigServiceImpl implements ConfigService { private void buildModuleConfig( String assistantName, String prompt, + String summaryMemory, String voice, String vadModelId, String asrModelId, @@ -282,5 +297,6 @@ public class ConfigServiceImpl implements ConfigService { prompt = prompt.replace("{{assistant_name}}", StringUtils.isBlank(assistantName) ? "小智" : assistantName); } result.put("prompt", prompt); + result.put("summaryMemory", summaryMemory); } } 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 19ab7178..ee31c41f 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 @@ -9,6 +9,8 @@ import java.util.TimeZone; import java.util.UUID; import org.apache.commons.lang3.StringUtils; +import org.springframework.aop.framework.AopContext; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; @@ -54,6 +56,24 @@ public class DeviceServiceImpl extends BaseServiceImpl private final RedisUtils redisUtils; private final OtaService otaService; + @Async + public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) { + try { + DeviceEntity device = new DeviceEntity(); + device.setId(deviceId); + device.setLastConnectedAt(new Date()); + if (StringUtils.isNotBlank(appVersion)) { + device.setAppVersion(appVersion); + } + deviceDao.updateById(device); + if (StringUtils.isNotBlank(agentId)) { + redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date()); + } + } catch (Exception e) { + log.error("异步更新设备连接信息失败", e); + } + } + @Override public Boolean deviceActivation(String agentId, String activationCode) { if (StringUtils.isBlank(activationCode)) { @@ -118,14 +138,20 @@ public class DeviceServiceImpl extends BaseServiceImpl DeviceEntity deviceById = getDeviceByMacAddress(macAddress); - // 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息 - if (deviceById != null && deviceById.getAutoUpdate() != 0) { - String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType(); - DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type, - deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion()); + // 设备未绑定,则返回当前上传的固件信息(不更新)以此兼容旧固件版本 + if (deviceById == null) { + DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware(); + firmware.setVersion(deviceReport.getApplication().getVersion()); + firmware.setUrl(Constant.INVALID_FIRMWARE_URL); response.setFirmware(firmware); } else { - response.setFirmware(null); + // 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息 + if (deviceById.getAutoUpdate() != 0) { + String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType(); + DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type, + deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion()); + response.setFirmware(firmware); + } } // 添加WebSocket配置 @@ -150,13 +176,12 @@ public class DeviceServiceImpl extends BaseServiceImpl response.setWebsocket(websocket); if (deviceById != null) { - // 如果设备存在,则更新上次连接时间 - deviceById.setLastConnectedAt(new Date()); - if (deviceReport.getApplication() != null - && StringUtils.isNotBlank(deviceReport.getApplication().getVersion())) { - deviceById.setAppVersion(deviceReport.getApplication().getVersion()); - } - deviceDao.updateById(deviceById); + // 如果设备存在,则异步更新上次连接时间和版本信息 + String appVersion = deviceReport.getApplication() != null ? deviceReport.getApplication().getVersion() + : null; + // 通过Spring代理调用异步方法 + ((DeviceServiceImpl) AopContext.currentProxy()).updateDeviceConnectionInfo(deviceById.getAgentId(), + deviceById.getId(), appVersion); } else { // 如果设备不存在,则生成激活码 DeviceReportRespDTO.Activation code = buildActivation(macAddress, deviceReport); @@ -353,7 +378,7 @@ public class DeviceServiceImpl extends BaseServiceImpl } firmware.setVersion(ota == null ? currentVersion : ota.getVersion()); - firmware.setUrl(downloadUrl == null ? "" : downloadUrl); + firmware.setUrl(downloadUrl == null ? Constant.INVALID_FIRMWARE_URL : downloadUrl); return firmware; } @@ -385,4 +410,4 @@ public class DeviceServiceImpl extends BaseServiceImpl } return 0; } -} \ No newline at end of file +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/controller/ModelProviderController.java b/main/manager-api/src/main/java/xiaozhi/modules/model/controller/ModelProviderController.java new file mode 100644 index 00000000..fc534a0a --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/controller/ModelProviderController.java @@ -0,0 +1,68 @@ +package xiaozhi.modules.model.controller; + +import java.util.List; + +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import xiaozhi.common.page.PageData; +import xiaozhi.common.utils.Result; +import xiaozhi.common.validator.group.UpdateGroup; +import xiaozhi.modules.model.dto.ModelProviderDTO; +import xiaozhi.modules.model.service.ModelProviderService; + +@AllArgsConstructor +@RestController +@RequestMapping("/models/provider") +@Tag(name = "模型供应器") +public class ModelProviderController { + + private final ModelProviderService modelProviderService; + + @GetMapping + @Operation(summary = "获取模型供应器列表") + @RequiresPermissions("sys:role:superAdmin") + public Result> getListPage(ModelProviderDTO modelProviderDTO, + @RequestParam(required = true, defaultValue = "0") String page, + @RequestParam(required = true, defaultValue = "10") String limit) { + return new Result>() + .ok(modelProviderService.getListPage(modelProviderDTO, page, limit)); + } + + @PostMapping + @Operation(summary = "新增模型供应器") + @RequiresPermissions("sys:role:superAdmin") + public Result add(@RequestBody @Validated ModelProviderDTO modelProviderDTO) { + ModelProviderDTO resp = modelProviderService.add(modelProviderDTO); + return new Result().ok(resp); + } + + @PutMapping + @Operation(summary = "修改模型供应器") + @RequiresPermissions("sys:role:superAdmin") + public Result edit(@RequestBody @Validated(UpdateGroup.class) ModelProviderDTO modelProviderDTO) { + ModelProviderDTO resp = modelProviderService.edit(modelProviderDTO); + return new Result().ok(resp); + } + + @PostMapping("/delete") + @Operation(summary = "删除模型供应器") + @RequiresPermissions("sys:role:superAdmin") + @Parameter(name = "ids", description = "ID数组", required = true) + public Result delete(@RequestBody List ids) { + modelProviderService.delete(ids); + return new Result<>(); + } + +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/dto/ModelProviderDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/model/dto/ModelProviderDTO.java index 605ed13a..317820d5 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/dto/ModelProviderDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/dto/ModelProviderDTO.java @@ -8,29 +8,37 @@ import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Data; +import xiaozhi.common.validator.group.UpdateGroup; @Data @Schema(description = "模型供应器/商") public class ModelProviderDTO implements Serializable { - // - // @Schema(description = "主键") - // private Long id; + @Schema(description = "主键") + @NotBlank(message = "id不能为空", groups = UpdateGroup.class) + private String id; @Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)") + @NotBlank(message = "modelType不能为空") private String modelType; @Schema(description = "供应器类型") + @NotBlank(message = "providerCode不能为空") private String providerCode; @Schema(description = "供应器名称") + @NotBlank(message = "name不能为空") private String name; @Schema(description = "供应器字段列表(JSON格式)") @TableField(typeHandler = JacksonTypeHandler.class) + @NotBlank(message = "fields(JSON格式)不能为空") private String fields; @Schema(description = "排序") + @NotNull(message = "sort不能为空") private Integer sort; @Schema(description = "更新者") diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/entity/ModelProviderEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/model/entity/ModelProviderEntity.java index 62e84f67..00904759 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/entity/ModelProviderEntity.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/entity/ModelProviderEntity.java @@ -3,10 +3,8 @@ package xiaozhi.modules.model.entity; import java.util.Date; import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -30,7 +28,6 @@ public class ModelProviderEntity { private String name; @Schema(description = "供应器字段列表(JSON格式)") - @TableField(typeHandler = JacksonTypeHandler.class) private String fields; @Schema(description = "排序") diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelProviderService.java b/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelProviderService.java index 2ace101e..d41766f6 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelProviderService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelProviderService.java @@ -2,8 +2,8 @@ package xiaozhi.modules.model.service; import java.util.List; +import xiaozhi.common.page.PageData; import xiaozhi.modules.model.dto.ModelProviderDTO; -import xiaozhi.modules.model.entity.ModelProviderEntity; public interface ModelProviderService { @@ -11,11 +11,15 @@ public interface ModelProviderService { List getListByModelType(String modelType); - ModelProviderDTO add(ModelProviderEntity modelProviderEntity); + ModelProviderDTO add(ModelProviderDTO modelProviderDTO); - ModelProviderDTO edit(ModelProviderEntity modelProviderEntity); + ModelProviderDTO edit(ModelProviderDTO modelProviderDTO); - void delete(); + void delete(String id); + + void delete(List id); + + PageData getListPage(ModelProviderDTO modelProviderDTO, String page, String limit); List getList(String modelType, String provideCode); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelProviderServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelProviderServiceImpl.java index fb06e4d5..ffeec288 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelProviderServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelProviderServiceImpl.java @@ -1,19 +1,29 @@ package xiaozhi.modules.model.service.impl; +import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import cn.hutool.json.JSONArray; import lombok.AllArgsConstructor; +import xiaozhi.common.constant.Constant; +import xiaozhi.common.exception.RenException; +import xiaozhi.common.page.PageData; import xiaozhi.common.service.impl.BaseServiceImpl; +import xiaozhi.common.user.UserDetail; import xiaozhi.common.utils.ConvertUtils; import xiaozhi.modules.model.dao.ModelProviderDao; import xiaozhi.modules.model.dto.ModelProviderDTO; import xiaozhi.modules.model.entity.ModelProviderEntity; import xiaozhi.modules.model.service.ModelProviderService; +import xiaozhi.modules.security.user.SecurityUser; @Service @AllArgsConstructor @@ -32,18 +42,78 @@ public class ModelProviderServiceImpl extends BaseServiceImpl getListPage(ModelProviderDTO modelProviderDTO, String page, String limit) { + + Map params = new HashMap(); + params.put(Constant.PAGE, page); + params.put(Constant.LIMIT, limit); + params.put(Constant.ORDER_FIELD, List.of("model_type", "sort")); + params.put(Constant.ORDER, "asc"); + + IPage pageParam = getPage(params, null, true); + + QueryWrapper wrapper = new QueryWrapper(); + + if (StringUtils.isNotBlank(modelProviderDTO.getModelType())) { + wrapper.eq("model_type", modelProviderDTO.getModelType()); + } + + if (StringUtils.isNotBlank(modelProviderDTO.getName())) { + wrapper.and(w -> w.like("name", modelProviderDTO.getName()) + .or() + .like("provider_code", modelProviderDTO.getName())); + } + return getPageData(modelProviderDao.selectPage(pageParam, wrapper), ModelProviderDTO.class); + } + + public static void main(String[] args) { + String jsonString = "\"[]\""; + JSONArray jsonArray = new JSONArray(jsonString); + System.out.println("字符串转 JSONArray: " + jsonArray.toString()); } @Override - public ModelProviderDTO edit(ModelProviderEntity modelProviderEntity) { - return null; + public ModelProviderDTO add(ModelProviderDTO modelProviderDTO) { + UserDetail user = SecurityUser.getUser(); + modelProviderDTO.setCreator(user.getId()); + modelProviderDTO.setUpdater(user.getId()); + modelProviderDTO.setCreateDate(new Date()); + modelProviderDTO.setUpdateDate(new Date()); + // 去除Fields左右的双引号 + + modelProviderDTO.setFields(modelProviderDTO.getFields()); + ModelProviderEntity entity = ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class); + if (modelProviderDao.insert(entity) == 0) { + throw new RenException("新增数据失败"); + } + + return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class); } @Override - public void delete() { + public ModelProviderDTO edit(ModelProviderDTO modelProviderDTO) { + UserDetail user = SecurityUser.getUser(); + modelProviderDTO.setUpdater(user.getId()); + modelProviderDTO.setUpdateDate(new Date()); + if (modelProviderDao + .updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) { + throw new RenException("修改数据失败"); + } + return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class); + } + @Override + public void delete(String id) { + if (modelProviderDao.deleteById(id) == 0) { + throw new RenException("删除数据失败"); + } + } + + @Override + public void delete(List ids) { + if (modelProviderDao.deleteBatchIds(ids) == 0) { + throw new RenException("删除数据失败"); + } } @Override 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 318ceeff..663adca0 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 @@ -87,6 +87,7 @@ public class ShiroConfig { // 将config路径使用server服务过滤器 filterMap.put("/config/**", "server"); filterMap.put("/agent/chat-history/report", "server"); + filterMap.put("/agent/saveMemory/**", "server"); filterMap.put("/agent/play/**", "anon"); filterMap.put("/**", "oauth2"); shiroFilter.setFilterChainDefinitionMap(filterMap); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserServiceImpl.java index 97944ba9..54048fe0 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserServiceImpl.java @@ -159,7 +159,7 @@ public class SysUserServiceImpl extends BaseServiceImpl page = baseDao.selectPage( getPage(params, "id", true), - new QueryWrapper().eq(StringUtils.isNotBlank(dto.getMobile()), "username", + new QueryWrapper().like(StringUtils.isNotBlank(dto.getMobile()), "username", dto.getMobile())); // 循环处理page获取回来的数据,返回需要的字段 List list = page.getRecords().stream().map(user -> { diff --git a/main/manager-api/src/main/resources/db/changelog/202505091555.sql b/main/manager-api/src/main/resources/db/changelog/202505091555.sql new file mode 100644 index 00000000..b7ad91d4 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202505091555.sql @@ -0,0 +1,25 @@ +-- 更新模型供应器表 +UPDATE `ai_model_provider` SET fields = '[{"key": "host", "type": "string", "label": "服务地址"}, {"key": "port", "type": "number", "label": "端口号"}, {"key": "type", "type": "string", "label": "服务类型"}, {"key": "is_ssl", "type": "boolean", "label": "是否使用SSL"}, {"key": "api_key", "type": "string", "label": "API密钥"}, {"key": "output_dir", "type": "string", "label": "输出目录"}]' WHERE id = 'SYSTEM_ASR_FunASRServer'; + +-- 更新模型配置表 +UPDATE `ai_model_config` SET +config_json = '{"host": "127.0.0.1", "port": 10096, "type": "fun_server", "is_ssl": true, "api_key": "none", "output_dir": "tmp/"}', +`doc_link` = 'https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md', +`remark` = '独立部署FunASR,使用FunASR的API服务,只需要五句话 +第一句:mkdir -p ./funasr-runtime-resources/models +第二句:sudo docker run -p 10096:10095 -it --privileged=true -v $PWD/funasr-runtime-resources/models:/workspace/models registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12 +上一句话执行后会进入到容器,继续第三句:cd FunASR/runtime +不要退出容器,继续在容器中执行第四句:nohup bash run_server_2pass.sh --download-model-dir /workspace/models --vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx --model-dir damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-onnx --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx --lm-dir damo/speech_ngram_lm_zh-cn-ai-wesp-fst --itn-dir thuduj12/fst_itn_zh --hotword /workspace/models/hotwords.txt > log.txt 2>&1 & +上一句话执行后会进入到容器,继续第五句:tail -f log.txt +第五句话执行完后,会看到模型下载日志,下载完后就可以连接使用了 +以上是使用CPU推理,如果有GPU,详细参考:https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md' WHERE `id` = 'ASR_FunASRServer'; + +-- FishSpeech配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/fish-speech-integration.md', +`remark` = 'FishSpeech配置说明: +1. 需要本地部署FishSpeech服务 +2. 支持自定义音色 +3. 本地推理,无需网络连接 +4. 输出文件保存在tmp/目录 +5. 可参照教程https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/fish-speech-integration.md' WHERE `id` = 'TTS_FishSpeech'; diff --git a/main/manager-api/src/main/resources/db/changelog/202505111914.sql b/main/manager-api/src/main/resources/db/changelog/202505111914.sql new file mode 100644 index 00000000..c67f3477 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202505111914.sql @@ -0,0 +1,6 @@ +-- 添加聊天记录配置字段 +ALTER TABLE `ai_agent` +ADD COLUMN `chat_history_conf` tinyint NOT NULL DEFAULT 0 COMMENT '聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)' AFTER `system_prompt`; + +ALTER TABLE `ai_agent_template` +ADD COLUMN `chat_history_conf` tinyint NOT NULL DEFAULT 0 COMMENT '聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)' AFTER `system_prompt`; \ No newline at end of file diff --git a/main/manager-api/src/main/resources/db/changelog/202505122348.sql b/main/manager-api/src/main/resources/db/changelog/202505122348.sql new file mode 100644 index 00000000..05bbeb84 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202505122348.sql @@ -0,0 +1,6 @@ +-- 添加总结记忆字段 +ALTER TABLE `ai_agent` +ADD COLUMN `summary_memory` text COMMENT '总结记忆' AFTER `system_prompt`; + +ALTER TABLE `ai_agent_template` +ADD COLUMN `summary_memory` text COMMENT '总结记忆' AFTER `system_prompt`; diff --git a/main/manager-api/src/main/resources/db/changelog/202505142037.sql b/main/manager-api/src/main/resources/db/changelog/202505142037.sql new file mode 100644 index 00000000..37a49013 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202505142037.sql @@ -0,0 +1,7 @@ +update ai_agent_template set system_prompt = replace(system_prompt, '我是', '你是'); + +delete from sys_params where id in (500,501,402); +INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (500, 'end_prompt.enable', 'true', 'boolean', 1, '是否开启结束语'); +INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (501, 'end_prompt.prompt', '请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧!', 'string', 1, '结束提示词'); + +INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (402, 'plugins.get_weather.api_host', 'mj7p3y7naa.re.qweatherapi.com', 'string', 1, '开发者apihost'); \ No newline at end of file diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index d4dfc03c..30dbd888 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -120,4 +120,32 @@ databaseChangeLog: changes: - sqlFile: encoding: utf8 - path: classpath:db/changelog/202505141132.sql \ No newline at end of file + path: classpath:db/changelog/202505141132.sql + - changeSet: + id: 202505091555 + author: whosmyqueen + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202505091555.sql + - changeSet: + id: 202505111914 + author: hrz + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202505111914.sql + - changeSet: + id: 202505122348 + author: ljwwd2 + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202505122348.sql + - changeSet: + id: 202505142037 + author: hrz + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202505142037.sql diff --git a/main/manager-api/src/main/resources/mapper/agent/AiAgentChatHistoryDao.xml b/main/manager-api/src/main/resources/mapper/agent/AiAgentChatHistoryDao.xml index 24a669c1..3e80f7fd 100644 --- a/main/manager-api/src/main/resources/mapper/agent/AiAgentChatHistoryDao.xml +++ b/main/manager-api/src/main/resources/mapper/agent/AiAgentChatHistoryDao.xml @@ -28,11 +28,17 @@ SELECT audio_id FROM ai_agent_chat_history WHERE agent_id = #{agentId} - ); + ) + + + UPDATE ai_agent_chat_history + SET audio_id = NULL + WHERE agent_id = #{agentId} + DELETE FROM ai_agent_chat_history - WHERE agent_id = #{agentId}; + WHERE agent_id = #{agentId} diff --git a/main/manager-web/src/apis/module/agent.js b/main/manager-web/src/apis/module/agent.js index e912c534..7f33d320 100644 --- a/main/manager-web/src/apis/module/agent.js +++ b/main/manager-web/src/apis/module/agent.js @@ -50,9 +50,9 @@ export default { }).send(); }, // 获取智能体配置 - getDeviceConfig(deviceId, callback) { + getDeviceConfig(agentId, callback) { RequestService.sendRequest() - .url(`${getServiceUrl()}/agent/${deviceId}`) + .url(`${getServiceUrl()}/agent/${agentId}`) .method('GET') .success((res) => { RequestService.clearRequestTime(); @@ -61,7 +61,7 @@ export default { .networkFail((err) => { console.error('获取配置失败:', err); RequestService.reAjaxFun(() => { - this.getDeviceConfig(deviceId, callback); + this.getDeviceConfig(agentId, callback); }); }).send(); }, diff --git a/main/manager-web/src/apis/module/model.js b/main/manager-web/src/apis/module/model.js index f0a6efbd..badf4174 100644 --- a/main/manager-web/src/apis/module/model.js +++ b/main/manager-web/src/apis/module/model.js @@ -197,5 +197,112 @@ export default { this.setDefaultModel(id, callback) }) }).send() - } + }, + + /** + * 获取模型配置列表(支持查询参数) + * @param {Object} params - 查询参数对象,例如 { name: 'test', modelType: 1 } + * @param {Function} callback - 回调函数 + */ + getModelProvidersPage(params, callback) { + // 构建查询参数 + const queryParams = new URLSearchParams(); + if (params.name) queryParams.append('name', params.name); + if (params.modelType !== undefined) queryParams.append('modelType', params.modelType); + if (params.page !== undefined) queryParams.append('page', params.page); + if (params.limit !== undefined) queryParams.append('limit', params.limit); + + RequestService.sendRequest() + .url(`${getServiceUrl()}/models/provider?${queryParams.toString()}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail((err) => { + this.$message.error(err.msg || '获取供应器列表失败'); + RequestService.reAjaxFun(() => { + this.getModelProviders(params, callback); + }); + }).send(); + }, + + /** + * 新增模型供应器配置 + * @param {Object} params - 请求参数对象,例如 { modelType: '1', providerCode: '1', name: '1', fields: '1', sort: 1 } + * @param {Function} callback - 成功回调函数 + */ + addModelProvider(params, callback) { + const postData = { + modelType: params.modelType || '', + providerCode: params.providerCode || '', + name: params.name || '', + fields: JSON.stringify(params.fields || []), + sort: params.sort || 0 + }; + + RequestService.sendRequest() + .url(`${getServiceUrl()}/models/provider`) + .method('POST') + .data(postData) + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail((err) => { + console.error('新增模型供应器失败:', err) + this.$message.error(err.msg || '新增模型供应器失败') + RequestService.reAjaxFun(() => { + this.addModelProvider(params, callback); + }); + }).send(); + }, + + /** + * 更新模型供应器配置 + * @param {Object} params - 请求参数对象,例如 { id: '111', modelType: '1', providerCode: '1', name: '1', fields: '1', sort: 1 } + * @param {Function} callback - 成功回调函数 + */ + updateModelProvider(params, callback) { + const putData = { + id: params.id || '', + modelType: params.modelType || '', + providerCode: params.providerCode || '', + name: params.name || '', + fields: JSON.stringify(params.fields || []), + sort: params.sort || 0 + }; + + RequestService.sendRequest() + .url(`${getServiceUrl()}/models/provider`) + .method('PUT') + .data(putData) + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail((err) => { + this.$message.error(err.msg || '更新模型供应器失败') + RequestService.reAjaxFun(() => { + this.updateModelProvider(params, callback); + }); + }).send(); + }, + // 删除 + deleteModelProviderByIds(ids, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/models/provider/delete`) + .method('POST') + .data(ids) + .success((res) => { + RequestService.clearRequestTime() + callback(res); + }) + .networkFail((err) => { + this.$message.error(err.msg || '删除模型供应器失败') + RequestService.reAjaxFun(() => { + this.deleteModelProviderByIds(ids, callback) + }) + }).send() + }, } diff --git a/main/manager-web/src/assets/login/login-person.png b/main/manager-web/src/assets/login/login-person.png index 27477f2f..081eff3e 100644 Binary files a/main/manager-web/src/assets/login/login-person.png and b/main/manager-web/src/assets/login/login-person.png differ diff --git a/main/manager-web/src/assets/model/model.png b/main/manager-web/src/assets/model/model.png index f08ecb86..21132b76 100644 Binary files a/main/manager-web/src/assets/model/model.png and b/main/manager-web/src/assets/model/model.png differ diff --git a/main/manager-web/src/components/DeviceItem.vue b/main/manager-web/src/components/DeviceItem.vue index 60f54b9d..0ecbcefa 100644 --- a/main/manager-web/src/components/DeviceItem.vue +++ b/main/manager-web/src/components/DeviceItem.vue @@ -26,8 +26,12 @@
设备管理({{ device.deviceCount }})
-
- 聊天记录 +
+ + 聊天记录 + + 聊天记录
@@ -77,6 +81,9 @@ export default { this.$router.push({ path: '/device-management', query: { agentId: this.device.agentId } }); }, handleChatHistory() { + if (this.device.memModelId === 'Memory_nomem') { + return + } this.$emit('chat-history', { agentId: this.device.agentId, agentName: this.device.agentName }) } } @@ -120,6 +127,12 @@ export default { color: #979db1; font-weight: 400; } + +.disabled-btn { + background: #e6e6e6; + color: #999; + cursor: not-allowed; +} \ No newline at end of file diff --git a/main/manager-web/src/components/FunctionDialog.vue b/main/manager-web/src/components/FunctionDialog.vue new file mode 100644 index 00000000..b4fec417 --- /dev/null +++ b/main/manager-web/src/components/FunctionDialog.vue @@ -0,0 +1,410 @@ + + + + + \ No newline at end of file diff --git a/main/manager-web/src/components/HeaderBar.vue b/main/manager-web/src/components/HeaderBar.vue index 3d6a62f0..842441ce 100644 --- a/main/manager-web/src/components/HeaderBar.vue +++ b/main/manager-web/src/components/HeaderBar.vue @@ -35,12 +35,12 @@ OTA管理
+ :class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' }" @visible-change="handleParamDropdownVisibleChange"> + :style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' ? 'brightness(0) invert(1)' : 'None' }" /> 参数字典 - + @@ -49,22 +49,26 @@ 字典管理 + + 供应器管理 +
-
+
- + - {{ userInfo.username || '加载中...' }} + {{ userInfo.username || '加载中...' }} + 修改密码 @@ -84,7 +88,6 @@ import userApi from '@/apis/module/user'; import { mapActions, mapGetters } from 'vuex'; import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件 - export default { name: 'HeaderBar', components: { @@ -98,7 +101,10 @@ export default { username: '', mobile: '' }, - isChangePasswordDialogVisible: false // 控制修改密码弹窗的显示 + isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示 + userDropdownVisible: false, + paramDropdownVisible: false, + isSmallScreen: false } }, computed: { @@ -108,7 +114,13 @@ export default { } }, mounted() { - this.fetchUserInfo() + this.fetchUserInfo(); + this.checkScreenSize(); + window.addEventListener('resize', this.checkScreenSize); + }, + //移除事件监听器 + beforeDestroy() { + window.removeEventListener('resize', this.checkScreenSize); }, methods: { goHome() { @@ -130,6 +142,9 @@ export default { goDictManagement() { this.$router.push('/dict-management') }, + goProviderManagement() { + this.$router.push('/provider-management') + }, // 获取用户信息 fetchUserInfo() { userApi.getUserInfo(({ data }) => { @@ -139,7 +154,9 @@ export default { } }) }, - + checkScreenSize() { + this.isSmallScreen = window.innerWidth <= 1386; + }, // 处理搜索 handleSearch() { const searchValue = this.search.trim(); @@ -184,6 +201,13 @@ export default { }); } }, + handleUserDropdownVisibleChange(visible) { + this.userDropdownVisible = visible; + }, + // 监听第二个下拉菜单的可见状态变化 + handleParamDropdownVisibleChange(visible) { + this.paramDropdownVisible = visible; + }, // 使用 mapActions 引入 Vuex 的 logout action ...mapActions(['logout']) @@ -191,7 +215,7 @@ export default { } - \ No newline at end of file diff --git a/main/manager-web/src/components/ProviderDialog.vue b/main/manager-web/src/components/ProviderDialog.vue new file mode 100644 index 00000000..7fd1fcf8 --- /dev/null +++ b/main/manager-web/src/components/ProviderDialog.vue @@ -0,0 +1,435 @@ + + + + + \ No newline at end of file diff --git a/main/manager-web/src/components/TtsModel.vue b/main/manager-web/src/components/TtsModel.vue index 7e8eb3a1..87356523 100644 --- a/main/manager-web/src/components/TtsModel.vue +++ b/main/manager-web/src/components/TtsModel.vue @@ -336,6 +336,14 @@ export default { }, saveEdit(row) { + if (!row.voiceCode || !row.voiceName || !row.languageType) { + this.$message.error({ + message: '音色编码、音色名称和语言类型不能为空', + showClose: true + }); + return; + } + try { const params = { id: row.id, @@ -408,6 +416,12 @@ export default { }, addNew() { + const hasEditing = this.ttsModels.some(row => row.editing); + if (hasEditing) { + this.$message.warning('请先完成当前编辑再新增'); + return; + } + const maxSort = this.ttsModels.length > 0 ? Math.max(...this.ttsModels.map(item => Number(item.sort) || 0)) : 0; diff --git a/main/manager-web/src/router/index.js b/main/manager-web/src/router/index.js index 57b3c7e7..a19b4ec6 100644 --- a/main/manager-web/src/router/index.js +++ b/main/manager-web/src/router/index.js @@ -90,7 +90,14 @@ const routes = [ component: function () { return import('../views/DictManagement.vue') } - } + }, + { + path: '/provider-management', + name: 'ProviderManagement', + component: function () { + return import('../views/ProviderManagement.vue') + } + }, ] const router = new VueRouter({ base: process.env.VUE_APP_PUBLIC_PATH || '/', diff --git a/main/manager-web/src/views/ProviderManagement.vue b/main/manager-web/src/views/ProviderManagement.vue new file mode 100644 index 00000000..7634b511 --- /dev/null +++ b/main/manager-web/src/views/ProviderManagement.vue @@ -0,0 +1,876 @@ + + + + + \ No newline at end of file diff --git a/main/manager-web/src/views/roleConfig.vue b/main/manager-web/src/views/roleConfig.vue index 8b9504da..03ec8060 100644 --- a/main/manager-web/src/views/roleConfig.vue +++ b/main/manager-web/src/views/roleConfig.vue @@ -15,9 +15,17 @@
{{ form.agentName }} - +
+
+ + 保存配置后,需要重启设备,新的配置才会生效。 +
+ 保存配置 + 重置 + +
@@ -26,7 +34,7 @@
- +
@@ -37,9 +45,15 @@
- + + + + @@ -48,24 +62,48 @@ -
- 保存配置 - 重置 -
- - 保存配置后,需要重启设备,新的配置才会生效。 -
-
- - - +
+ + + +
+ +
+
功能名称: {{ func.name }}
+
+ 参数配置: +
+ {{ key }}: {{ value }} +
+
+
无参数配置
+
+
+ {{ func.name.charAt(0) }} +
+
+ + 编辑功能 + +
+
+ + 上报文字 + 上报文字+语音 + +
+
- + @@ -75,28 +113,33 @@
- + +