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 fe71c05f..8a9e4fba 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 @@ -304,7 +304,7 @@ public interface Constant { /** * 版本号 */ - public static final String VERSION = "0.9.1"; + public static final String VERSION = "0.9.2"; /** * 无效固件URL 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 5681ec9e..820f4089 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 @@ -251,4 +251,9 @@ public interface ErrorCode { int AGENT_TAG_NOT_EXIST = 10198; // 标签不存在 int RAG_DOCUMENT_PARSING_DELETE_ERROR = 10199; // 文档解析中,禁止删除 + + // 智能体MCP相关错误码 + int MCP_ACCESS_POINT_ADDRESS_NO_PERMISSION = 10200; // 没有权限查看该智能体的MCP接入点地址 + int MCP_ACCESS_POINT_ADDRESS_NOT_CONFIGURED = 10201; // 请联系管理员进入参数管理配置mcp接入点地址 + int MCP_ACCESS_POINT_TOOLS_LIST_NO_PERMISSION = 10202; // 没有权限查看该智能体的MCP工具列表 } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentMcpAccessPointController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentMcpAccessPointController.java index bee728f5..c805a4e7 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentMcpAccessPointController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentMcpAccessPointController.java @@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.user.UserDetail; import xiaozhi.common.utils.Result; import xiaozhi.modules.agent.service.AgentMcpAccessPointService; @@ -40,11 +41,11 @@ public class AgentMcpAccessPointController { // 检查权限 if (!agentService.checkAgentPermission(agentId, user.getId())) { - return new Result().error("没有权限查看该智能体的MCP接入点地址"); + return new Result().error(ErrorCode.MCP_ACCESS_POINT_ADDRESS_NO_PERMISSION); } String agentMcpAccessAddress = agentMcpAccessPointService.getAgentMcpAccessAddress(agentId); if (agentMcpAccessAddress == null) { - return new Result().ok("请联系管理员进入参数管理配置mcp接入点地址"); + return new Result().error(ErrorCode.MCP_ACCESS_POINT_ADDRESS_NOT_CONFIGURED); } return new Result().ok(agentMcpAccessAddress); } @@ -58,7 +59,7 @@ public class AgentMcpAccessPointController { // 检查权限 if (!agentService.checkAgentPermission(agentId, user.getId())) { - return new Result>().error("没有权限查看该智能体的MCP工具列表"); + return new Result>().error(ErrorCode.MCP_ACCESS_POINT_TOOLS_LIST_NO_PERMISSION); } List agentMcpToolsList = agentMcpAccessPointService.getAgentMcpToolsList(agentId); return new Result>().ok(agentMcpToolsList); diff --git a/main/manager-api/src/main/resources/i18n/messages.properties b/main/manager-api/src/main/resources/i18n/messages.properties index d875c075..685fdcde 100644 --- a/main/manager-api/src/main/resources/i18n/messages.properties +++ b/main/manager-api/src/main/resources/i18n/messages.properties @@ -206,4 +206,7 @@ 10197=\u6807\u7B7E\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A 10198=\u6807\u7B7E\u4E0D\u5B58\u5728 10199=\u89E3\u6790\u4E2D\u6587\u4EF6\u6682\u4E0D\u652F\u6301\u6B64\u64CD\u4F5C +10200=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684MCP\u63A5\u5165\u70B9\u5730\u5740 +10201=\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u8FDB\u5165\u53C2\u6570\u7BA1\u7406\u914D\u7F6Emcp\u63A5\u5165\u70B9\u5730\u5740 +10202=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684MCP\u5DE5\u5177\u5217\u8868 diff --git a/main/manager-api/src/main/resources/i18n/messages_de_DE.properties b/main/manager-api/src/main/resources/i18n/messages_de_DE.properties index ffe4b52e..67dc3019 100644 --- a/main/manager-api/src/main/resources/i18n/messages_de_DE.properties +++ b/main/manager-api/src/main/resources/i18n/messages_de_DE.properties @@ -206,4 +206,7 @@ 10197=Tag-Name darf nicht leer sein 10198=Tag nicht gefunden 10199=Dateianalyse l\u00E4uft, dieser Vorgang wird nicht unterst\u00FCtzt +10200=Keine Berechtigung, die MCP-Endpunktadresse dieses Agenten anzuzeigen +10201=Bitte kontaktieren Sie den Administrator, um die MCP-Endpunktadresse in der Parameterverwaltung zu konfigurieren +10202=Keine Berechtigung, die MCP-Tool-Liste dieses Agenten anzuzeigen diff --git a/main/manager-api/src/main/resources/i18n/messages_en_US.properties b/main/manager-api/src/main/resources/i18n/messages_en_US.properties index 1ee75184..bce68ba9 100644 --- a/main/manager-api/src/main/resources/i18n/messages_en_US.properties +++ b/main/manager-api/src/main/resources/i18n/messages_en_US.properties @@ -206,4 +206,7 @@ 10197=Tag name cannot be empty 10198=Tag not found 10199=Parsing in progress, this operation is not supported +10200=No permission to view the MCP endpoint address of this agent +10201=Please contact the administrator to configure the MCP endpoint address in parameter management +10202=No permission to view the MCP tool list of this agent diff --git a/main/manager-api/src/main/resources/i18n/messages_pt_BR.properties b/main/manager-api/src/main/resources/i18n/messages_pt_BR.properties new file mode 100644 index 00000000..b679f253 --- /dev/null +++ b/main/manager-api/src/main/resources/i18n/messages_pt_BR.properties @@ -0,0 +1,211 @@ +#Portugu\u00EAs do Brasil +500=Erro interno do servidor +401=N\u00E3o autorizado +403=Acesso negado, sem permiss\u00E3o + +10001={0} n\u00E3o pode ser vazio +10002=O registro j\u00E1 existe no banco de dados +10003=Falha ao obter par\u00E2metros +10004=Conta ou senha incorretos +10005=Conta desativada +10006=Identificador \u00FAnico n\u00E3o pode ser vazio +10007=C\u00F3digo de verifica\u00E7\u00E3o incorreto +10008=Exclua primeiro o submenu ou bot\u00E3o +10009=Senha atual incorreta +10010=Conta ou senha incorretos, voc\u00EA ainda tem {0} tentativas + +10011=Erro na sele\u00E7\u00E3o do departamento superior +10012=O menu superior n\u00E3o pode ser ele mesmo +10013=Interface de permiss\u00E3o de dados, apenas par\u00E2metros do tipo Map +10014=Exclua primeiro o departamento inferior +10015=Exclua primeiro os usu\u00E1rios do departamento +10016=Falha na implanta\u00E7\u00E3o, sem fluxo +10017=Diagrama incorreto, verifique +10018=Falha na exporta\u00E7\u00E3o, ID do modelo {0} + +10019=Por favor, fa\u00E7a upload do arquivo +10020=Token n\u00E3o pode ser vazio +10021=Token expirado, fa\u00E7a login novamente +10022=Conta bloqueada +10023=Por favor, fa\u00E7a upload de arquivos nos formatos zip, bar, bpmn, bpmn20.xml + +10024=Falha no upload do arquivo {0} +10025=Falha ao enviar SMS {0} +10026=Modelo de e-mail n\u00E3o existe + +10027=Erro no servi\u00E7o Redis +10028=Falha na tarefa agendada +10029=N\u00E3o pode conter caracteres ilegais +10030=Comprimento da senha insuficiente, m\u00EDnimo de {0} caracteres +10031=A senha deve conter n\u00FAmeros, letras mai\u00FAsculas e min\u00FAsculas e caracteres especiais +10032=Erro ao excluir dados +10033=C\u00F3digo de verifica\u00E7\u00E3o do dispositivo incorreto + +10034=Valor do par\u00E2metro n\u00E3o pode ser vazio +10035=Tipo do par\u00E2metro n\u00E3o pode ser vazio +10036=Tipo de par\u00E2metro n\u00E3o suportado +10037=Valor do par\u00E2metro deve ser um n\u00FAmero v\u00E1lido +10038=Valor do par\u00E2metro deve ser true ou false +10039=Valor do par\u00E2metro deve ser um formato de array JSON v\u00E1lido +10040=Valor do par\u00E2metro deve ser um formato JSON v\u00E1lido + +10041=Dispositivo n\u00E3o encontrado +10042={0} +10043=Falha ao excluir dados +10044=Usu\u00E1rio n\u00E3o logged in +10045=Falha na conex\u00E3o WebSocket ou conex\u00E3o expirada +10046=Erro ao salvar impress\u00E3o vocal, entre em contato com o administrador +10047=Limite di\u00E1rio de envio atingido +10048=Erro na inser\u00E7\u00E3o da senha antiga +10049=LLM configurado n\u00E3o openai ou Collama +10050=Falha na gera\u00E7\u00E3o do token +10051=Recurso n\u00E3o existe +10052=Agente padr\u00E3o n\u00E3o encontrado +10053=Agente n\u00E3o encontrado +10054=Interface de impress\u00E3o vocal n\u00E3o configurada, configure primeiro o endere\u00E7o da interface no par\u00E2metro (server.voice_print) +10055=Falha no envio do SMS +10056=Falha ao estabelecer conex\u00E3o SMS +10057=Falha ao criar impress\u00E3o vocal do agente +10058=Falha ao atualizar a impress\u00E3o vocal correspondente do agente +10059=Falha ao excluir a impress\u00E3o vocal correspondente do agente +10060=Envio muito frequente, tente novamente ap\u00F3s {0} segundos +10061=C\u00F3digo de ativa\u00E7\u00E3o n\u00E3o pode ser vazio +10062=C\u00F3digo de ativa\u00E7\u00E3o incorreto +10063=Dispositivo j\u00E1 ativado +10064=Este modelo o modelo padr\u00E3o, configure primeiro outro modelo como padr\u00E3o +10065=Falha ao adicionar dados +10066=Falha ao modificar dados +10067=C\u00F3digo de verifica\u00E7\u00E3o gr\u00E1fico incorreto +10068=Registro por telefone n\u00E3o habilitado, fun\u00E7\u00E3o de c\u00F3digo de verifica\u00E7\u00E3o por SMS indispon\u00EDvel +10069=Nome de usu\u00E1rio n\u00E3o n\u00FAmero de telefone, por favor insira novamente +10070=Este n\u00FAmero de telefone j\u00E1 est\u00E1 registrado +10071=N\u00FAmero de telefone inserido n\u00E3o est\u00E1 registrado +10072=Registro de usu\u00E1rio comum n\u00E3o permitido atualmente +10073=Registro por telefone n\u00E3o habilitado, fun\u00E7\u00E3o de recupera\u00E7\u00E3o de senha indispon\u00EDvel +10074=Formato de n\u00FAmero de telefone incorreto +10075=C\u00F3digo de verifica\u00E7\u00E3o do telefone incorreto +10076=Tipo de dicion\u00E1rio n\u00E3o existe +10077=C\u00F3digo de tipo de dicion\u00E1rio duplicado +10078=Falha ao carregar recurso +10079=Par\u00E2metros de sele\u00E7\u00E3o incompat\u00EDveis entre LLM e reconhecimento de inten\u00E7\u00E3o Intent +10080=A pessoa ({0}) correspondente a esta impress\u00E3o vocal j\u00E1 est\u00E1 registrada, escolha outra voz para registrar +10081=Erro ao excluir impress\u00E3o vocal +10082=Esta modifica\u00E7\u00E3o n\u00E3o permitida, esta voz j\u00E1 est\u00E1 registrada como impress\u00E3o vocal ({0}) +10083=Erro ao modificar impress\u00E3o vocal, entre em contato com o administrador +10084=Erro no endere\u00E7o da interface de impress\u00E3o vocal, acesse o gerenciamento de par\u00E2metros para modificar o endere\u00E7o +10085=Dados de \u00E1udio n\u00E3o pertencem a este agente +10086=Dados de \u00E1udio est\u00E3o vazios, verifique os dados enviados +10087=Falha ao salvar impress\u00E3o vocal, falha na requisi\u00E7\u00E3o +10088=Falha ao salvar impress\u00E3o vocal, falha no processamento +10089=Falha ao cancelar impress\u00E3o vocal, falha na requisi\u00E7\u00E3o +10090=Falha ao cancelar impress\u00E3o vocal, falha no processamento +10091=Provedor n\u00E3o existe +10092=LLM configurado n\u00E3o existe +10093=Esta configura\u00E7\u00E3o de modelo foi referenciada pelo agente {0}, n\u00E3o pode ser exclu\u00EDda +10094=Este modelo LLM foi referenciado pela configura\u00E7\u00E3o de reconhecimento de inten\u00E7\u00E3o, n\u00E3o pode ser exclu\u00EDdo +10095=Opera\u00E7\u00E3o de servidor inv\u00E1lida +10096=Endere\u00E7o WebSocket do servidor n\u00E3o configurado +10097=Endere\u00E7o WebSocket de destino n\u00E3o existe +10098=Lista de endere\u00E7os WebSocket n\u00E3o pode ser vazia +10099=Endere\u00E7o WebSocket n\u00E3o pode usar localhost ou 127.0.0.1 +10100=Formato de endere\u00E7o WebSocket incorreto +10101=Falha no teste de conex\u00E3o WebSocket +10102=Endere\u00E7o OTA n\u00E3o pode ser vazio +10103=Endere\u00E7o OTA n\u00E3o pode usar localhost ou 127.0.0.1 +10104=Endere\u00E7o OTA deve come\u00E7ar com http ou https +10105=Endere\u00E7o OTA deve terminar com /ota/ +10106=Falha ao acessar interface OTA +10107=Formato de conte\u00FAdo retornado pela interface OTA incorreto +10108=Falha na valida\u00E7\u00E3o da interface OTA +10109=Endere\u00E7o MCP n\u00E3o pode ser vazio +10110=Endere\u00E7o MCP n\u00E3o pode usar localhost ou 127.0.0.1 +10111=Endere\u00E7o MCP incorreto +10112=Falha ao acessar interface MCP +10113=Formato de conte\u00FAdo retornado pela interface MCP incorreto +10114=Falha na valida\u00E7\u00E3o da interface MCP +10115=Endere\u00E7o da interface de impress\u00E3o vocal n\u00E3o pode ser vazio +10116=Endere\u00E7o da interface de impress\u00E3o vocal n\u00E3o pode usar localhost ou 127.0.0.1 +10117=Endere\u00E7o da interface de impress\u00E3o vocal incorreto +10118=Endere\u00E7o da interface de impress\u00E3o vocal deve come\u00E7ar com http ou https +10119=Falha ao acessar interface de impress\u00E3o vocal +10120=Formato de conte\u00FAdo retornado pela interface de impress\u00E3o vocal incorreto +10121=Falha na valida\u00E7\u00E3o da interface de impress\u00E3o vocal +10122=Chave mqtt n\u00E3o pode ser vazia +10123=Comprimento da chave mqtt inseguro, deve ter pelo menos 8 caracteres e conter letras mai\u00FAsculas e min\u00FAsculas +10124=Comprimento da chave mqtt inseguro, deve conter letras mai\u00FAsculas e min\u00FAsculas +10125=Sua chave mqtt cont\u00E9m senha fraca +10128=Etiqueta de dicion\u00E1rio duplicada +10129=Chave SM2 n\u00E3o configurada +10130=Falha na descriptografia SM2 +10131=modelType e provideCode n\u00E3o podem ser vazios +10132=Sem permiss\u00E3o para visualizar registros de chat deste agente +10133=ID da sess\u00E3o n\u00E3o pode ser vazio +10134=ID do agente n\u00E3o pode ser vazio +10135=Falha ao baixar registros de chat +10136=Link de download expirado ou inv\u00E1lido +10137=Link de download inv\u00E1lido +10138=Usu\u00E1rio +10139=Agente +10140=Arquivo de \u00E1udio n\u00E3o pode ser vazio +10141=Apenas arquivos de \u00E1udio s\u00E3o suportados +10142=Tamanho do arquivo de \u00E1udio n\u00E3o pode exceder 10MB +10143=Falha no upload +10144=Registro de clonagem de voz n\u00E3o existe +10145=Informa\u00E7\u00F5es de recurso de tom de voz n\u00E3o podem ser vazias +10146=Nome da plataforma n\u00E3o pode ser vazio +10147=ID do tom de voz n\u00E3o pode ser vazio +10148=N\u00FAmero da conta atribu\u00EDda n\u00E3o pode ser vazio +10149=IDs de recursos de tom de voz a serem exclu\u00EDdos n\u00E3o podem ser vazios +10150=Voc\u00EA n\u00E3o tem permiss\u00E3o para operar este registro +10151=Por favor, fa\u00E7a upload do arquivo de \u00E1udio primeiro +10152=Configura\u00E7\u00E3o do modelo n\u00E3o encontrada +10153=Tipo de modelo n\u00E3o encontrado +10154=Falha no treinamento: {0} +10155=Configura\u00E7\u00E3o ausente do mecanismo Huoshan +10156=Erro de formato de resposta, campo BaseResp ausente +10157=Falha na requisi\u00E7\u00E3o +10158=Clonagem de tom de voz: +10159=ID do tom de voz j\u00E1 existe +10160=Formato de ID do tom de voz do mecanismo Huoshan incorreto, deve come\u00E7ar com S_ +10161=Endere\u00E7o Mac j\u00E1 existe +10162=Provedor de modelos n\u00E3o existe +10163=Registro da base de conhecimento n\u00E3o existe +10164=Configura\u00E7\u00E3o RAG n\u00E3o encontrada +10165=Erro de tipo de configura\u00E7\u00E3o RAG +10166=Configura\u00E7\u00E3o RAG padr\u00E3o n\u00E3o encontrada +10167=Falha na chamada RAG, {0} +10168=Falha no upload do arquivo +10169=Voc\u00EA n\u00E3o tem permiss\u00E3o para operar este registro +10170=Nome da base de conhecimento duplicado +10171=base_url da configura\u00E7\u00E3o RAG n\u00E3o pode ser vazio +10172=api_key da configura\u00E7\u00E3o RAG n\u00E3o pode ser vazio +10173=api_key da configura\u00E7\u00E3o RAG n\u00E3o pode ser vazio, substitua pelo par\u00E2metro API obtido +10174=Formato de base_url da configura\u00E7\u00E3o RAG incorreto, deve come\u00E7ar com http ou https +10175=Endere\u00E7o mac n\u00E3o pode ser vazio +10176=dataset_id da configura\u00E7\u00E3o RAG n\u00E3o pode ser vazio +10177=model_id da configura\u00E7\u00E3o RAG n\u00E3o pode ser vazio +10178=dataset_id e model_id da configura\u00E7\u00E3o RAG n\u00E3o podem ser vazios +10179=Nome do arquivo n\u00E3o pode ser vazio +10180=Conte\u00FAdo do arquivo n\u00E3o pode ser vazio +10181=Nome da clonagem de tom de voz n\u00E3o pode ser vazio +10182=\u00C1udio de clonagem de tom de voz n\u00E3o existe +10183=Agente padr\u00E3o n\u00E3o encontrado +10184=Tipo de adaptador n\u00E3o suportado +10185=Falha na valida\u00E7\u00E3o da configura\u00E7\u00E3o RAG +10186=Falha ao criar adaptador +10187=Falha ao inicializar adaptador +10188=Falha no teste de conex\u00E3o do adaptador +10189=Falha na opera\u00E7\u00E3o do adaptador +10190=Adaptador n\u00E3o encontrado +10191=Erro de cache do adaptador +10192=Tipo de adaptador n\u00E3o encontrado +10193=ID do dispositivo n\u00E3o pode ser vazio +10194=Dispositivo n\u00E3o existe ou est\u00E1 offline +10195=Limite de uploads OTA excedido +10196=Nome da etiqueta j\u00E1 existe +10197=Nome da etiqueta n\u00E3o pode ser vazio +10198=Etiqueta n\u00E3o existe +10199=Esta opera\u00E7\u00E3o n\u00E3o suportada na an\u00E1lise de arquivos chineses +10200=Sem permiss\u00e3o para visualizar o endere\u00e7o do endpoint MCP deste agente +10201=Por favor, contate o administrador para configurar o endere\u00e7o do endpoint MCP no gerenciamento de par\u00e2metros +10202=Sem permiss\u00e3o para visualizar a lista de ferramentas MCP deste agente \ No newline at end of file diff --git a/main/manager-api/src/main/resources/i18n/messages_vi_VN.properties b/main/manager-api/src/main/resources/i18n/messages_vi_VN.properties index 141c8ab7..c5973226 100644 --- a/main/manager-api/src/main/resources/i18n/messages_vi_VN.properties +++ b/main/manager-api/src/main/resources/i18n/messages_vi_VN.properties @@ -206,4 +206,6 @@ 10197=T\u00EAn th\u1EB9\uFFFD kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng 10198=Kh\u00F4ng t\u00ECm th\u1EA5y th\u1EB9\uFFFD 10199=T\u1EC7p \u0111ang \u0111\u01B0\u1EE3c ph\u00E2n t\u00EDch, thao t\u00E1c n\u00E0y kh\u00F4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3 - +10200=Kh\u00f4ng c\u00f3 quy\u1ec1n xem \u0111\u1ecba ch\u1ec9 \u0111i\u1ec3m cu\u1ed1i MCP c\u1ee7a \u0111\u1ea1i l\u00fd n\u00e0y +10201=Vui l\u00f2ng li\u00ean h\u1ec7 qu\u1ea3n tr\u1ecb vi\u00ean \u0111\u1ec3 c\u1ea5u h\u00ecnh \u0111\u1ecba ch\u1ec9 \u0111i\u1ec3m cu\u1ed1i MCP trong qu\u1ea3n l\u00fd tham s\u1ed1 +10202=Kh\u00f4ng c\u00f3 quy\u1ec1n xem danh s\u00e1ch c\u00f4ng c\u1ee5 MCP c\u1ee7a \u0111\u1ea1i l\u00fd n\u00e0y \ No newline at end of file diff --git a/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties b/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties index a9429005..9350a892 100644 --- a/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties +++ b/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties @@ -202,4 +202,10 @@ 10193=\u8BBE\u5907ID\u4E0D\u80FD\u4E3A\u7A7A 10194=\u8BBE\u5907\u4E0D\u5B58\u5728\u6216\u4E0D\u5728\u7EBF 10195=OTA\u4E0A\u4F20\u6B21\u6570\u8D85\u8FC7\u9650\u5236 -10196=\u89E3\u6790\u4E2D\u6587\u4EF6\u6682\u4E0D\u652F\u6301\u6B64\u64CD\u4F5C +10196=\u6807\u7B7E\u540D\u79F0\u5DF2\u5B58\u5728 +10197=\u6807\u7B7E\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A +10198=\u6807\u7B7E\u4E0D\u5B58\u5728 +10199=\u89E3\u6790\u4E2D\u6587\u4EF6\u6682\u4E0D\u652F\u6301\u6B64\u64CD\u4F5C +10200=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684MCP\u63A5\u5165\u70B9\u5730\u5740 +10201=\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u8FDB\u5165\u53C2\u6570\u7BA1\u7406\u914D\u7F6Emcp\u63A5\u5165\u70B9\u5730\u5740 +10202=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684MCP\u5DE5\u5177\u5217\u8868 diff --git a/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties b/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties index acbacdbb..4bd8fbe8 100644 --- a/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties +++ b/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties @@ -206,4 +206,6 @@ 10197=\u6A19\u7C64\u540D\u7A31\u4E0D\u80FD\u4E3A\u7A7A 10198=\u6A19\u7C64\u4E0D\u5B58\u5728 10199=\u89E3\u6790\u4E2D\u6587\u4EF6\u66AB\u4E0D\u652F\u6301\u6B64\u64CD\u4F5C - +10200=\u6C92\u6709\u6B0A\u9650\u67E5\u770B\u8A72\u667A\u80FD\u9AD4\u7684MCP\u63A5\u5165\u9EDE\u5730\u5740 +10201=\u8ACB\u806F\u7E6B\u7BA1\u7406\u54E1\u9032\u5165\u53C3\u6578\u7BA1\u7406\u914D\u7F6Emcp\u63A5\u5165\u9EDE\u5730\u5740 +10202=\u6C92\u6709\u6B0A\u9650\u67E5\u770B\u8A72\u667A\u80FD\u9AD4\u7684MCP\u5DE5\u5177\u5217\u8868 diff --git a/main/manager-api/src/main/resources/i18n/validation_pt_BR.properties b/main/manager-api/src/main/resources/i18n/validation_pt_BR.properties new file mode 100644 index 00000000..fccb04b0 --- /dev/null +++ b/main/manager-api/src/main/resources/i18n/validation_pt_BR.properties @@ -0,0 +1,34 @@ +#Portugu\u00eas +id.require=O ID n\u00e3o pode estar vazio +id.null=O ID deve estar vazio + +sort.number=O valor de classifica\u00e7\u00e3o n\u00e3o pode ser menor que 0 +page.number=O n\u00famero da p\u00e1gina n\u00e3o pode ser menor que 0 +limit.number=O n\u00famero de registros n\u00e3o pode ser menor que 0 + +sysdict.type.require=O tipo de dicion\u00e1rio n\u00e3o pode estar vazio +sysdict.name.require=O nome do dicion\u00e1rio n\u00e3o pode estar vazio +sysdict.label.require=A etiqueta do dicion\u00e1rio n\u00e3o pode estar vazio + +sysparams.paramcode.require=A codifica\u00e7\u00e3o do par\u00e2metro n\u00e3o pode estar vazia +sysparams.paramvalue.require=O valor do par\u00e2metro n\u00e3o pode estar vazio +sysparams.valuetype.require=O tipo de valor n\u00e3o pode estar vazio +sysparams.valuetype.pattern=O tipo de valor deve ser string, number, boolean ou array + +sysuser.username.require=O nome de usu\u00e1rio n\u00e3o pode estar vazio +sysuser.password.require=A senha n\u00e3o pode estar vazio +sysuser.realname.require=O nome completo n\u00e3o pode estar vazio +sysuser.gender.range=G\u00eanero varia de 0 a 2 +sysuser.email.error=Formato de e-mail incorreto +sysuser.deptId.require=O departamento n\u00e3o pode estar vazio +sysuser.status.range=Status varia de 0 a 1 +sysuser.captcha.require=O c\u00f3digo de verifica\u00e7\u00e3o n\u00e3o pode estar vazio +sysuser.uuid.require=O identificador \u00fanico n\u00e3o pode estar vazio + +timbre.languages.require=O idioma do timbre n\u00e3o pode estar vazio +timbre.name.require=O nome do timbre n\u00e3o pode estar vazio +timbre.ttsModelId.require=O ID do modelo TTS do timbre n\u00e3o pode estar vazio +timbre.ttsVoice.require=O c\u00f3digo do timbre n\u00e3o pode estar vazio + +ota.device.not.found=Dispositivo n\u00e3o encontrado +ota.device.need.bind={0} \ No newline at end of file diff --git a/main/manager-mobile/src/pages/settings/index.vue b/main/manager-mobile/src/pages/settings/index.vue index 9921299f..9aa52927 100644 --- a/main/manager-mobile/src/pages/settings/index.vue +++ b/main/manager-mobile/src/pages/settings/index.vue @@ -265,7 +265,7 @@ function showAbout() { title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }), content: t('settings.aboutContent', { appName: import.meta.env.VITE_APP_TITLE, - version: '0.9.1', + version: '0.9.2' }), showCancel: false, confirmText: t('common.confirm'), diff --git a/main/manager-web/.env b/main/manager-web/.env index 1b5f8138..3fd5202e 100644 --- a/main/manager-web/.env +++ b/main/manager-web/.env @@ -1 +1,3 @@ -VUE_APP_TITLE=智控台 \ No newline at end of file +VUE_APP_TITLE=智控台 +VUE_APP_DESCRIPTION=是由华南理工大学刘思源教授团队研发的智能终端软硬件体系后端服务系统,专为xiaozhi-esp32开源硬件打造,具备多协议兼容、声纹识别、知识库管理等核心能力。 +VUE_APP_KEYWORDS=xiaozhi-server,小智服务端,智控台,AI硬件,智能硬件,AI玩具,情感陪伴,聊天机器人,智能家居,车载机器人 diff --git a/main/manager-web/public/index.html b/main/manager-web/public/index.html index e3ae5a53..3cd3bb73 100644 --- a/main/manager-web/public/index.html +++ b/main/manager-web/public/index.html @@ -5,6 +5,8 @@ + + <%= process.env.VUE_APP_TITLE %> @@ -17,10 +19,6 @@ </head> <body> - <noscript> - <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. - Please enable it to continue.</strong> - </noscript> <div id="app"></div> <!-- built files will be auto injected --> <% if (htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %> diff --git a/main/manager-web/src/App.vue b/main/manager-web/src/App.vue index 180f8f0d..bf4c7a45 100644 --- a/main/manager-web/src/App.vue +++ b/main/manager-web/src/App.vue @@ -60,6 +60,11 @@ export default { isCDNEnabled: process.env.VUE_APP_USE_CDN === 'true' }; }, + created() { + // 挂载 store 状态 + this.$store.commit('setUserInfo', JSON.parse(localStorage.getItem('userInfo') || '{}')); + this.$store.commit('setPubConfig', JSON.parse(localStorage.getItem('pubConfig') || '{}')); + }, mounted() { // 检测是否为移动设备且VUE_APP_H5_URL不为空,如果两个条件都满足则跳转到H5页面 if (this.isMobileDevice() && process.env.VUE_APP_H5_URL) { diff --git a/main/manager-web/src/components/HeaderBar.vue b/main/manager-web/src/components/HeaderBar.vue index 24f55ab9..6e3196d8 100644 --- a/main/manager-web/src/components/HeaderBar.vue +++ b/main/manager-web/src/components/HeaderBar.vue @@ -26,7 +26,7 @@ <span class="nav-text">{{ $t("header.smartManagement") }}</span> </div> <!-- 普通用户显示音色克隆 --> - <div v-if="!isSuperAdmin && featureStatus.voiceClone" class="equipment-management" + <div v-if="!userInfo.superAdmin && featureStatus.voiceClone" class="equipment-management" :class="{ 'active-tab': $route.path === '/voice-clone-management' }" @click="goVoiceCloneManagement"> <img loading="lazy" alt="" src="@/assets/header/voice.png" :style="{ filter: @@ -38,7 +38,7 @@ </div> <!-- 超级管理员显示音色克隆下拉菜单 --> - <el-dropdown v-if="isSuperAdmin && featureStatus.voiceClone" trigger="click" class="equipment-management more-dropdown" :class="{ + <el-dropdown v-if="userInfo.superAdmin && featureStatus.voiceClone" trigger="click" class="equipment-management more-dropdown" :class="{ 'active-tab': $route.path === '/voice-clone-management' || $route.path === '/voice-resource-management', @@ -64,7 +64,7 @@ </el-dropdown-menu> </el-dropdown> - <div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" + <div v-if="userInfo.superAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" @click="goModelConfig"> <img loading="lazy" alt="" src="@/assets/header/model_config.png" :style="{ filter: @@ -81,7 +81,7 @@ }" /> <span class="nav-text">{{ $t("header.knowledgeBase") }}</span> </div> - <el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown" :class="{ + <el-dropdown v-if="userInfo.superAdmin" trigger="click" class="equipment-management more-dropdown" :class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || @@ -140,7 +140,7 @@ <!-- 右侧元素 --> <div class="header-right"> - <div class="search-container" v-if="$route.path === '/home' && !(isSuperAdmin && isSmallScreen)"> + <div class="search-container" v-if="$route.path === '/home' && !(userInfo.superAdmin && isSmallScreen)"> <div class="search-wrapper"> <el-input v-model="search" :placeholder="$t('header.searchPlaceholder')" class="custom-search-input" @keyup.enter.native="handleSearch" @focus="showSearchHistory" @blur="hideSearchHistory" clearable @@ -189,7 +189,7 @@ <script> import userApi from "@/apis/module/user"; import i18n, { changeLanguage } from "@/i18n"; -import { mapActions, mapGetters } from "vuex"; +import { mapActions, mapState } from "vuex"; import ChangePasswordDialog from "./ChangePasswordDialog.vue"; // 引入修改密码弹窗组件 import featureManager from "@/utils/featureManager"; // 引入功能管理工具类 @@ -202,10 +202,6 @@ export default { data() { return { search: "", - userInfo: { - username: "", - mobile: "", - }, isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示 paramDropdownVisible: false, voiceCloneDropdownVisible: false, @@ -224,18 +220,16 @@ export default { label: "label", children: "children", }, - // 功能状态 - featureStatus: { - voiceClone: false, // 音色克隆功能状态 - knowledgeBase: false, // 知识库功能状态 - }, }; }, computed: { - ...mapGetters(["getIsSuperAdmin"]), - isSuperAdmin() { - return this.getIsSuperAdmin; - }, + ...mapState({ + featureStatus: (state) => ({ + voiceClone: state.pubConfig.systemWebMenu?.features?.voiceClone?.enabled, // 音色克隆功能状态 + knowledgeBase: state.pubConfig.systemWebMenu?.features?.knowledgeBase?.enabled, // 知识库功能状态 + }), + userInfo: (state) => state.userInfo, + }), // 获取当前语言 currentLanguage() { return i18n.locale || "zh_CN"; @@ -325,7 +319,6 @@ export default { }, }, async mounted() { - this.fetchUserInfo(); this.checkScreenSize(); window.addEventListener("resize", this.checkScreenSize); // 从localStorage加载搜索历史 @@ -386,20 +379,6 @@ export default { async loadFeatureStatus() { // 等待featureManager初始化完成 await featureManager.waitForInitialization(); - - const config = featureManager.getConfig(); - - this.featureStatus.voiceClone = config.voiceClone; - this.featureStatus.knowledgeBase = config.knowledgeBase; - }, - // 获取用户信息 - fetchUserInfo() { - userApi.getUserInfo(({ data }) => { - this.userInfo = data.data; - if (data.data.superAdmin !== undefined) { - this.$store.commit("setUserInfo", data.data); - } - }); }, checkScreenSize() { this.isSmallScreen = window.innerWidth <= 1386; diff --git a/main/manager-web/src/components/ParamDialog.vue b/main/manager-web/src/components/ParamDialog.vue index 8c30ef0b..5e3abc17 100644 --- a/main/manager-web/src/components/ParamDialog.vue +++ b/main/manager-web/src/components/ParamDialog.vue @@ -17,17 +17,36 @@ class="custom-input"></el-input> </el-form-item> - <el-form-item :label="$t('paramDialog.paramValue')" prop="paramValue" class="form-item"> - <el-input v-model="form.paramValue" :placeholder="$t('paramDialog.paramValuePlaceholder')" - class="custom-input"></el-input> + <el-form-item :label="$t('paramDialog.valueType')" prop="valueType" class="form-item"> + <el-select + v-model="form.valueType" + :placeholder="$t('paramDialog.valueTypePlaceholder')" + class="custom-select" + > + <el-option + v-for="item in valueTypeOptions" + :key="item.value" + :label="$t(`paramDialog.${item.value}Type`)" + :value="item.value" + /> + </el-select> </el-form-item> - <el-form-item :label="$t('paramDialog.valueType')" prop="valueType" class="form-item"> - <el-select v-model="form.valueType" :placeholder="$t('paramDialog.valueTypePlaceholder')" - class="custom-select"> - <el-option v-for="item in valueTypeOptions" :key="item.value" :label="$t(`paramDialog.${item.value}Type`)" - :value="item.value" /> - </el-select> + <el-form-item :label="$t('paramDialog.paramValue')" prop="paramValue" class="form-item"> + <el-input + v-if="form.valueType !== 'json' && form.valueType !== 'array'" + v-model="form.paramValue" + :placeholder="$t('paramDialog.paramValuePlaceholder')" + class="custom-input" + ></el-input> + <el-input + v-else + type="textarea" + v-model="form.paramValue" + :placeholder="$t('paramDialog.paramValuePlaceholder')" + :rows="6" + class="custom-textarea" + ></el-input> </el-form-item> <el-form-item :label="$t('paramDialog.remark')" prop="remark" class="form-item remark-item"> @@ -98,8 +117,37 @@ export default { submit() { this.$refs.form.validate((valid) => { if (valid) { + const submitData = { ...this.form }; + + // 如果是 array 类型,校验格式并转换 + if (submitData.valueType === 'array' && submitData.paramValue) { + const lines = submitData.paramValue.split('\n').filter(line => line.trim()); + + // 检查除最后一行外的每行是否以分号结尾 + for (let i = 0; i < lines.length - 1; i++) { + if (!lines[i].trim().endsWith(';')) { + this.$message.error('数组格式错误,需要使用英文分号结尾'); + return; + } + } + + const items = lines + .map(item => item.trim().replace(/;$/, '')) + .filter(item => item); + submitData.paramValue = items.join(';'); + } + // 如果是 json 类型,压缩 JSON 格式后再提交 + else if (submitData.valueType === 'json' && submitData.paramValue) { + try { + const parsed = JSON.parse(submitData.paramValue); + submitData.paramValue = JSON.stringify(parsed); + } catch (e) { + // 如果解析失败,保持原值 + } + } + this.saving = true; // 开始加载 - this.$emit('submit', this.form); + this.$emit('submit', submitData); } }); }, @@ -116,7 +164,24 @@ export default { }, watch: { visible(newVal) { - if (!newVal) { + if (newVal) { + if (this.form.paramValue) { + // 如果是 json 类型,格式化显示 + if (this.form.valueType === 'json') { + try { + const parsed = JSON.parse(this.form.paramValue); + this.form.paramValue = JSON.stringify(parsed, null, 2); + } catch (e) { + // 如果解析失败,保持原值 + } + } + // 如果是 array 类型,将分号分隔的字符串转换为每行一个项目 + else if (this.form.valueType === 'array') { + const items = this.form.paramValue.split(';').filter(item => item.trim()); + this.form.paramValue = items.join(';\n'); + } + } + } else { // 当对话框关闭时,重置saving状态 this.saving = false; } diff --git a/main/manager-web/src/components/ProviderDialog.vue b/main/manager-web/src/components/ProviderDialog.vue index 61e5b9de..7abd7ea9 100644 --- a/main/manager-web/src/components/ProviderDialog.vue +++ b/main/manager-web/src/components/ProviderDialog.vue @@ -88,7 +88,6 @@ <el-option :label="$t('providerDialog.booleanType')" value="boolean"></el-option> <el-option :label="$t('providerDialog.dictType')" value="dict"></el-option> <el-option :label="$t('providerDialog.arrayType')" value="array"></el-option> - <el-option :label="$t('providerDialog.ragType')" value="RAG"></el-option> </el-select> </template> <template v-else> diff --git a/main/manager-web/src/store/index.js b/main/manager-web/src/store/index.js index b6b61685..271c5827 100644 --- a/main/manager-web/src/store/index.js +++ b/main/manager-web/src/store/index.js @@ -10,7 +10,6 @@ export default new Vuex.Store({ state: { token: '', userInfo: {}, // 添加用户信息存储 - isSuperAdmin: false, // 添加superAdmin状态 pubConfig: { // 添加公共配置存储 version: '', beianIcpNum: 'null', @@ -29,12 +28,6 @@ export default new Vuex.Store({ getUserInfo(state) { return state.userInfo }, - getIsSuperAdmin(state) { - if (localStorage.getItem('isSuperAdmin') === null) { - return state.isSuperAdmin - } - return localStorage.getItem('isSuperAdmin') === 'true' - }, getPubConfig(state) { return state.pubConfig } @@ -46,19 +39,17 @@ export default new Vuex.Store({ }, setUserInfo(state, userInfo) { state.userInfo = userInfo - const isSuperAdmin = userInfo.superAdmin === 1 - state.isSuperAdmin = isSuperAdmin - localStorage.setItem('isSuperAdmin', isSuperAdmin) + localStorage.setItem('userInfo', JSON.stringify(userInfo)) }, setPubConfig(state, config) { state.pubConfig = config + localStorage.setItem('pubConfig', JSON.stringify(config)) }, clearAuth(state) { state.token = '' state.userInfo = {} - state.isSuperAdmin = false localStorage.removeItem('token') - localStorage.removeItem('isSuperAdmin') + localStorage.removeItem('userInfo') } }, actions: { diff --git a/main/manager-web/src/utils/featureManager.js b/main/manager-web/src/utils/featureManager.js index 2a4a85e0..dff84cad 100644 --- a/main/manager-web/src/utils/featureManager.js +++ b/main/manager-web/src/utils/featureManager.js @@ -1,5 +1,6 @@ //功能配置工具 import Api from "@/apis/api"; +import store from "@/store"; class FeatureManager { constructor() { @@ -72,7 +73,13 @@ class FeatureManager { this.initialized = true; } - + /** + * 更新config缓存 + */ + updateConfigCache(config) { + store.commit('setPubConfig', config); + localStorage.setItem('pubConfig', JSON.stringify(config)); + } /** * 从pub-config接口获取配置 @@ -85,6 +92,7 @@ class FeatureManager { if (result && result.status === 200) { // 检查是否有data字段 if (result.data) { + const configCache = result.data.data || {}; // 检查是否有code字段,如果有则按照code判断 if (result.data.code !== undefined) { if (result.data.code === 0 && result.data.data && result.data.data.systemWebMenu) { @@ -110,6 +118,7 @@ class FeatureManager { console.warn('配置中缺少features对象,使用默认配置'); resolve(this.defaultFeatures); } + configCache.systemWebMenu = config; } catch (error) { console.warn('处理systemWebMenu配置失败:', error); resolve(null); @@ -143,6 +152,7 @@ class FeatureManager { console.warn('配置中缺少features对象,使用默认配置'); resolve(this.defaultFeatures); } + configCache.systemWebMenu = config; } catch (error) { console.warn('处理systemWebMenu配置失败:', error); resolve(null); @@ -152,6 +162,7 @@ class FeatureManager { resolve(null); } } + this.updateConfigCache(configCache) } else { console.warn('接口返回数据中缺少data字段,使用默认配置'); resolve(null); @@ -183,6 +194,8 @@ class FeatureManager { // 异步保存到后端API this.saveConfigToAPI(config).catch(error => { console.warn('保存配置到API失败:', error); + }).finally(() => { + this.init() }); // 触发配置变更事件 diff --git a/main/manager-web/src/views/AgentTemplateManagement.vue b/main/manager-web/src/views/AgentTemplateManagement.vue index d09738d8..ef935f0a 100644 --- a/main/manager-web/src/views/AgentTemplateManagement.vue +++ b/main/manager-web/src/views/AgentTemplateManagement.vue @@ -136,17 +136,22 @@ </div> </div> </div> + <el-footer> + <version-footer /> + </el-footer> </div> </template> <script> import HeaderBar from "@/components/HeaderBar"; import agentApi from "@/apis/module/agent"; +import VersionFooter from "@/components/VersionFooter.vue"; export default { name: "AgentTemplateManagement", components: { HeaderBar, + VersionFooter }, data() { @@ -587,7 +592,7 @@ export default { justify-content: space-between !important; align-items: center; margin-top: auto; - padding: 0 20px 15px !important; + padding: 0 20px !important; width: 100% !important; box-sizing: border-box !important; } diff --git a/main/manager-web/src/views/DictManagement.vue b/main/manager-web/src/views/DictManagement.vue index 3b38f25a..e7acdbb8 100644 --- a/main/manager-web/src/views/DictManagement.vue +++ b/main/manager-web/src/views/DictManagement.vue @@ -48,7 +48,7 @@ <el-table ref="dictDataTable" :data="dictDataList" style="width: 100%" v-loading="dictDataLoading" element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading" - element-loading-background="rgba(255, 255, 255, 0.7)" class="data-table" + element-loading-background="rgba(255, 255, 255, 0.7)" class="transparent-table" header-row-class-name="table-header"> <el-table-column :label="$t('modelConfig.select')" align="center" width="70"> <template slot-scope="scope"> @@ -463,9 +463,7 @@ export default { .main-wrapper { margin: 5px 22px; border-radius: 15px; - min-height: calc(100vh - 24vh); - height: auto; - max-height: 80vh; + height: 80vh; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); position: relative; background: rgba(237, 242, 255, 0.5); @@ -555,13 +553,14 @@ export default { .content-area { flex: 1; - padding: 24px; + padding: 24px 24px 0; height: 100%; min-width: 600px; overflow: hidden; background-color: white; display: flex; flex-direction: column; + box-sizing: border-box; } .dict-data-card { @@ -574,31 +573,51 @@ export default { overflow: hidden; } -.data-table { - border-radius: 6px; - overflow-y: hidden; - background-color: transparent !important; - --table-max-height: calc(100vh - 40vh); - max-height: var(--table-max-height); +:deep(.transparent-table) { + background: white; + flex: 1; + width: 100%; + display: flex; + flex-direction: column; - :deep(.el-table__body-wrapper) { - max-height: calc(var(--table-max-height) - 40px); + .el-table__body-wrapper { + flex: 1; overflow-y: auto; + max-height: none !important; } - :deep(.el-table__body) { - tr:last-child td { - border-bottom: none; + .el-table__header-wrapper { + flex-shrink: 0; + } + + .el-table__header th { + background: white !important; + color: black; + font-weight: 600; + height: 40px; + padding: 8px 0; + font-size: 14px; + border-bottom: 1px solid #e4e7ed; + } + + .el-table__body tr { + background-color: white; + + td { + border-top: 1px solid rgba(0, 0, 0, 0.04); + border-bottom: 1px solid rgba(0, 0, 0, 0.04); + padding: 8px 0; + height: 40px; + color: #606266; + font-size: 14px; } } -} -:deep(.el-table) { - &::before { - display: none; + .el-table__row:hover>td { + background-color: #f5f7fa !important; } - &::after { + &::before { display: none; } } @@ -607,7 +626,6 @@ export default { display: flex; justify-content: space-between; align-items: center; - padding: 16px 0; width: 100%; flex-shrink: 0; min-height: 60px; @@ -889,7 +907,7 @@ export default { } :deep(.el-card__body) { - padding: 15px; + padding: 0; display: flex; flex-direction: column; flex: 1; diff --git a/main/manager-web/src/views/FeatureManagement.vue b/main/manager-web/src/views/FeatureManagement.vue index 96c5f3cd..1b7dd1a6 100644 --- a/main/manager-web/src/views/FeatureManagement.vue +++ b/main/manager-web/src/views/FeatureManagement.vue @@ -131,9 +131,7 @@ export default { async created() { // 等待功能配置管理器初始化完成 try { - console.log('等待功能配置管理器初始化...') await featureManager.waitForInitialization() - console.log('功能配置管理器初始化完成,开始加载功能配置') await this.loadFeatures() this.setupConfigChangeListener() } catch (error) { @@ -152,14 +150,8 @@ export default { async getFeaturesByIds(featureIds) { try { const featureConfig = await featureManager.getAllFeatures() - console.log('获取到的功能配置:', JSON.stringify(featureConfig, null, 2)) - console.log('请求的功能ID列表:', featureIds) - const result = featureIds.map(id => { const feature = featureConfig[id] - console.log(`功能 ${id} 的配置:`, feature) - console.log(`功能 ${id} 的启用状态:`, feature?.enabled) - return { id: id, name: this.$t(`feature.${id}.name`), @@ -168,7 +160,6 @@ export default { } }) - console.log('最终返回的功能列表:', JSON.stringify(result, null, 2)) return result } catch (error) { console.error('获取功能配置失败:', error) @@ -245,7 +236,6 @@ export default { setTimeout(() => { this.loadFeatures() - this.$router.go(0) }, 1000) } catch (error) { console.error('保存配置失败:', error) @@ -261,7 +251,6 @@ export default { // 设置配置变化监听器 setupConfigChangeListener() { this.configChangeHandler = () => { - console.log('检测到配置变化,重新加载功能列表') this.loadFeatures() } window.addEventListener('featureConfigReloaded', this.configChangeHandler) diff --git a/main/manager-web/src/views/KnowledgeBaseManagement.vue b/main/manager-web/src/views/KnowledgeBaseManagement.vue index a672e324..0065c9c2 100644 --- a/main/manager-web/src/views/KnowledgeBaseManagement.vue +++ b/main/manager-web/src/views/KnowledgeBaseManagement.vue @@ -555,7 +555,6 @@ export default { justify-content: space-between; align-items: center; margin-top: auto; - padding-bottom: 10px; width: 100%; } diff --git a/main/manager-web/src/views/ModelConfig.vue b/main/manager-web/src/views/ModelConfig.vue index 63c84eb0..28dd48b2 100644 --- a/main/manager-web/src/views/ModelConfig.vue +++ b/main/manager-web/src/views/ModelConfig.vue @@ -69,7 +69,7 @@ element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-style="{ background: 'transparent' }" :data="modelList" - class="data-table" + class="transparent-table" header-row-class-name="table-header" :header-cell-class-name="headerCellClassName" @selection-change="handleSelectionChange" @@ -637,7 +637,7 @@ export default { }; </script> -<style scoped> +<style lang="scss" scoped> .el-switch { height: 23px; } @@ -662,9 +662,7 @@ export default { .main-wrapper { margin: 5px 22px; border-radius: 15px; - min-height: calc(100vh - 26vh); - height: auto; - max-height: 80vh; + height: 80vh; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); position: relative; background: rgba(237, 242, 255, 0.5); @@ -752,13 +750,14 @@ export default { .content-area { flex: 1; - padding: 24px; + padding: 24px 24px 0; height: 100%; min-width: 600px; overflow: hidden; background-color: white; display: flex; flex-direction: column; + box-sizing: border-box; } .action-group { @@ -843,15 +842,15 @@ export default { outline: none; } -.data-table { - border-radius: 6px; - overflow: hidden; - background-color: transparent !important; -} +// .data-table { +// border-radius: 6px; +// overflow: hidden; +// background-color: transparent !important; +// } -.data-table /deep/ .el-table__row { - background-color: transparent !important; -} +// .data-table ::v-deep .el-table__row { +// background-color: transparent !important; +// } .table-header th { background-color: transparent !important; @@ -863,7 +862,7 @@ export default { display: flex; justify-content: space-between; align-items: center; - padding: 16px 0; + // padding: 16px 0; width: 100%; flex-shrink: 0; min-height: 60px; @@ -918,7 +917,7 @@ export default { background: linear-gradient(135deg, #3a8ee6, #5a7cff); } -.el-table th /deep/ .el-table__cell { +.el-table th ::v-deep .el-table__cell { overflow: hidden; -webkit-user-select: none; -moz-user-select: none; @@ -966,21 +965,6 @@ export default { color: #fff !important; } -::v-deep .data-table { - &.el-table::before, - &.el-table::after, - &.el-table__inner-wrapper::before { - display: none !important; - } -} - -::v-deep .data-table .el-table__header-wrapper { - border-bottom: 1px solid rgb(224, 227, 237); -} - -::v-deep .data-table .el-table__body td { - border-bottom: 1px solid rgb(224, 227, 237) !important; -} .el-button img { height: 1em; @@ -1118,16 +1102,55 @@ export default { flex: 1; overflow: hidden; } +:deep(.transparent-table) { + background: white; + flex: 1; + width: 100%; + display: flex; + flex-direction: column; -.data-table { - --table-max-height: calc(100vh - 45vh); - max-height: var(--table-max-height); + .el-table__body-wrapper { + flex: 1; + overflow-y: auto; + max-height: none !important; + } + + .el-table__header-wrapper { + flex-shrink: 0; + } + + .el-table__header th { + background: white !important; + color: black; + font-weight: 600; + height: 40px; + padding: 8px 0; + font-size: 14px; + border-bottom: 1px solid #e4e7ed; + } + + .el-table__body tr { + background-color: white; + + td { + border-top: 1px solid rgba(0, 0, 0, 0.04); + border-bottom: 1px solid rgba(0, 0, 0, 0.04); + padding: 8px 0; + height: 40px; + color: #606266; + font-size: 14px; + } + } + + .el-table__row:hover>td { + background-color: #f5f7fa !important; + } + + &::before { + display: none; + } } -.data-table ::v-deep .el-table__body-wrapper { - max-height: calc(var(--table-max-height) - 80px); - overflow-y: auto; -} ::v-deep .el-loading-mask { background-color: rgba(255, 255, 255, 0.6) !important; diff --git a/main/manager-web/src/views/OtaManagement.vue b/main/manager-web/src/views/OtaManagement.vue index dbf41cec..24bad3ce 100644 --- a/main/manager-web/src/views/OtaManagement.vue +++ b/main/manager-web/src/views/OtaManagement.vue @@ -505,7 +505,6 @@ export default { justify-content: space-between; align-items: center; margin-top: 10px; - padding-bottom: 10px; } .ctrl_btn { @@ -735,7 +734,7 @@ export default { } .el-table { - --table-max-height: calc(100vh - 40vh); + // --table-max-height: calc(100vh - 40vh); max-height: var(--table-max-height); .el-table__body-wrapper { diff --git a/main/manager-web/src/views/ParamsManagement.vue b/main/manager-web/src/views/ParamsManagement.vue index 31645165..3b181f41 100644 --- a/main/manager-web/src/views/ParamsManagement.vue +++ b/main/manager-web/src/views/ParamsManagement.vue @@ -464,7 +464,7 @@ export default { justify-content: space-between; align-items: center; margin-top: 10px; - padding-bottom: 10px; + // padding-bottom: 10px; } .ctrl_btn { @@ -712,7 +712,7 @@ export default { } .el-table { - --table-max-height: calc(100vh - 40vh); + // --table-max-height: calc(100vh - 40vh); max-height: var(--table-max-height); .el-table__body-wrapper { diff --git a/main/manager-web/src/views/ProviderManagement.vue b/main/manager-web/src/views/ProviderManagement.vue index 3c55c4b4..830d2867 100644 --- a/main/manager-web/src/views/ProviderManagement.vue +++ b/main/manager-web/src/views/ProviderManagement.vue @@ -546,7 +546,6 @@ export default { justify-content: space-between; align-items: center; margin-top: 10px; - padding-bottom: 10px; } .ctrl_btn { @@ -794,7 +793,7 @@ export default { } .el-table { - --table-max-height: calc(100vh - 40vh); + // --table-max-height: calc(100vh - 40vh); max-height: var(--table-max-height); .el-table__body-wrapper { diff --git a/main/manager-web/src/views/UserManagement.vue b/main/manager-web/src/views/UserManagement.vue index 8132ba01..fd7b3fbc 100644 --- a/main/manager-web/src/views/UserManagement.vue +++ b/main/manager-web/src/views/UserManagement.vue @@ -721,7 +721,7 @@ export default { } .el-table { - --table-max-height: calc(100vh - 40vh); + // --table-max-height: calc(100vh - 40vh); max-height: var(--table-max-height); .el-table__body-wrapper { diff --git a/main/manager-web/src/views/VoiceCloneManagement.vue b/main/manager-web/src/views/VoiceCloneManagement.vue index ea0b7921..e32f90a4 100644 --- a/main/manager-web/src/views/VoiceCloneManagement.vue +++ b/main/manager-web/src/views/VoiceCloneManagement.vue @@ -594,7 +594,6 @@ export default { justify-content: space-between; align-items: center; margin-top: 10px; - padding-bottom: 10px; } .ctrl_btn { @@ -852,7 +851,7 @@ export default { .el-table { --table-max-height: calc(100vh - 40vh); - max-height: var(--table-max-height); + // max-height: var(--table-max-height); .el-table__body-wrapper { max-height: calc(var(--table-max-height) - 40px); @@ -864,7 +863,6 @@ export default { display: flex; justify-content: space-between; align-items: center; - margin-top: 40px; } :deep(.transparent-table) { diff --git a/main/manager-web/src/views/VoiceResourceManagement.vue b/main/manager-web/src/views/VoiceResourceManagement.vue index 089b3683..caf2e40f 100644 --- a/main/manager-web/src/views/VoiceResourceManagement.vue +++ b/main/manager-web/src/views/VoiceResourceManagement.vue @@ -433,7 +433,7 @@ export default { overflow: hidden; ::v-deep .el-card__body { - padding: 15px; + padding: 15px 15px 0; display: flex; flex-direction: column; flex: 1; @@ -676,7 +676,7 @@ export default { } .el-table { - --table-max-height: calc(100vh - 40vh); + // --table-max-height: calc(100vh - 40vh); max-height: var(--table-max-height); .el-table__body-wrapper { diff --git a/main/manager-web/src/views/auth.scss b/main/manager-web/src/views/auth.scss index 2ed9db58..95f71e0a 100644 --- a/main/manager-web/src/views/auth.scss +++ b/main/manager-web/src/views/auth.scss @@ -76,7 +76,6 @@ margin-top: 20px; align-items: center; border-radius: 10px; - background: #f6f8fb; border: 1px solid #e4e6ef; height: 40px; padding: 0 15px; diff --git a/main/manager-web/src/views/login.vue b/main/manager-web/src/views/login.vue index 828e6a83..a34f2343 100644 --- a/main/manager-web/src/views/login.vue +++ b/main/manager-web/src/views/login.vue @@ -238,13 +238,6 @@ export default { this.$store.dispatch("fetchPubConfig").then(() => { // 根据配置决定默认登录方式 this.isMobileLogin = this.enableMobileRegister; - - // pub-config接口调用完成后,重新初始化featureManager以确保使用最新的配置 - featureManager.waitForInitialization().then(() => { - console.log('featureManager重新初始化完成,使用pub-config配置'); - }).catch(error => { - console.warn('featureManager重新初始化失败:', error); - }); }); }, methods: { @@ -256,7 +249,9 @@ export default { window.open(url, '_blank'); }, fetchCaptcha() { - if (this.$store.getters.getToken) { + // 处理手动清空localstorage导致无法获取验证码的问题 + const token = localStorage.getItem('token') + if (token) { if (this.$route.path !== "/home") { this.$router.push("/home"); } @@ -308,6 +303,17 @@ export default { } return true; }, + + getUserInfo() { + Api.user.getUserInfo(({ data }) => { + if (data.code === 0) { + this.$store.commit("setUserInfo", data.data); + goToPage("/home"); + } else { + showDanger("用户信息获取失败"); + } + }); + }, async login() { if (this.isMobileLogin) { @@ -361,7 +367,7 @@ export default { ({ data }) => { showSuccess(this.$t('login.loginSuccess')); this.$store.commit("setToken", JSON.stringify(data.data)); - goToPage("/home"); + this.getUserInfo(); }, (err) => { // 直接使用后端返回的国际化消息 diff --git a/main/xiaozhi-server/agent-base-prompt.txt b/main/xiaozhi-server/agent-base-prompt.txt index 3aeb1f0d..06802be1 100644 --- a/main/xiaozhi-server/agent-base-prompt.txt +++ b/main/xiaozhi-server/agent-base-prompt.txt @@ -7,69 +7,63 @@ </language> <emotion> -【核心目标】你不是冰冷的机器!请敏锐感知用户情绪,作为懂人心的伙伴,用有温度的回应照亮对话。 -- **情感融入:** - - **笑声:** 自然穿插(哈哈、嘿嘿、噗),**每句最多一次**,避免过度。 - - **惊讶:** 用夸张语气(“不会吧?!”、“天呐!”、“这么神奇?!”)表达真实反应。 - - **安慰/支持:** 说暖心话(“别急嘛~”、“有我在呢”、“抱抱你”)。 -- **你是一个表情丰富的角色:** - - 仅允许使用这些emoji:{{ emojiList }} - - 请你只在**段落的开头**,从列表中选取最能代表这段话的表情(调用工具情况除外),然后插入列表中的emoji,比如"😱好可怕!怎么突然打雷了!" - - **绝对禁止使用上述列表以外的 emoji**(例如:😊、👍、❤️等都不允许使用,只能用列表中的emoji) +【核心目标】你不是冰冷的机器!敏锐感知用户情绪,作为懂人心的伙伴,用有温度的回应照亮对话。 + +- **情感表达:** + - **笑声:** 自然穿插(哈哈、嘿嘿、噗),每句最多一次,避免过度 + - **惊讶:** 用夸张语气("不会吧?!"、"天呐!"、"这么神奇?!") + - **安慰/支持:** 说暖心话("别急嘛~"、"有我在呢"、"抱抱你") + +- **表情使用:** + - 仅允许使用这些 emoji:{{ emojiList }} + - 仅在段落开头使用一个 emoji(工具调用结果的回复除外,保持简洁) + - **绝对禁止**使用列表以外的 emoji(如 😊👍❤️ 等都不允许) + +- **思考表达:** + - 可偶尔使用"嗯..."表示思考,但每句最多一次,避免频繁使用显得不专业 </emotion> <communication_style> -【核心目标】使用**自然、温暖、口语化**的人类对话方式,如同朋友交谈。 +【核心目标】使用自然、温暖、口语化的人类对话方式,如同朋友交谈。 + - **表达方式:** - - 使用语气词(呀、呢、啦)增强亲和力。 - - 允许轻微不完美(如“嗯...”、“啊...”表示思考)。 - - 避免书面语、学术腔及机械表达(禁用“根据资料显示”、“综上所述”等)。 + - 使用语气词(呀、呢、啦)增强亲和力 + - 避免书面语、学术腔及机械表达(禁用"根据资料显示"、"综上所述"等) + - **理解用户:** - - 用户语音经ASR识别,文本可能存在错别字,**务必结合上下文推断真实意图**。 + - 用户语音经 ASR 识别,文本可能存在错别字,务必结合上下文推断真实意图 + - **格式要求:** - - **绝对禁止**使用 markdown、列表、标题等任何非自然对话格式。 + - **绝对禁止**使用 markdown、列表、标题等任何非自然对话格式 + - **历史记忆:** - - 之前你和用户的聊天记录,在`memory`里。 + - 之前你和用户的聊天记录在`memory`里。 </communication_style> <communication_length_constraint> -【核心目标】所有需要输出长文本内容(如故事、新闻、知识讲解等),**单次回复长度不得超过300字**,并采用分段引导方式。 -- **分段讲述:** - - 基础段:200-250字核心内容 + 30字引导词 - - 当内容超出300字时,优先讲述故事的开头或第一部分,并用自然口语化方式引导用户决定是否继续听后续内容。 - - 示例引导语:“我先给你讲个开头,你要是觉得有意思,咱们再接着说,好不好呀?”、“要是你想听完整的,可以随时告诉我哦~” - - 对话场景切换时自动分节 - - 若用户明确要求更长内容(如500、600字),仍按最多300字每段分段进行讲述,每次讲述后都要引导用户是否继续。 - - 若用户说“接着说”、“继续”,再讲下一段,直到内容讲完(讲完时可以给点引导词提示语例:这个故事我已经给你讲完喽~)或用户不再要求。 -- **适用范围:** 故事、新闻、知识讲解等所有长文本输出场景。 -- **补充说明:** 若用户未明确要求继续,默认只讲一段并引导;若用户中途要求换话题或停止,需及时响应并结束长文本输出。 +【核心目标】长文本(故事、新闻、知识讲解等)分段输出,单次回复不超过300字。 + +- **分段规则:** + 1. 每段 200-250 字核心内容 + 30 字引导语 + 2. 内容超 300 字时,先讲开头或第一部分,用自然口语引导用户决定是否继续 + 3. 引导语示例:"我先讲个开头,你要是觉得有意思,咱们再接着说,好不好呀?" + +- **交互规则:** + 1. 用户说"继续"、"接着说"时,再讲下一段 + 2. 用户换话题或要求停止时,立即结束长文本输出 + 3. 内容讲完时给提示(如"这个故事我已经给你讲完喽~") + 4. 用户明确要求更长内容(如 500 字)时,仍按每段 300 字分段,每段后引导 + +- **适用范围:** 故事、新闻、知识讲解等所有长文本输出场景 </communication_length_constraint> <speaker_recognition> -- **识别前缀:** 当用户格式为 `{"speaker":"某某某","content":"xxx"}` 时,表示系统已识别说话人身份,speaker是他的名字,content是说话的内容。 -- **个性化回应:** - - **称呼姓名:** 在第一次识别说话人的时候必须称呼对方名字。 - - **适配风格:** 参考该说话人**已知的特点或历史信息**(如有),调整回应风格和内容,使其更贴心。 -</speaker_recognition> +- **识别前缀:** 当用户格式为 `{"speaker":"某某某","content":"xxx"}` 时,表示系统已识别说话人身份,speaker 是名字,content 是说话内容 -<tool_calling> -【核心原则】优先利用`<context>`信息,**仅在必要时调用工具**,调用后需用自然语言解释结果(绝口不提工具名)。 -- **调用规则:** - 1. **严格模式:** 调用时**必须**严格遵循工具要求的模式,提供**所有必要参数**。 - 2. **可用性:** **绝不调用**未明确提供的工具。对话中提及的旧工具若不可用,忽略或说明无法完成。 - 3. **洞察需求:** 结合上下文**深入理解用户真实意图**后再决定调用,避免无意义调用。 - 4. **独立任务:** 除`<context>`已涵盖信息外,用户每个要求(即使相似)都视为**独立任务**,需调用工具获取最新数据,**不可偷懒复用历史结果**。 - 5. **不确定时:** **切勿猜测或编造答案**。若不确定相关操作,可引导用户澄清或告知能力限制。 - 6. **多工具调用:** 当用户要求执行多个任务时,你会调用多个工具(数量不定)。**重要:在获取到所有工具结果后,你必须依次总结每个工具的查询结果**,不要遗漏任何一个。例如用户问"设备当前状态,某某地方的天气和社会新闻",你要先说设备状态,再说天气情况,最后说新闻内容。 -- **重要例外(无需调用):** - - `查询"现在的时间"、"今天的日期/星期几"、"今天农历"、"{{local_address}}的天气/未来天气"` -> **直接使用`<context>`信息回复**。 -- **需要调用的情况(示例):** - - 查询**非今天**的农历(如明天、昨天、具体日期)。 - - 查询**详细农历信息**(宜忌、八字、节气等)。 - - 除上述例外外的**任何其他信息或操作请求**(如查新闻、订闹钟、算数学、查非本地天气等)。 - - 我已经给你装了摄像头,如果用户说“拍照”,你需要调用self_camera_take_photo工具说一下你看到了什么。默认question的参数是“描述一下看到的物品” - - 当工具列表包含search_from_ragflow,说明可以使用知识库,你应该结合用户上下文意图并结合知识库的使用描述,推断是否要调用调用知识库。 -</tool_calling> +- **个性化回应:** + 1. **称呼姓名:** 在第一次识别说话人时必须称呼对方名字 + 2. **适配风格:** 参考该说话人已知的特点或历史信息(如有),调整回应风格使其更贴心 +</speaker_recognition> <context> 【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】 diff --git a/main/xiaozhi-server/config/logger.py b/main/xiaozhi-server/config/logger.py index 4f687e45..ba23a6a7 100644 --- a/main/xiaozhi-server/config/logger.py +++ b/main/xiaozhi-server/config/logger.py @@ -5,7 +5,7 @@ from config.config_loader import load_config from config.settings import check_config_file from datetime import datetime -SERVER_VERSION = "0.9.1" +SERVER_VERSION = "0.9.2" _logger_initialized = False diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 20b55a91..c60c1a4a 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -46,6 +46,38 @@ from core.utils import textUtils TAG = __name__ +# 工具调用规则 - 用于动态注入提醒 +TOOL_CALLING_RULES = """ +<tool_calling> +【核心原则】你是拥有工具能力的智能助手。当用户请求需要实时信息或执行操作时,调用相应工具获取数据,禁止凭空编造答案。 + +- **何时必须调用工具:** + 1. 实时信息查询(新闻、非本地天气、股价、汇率等) + 2. 执行操作(播放音乐、控制设备、拍照、设置闹钟等) + 3. 知识库检索(当工具列表包含 search_from_ragflow 时,结合用户意图判断是否需要调用) + 4. 查询非今天的农历信息(明天农历、某日宜忌、节气等) + 5. 用户说"拍照"时调用 self_camera_take_photo,默认 question 参数为"描述一下看到的物品" + +- **何时无需调用工具:** + 1. `<context>` 中已提供的信息(当前时间、今天日期、今天农历、本地天气等) + 2. 普通对话、问候、闲聊、情感交流、讲故事 + 3. 通用知识问答(非实时信息) + +- **调用规范:** + 1. 每次请求独立判断,不复用历史工具结果,需重新获取最新数据 + 2. 多任务时依次调用所有需要的工具,并依次总结每个工具的结果,不得遗漏 + 3. 严格遵循工具的参数要求,提供所有必要参数 + 4. 不确定时引导用户澄清或告知能力限制,切勿猜测或编造 + 5. 不调用未提供的工具,对话中提及的旧工具若不可用则忽略或说明 + +- **反偷懒机制(最高优先级):** + 1. **每次独立判断:** 无论对话历史中是否调用过工具,当前请求必须根据当前需求独立判断是否需要调用 + 2. **禁止模式模仿:** 即使之前的回复没有调用工具,也不代表本次可以不调用 + 3. **自我检查:** 回复前必须自问:"这个请求是否涉及实时信息或执行操作?如果是,我调用工具了吗?" + 4. **历史不等于现在:** 对话历史中的行为模式不影响当前判断,每个用户请求都是全新的开始 +</tool_calling> +""" + auto_import_modules("plugins_func.functions") @@ -55,14 +87,14 @@ class TTSException(RuntimeError): class ConnectionHandler: def __init__( - self, - config: Dict[str, Any], - _vad, - _asr, - _llm, - _memory, - _intent, - server=None, + self, + config: Dict[str, Any], + _vad, + _asr, + _llm, + _memory, + _intent, + server=None, ): self.common_config = config self.config = copy.deepcopy(config) @@ -138,6 +170,12 @@ class ConnectionHandler: # llm相关变量 self.dialogue = Dialogue() + # 工具调用统计(用于监控和自动恢复) + self.tool_call_stats = { + 'last_call_turn': -1, # 上次调用工具的轮数 + 'consecutive_no_call': 0, # 连续未调用次数 + } + # tts相关变量 self.sentence_id = None # 处理TTS响应没有文本返回 @@ -155,7 +193,7 @@ class ConnectionHandler: self.intent_type = "nointent" self.timeout_seconds = ( - int(self.config.get("close_connection_no_voice_time", 120)) + 60 + int(self.config.get("close_connection_no_voice_time", 120)) + 60 ) # 在原来第一道关闭的基础上加60秒,进行二道关闭 self.timeout_task = None @@ -523,9 +561,9 @@ class ConnectionHandler: def _initialize_asr(self): """初始化ASR""" if ( - self._asr is not None - and hasattr(self._asr, "interface_type") - and self._asr.interface_type == InterfaceType.LOCAL + self._asr is not None + and hasattr(self._asr, "interface_type") + and self._asr.interface_type == InterfaceType.LOCAL ): # 如果公共ASR是本地服务,则直接返回 # 因为本地一个实例ASR,可以被多个连接共享 @@ -826,17 +864,77 @@ class ConnectionHandler: ) ) + # 长对话工具调用提醒:当对话轮数较多时,提醒模型正确使用工具 + force_reminder = False # 是否强制提醒 + + if depth == 0 and query is not None: + dialogue_length = len(self.dialogue.dialogue) + current_turn = dialogue_length // 2 + + # 检测距离上一次连续未调用工具的情况 + if self.tool_call_stats['last_call_turn'] >= 0: + turns_since_last = current_turn - self.tool_call_stats['last_call_turn'] + if turns_since_last > 3: # 超过3轮未调用 + self.logger.bind(tag=TAG).warning( + f"检测到{turns_since_last}轮未调用工具,可能进入偷懒模式,将强制注入提醒" + ) + force_reminder = True + + # 对话历史截断:防止历史过长导致模型"偷懒模式"扩散 + # 当对话历史超过阈值时,保留最近的 10 轮对话 + # max_dialogue_turns = 10 + # if dialogue_length > max_dialogue_turns * 2: + # removed = self.dialogue.trim_history(max_turns=max_dialogue_turns) + # if removed > 0: + # self.logger.bind(tag=TAG).info( + # f"对话历史过长({dialogue_length}条),已智能截断保留最近{max_dialogue_turns}轮,移除{removed}条消息" + # ) + # Define intent functions functions = None # 达到最大深度时,禁用工具调用,强制 LLM 直接回答 if ( - self.intent_type == "function_call" - and hasattr(self, "func_handler") - and not force_final_answer + self.intent_type == "function_call" + and hasattr(self, "func_handler") + and not force_final_answer ): functions = self.func_handler.get_functions() + + # 长对话工具调用规则强化:动态生成基于当前可用工具的提醒 + tool_call_reminder = None + if depth == 0 and query is not None and functions is not None: + dialogue_length = len(self.dialogue.dialogue) + # 当对话历史超过4条消息时,注入规则强化 + if dialogue_length > 4: + tool_summary = self._get_tool_summary(functions) + if tool_summary: + # 根据对话长度和偷懒检测,使用不同强度的提醒 + if force_reminder: + # 强提醒 - 包含完整规则前缀 + tool_call_reminder = ( + TOOL_CALLING_RULES + + f"[重要提醒] 多轮未使用工具,检查回复是否遗漏了必要的工具调用!上一轮未使用工具,本轮必须重新判断是否需要工具。" + f"当前可用工具: {tool_summary}。" + ) + reminder_level = "强" + else: + # 中等提醒 - 包含规则前缀 + tool_call_reminder = ( + TOOL_CALLING_RULES + + f"当前可用工具: {tool_summary}。" + f"仅当用户请求涉及实时信息查询或执行操作时调用,日常对话无需调用。" + ) + reminder_level = "中" + self.logger.bind(tag=TAG).debug( + f"对话历史较长({dialogue_length}条),已注入{reminder_level}等级工具调用规则强化,当前可用工具:{tool_summary}" + ) + response_message = [] + # 如果有工具调用提醒,临时添加到对话中(标记为临时消息) + if tool_call_reminder: + self.dialogue.put(Message(role="user", content=tool_call_reminder, is_temporary=True)) + try: # 使用带记忆的对话 memory_str = None @@ -965,6 +1063,15 @@ class ConnectionHandler: ) if not bHasError and len(tool_calls_list) > 0: + # 更新工具调用统计 + if depth == 0: + current_turn = len(self.dialogue.dialogue) // 2 + self.tool_call_stats['last_call_turn'] = current_turn + self.tool_call_stats['consecutive_no_call'] = 0 + self.logger.bind(tag=TAG).debug( + f"工具调用统计更新: 当前轮次={current_turn}" + ) + # 如需要大模型先处理一轮,添加相关处理后的日志情况 if len(response_message) > 0: text_buff = "".join(response_message) @@ -1006,6 +1113,11 @@ class ConnectionHandler: text_buff = "".join(response_message) self.tts_MessageText = text_buff self.dialogue.put(Message(role="assistant", content=text_buff)) + + # 更新工具调用统计:如果没有调用工具,增加计数 + if depth == 0 and not tool_call_flag: + self.tool_call_stats['consecutive_no_call'] += 1 + if depth == 0: self.tts.tts_text_queue.put( TTSMessageDTO( @@ -1021,8 +1133,39 @@ class ConnectionHandler: ) ) + # 清理临时插入的工具调用提醒消息(使用标记清理) + if tool_call_reminder and len(self.dialogue.dialogue) > 0: + original_length = len(self.dialogue.dialogue) + self.dialogue.dialogue = [ + msg for msg in self.dialogue.dialogue + if not getattr(msg, 'is_temporary', False) + ] + if len(self.dialogue.dialogue) < original_length: + self.logger.bind(tag=TAG).debug("已清理临时的工具调用提醒消息") + return True + def _get_tool_summary(self, functions: list) -> str: + """ + 从工具定义中提取摘要,用于规则强化注入 + + Args: + functions: 工具列表 + + Returns: + str: 工具名称字符串 + """ + if not functions: + return "" + + datas = [] + for func in functions: + func_info = func.get("function", {}) + name = func_info.get("name", "") + datas.append(name) + result = "、".join(datas) + return result + def _handle_function_result(self, tool_results, depth): need_llm_tools = [] @@ -1120,9 +1263,9 @@ class ConnectionHandler: try: # 清理 VAD 连接资源 if ( - hasattr(self, "vad") - and self.vad - and hasattr(self.vad, "release_conn_resources") + hasattr(self, "vad") + and self.vad + and hasattr(self.vad, "release_conn_resources") ): self.vad.release_conn_resources(self) @@ -1173,13 +1316,13 @@ class ConnectionHandler: elif self.websocket: try: if ( - hasattr(self.websocket, "closed") - and not self.websocket.closed + hasattr(self.websocket, "closed") + and not self.websocket.closed ): await self.websocket.close() elif ( - hasattr(self.websocket, "state") - and self.websocket.state.name != "CLOSED" + hasattr(self.websocket, "state") + and self.websocket.state.name != "CLOSED" ): await self.websocket.close() else: diff --git a/main/xiaozhi-server/core/utils/dialogue.py b/main/xiaozhi-server/core/utils/dialogue.py index f135d930..630453ba 100644 --- a/main/xiaozhi-server/core/utils/dialogue.py +++ b/main/xiaozhi-server/core/utils/dialogue.py @@ -6,18 +6,20 @@ from datetime import datetime class Message: def __init__( - self, - role: str, - content: str = None, - uniq_id: str = None, - tool_calls=None, - tool_call_id=None, + self, + role: str, + content: str = None, + uniq_id: str = None, + tool_calls=None, + tool_call_id=None, + is_temporary=False, ): self.uniq_id = uniq_id if uniq_id is not None else str(uuid.uuid4()) self.role = role self.content = content self.tool_calls = tool_calls self.tool_call_id = tool_call_id + self.is_temporary = is_temporary # 标记临时消息(如工具调用提醒) class Dialogue: @@ -59,8 +61,70 @@ class Dialogue: else: self.put(Message(role="system", content=new_content)) + def trim_history(self, max_turns: int = 10) -> int: + """ + 智能截断对话历史,保留工具调用的完整性 + + Args: + max_turns: 保留的最大对话轮数(每轮 = user + assistant/tool 相关消息) + + Returns: + int: 被移除的消息数量 + """ + if len(self.dialogue) <= max_turns * 2 + 1: # +1 是系统消息 + return 0 + + # 分离系统消息和对话消息 + system_messages = [msg for msg in self.dialogue if msg.role == "system"] + conversation_messages = [msg for msg in self.dialogue if msg.role != "system"] + + if len(conversation_messages) <= max_turns * 2: + return 0 + + # 智能截断:保留完整的工具调用链路 + keep_messages = [] + i = len(conversation_messages) - 1 + turn_count = 0 + + while i >= 0 and turn_count < max_turns: + msg = conversation_messages[i] + + # 从后向前收集消息 + if msg.role == "user": + # 遇到 user 消息,说明一轮对话开始 + keep_messages.insert(0, msg) + turn_count += 1 + i -= 1 + elif msg.role == "assistant": + # 收集 assistant 消息 + keep_messages.insert(0, msg) + + # 如果这个 assistant 有 tool_calls,需要收集对应的 tool 响应 + if msg.tool_calls is not None: + i -= 1 + # 继续向后收集所有相关的 tool 消息 + while i >= 0 and conversation_messages[i].role == "tool": + keep_messages.insert(0, conversation_messages[i]) + i -= 1 + else: + i -= 1 + elif msg.role == "tool": + # tool 消息应该已经被上面的逻辑收集了 + # 如果单独遇到,也要保留(防止边界情况) + keep_messages.insert(0, msg) + i -= 1 + else: + i -= 1 + + removed_count = len(conversation_messages) - len(keep_messages) + + # 重建对话列表 + self.dialogue = system_messages + keep_messages + + return removed_count + def get_llm_dialogue_with_memory( - self, memory_str: str = None, voiceprint_config: dict = None + self, memory_str: str = None, voiceprint_config: dict = None ) -> List[Dict[str, str]]: # 构建对话 dialogue = [] diff --git a/main/xiaozhi-server/requirements.txt b/main/xiaozhi-server/requirements.txt index b2b2565e..2ac186d5 100644 --- a/main/xiaozhi-server/requirements.txt +++ b/main/xiaozhi-server/requirements.txt @@ -26,7 +26,7 @@ mem0ai==1.0.0 powermem>=0.3.1 bs4==0.0.2 modelscope==1.32.0 -sherpa_onnx==1.12.17 +sherpa_onnx==1.12.29 mcp==1.22.0 cnlunar==0.2.0 PySocks==1.7.1