diff --git a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java index 6a9af011..e84fbba8 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java +++ b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java @@ -55,4 +55,113 @@ public interface ErrorCode { int OTA_DEVICE_NOT_FOUND = 10041; int OTA_DEVICE_NEED_BIND = 10042; + + // 新增错误编码 + int DELETE_DATA_FAILED = 10043; + int USER_NOT_LOGIN = 10044; + int WEB_SOCKET_CONNECT_FAILED = 10045; + int VOICE_PRINT_SAVE_ERROR = 10046; + int TODAY_SMS_LIMIT_REACHED = 10047; + int OLD_PASSWORD_ERROR = 10048; + int INVALID_LLM_TYPE = 10049; + int TOKEN_GENERATE_ERROR = 10050; + int RESOURCE_NOT_FOUND = 10051; + + // 新增错误编码 + int DEFAULT_AGENT_NOT_FOUND = 10052; + int AGENT_NOT_FOUND = 10053; + int VOICEPRINT_API_NOT_CONFIGURED = 10054; + int SMS_SEND_FAILED = 10055; + int SMS_CONNECTION_FAILED = 10056; + int AGENT_VOICEPRINT_CREATE_FAILED = 10057; + int AGENT_VOICEPRINT_UPDATE_FAILED = 10058; + int AGENT_VOICEPRINT_DELETE_FAILED = 10059; + int SMS_SEND_TOO_FREQUENTLY = 10060; + int ACTIVATION_CODE_EMPTY = 10061; + int ACTIVATION_CODE_ERROR = 10062; + int DEVICE_ALREADY_ACTIVATED = 10063; + // 默认模型删除错误 + int DEFAULT_MODEL_DELETE_ERROR = 10064; + // 设备相关错误码 + int MAC_ADDRESS_ALREADY_EXISTS = 10090; // Mac地址已存在 + // 模型相关错误码 + int MODEL_PROVIDER_NOT_EXIST = 10091; // 供应器不存在 + int LLM_NOT_EXIST = 10092; // 设置的LLM不存在 + int MODEL_REFERENCED_BY_AGENT = 10093; // 该模型配置已被智能体引用,无法删除 + int LLM_REFERENCED_BY_INTENT = 10094; // 该LLM模型已被意图识别配置引用,无法删除 + + // 登录相关错误码 + int ADD_DATA_FAILED = 10065; // 新增数据失败 + int UPDATE_DATA_FAILED = 10066; // 修改数据失败 + int SMS_CAPTCHA_ERROR = 10067; // 短信验证码错误 + int MOBILE_REGISTER_DISABLED = 10068; // 未开启手机注册 + int USERNAME_NOT_PHONE = 10069; // 用户名不是手机号码 + int PHONE_ALREADY_REGISTERED = 10070; // 手机号码已注册 + int PHONE_NOT_REGISTERED = 10071; // 手机号码未注册 + int USER_REGISTER_DISABLED = 10072; // 不允许用户注册 + int RETRIEVE_PASSWORD_DISABLED = 10073; // 未开启找回密码功能 + int PHONE_FORMAT_ERROR = 10074; // 手机号码格式不正确 + int SMS_CODE_ERROR = 10075; // 手机验证码错误 + + // 字典类型相关错误码 + int DICT_TYPE_NOT_EXIST = 10076; // 字典类型不存在 + int DICT_TYPE_DUPLICATE = 10077; // 字典类型编码重复 + + // 资源处理相关错误码 + int RESOURCE_READ_ERROR = 10078; // 读取资源失败 + + // 智能体相关错误码 + int LLM_INTENT_PARAMS_MISMATCH = 10079; // LLM大模型和Intent意图识别,选择参数不匹配 + + // 声纹相关错误码 + int VOICEPRINT_ALREADY_REGISTERED = 10080; // 此声音声纹已经注册 + int VOICEPRINT_DELETE_ERROR = 10081; // 删除声纹出现错误 + int VOICEPRINT_UPDATE_NOT_ALLOWED = 10082; // 声纹修改不允许,声音已注册 + int VOICEPRINT_UPDATE_ADMIN_ERROR = 10083; // 修改声纹错误,请联系管理员 + int VOICEPRINT_API_URI_ERROR = 10084; // 声纹接口地址错误 + int VOICEPRINT_AUDIO_NOT_BELONG_AGENT = 10085; // 音频数据不属于智能体 + int VOICEPRINT_AUDIO_EMPTY = 10086; // 音频数据为空 + int VOICEPRINT_REGISTER_REQUEST_ERROR = 10087; // 声纹保存请求失败 + int VOICEPRINT_REGISTER_PROCESS_ERROR = 10088; // 声纹保存处理失败 + int VOICEPRINT_UNREGISTER_REQUEST_ERROR = 10089; // 声纹注销请求失败 + int VOICEPRINT_UNREGISTER_PROCESS_ERROR = 10090; // 声纹注销处理失败 + int VOICEPRINT_IDENTIFY_REQUEST_ERROR = 10091; // 声纹识别请求失败 + + // 服务端管理相关错误码 + int INVALID_SERVER_ACTION = 10095; // 无效服务端操作 + int SERVER_WEBSOCKET_NOT_CONFIGURED = 10096; // 未配置服务端WebSocket地址 + int TARGET_WEBSOCKET_NOT_EXIST = 10097; // 目标WebSocket地址不存在 + + // 参数验证相关错误码 + int WEBSOCKET_URLS_EMPTY = 10098; // WebSocket地址列表不能为空 + int WEBSOCKET_URL_LOCALHOST = 10099; // WebSocket地址不能使用localhost或127.0.0.1 + int WEBSOCKET_URL_FORMAT_ERROR = 10100; // WebSocket地址格式不正确 + int WEBSOCKET_CONNECTION_FAILED = 10101; // WebSocket连接测试失败 + int OTA_URL_EMPTY = 10102; // OTA地址不能为空 + int OTA_URL_LOCALHOST = 10103; // OTA地址不能使用localhost或127.0.0.1 + int OTA_URL_PROTOCOL_ERROR = 10104; // OTA地址必须以http或https开头 + int OTA_URL_FORMAT_ERROR = 10105; // OTA地址必须以/ota/结尾 + int OTA_INTERFACE_ACCESS_FAILED = 10106; // OTA接口访问失败 + int OTA_INTERFACE_FORMAT_ERROR = 10107; // OTA接口返回内容格式不正确 + int OTA_INTERFACE_VALIDATION_FAILED = 10108; // OTA接口验证失败 + int MCP_URL_EMPTY = 10109; // MCP地址不能为空 + int MCP_URL_LOCALHOST = 10110; // MCP地址不能使用localhost或127.0.0.1 + int MCP_URL_INVALID = 10111; // 不是正确的MCP地址 + int MCP_INTERFACE_ACCESS_FAILED = 10112; // MCP接口访问失败 + int MCP_INTERFACE_FORMAT_ERROR = 10113; // MCP接口返回内容格式不正确 + int MCP_INTERFACE_VALIDATION_FAILED = 10114; // MCP接口验证失败 + int VOICEPRINT_URL_EMPTY = 10115; // 声纹接口地址不能为空 + int VOICEPRINT_URL_LOCALHOST = 10116; // 声纹接口地址不能使用localhost或127.0.0.1 + int VOICEPRINT_URL_INVALID = 10117; // 不是正确的声纹接口地址 + int VOICEPRINT_URL_PROTOCOL_ERROR = 10118; // 声纹接口地址必须以http或https开头 + int VOICEPRINT_INTERFACE_ACCESS_FAILED = 10119; // 声纹接口访问失败 + int VOICEPRINT_INTERFACE_FORMAT_ERROR = 10120; // 声纹接口返回内容格式不正确 + int VOICEPRINT_INTERFACE_VALIDATION_FAILED = 10121; // 声纹接口验证失败 + int MQTT_SECRET_EMPTY = 10122; // mqtt密钥不能为空 + int MQTT_SECRET_LENGTH_INSECURE = 10123; // mqtt密钥长度不安全 + int MQTT_SECRET_CHARACTER_INSECURE = 10124; // mqtt密钥必须同时包含大小写字母 + int MQTT_SECRET_WEAK_PASSWORD = 10125; // mqtt密钥包含弱密码 + + // 字典相关错误码 + int DICT_LABEL_DUPLICATE = 10128; // 字典标签重复 } 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 01f1ccf2..e016bc09 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 @@ -13,6 +13,7 @@ import org.springframework.web.servlet.resource.NoResourceFoundException; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import xiaozhi.common.utils.MessageUtils; import xiaozhi.common.utils.Result; /** @@ -62,7 +63,7 @@ public class RenExceptionHandler { @ExceptionHandler(NoResourceFoundException.class) public Result handleNoResourceFoundException(NoResourceFoundException ex) { log.warn("Resource not found: {}", ex.getMessage()); - return new Result().error(404, "资源不存在"); + return new Result().error(404, MessageUtils.getMessage(ErrorCode.RESOURCE_NOT_FOUND)); } @ExceptionHandler(MethodArgumentNotValidException.class) @@ -76,7 +77,7 @@ public class RenExceptionHandler { }) .filter(Objects::nonNull) .findFirst() - .orElse("请求参数错误!"); + .orElse(MessageUtils.getMessage(ErrorCode.PARAM_VALUE_NULL)); return new Result().error(ErrorCode.PARAM_VALUE_NULL, errorMsg); } diff --git a/main/manager-api/src/main/java/xiaozhi/common/utils/ResourcesUtils.java b/main/manager-api/src/main/java/xiaozhi/common/utils/ResourcesUtils.java index 49d9a727..cf2d0bcc 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/utils/ResourcesUtils.java +++ b/main/manager-api/src/main/java/xiaozhi/common/utils/ResourcesUtils.java @@ -6,6 +6,7 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.Resource; import org.springframework.stereotype.Component; import xiaozhi.common.exception.RenException; +import xiaozhi.common.exception.ErrorCode; import java.io.BufferedReader; @@ -37,7 +38,7 @@ public class ResourcesUtils { } } catch (IOException e){ log.error("方法:loadString()读取资源失败--{}",e.getMessage()); - throw new RenException("读取资源失败"); + throw new RenException(ErrorCode.RESOURCE_READ_ERROR); } return luaScriptBuilder.toString(); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentVoicePrintController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentVoicePrintController.java index 1615cee5..9b7555eb 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentVoicePrintController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentVoicePrintController.java @@ -17,6 +17,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.AllArgsConstructor; +import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.RenException; import xiaozhi.common.utils.Result; import xiaozhi.modules.agent.dto.AgentVoicePrintSaveDTO; @@ -42,7 +43,7 @@ public class AgentVoicePrintController { if (b) { return new Result<>(); } - return new Result().error("智能体的声纹创建失败"); + return new Result().error(ErrorCode.AGENT_VOICEPRINT_CREATE_FAILED); } @PutMapping @@ -54,7 +55,7 @@ public class AgentVoicePrintController { if (b) { return new Result<>(); } - return new Result().error("智能体的对应声纹更新失败"); + return new Result().error(ErrorCode.AGENT_VOICEPRINT_UPDATE_FAILED); } @DeleteMapping("/{id}") @@ -67,7 +68,7 @@ public class AgentVoicePrintController { if (delete) { return new Result<>(); } - return new Result().error("智能体的对应声纹删除失败"); + return new Result().error(ErrorCode.AGENT_VOICEPRINT_DELETE_FAILED); } @GetMapping("/list/{id}") @@ -76,7 +77,7 @@ public class AgentVoicePrintController { public Result> list(@PathVariable String id) { String voiceprintUrl = sysParamsService.getValue("server.voice_print", true); if (StringUtils.isBlank(voiceprintUrl) || "null".equals(voiceprintUrl)) { - throw new RenException("声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print)"); + throw new RenException(ErrorCode.VOICEPRINT_API_NOT_CONFIGURED); } Long userId = SecurityUser.getUserId(); List list = agentVoicePrintService.list(userId, id); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java index 4f5681e2..9ec04a4e 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java @@ -20,6 +20,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import lombok.AllArgsConstructor; import xiaozhi.common.constant.Constant; import xiaozhi.common.exception.RenException; +import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.page.PageData; import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; @@ -74,7 +75,7 @@ public class AgentServiceImpl extends BaseServiceImpl imp AgentInfoVO agent = agentDao.selectAgentInfoById(id); if (agent == null) { - throw new RenException("智能体不存在"); + throw new RenException(ErrorCode.AGENT_NOT_FOUND); } if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) { @@ -204,7 +205,7 @@ public class AgentServiceImpl extends BaseServiceImpl imp // 先查询现有实体 AgentEntity existingEntity = this.getAgentById(agentId); if (existingEntity == null) { - throw new RuntimeException("智能体不存在"); + throw new RenException(ErrorCode.AGENT_NOT_FOUND); } // 只更新提供的非空字段 @@ -328,7 +329,7 @@ public class AgentServiceImpl extends BaseServiceImpl imp boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId()); if (!b) { - throw new RenException("LLM大模型和Intent意图识别,选择参数不匹配"); + throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH); } this.updateById(existingEntity); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentVoicePrintServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentVoicePrintServiceImpl.java index 31accbab..088e4663 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentVoicePrintServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentVoicePrintServiceImpl.java @@ -26,6 +26,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.extern.slf4j.Slf4j; import xiaozhi.common.constant.Constant; +import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.RenException; import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.JsonUtils; @@ -78,7 +79,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl @Override public Boolean deviceActivation(String agentId, String activationCode) { if (StringUtils.isBlank(activationCode)) { - throw new RenException("激活码不能为空"); + throw new RenException(ErrorCode.ACTIVATION_CODE_EMPTY); } String deviceKey = "ota:activation:code:" + activationCode; Object cacheDeviceId = redisUtils.get(deviceKey); if (cacheDeviceId == null) { - throw new RenException("激活码错误"); + throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR); } String deviceId = (String) cacheDeviceId; String safeDeviceId = deviceId.replace(":", "_").toLowerCase(); String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId); Map cacheMap = (Map) redisUtils.get(cacheDeviceKey); if (cacheMap == null) { - throw new RenException("激活码错误"); + throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR); } String cachedCode = (String) cacheMap.get("activation_code"); if (!activationCode.equals(cachedCode)) { - throw new RenException("激活码错误"); + throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR); } // 检查设备有没有被激活 if (selectById(deviceId) != null) { - throw new RenException("设备已激活"); + throw new RenException(ErrorCode.DEVICE_ALREADY_ACTIVATED); } String macAddress = (String) cacheMap.get("mac_address"); @@ -111,7 +112,7 @@ public class DeviceServiceImpl extends BaseServiceImpl String appVersion = (String) cacheMap.get("app_version"); UserDetail user = SecurityUser.getUser(); if (user.getId() == null) { - throw new RenException("用户未登录"); + throw new RenException(ErrorCode.USER_NOT_LOGIN); } Date currentTime = new Date(); @@ -441,7 +442,7 @@ public class DeviceServiceImpl extends BaseServiceImpl wrapper.eq("mac_address", dto.getMacAddress()); DeviceEntity exist = baseDao.selectOne(wrapper); if (exist != null) { - throw new RenException("该Mac地址已存在"); + throw new RenException(ErrorCode.MAC_ADDRESS_ALREADY_EXISTS); } Date now = new Date(); DeviceEntity entity = new DeviceEntity(); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java index c156b408..936e67f3 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java @@ -15,6 +15,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import cn.hutool.core.collection.CollectionUtil; import lombok.AllArgsConstructor; import xiaozhi.common.constant.Constant; +import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.RenException; import xiaozhi.common.page.PageData; import xiaozhi.common.redis.RedisKeys; @@ -94,7 +95,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl providerList = modelProviderService.getList(modelType, provideCode); if (CollectionUtil.isEmpty(providerList)) { - throw new RenException("供应器不存在"); + throw new RenException(ErrorCode.MODEL_PROVIDER_NOT_EXIST); } // 再保存供应器提供的模型 @@ -113,7 +114,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl providerList = modelProviderService.getList(modelType, provideCode); if (CollectionUtil.isEmpty(providerList)) { - throw new RenException("供应器不存在"); + throw new RenException(ErrorCode.MODEL_PROVIDER_NOT_EXIST); } if (modelConfigBodyDTO.getConfigJson().containsKey("llm")) { String llm = modelConfigBodyDTO.getConfigJson().get("llm").toString(); @@ -122,12 +123,12 @@ public class ModelConfigServiceImpl extends BaseServiceImpl ids) { if (modelProviderDao.deleteBatchIds(ids) == 0) { - throw new RenException("删除数据失败"); + throw new RenException(ErrorCode.DELETE_DATA_FAILED); } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/config/WebMvcConfig.java b/main/manager-api/src/main/java/xiaozhi/modules/security/config/WebMvcConfig.java index 02538caf..f13194b1 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/config/WebMvcConfig.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/config/WebMvcConfig.java @@ -2,8 +2,11 @@ package xiaozhi.modules.security.config; import java.text.SimpleDateFormat; import java.util.List; +import java.util.Locale; import java.util.TimeZone; +import jakarta.servlet.http.HttpServletRequest; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.ByteArrayHttpMessageConverter; @@ -12,8 +15,10 @@ import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; +import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -95,5 +100,47 @@ public class WebMvcConfig implements WebMvcConfigurer { converter.setObjectMapper(mapper); return converter; } + + /** + * 国际化配置 - 根据请求头中的Accept-Language设置语言环境 + */ + @Bean + public LocaleResolver localeResolver() { + return new AcceptHeaderLocaleResolver() { + @Override + public Locale resolveLocale(HttpServletRequest request) { + String acceptLanguage = request.getHeader("Accept-Language"); + if (acceptLanguage == null || acceptLanguage.isEmpty()) { + return Locale.getDefault(); + } + + // 解析Accept-Language请求头中的首选语言 + String[] languages = acceptLanguage.split(","); + if (languages.length > 0) { + // 提取第一个语言代码,去除可能的质量值(q=...) + String[] parts = languages[0].split(";" + "\\s*"); + String primaryLanguage = parts[0].trim(); + + // 根据前端发送的语言代码直接创建Locale对象 + if (primaryLanguage.equals("zh-CN")) { + return Locale.SIMPLIFIED_CHINESE; + } else if (primaryLanguage.equals("zh-TW")) { + return Locale.TRADITIONAL_CHINESE; + } else if (primaryLanguage.equals("en-US")) { + return Locale.US; + } else if (primaryLanguage.startsWith("zh")) { + // 对于其他中文变体,默认使用简体中文 + return Locale.SIMPLIFIED_CHINESE; + } else if (primaryLanguage.startsWith("en")) { + // 对于其他英文变体,默认使用美式英语 + return Locale.US; + } + } + + // 如果没有匹配的语言,使用默认语言 + return Locale.getDefault(); + } + }; + } } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/controller/LoginController.java b/main/manager-api/src/main/java/xiaozhi/modules/security/controller/LoginController.java index 0215cf7e..d7333273 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/controller/LoginController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/controller/LoginController.java @@ -68,12 +68,12 @@ public class LoginController { // 验证图形验证码 boolean validate = captchaService.validate(dto.getCaptchaId(), dto.getCaptcha(), true); if (!validate) { - throw new RenException("图形验证码错误"); + throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR); } Boolean isMobileRegister = sysParamsService .getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class); if (!isMobileRegister) { - throw new RenException("没有开启手机注册,没法使用短信验证码功能"); + throw new RenException(ErrorCode.MOBILE_REGISTER_DISABLED); } // 发送短信验证码 captchaService.sendSMSValidateCode(dto.getPhone()); @@ -86,17 +86,17 @@ public class LoginController { // 验证是否正确输入验证码 boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true); if (!validate) { - throw new RenException("图形验证码错误,请重新获取"); + throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR); } // 按照用户名获取用户 SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername()); // 判断用户是否存在 if (userDTO == null) { - throw new RenException("请检测用户和密码是否输入错误"); + throw new RenException(ErrorCode.ACCOUNT_PASSWORD_ERROR); } // 判断密码是否正确,不一样则进入if if (!PasswordUtils.matches(login.getPassword(), userDTO.getPassword())) { - throw new RenException("请检测用户和密码是否输入错误"); + throw new RenException(ErrorCode.ACCOUNT_PASSWORD_ERROR); } return sysUserTokenService.createToken(userDTO.getId()); } @@ -105,7 +105,7 @@ public class LoginController { @Operation(summary = "注册") public Result register(@RequestBody LoginDTO login) { if (!sysUserService.getAllowUserRegister()) { - throw new RenException("当前不允许普通用户注册"); + throw new RenException(ErrorCode.USER_REGISTER_DISABLED); } // 是否开启手机注册 Boolean isMobileRegister = sysParamsService @@ -115,25 +115,25 @@ public class LoginController { // 验证用户是否是手机号码 boolean validPhone = ValidatorUtils.isValidPhone(login.getUsername()); if (!validPhone) { - throw new RenException("用户名不是手机号码,请重新输入"); + throw new RenException(ErrorCode.USERNAME_NOT_PHONE); } // 验证短信验证码是否正常 validate = captchaService.validateSMSValidateCode(login.getUsername(), login.getMobileCaptcha(), false); if (!validate) { - throw new RenException("手机验证码错误,请重新获取"); + throw new RenException(ErrorCode.SMS_CODE_ERROR); } } else { // 验证是否正确输入验证码 validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true); if (!validate) { - throw new RenException("图形验证码错误,请重新获取"); + throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR); } } // 按照用户名获取用户 SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername()); if (userDTO != null) { - throw new RenException("此手机号码已经注册过"); + throw new RenException(ErrorCode.PHONE_ALREADY_REGISTERED); } userDTO = new SysUserDTO(); userDTO.setUsername(login.getUsername()); @@ -168,26 +168,26 @@ public class LoginController { Boolean isMobileRegister = sysParamsService .getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class); if (!isMobileRegister) { - throw new RenException("没有开启手机注册,没法使用找回密码功能"); + throw new RenException(ErrorCode.RETRIEVE_PASSWORD_DISABLED); } // 判断非空 ValidatorUtils.validateEntity(dto); // 验证用户是否是手机号码 boolean validPhone = ValidatorUtils.isValidPhone(dto.getPhone()); if (!validPhone) { - throw new RenException("输入的手机号码格式不正确"); + throw new RenException(ErrorCode.PHONE_FORMAT_ERROR); } // 按照用户名获取用户 SysUserDTO userDTO = sysUserService.getByUsername(dto.getPhone()); if (userDTO == null) { - throw new RenException("输入的手机号码未注册"); + throw new RenException(ErrorCode.PHONE_NOT_REGISTERED); } // 验证短信验证码是否正常 boolean validate = captchaService.validateSMSValidateCode(dto.getPhone(), dto.getCode(), false); // 判断是否通过验证 if (!validate) { - throw new RenException("输入的手机验证码错误"); + throw new RenException(ErrorCode.SMS_CODE_ERROR); } sysUserService.changePasswordDirectly(userDTO.getId(), dto.getPassword()); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/oauth2/Oauth2Filter.java b/main/manager-api/src/main/java/xiaozhi/modules/security/oauth2/Oauth2Filter.java index aad888df..59d7e09e 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/oauth2/Oauth2Filter.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/oauth2/Oauth2Filter.java @@ -18,6 +18,7 @@ import xiaozhi.common.constant.Constant; import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.utils.HttpContextUtils; import xiaozhi.common.utils.JsonUtils; +import xiaozhi.common.utils.MessageUtils; import xiaozhi.common.utils.Result; /** @@ -82,8 +83,8 @@ public class Oauth2Filter extends AuthenticatingFilter { httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); try { - Throwable throwable = e.getCause() == null ? e : e.getCause(); - Result r = new Result().error(ErrorCode.UNAUTHORIZED, throwable.getMessage()); + // 使用国际化消息替代直接使用异常消息 + Result r = new Result().error(ErrorCode.UNAUTHORIZED); String json = JsonUtils.toJsonString(r); httpResponse.getWriter().print(json); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/oauth2/TokenGenerator.java b/main/manager-api/src/main/java/xiaozhi/modules/security/oauth2/TokenGenerator.java index 6d7be47a..17ea9beb 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/oauth2/TokenGenerator.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/oauth2/TokenGenerator.java @@ -3,6 +3,7 @@ package xiaozhi.modules.security.oauth2; import java.security.MessageDigest; import java.util.UUID; +import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.RenException; /** @@ -38,7 +39,7 @@ public class TokenGenerator { byte[] messageDigest = algorithm.digest(); return toHexString(messageDigest); } catch (Exception e) { - throw new RenException("token invalid", e); + throw new RenException(ErrorCode.TOKEN_GENERATE_ERROR, e); } } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/service/impl/CaptchaServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/security/service/impl/CaptchaServiceImpl.java index e823d70e..0d3666db 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/service/impl/CaptchaServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/service/impl/CaptchaServiceImpl.java @@ -16,6 +16,7 @@ import com.wf.captcha.base.Captcha; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import xiaozhi.common.constant.Constant; +import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.RenException; import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; @@ -88,7 +89,7 @@ public class CaptchaServiceImpl implements CaptchaService { long currentTime = System.currentTimeMillis(); long timeDiff = currentTime - lastSendTimeLong; if (timeDiff < 60000) { - throw new RenException("发送太频繁,请" + (60000 - timeDiff) / 1000 + "秒后再试"); + throw new RenException(ErrorCode.SMS_SEND_TOO_FREQUENTLY, String.valueOf((60000 - timeDiff) / 1000)); } } @@ -108,7 +109,7 @@ public class CaptchaServiceImpl implements CaptchaService { } if (todayCount >= maxSendCount) { - throw new RenException("今日发送次数已达上限"); + throw new RenException(ErrorCode.TODAY_SMS_LIMIT_REACHED); } String key = RedisKeys.getSMSValidateCodeKey(phone); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sms/service/imp/ALiYunSmsService.java b/main/manager-api/src/main/java/xiaozhi/modules/sms/service/imp/ALiYunSmsService.java index e0ebb523..a8a378f0 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sms/service/imp/ALiYunSmsService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sms/service/imp/ALiYunSmsService.java @@ -9,6 +9,7 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import xiaozhi.common.constant.Constant; +import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.RenException; import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; @@ -45,7 +46,7 @@ public class ALiYunSmsService implements SmsService { redisUtils.delete(todayCountKey); // 错误 message log.error(e.getMessage()); - throw new RenException("短信发送失败"); + throw new RenException(ErrorCode.SMS_SEND_FAILED); } } @@ -70,7 +71,7 @@ public class ALiYunSmsService implements SmsService { }catch (Exception e){ // 错误 message log.error(e.getMessage()); - throw new RenException("短信连接建立失败"); + throw new RenException(ErrorCode.SMS_CONNECTION_FAILED); } } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/ServerSideManageController.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/ServerSideManageController.java index 0b6b6a5b..893ea8e2 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/ServerSideManageController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/ServerSideManageController.java @@ -22,6 +22,7 @@ import jakarta.validation.Valid; import lombok.AllArgsConstructor; import xiaozhi.common.annotation.LogOperation; import xiaozhi.common.constant.Constant; +import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.RenException; import xiaozhi.common.utils.Result; import xiaozhi.modules.sys.dto.EmitSeverActionDTO; @@ -64,17 +65,17 @@ public class ServerSideManageController { @RequiresPermissions("sys:role:superAdmin") public Result emitServerAction(@RequestBody @Valid EmitSeverActionDTO emitSeverActionDTO) { if (emitSeverActionDTO.getAction() == null) { - throw new RenException("无效服务端操作"); + throw new RenException(ErrorCode.INVALID_SERVER_ACTION); } String wsText = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true); if (StringUtils.isBlank(wsText)) { - throw new RenException("未配置服务端WebSocket地址"); + throw new RenException(ErrorCode.SERVER_WEBSOCKET_NOT_CONFIGURED); } String targetWs = emitSeverActionDTO.getTargetWs(); String[] wsList = wsText.split(";"); // 找到需要发起的 if (StringUtils.isBlank(targetWs) || !Arrays.asList(wsList).contains(targetWs)) { - throw new RenException("目标WebSocket地址不存在"); + throw new RenException(ErrorCode.TARGET_WEBSOCKET_NOT_EXIST); } return new Result().ok(emitServerActionByWs(targetWs, emitSeverActionDTO.getAction())); } @@ -114,7 +115,7 @@ public class ServerSideManageController { }); } catch (Exception e) { // 捕获全部错误,由全局异常处理器返回 - throw new RenException("WebSocket连接失败或连接超时"); + throw new RenException(ErrorCode.WEB_SOCKET_CONNECT_FAILED); } return true; } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java index d1c116c3..24b070c0 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java @@ -25,6 +25,7 @@ import lombok.AllArgsConstructor; import xiaozhi.common.annotation.LogOperation; import xiaozhi.common.constant.Constant; import xiaozhi.common.exception.RenException; +import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.page.PageData; import xiaozhi.common.utils.Result; import xiaozhi.common.validator.AssertUtils; @@ -130,23 +131,23 @@ public class SysParamsController { } String[] wsUrls = urls.split("\\;"); if (wsUrls.length == 0) { - throw new RenException("WebSocket地址列表不能为空"); + throw new RenException(ErrorCode.WEBSOCKET_URLS_EMPTY); } for (String url : wsUrls) { if (StringUtils.isNotBlank(url)) { // 检查是否包含localhost或127.0.0.1 if (url.contains("localhost") || url.contains("127.0.0.1")) { - throw new RenException("WebSocket地址不能使用localhost或127.0.0.1"); + throw new RenException(ErrorCode.WEBSOCKET_URL_LOCALHOST); } // 验证WebSocket地址格式 if (!WebSocketValidator.validateUrlFormat(url)) { - throw new RenException("WebSocket地址格式不正确: " + url); + throw new RenException(ErrorCode.WEBSOCKET_URL_FORMAT_ERROR); } // 测试WebSocket连接 if (!WebSocketValidator.testConnection(url)) { - throw new RenException("WebSocket连接测试失败: " + url); + throw new RenException(ErrorCode.WEBSOCKET_CONNECTION_FAILED); } } } @@ -173,35 +174,35 @@ public class SysParamsController { return; } if (StringUtils.isBlank(url) || url.equals("null")) { - throw new RenException("OTA地址不能为空"); + throw new RenException(ErrorCode.OTA_URL_EMPTY); } // 检查是否包含localhost或127.0.0.1 if (url.contains("localhost") || url.contains("127.0.0.1")) { - throw new RenException("OTA地址不能使用localhost或127.0.0.1"); + throw new RenException(ErrorCode.OTA_URL_LOCALHOST); } // 验证URL格式 if (!url.toLowerCase().startsWith("http")) { - throw new RenException("OTA地址必须以http或https开头"); + throw new RenException(ErrorCode.OTA_URL_PROTOCOL_ERROR); } if (!url.endsWith("/ota/")) { - throw new RenException("OTA地址必须以/ota/结尾"); + throw new RenException(ErrorCode.OTA_URL_FORMAT_ERROR); } try { // 发送GET请求 ResponseEntity response = restTemplate.getForEntity(url, String.class); if (response.getStatusCode() != HttpStatus.OK) { - throw new RenException("OTA接口访问失败,状态码:" + response.getStatusCode()); + throw new RenException(ErrorCode.OTA_INTERFACE_ACCESS_FAILED); } // 检查响应内容是否包含OTA相关信息 String body = response.getBody(); if (body == null || !body.contains("OTA")) { - throw new RenException("OTA接口返回内容格式不正确,可能不是一个真实的OTA接口"); + throw new RenException(ErrorCode.OTA_INTERFACE_FORMAT_ERROR); } } catch (Exception e) { - throw new RenException("OTA接口验证失败:" + e.getMessage()); + throw new RenException(ErrorCode.OTA_INTERFACE_VALIDATION_FAILED); } } @@ -210,28 +211,28 @@ public class SysParamsController { return; } if (StringUtils.isBlank(url) || url.equals("null")) { - throw new RenException("MCP地址不能为空"); + throw new RenException(ErrorCode.MCP_URL_EMPTY); } if (url.contains("localhost") || url.contains("127.0.0.1")) { - throw new RenException("MCP地址不能使用localhost或127.0.0.1"); + throw new RenException(ErrorCode.MCP_URL_LOCALHOST); } if (!url.toLowerCase().contains("key")) { - throw new RenException("不是正确的MCP地址"); + throw new RenException(ErrorCode.MCP_URL_INVALID); } try { // 发送GET请求 ResponseEntity response = restTemplate.getForEntity(url, String.class); if (response.getStatusCode() != HttpStatus.OK) { - throw new RenException("MCP接口访问失败,状态码:" + response.getStatusCode()); + throw new RenException(ErrorCode.MCP_INTERFACE_ACCESS_FAILED); } // 检查响应内容是否包含mcp相关信息 String body = response.getBody(); if (body == null || !body.contains("success")) { - throw new RenException("MCP接口返回内容格式不正确,可能不是一个真实的MCP接口"); + throw new RenException(ErrorCode.MCP_INTERFACE_FORMAT_ERROR); } } catch (Exception e) { - throw new RenException("MCP接口验证失败:" + e.getMessage()); + throw new RenException(ErrorCode.MCP_INTERFACE_VALIDATION_FAILED); } } @@ -241,31 +242,31 @@ public class SysParamsController { return; } if (StringUtils.isBlank(url) || url.equals("null")) { - throw new RenException("声纹接口地址不能为空"); + throw new RenException(ErrorCode.VOICEPRINT_URL_EMPTY); } if (url.contains("localhost") || url.contains("127.0.0.1")) { - throw new RenException("声纹接口地址不能使用localhost或127.0.0.1"); + throw new RenException(ErrorCode.VOICEPRINT_URL_LOCALHOST); } if (!url.toLowerCase().contains("key")) { - throw new RenException("不是正确的声纹接口地址"); + throw new RenException(ErrorCode.VOICEPRINT_URL_INVALID); } // 验证URL格式 if (!url.toLowerCase().startsWith("http")) { - throw new RenException("声纹接口地址必须以http或https开头"); + throw new RenException(ErrorCode.VOICEPRINT_URL_PROTOCOL_ERROR); } try { // 发送GET请求 ResponseEntity response = restTemplate.getForEntity(url, String.class); if (response.getStatusCode() != HttpStatus.OK) { - throw new RenException("声纹接口访问失败,状态码:" + response.getStatusCode()); + throw new RenException(ErrorCode.VOICEPRINT_INTERFACE_ACCESS_FAILED); } // 检查响应内容 String body = response.getBody(); if (body == null || !body.contains("healthy")) { - throw new RenException("声纹接口返回内容格式不正确,可能不是一个真实的MCP接口"); + throw new RenException(ErrorCode.VOICEPRINT_INTERFACE_FORMAT_ERROR); } } catch (Exception e) { - throw new RenException("声纹接口验证失败:" + e.getMessage()); + throw new RenException(ErrorCode.VOICEPRINT_INTERFACE_VALIDATION_FAILED); } } @@ -275,20 +276,20 @@ public class SysParamsController { return; } if (StringUtils.isBlank(secret) || secret.equals("null")) { - throw new RenException("mqtt密钥不能为空"); + throw new RenException(ErrorCode.MQTT_SECRET_EMPTY); } if (secret.length() < 8) { - throw new RenException("您的mqtt密钥长度不安全,字符数需要至少8个字符且必须同时包含大小写字母"); + throw new RenException(ErrorCode.MQTT_SECRET_LENGTH_INSECURE); } // 检查是否同时包含大小写字母 if (!secret.matches(".*[a-z].*") || !secret.matches(".*[A-Z].*")) { - throw new RenException("您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母"); + throw new RenException(ErrorCode.MQTT_SECRET_CHARACTER_INSECURE); } // 不允许包含弱密码 String[] weakPasswords = { "test", "1234", "admin", "password", "qwerty", "xiaozhi" }; for (String weakPassword : weakPasswords) { if (secret.toLowerCase().contains(weakPassword)) { - throw new RenException("您的mqtt密钥包含弱密码:" + weakPassword); + throw new RenException(ErrorCode.MQTT_SECRET_WEAK_PASSWORD); } } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java index ffe88986..a2adb150 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java @@ -17,6 +17,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import lombok.AllArgsConstructor; import xiaozhi.common.exception.RenException; +import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.page.PageData; import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; @@ -153,7 +154,7 @@ public class SysDictDataServiceImpl extends BaseServiceImpl { - showSuccess("登录成功!"); + showSuccess(this.$t('login.loginSuccess')); this.$store.commit("setToken", JSON.stringify(data.data)); goToPage("/home"); }, (err) => { - showDanger(err.data.msg || "登录失败"); + // 直接使用后端返回的国际化消息 + let errorMessage = err.data.msg || "登录失败"; + + showDanger(errorMessage); if ( err.data != null && err.data.msg != null && - err.data.msg.indexOf("图形验证码") > -1 + err.data.msg.indexOf("图形验证码") > -1 || err.data.msg.indexOf("Captcha") > -1 ) { this.fetchCaptcha(); } diff --git a/main/manager-web/src/views/register.vue b/main/manager-web/src/views/register.vue index 873cc9a6..c839abc8 100644 --- a/main/manager-web/src/views/register.vue +++ b/main/manager-web/src/views/register.vue @@ -47,7 +47,7 @@ - 验证码 @@ -61,7 +61,7 @@ - {{ countdown > 0 ? `${countdown}秒后重试` : $t('register.sendCaptcha') }} + {{ countdown > 0 ? `${countdown}${$t('register.secondsLater')}` : $t('register.sendCaptcha') }} @@ -248,18 +248,18 @@ export default { return; } if (!this.form.mobileCaptcha) { - showDanger('请输入手机验证码'); + showDanger(this.$t('register.requiredMobileCaptcha')); return; } } else { // 用户名注册验证 - if (!this.validateInput(this.form.username, '用户名不能为空')) { + if (!this.validateInput(this.form.username, this.$t('register.requiredUsername'))) { return; } } // 验证密码 - if (!this.validateInput(this.form.password, '密码不能为空')) { + if (!this.validateInput(this.form.password, this.$t('register.requiredPassword'))) { return; } if (this.form.password !== this.form.confirmPassword) { @@ -267,7 +267,7 @@ export default { return } // 验证验证码 - if (!this.validateInput(this.form.captcha, '验证码不能为空')) { + if (!this.validateInput(this.form.captcha, this.$t('register.requiredCaptcha'))) { return; } diff --git a/main/xiaozhi-server/agent-base-prompt.txt b/main/xiaozhi-server/agent-base-prompt.txt index 9eb7731b..34245291 100644 --- a/main/xiaozhi-server/agent-base-prompt.txt +++ b/main/xiaozhi-server/agent-base-prompt.txt @@ -67,6 +67,7 @@ 【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】 +- **设备ID:** {{device_id}} - **当前时间:** {{current_time}} - **今天日期:** {{today_date}} ({{today_weekday}}) - **今天农历:** {{lunar_date}} diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 42066826..a02de435 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -922,3 +922,18 @@ TTS: # 默认音色,如需其他音色可到项目assets文件夹下注册 voice: "jay_klee" output_dir: tmp/ + AliBLTTS: + # 阿里百炼CosyVoice大模型流式文本语音合成 + # 可在这里找到你的 api_key https://bailian.console.aliyun.com/?apiKey=1#/api-key + # cosyvoice-v3和部分音色需要申请开通 + type: alibl_stream + api_key: 你的api_key + model: "cosyvoice-v2" + voice: "longcheng_v2" + output_dir: tmp/ + # 以下可不用设置,使用默认设置 + # format: pcm # 音频格式:pcm、wav、mp3、opus + # sample_rate: 24000 # 采样率:16000, 24000, 48000 + # volume: 50 # 音量:0-100 + # rate: 1 # 语速:0.5~2 + # pitch: 1 # 语调:0.5~2 \ No newline at end of file diff --git a/main/xiaozhi-server/core/handle/sendAudioHandle.py b/main/xiaozhi-server/core/handle/sendAudioHandle.py index d159d3ff..8beda577 100644 --- a/main/xiaozhi-server/core/handle/sendAudioHandle.py +++ b/main/xiaozhi-server/core/handle/sendAudioHandle.py @@ -114,6 +114,9 @@ async def sendAudio(conn, audios, frame_duration=60): delay = expected_time - current_time if delay > 0: await asyncio.sleep(delay) + else: + # 纠正误差 + flow_control["start_time"] += abs(delay) if conn.conn_from_mqtt_gateway: # 计算时间戳和序列号 diff --git a/main/xiaozhi-server/core/providers/tts/alibl_stream.py b/main/xiaozhi-server/core/providers/tts/alibl_stream.py new file mode 100644 index 00000000..c0d187f0 --- /dev/null +++ b/main/xiaozhi-server/core/providers/tts/alibl_stream.py @@ -0,0 +1,522 @@ +import os +import uuid +import json +import time +import queue +import asyncio +import traceback +import websockets +from asyncio import Task +from config.logger import setup_logging +from core.utils import opus_encoder_utils +from core.utils.tts import MarkdownCleaner +from core.providers.tts.base import TTSProviderBase +from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType + +TAG = __name__ +logger = setup_logging() + + +class TTSProvider(TTSProviderBase): + def __init__(self, config, delete_audio_file): + super().__init__(config, delete_audio_file) + + self.interface_type = InterfaceType.DUAL_STREAM + # 基础配置 + self.api_key = config.get("api_key") + if not self.api_key: + raise ValueError("api_key is required for CosyVoice TTS") + + # WebSocket配置 + self.ws_url = "wss://dashscope.aliyuncs.com/api-ws/v1/inference/" + self.ws = None + self._monitor_task = None + self.last_active_time = None + + # 模型和音色配置 + self.model = config.get("model", "cosyvoice-v2") + self.voice = config.get("voice", "longxiaochun_v2") # 默认音色 + if config.get("private_voice"): + self.voice = config.get("private_voice") + + # 音频参数配置 + self.format = config.get("format", "pcm") + sample_rate = config.get("sample_rate", "24000") + self.sample_rate = int(sample_rate) if sample_rate else 24000 + + volume = config.get("volume", "50") + self.volume = int(volume) if volume else 50 + + rate = config.get("rate", "1.0") + self.rate = float(rate) if rate else 1.0 + + pitch = config.get("pitch", "1.0") + self.pitch = float(pitch) if pitch else 1.0 + + self.header = { + "Authorization": f"Bearer {self.api_key}", + # "user-agent": "your_platform_info", // 可选 + # "X-DashScope-WorkSpace": workspace, // 可选,阿里云百炼业务空间ID + "X-DashScope-DataInspection": "enable", + } + + # 创建Opus编码器 + self.opus_encoder = opus_encoder_utils.OpusEncoderUtils( + sample_rate=self.sample_rate, channels=1, frame_size_ms=60 + ) + + async def _ensure_connection(self): + """确保WebSocket连接可用,支持60秒内连接复用""" + try: + current_time = time.time() + if self.ws and current_time - self.last_active_time < 60: + # 一分钟内才可以复用链接进行连续对话 + logger.bind(tag=TAG).info(f"使用已有链接...") + return self.ws + logger.bind(tag=TAG).info("开始建立新连接...") + + self.ws = await websockets.connect( + self.ws_url, + additional_headers=self.header, + ping_interval=30, + ping_timeout=10, + close_timeout=10, + ) + + logger.bind(tag=TAG).info("WebSocket连接建立成功") + self.last_active_time = current_time + return self.ws + except Exception as e: + logger.bind(tag=TAG).error(f"建立连接失败: {str(e)}") + self.ws = None + self.last_active_time = None + raise + + def tts_text_priority_thread(self): + """流式TTS文本处理线程""" + while not self.conn.stop_event.is_set(): + try: + message = self.tts_text_queue.get(timeout=1) + logger.bind(tag=TAG).debug( + f"收到TTS任务|{message.sentence_type.name} | {message.content_type.name} | 会话ID: {self.conn.sentence_id}" + ) + + if message.sentence_type == SentenceType.FIRST: + self.conn.client_abort = False + + if self.conn.client_abort: + try: + logger.bind(tag=TAG).info("收到打断信息,终止TTS文本处理线程") + continue + except Exception as e: + logger.bind(tag=TAG).error(f"取消TTS会话失败: {str(e)}") + continue + + if message.sentence_type == SentenceType.FIRST: + # 初始化会话 + try: + if not getattr(self.conn, "sentence_id", None): + self.conn.sentence_id = uuid.uuid4().hex + logger.bind(tag=TAG).info(f"自动生成新的 会话ID: {self.conn.sentence_id}") + + logger.bind(tag=TAG).info("开始启动TTS会话...") + future = asyncio.run_coroutine_threadsafe( + self.start_session(self.conn.sentence_id), + loop=self.conn.loop, + ) + future.result() + self.before_stop_play_files.clear() + logger.bind(tag=TAG).info("TTS会话启动成功") + except Exception as e: + logger.bind(tag=TAG).error(f"启动TTS会话失败: {str(e)}") + continue + + elif ContentType.TEXT == message.content_type: + if message.content_detail: + try: + logger.bind(tag=TAG).debug( + f"开始发送TTS文本: {message.content_detail}" + ) + future = asyncio.run_coroutine_threadsafe( + self.text_to_speak(message.content_detail, None), + loop=self.conn.loop, + ) + future.result() + logger.bind(tag=TAG).debug("TTS文本发送成功") + except Exception as e: + logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}") + continue + + elif ContentType.FILE == message.content_type: + logger.bind(tag=TAG).info( + f"添加音频文件到待播放列表: {message.content_file}" + ) + if message.content_file and os.path.exists(message.content_file): + # 先处理文件音频数据 + self._process_audio_file_stream(message.content_file, callback=lambda audio_data: self.handle_audio_file(audio_data, message.content_detail)) + + if message.sentence_type == SentenceType.LAST: + try: + logger.bind(tag=TAG).info("开始结束TTS会话...") + future = asyncio.run_coroutine_threadsafe( + self.finish_session(self.conn.sentence_id), + loop=self.conn.loop, + ) + future.result() + except Exception as e: + logger.bind(tag=TAG).error(f"结束TTS会话失败: {str(e)}") + continue + + except queue.Empty: + continue + except Exception as e: + logger.bind(tag=TAG).error( + f"处理TTS文本失败: {str(e)}, 类型: {type(e).__name__}, 堆栈: {traceback.format_exc()}" + ) + continue + + async def text_to_speak(self, text, _): + """发送文本到TTS服务进行合成""" + try: + if self.ws is None: + logger.bind(tag=TAG).warning("WebSocket连接不存在,终止发送文本") + return + + # 过滤Markdown + filtered_text = MarkdownCleaner.clean_markdown(text) + + # 发送continue-task消息 + continue_task_message = { + "header": { + "action": "continue-task", + "task_id": self.conn.sentence_id, + "streaming": "duplex", + }, + "payload": {"input": {"text": filtered_text}}, + } + + await self.ws.send(json.dumps(continue_task_message)) + self.last_active_time = time.time() + logger.bind(tag=TAG).debug(f"已发送文本: {filtered_text}") + + except Exception as e: + logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}") + if self.ws: + try: + await self.ws.close() + except: + pass + self.ws = None + raise + + async def start_session(self, session_id): + """启动TTS会话""" + logger.bind(tag=TAG).info(f"开始会话~~{session_id}") + try: + # 检查并清理上一个会话的监听任务 + if ( + self._monitor_task is not None + and isinstance(self._monitor_task, Task) + and not self._monitor_task.done() + ): + logger.bind(tag=TAG).info("检测到未完成的上个会话,关闭监听任务...") + await self.close() + + # 确保连接可用 + await self._ensure_connection() + + # 启动监听任务 + self._monitor_task = asyncio.create_task(self._start_monitor_tts_response()) + + # 发送run-task消息启动会话 + run_task_message = { + "header": { + "action": "run-task", + "task_id": session_id, + "streaming": "duplex", + }, + "payload": { + "task_group": "audio", + "task": "tts", + "function": "SpeechSynthesizer", + "model": self.model, + "parameters": { + "text_type": "PlainText", + "voice": self.voice, + "format": self.format, + "sample_rate": self.sample_rate, + "volume": self.volume, + "rate": self.rate, + "pitch": self.pitch, + }, + "input": {} + }, + } + + await self.ws.send(json.dumps(run_task_message)) + self.last_active_time = time.time() + logger.bind(tag=TAG).info("会话启动请求已发送") + except Exception as e: + logger.bind(tag=TAG).error(f"启动会话失败: {str(e)}") + await self.close() + raise + + async def finish_session(self, session_id): + """结束TTS会话""" + logger.bind(tag=TAG).info(f"关闭会话~~{session_id}") + try: + if self.ws and session_id: + # 发送finish-task消息 + finish_task_message = { + "header": { + "action": "finish-task", + "task_id": session_id, + "streaming": "duplex", + }, + "payload": { + "input": {} + } + } + + await self.ws.send(json.dumps(finish_task_message)) + self.last_active_time = time.time() + logger.bind(tag=TAG).info("会话结束请求已发送") + # 等待监听任务完成 + if self._monitor_task: + try: + await self._monitor_task + except Exception as e: + logger.bind(tag=TAG).error( + f"等待监听任务完成时发生错误: {str(e)}" + ) + finally: + self._monitor_task = None + + except Exception as e: + logger.bind(tag=TAG).error(f"关闭会话失败: {str(e)}") + await self.close() + raise + + async def close(self): + """清理资源""" + # 取消监听任务 + if self._monitor_task: + try: + self._monitor_task.cancel() + await self._monitor_task + except asyncio.CancelledError: + pass + except Exception as e: + logger.bind(tag=TAG).warning(f"关闭时取消监听任务错误: {e}") + self._monitor_task = None + + # 关闭WebSocket连接 + if self.ws: + try: + await self.ws.close() + except: + pass + self.ws = None + self.last_active_time = None + + async def _start_monitor_tts_response(self): + """监听TTS响应""" + try: + session_finished = False + while not self.conn.stop_event.is_set(): + try: + msg = await self.ws.recv() + self.last_active_time = time.time() + + # 检查客户端是否中止 + if self.conn.client_abort: + logger.bind(tag=TAG).info("收到打断信息,终止监听TTS响应") + break + + if isinstance(msg, str): # JSON控制消息 + try: + data = json.loads(msg) + event = data["header"].get("event") + + if event == "task-started": + logger.bind(tag=TAG).debug("TTS任务启动成功~") + self.tts_audio_queue.put((SentenceType.FIRST, [], None)) + elif event == "result-generated": + # 发送缓存的数据 + if self.conn.tts_MessageText: + logger.bind(tag=TAG).info( + f"句子语音生成成功: {self.conn.tts_MessageText}" + ) + self.tts_audio_queue.put( + (SentenceType.FIRST, [], self.conn.tts_MessageText) + ) + self.conn.tts_MessageText = None + elif event == "task-finished": + logger.bind(tag=TAG).debug("TTS任务完成~") + self._process_before_stop_play_files() + session_finished = True + break + elif event == "task-failed": + error_code = data["header"].get("error_code", "unknown") + error_message = data["header"].get("error_message", "未知错误") + logger.bind(tag=TAG).error( + f"TTS任务失败: {error_code} - {error_message}" + ) + break + except json.JSONDecodeError: + logger.bind(tag=TAG).warning("收到无效的JSON消息") + elif isinstance(msg, (bytes, bytearray)): + self.opus_encoder.encode_pcm_to_opus_stream( + msg, False, callback=self.handle_opus + ) + except websockets.ConnectionClosed: + logger.bind(tag=TAG).warning("WebSocket连接已关闭") + break + except Exception as e: + logger.bind(tag=TAG).error( + f"处理TTS响应时出错: {e}\n{traceback.format_exc()}" + ) + break + + # 仅在连接异常且非正常结束时才关闭连接 + if not session_finished and self.ws: + try: + await self.ws.close() + except: + pass + self.ws = None + # 监听任务退出时清理引用 + finally: + self._monitor_task = None + + def to_tts(self, text: str) -> list: + """非流式生成音频数据,用于生成音频及测试场景""" + try: + # 创建事件循环 + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + # 生成会话ID + session_id = uuid.uuid4().hex + # 存储音频数据 + audio_data = [] + + async def _generate_audio(): + ws = await websockets.connect( + self.ws_url, + additional_headers=self.header, + ping_interval=30, + ping_timeout=10, + close_timeout=10, + max_size=10 * 1024 * 1024, + ) + + try: + # 发送run-task消息启动会话 + run_task_message = { + "header": { + "action": "run-task", + "task_id": session_id, + "streaming": "duplex", + }, + "payload": { + "task_group": "audio", + "task": "tts", + "function": "SpeechSynthesizer", + "model": self.model, + "parameters": { + "text_type": "PlainText", + "voice": self.voice, + "format": self.format, + "sample_rate": self.sample_rate, + "volume": self.volume, + "rate": self.rate, + "pitch": self.pitch, + }, + "input": {} + }, + } + await ws.send(json.dumps(run_task_message)) + + # 等待任务启动 + task_started = False + while not task_started: + msg = await ws.recv() + if isinstance(msg, str): + data = json.loads(msg) + header = data.get("header", {}) + if header.get("event") == "task-started": + task_started = True + logger.bind(tag=TAG).debug("TTS任务已启动") + elif header.get("event") == "task-failed": + error_code = header.get("error_code", "unknown") + error_message = header.get("error_message", "未知错误") + raise Exception( + f"启动任务失败: {error_code} - {error_message}" + ) + + # 发送文本 + filtered_text = MarkdownCleaner.clean_markdown(text) + # 发送continue-task消息 + continue_task_message = { + "header": { + "action": "continue-task", + "task_id": session_id, + "streaming": "duplex", + }, + "payload": {"input": {"text": filtered_text}}, + } + await ws.send(json.dumps(continue_task_message)) + + # 发送finish-task消息 + finish_task_message = { + "header": { + "action": "finish-task", + "task_id": session_id, + "streaming": "duplex", + }, + "payload": { + "input": {} + } + } + await ws.send(json.dumps(finish_task_message)) + + # 接收音频数据 + task_finished = False + while not task_finished: + msg = await ws.recv() + if isinstance(msg, (bytes, bytearray)): + self.opus_encoder.encode_pcm_to_opus_stream( + msg, + end_of_stream=False, + callback=lambda opus: audio_data.append(opus) + ) + elif isinstance(msg, str): + data = json.loads(msg) + header = data.get("header", {}) + if header.get("event") == "task-finished": + task_finished = True + logger.bind(tag=TAG).debug("TTS任务完成") + elif header.get("event") == "task-failed": + error_code = header.get("error_code", "unknown") + error_message = header.get("error_message", "未知错误") + raise Exception( + f"合成失败: {error_code} - {error_message}" + ) + + finally: + # 清理资源 + try: + await ws.close() + except: + pass + + # 运行异步任务 + loop.run_until_complete(_generate_audio()) + loop.close() + + return audio_data + + except Exception as e: + logger.bind(tag=TAG).error(f"生成音频数据失败: {str(e)}") + return [] diff --git a/main/xiaozhi-server/core/providers/tts/aliyun_stream.py b/main/xiaozhi-server/core/providers/tts/aliyun_stream.py index 38e70927..9178e2d3 100644 --- a/main/xiaozhi-server/core/providers/tts/aliyun_stream.py +++ b/main/xiaozhi-server/core/providers/tts/aliyun_stream.py @@ -457,7 +457,6 @@ class TTSProvider(TTSProviderBase): logger.bind(tag=TAG).warning("收到无效的JSON消息") # 二进制消息(音频数据) elif isinstance(msg, (bytes, bytearray)): - logger.bind(tag=TAG).debug(f"推送数据到队列里面~~") self.opus_encoder.encode_pcm_to_opus_stream(msg, False, self.handle_opus) except websockets.ConnectionClosed: logger.bind(tag=TAG).warning("WebSocket连接已关闭") diff --git a/main/xiaozhi-server/core/providers/tts/base.py b/main/xiaozhi-server/core/providers/tts/base.py index c4b32355..018215d5 100644 --- a/main/xiaozhi-server/core/providers/tts/base.py +++ b/main/xiaozhi-server/core/providers/tts/base.py @@ -331,15 +331,6 @@ class TTSProviderBase(ABC): # 收到下一个文本开始或会话结束时进行上报 if sentence_type is not SentenceType.MIDDLE: - # 重置音频流控状态(新句子开始或者结束) - if hasattr(self.conn, 'audio_flow_control'): - self.conn.audio_flow_control = { - 'last_send_time': 0, - 'packet_count': 0, - 'start_time': time.perf_counter(), - 'sequence': 0 # 添加序列号 - } - # 上报TTS数据 if enqueue_text is not None and enqueue_audio is not None: enqueue_tts_report(self.conn, enqueue_text, enqueue_audio) diff --git a/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py b/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py index ff295581..45cc5967 100644 --- a/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py +++ b/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py @@ -449,7 +449,6 @@ class TTSProvider(TTSProviderBase): res.optional.event == EVENT_TTSResponse and res.header.message_type == AUDIO_ONLY_RESPONSE ): - logger.bind(tag=TAG).debug(f"推送数据到队列里面~~") self.wav_to_opus_data_audio_raw_stream(res.payload, callback=self.handle_opus) elif res.optional.event == EVENT_TTSSentenceEnd: logger.bind(tag=TAG).info(f"句子语音生成成功:{self.tts_text}") @@ -616,12 +615,13 @@ class TTSProvider(TTSProviderBase): "speech_rate": self.speech_rate, "loudness_rate": self.loudness_rate }, + "additions": json.dumps({ + "post_process": { + "pitch": self.pitch + } + }) }, - "additions": { - "post_process": { - "pitch": self.pitch - } - } + } ) ) diff --git a/main/xiaozhi-server/core/utils/prompt_manager.py b/main/xiaozhi-server/core/utils/prompt_manager.py index b6c4bef0..2b6bde15 100644 --- a/main/xiaozhi-server/core/utils/prompt_manager.py +++ b/main/xiaozhi-server/core/utils/prompt_manager.py @@ -224,6 +224,7 @@ class PromptManager: local_address=local_address, weather_info=weather_info, emojiList=EMOJI_List, + device_id=device_id, ) device_cache_key = f"device_prompt:{device_id}" self.cache_manager.set(