Merge branch 'main' into update-tts-voice-data

This commit is contained in:
Sakura-RanChen
2026-02-27 11:21:18 +08:00
27 changed files with 2447 additions and 115 deletions
+1 -1
View File
@@ -130,7 +130,7 @@ export default {
'chatHistory.loading': 'Loading...',
'chatHistory.noMoreData': 'No more data',
'chatHistory.noChatRecords': 'No chat records',
'chatRecordsDescription': 'Conversation records with agents will be displayed here',
'chatHistory.chatRecordsDescription': 'Conversation records with agents will be displayed here',
// Chat History Detail Page
'chatHistory.pageTitle': 'Chat Detail',
'chatHistory.assistantName': 'Intelligent Assistant',
+3
View File
@@ -8,6 +8,7 @@ import en from './en'
import zh_TW from './zh_TW'
import de from './de'
import vi from './vi'
import pt_BR from './pt_BR'
// 语言包映射
const messages = {
@@ -16,6 +17,7 @@ const messages = {
zh_TW: zh_TW,
de,
vi,
pt_BR: pt_BR,
}
// 当前使用的语言
@@ -72,5 +74,6 @@ export function getSupportedLanguages(): { code: Language, name: string }[] {
{ code: 'zh_TW', name: '繁體中文' },
{ code: 'de', name: 'Deutsch' },
{ code: 'vi', name: 'Tiếng Việt' },
{ code: 'pt_BR', name: 'Português (Brasil)' },
]
}
+440
View File
@@ -0,0 +1,440 @@
// Pacote de idioma Português Brasileiro
export default {
// TabBar
'tabBar.home': 'Início',
'tabBar.deviceConfig': 'Config. de Rede',
'tabBar.settings': 'Sistema',
// Settings page title
'settings.title': 'Configurações',
// Login page
'login.pageTitle': 'Entrar',
'login.navigationTitle': 'Entrar',
'login.fetchConfigError': 'Falha ao buscar configuração:',
'login.selectLanguage': 'Selecionar Idioma',
'login.selectLanguageTip': '中文',
'login.welcomeBack': 'Bem-vindo de Volta',
'login.pleaseLogin': 'Por favor, entre na sua conta',
'login.enterUsername': 'Por favor, insira o nome de usuário',
'login.enterPassword': 'Por favor, insira a senha',
'login.enterCaptcha': 'Por favor, insira o código de verificação',
'login.loginButton': 'Entrar',
'login.loggingIn': 'Entrando...',
'login.noAccount': 'Cadastrar',
'login.enterPhone': 'Por favor, insira o número de telefone',
'login.selectCountry': 'Selecionar País/Região',
'login.confirm': 'Confirmar',
'login.serverSetting': 'Configurações do Servidor',
'login.requiredUsername': 'O nome de usuário não pode estar vazio',
'login.requiredPassword': 'A senha não pode estar vazia',
'login.requiredCaptcha': 'O código de verificação não pode estar vazio',
'login.requiredMobile': 'Por favor, insira um número de telefone válido',
'login.captchaError': 'Erro no código de verificação gráfico',
'login.forgotPassword': 'Esqueceu a Senha',
// Register page
'register.pageTitle': 'Cadastro',
'register.createAccount': 'Criar Conta',
'register.enterUsername': 'Por favor, insira o nome de usuário',
'register.enterPassword': 'Por favor, insira a senha',
'register.confirmPassword': 'Por favor, confirme a senha',
'register.enterPhone': 'Por favor, insira o número de telefone',
'register.enterCode': 'Por favor, insira o código de verificação',
'register.getCode': 'Obter Código',
'register.agreeTerms': 'Li e concordo com os',
'register.terms': 'Termos de Uso',
'register.privacy': 'Política de Privacidade',
'register.registerButton': 'Cadastrar',
'register.registering': 'Cadastrando...',
'register.haveAccount': 'Já tem uma conta?',
'register.loginNow': 'Entrar Agora',
'register.selectCountry': 'Selecionar País/Região',
'register.confirm': 'Confirmar',
'register.captchaSendSuccess': 'Código de verificação enviado com sucesso',
// Home page
'home.pageTitle': 'Início',
'home.createAgent': 'Criar Agente',
'home.agentName': 'Agente',
'home.modelInfo': 'Info do Modelo',
'home.lastActive': 'Última Atividade',
'home.greeting': 'Olá Jarvis',
'home.subtitle': 'Vamos ter',
'home.wonderfulDay': 'um dia maravilhoso!',
'home.emptyState': 'Nenhum agente disponível',
'home.deviceManagement': 'Gerenciamento de Dispositivos',
'home.lastConversation': 'Última Conversa:',
'home.delete': 'Excluir',
'home.createFirstAgent': 'Clique no botão + no canto inferior direito para criar seu primeiro agente',
'home.dialogTitle': 'Criar Agente',
'home.inputPlaceholder': 'ex: Assistente de Atendimento, Assistente de Voz, Perguntas e Respostas',
'home.createError': 'Por favor, insira o nome do agente',
'home.createNow': 'Criar Agora',
'home.justNow': 'Agora mesmo',
'home.minutesAgo': 'minutos atrás',
'home.hoursAgo': 'horas atrás',
'home.daysAgo': 'dias atrás',
// Agent page
'agent.pageTitle': 'Agente',
'agent.roleConfig': 'Configuração de Papel',
'agent.deviceManagement': 'Gerenciamento de Dispositivos',
'agent.chatHistory': 'Histórico de Conversas',
'agent.voiceprintManagement': 'Gerenciamento de Impressão Vocal',
'agent.editTitle': 'Editar Agente',
'agent.toolsTitle': 'Editar Funcionalidades',
'agent.voiceActivityDetection': 'Detecção de Atividade de Voz',
'agent.speechRecognition': 'Reconhecimento de Fala',
'agent.largeLanguageModel': 'Modelo de Linguagem Grande',
'agent.save': 'Salvar',
'agent.cancel': 'Cancelar',
// Agent Edit Page
'agent.basicInfo': 'Informações Básicas',
'agent.agentName': 'Nome do Agente',
'agent.inputAgentName': 'Por favor, insira o nome do agente',
'agent.roleMode': 'Modo de Papel',
'agent.roleDescription': 'Descrição do Papel',
'agent.inputRoleDescription': 'Por favor, insira a descrição do papel',
'agent.modelConfig': 'Configuração do Modelo',
'agent.vad': 'Detecção de Atividade de Voz',
'agent.asr': 'Reconhecimento de Fala',
'agent.llm': 'Modelo de Linguagem Grande',
'agent.vllm': 'Modelo de Linguagem Visual',
'agent.intent': 'Reconhecimento de Intenção',
'agent.memory': 'Memória',
'agent.voiceSettings': 'Configurações de Voz',
'agent.tts': 'Texto para Fala',
'agent.voiceprint': 'Voz do Agente',
'agent.plugins': 'Plugins',
'agent.editFunctions': 'Editar Funções',
'agent.historyMemory': 'Memória de Histórico',
'agent.memoryContent': 'Conteúdo da memória',
'agent.saving': 'Salvando...',
'agent.saveSuccess': 'Salvo com sucesso',
'agent.saveFail': 'Falha ao salvar',
'agent.loadFail': 'Falha ao carregar',
'agent.pleaseInputAgentName': 'Por favor, insira o nome do agente',
'agent.pleaseInputRoleDescription': 'Por favor, insira a descrição do papel',
'agent.pleaseSelect': 'Por favor, selecione',
// Chat History Page
'chatHistory.getChatSessions': 'Obter lista de sessões de conversa',
'chatHistory.noSelectedAgent': 'Nenhum agente selecionado',
'chatHistory.getChatSessionsFailed': 'Falha ao obter lista de sessões de conversa:',
'chatHistory.unknownTime': 'Hora desconhecida',
'chatHistory.justNow': 'Agora mesmo',
'chatHistory.minutesAgo': '{minutes} minutos atrás',
'chatHistory.hoursAgo': '{hours} horas atrás',
'chatHistory.daysAgo': '{days} dias atrás',
'chatHistory.conversationRecord': 'Registro de Conversas',
'chatHistory.totalChats': '{count} conversas no total',
'chatHistory.loading': 'Carregando...',
'chatHistory.noMoreData': 'Não há mais dados',
'chatHistory.noChatRecords': 'Nenhum registro de conversa',
'chatHistory.chatRecordsDescription': 'Os registros de conversas com agentes serão exibidos aqui',
// Chat History Detail Page
'chatHistory.pageTitle': 'Detalhe da Conversa',
'chatHistory.assistantName': 'Assistente Inteligente',
'chatHistory.userName': 'Usuário',
'chatHistory.aiAssistantName': 'Assistente IA',
'chatHistory.loadFailed': 'Falha ao carregar histórico de conversas',
'chatHistory.parameterError': 'Erro de parâmetro da página',
'chatHistory.invalidAudioId': 'ID de áudio inválido',
'chatHistory.audioPlayFailed': 'Falha na reprodução de áudio',
'chatHistory.playAudioFailed': 'Falha ao reproduzir áudio',
// Device management page
'device.pageTitle': 'Gerenciamento de Dispositivos',
'device.noDevices': 'Nenhum dispositivo disponível',
'device.macAddress': 'Endereço MAC',
'device.firmwareVersion': 'Versão do Firmware',
'device.lastConnected': 'Última Conversa',
'device.otaUpdate': 'Atualização OTA',
'device.unbind': 'Desvincular',
'device.confirmUnbind': 'Confirmar',
'device.bindDevice': 'Vincular Novo Dispositivo',
'device.deviceType': 'Tipo de Dispositivo',
'device.loading': 'Carregando...',
'device.neverConnected': 'Nunca conectado',
'device.justNow': 'Agora mesmo',
'device.minutesAgo': '{minutes} minutos atrás',
'device.hoursAgo': '{hours} horas atrás',
'device.daysAgo': '{days} dias atrás',
'device.otaAutoUpdateEnabled': 'Atualização automática OTA ativada',
'device.otaAutoUpdateDisabled': 'Atualização automática OTA desativada',
'device.operationFailed': 'Operação falhou, por favor tente novamente',
'device.deviceUnbound': 'Dispositivo desvinculado',
'device.unbindFailed': 'Falha ao desvincular, por favor tente novamente',
'device.unbindDevice': 'Desvincular Dispositivo',
'device.confirmUnbindDevice': 'Tem certeza que deseja desvincular o dispositivo "{macAddress}"?',
'device.cancel': 'Cancelar',
'device.noDevice': 'Nenhum Dispositivo',
'device.pleaseSelectAgent': 'Por favor, selecione um agente primeiro',
'device.deviceBindSuccess': 'Dispositivo vinculado com sucesso!',
'device.bindFailed': 'Falha ao vincular, por favor verifique se o código de verificação está correto',
'device.enterDeviceCode': 'Por favor, insira o código de verificação do dispositivo',
'device.bindNow': 'Vincular Agora',
'device.lastConnection': 'Última Conexão',
'device.clickToBindFirstDevice': 'Clique no botão + no canto inferior direito para vincular seu primeiro dispositivo',
// Common
'common.success': 'Sucesso',
'common.fail': 'Falhou',
'common.loading': 'Carregando...',
'common.confirm': 'Confirmar',
'common.cancel': 'Cancelar',
'common.delete': 'Excluir',
'common.edit': 'Editar',
'common.add': 'Adicionar',
'common.pleaseSelect': 'Por favor, selecione',
'common.unknownError': 'Erro desconhecido',
'common.networkError': 'Erro de rede',
// Retrieve Password page
'retrievePassword.title': 'Redefinir Senha',
'retrievePassword.subtitle': 'Recupere a senha da sua conta pelo número de celular',
'retrievePassword.mobileRequired': 'Por favor, insira o número de celular',
'retrievePassword.inputCorrectMobile': 'Por favor, insira um número de celular válido',
'retrievePassword.captchaRequired': 'Por favor, insira o código de verificação gráfico',
'retrievePassword.mobileCaptchaRequired': 'Por favor, insira o código de verificação por SMS',
'retrievePassword.newPasswordRequired': 'Por favor, insira a nova senha',
'retrievePassword.confirmNewPasswordRequired': 'Por favor, confirme a nova senha',
'retrievePassword.passwordsNotMatch': 'As senhas não coincidem',
'retrievePassword.mobilePlaceholder': 'Por favor, insira o número de celular',
'retrievePassword.captchaPlaceholder': 'Por favor, insira o código de verificação gráfico',
'retrievePassword.mobileCaptchaPlaceholder': 'Por favor, insira o código de verificação por SMS',
'retrievePassword.newPasswordPlaceholder': 'Por favor, insira a nova senha',
'retrievePassword.confirmNewPasswordPlaceholder': 'Por favor, confirme a nova senha',
'retrievePassword.getMobileCaptcha': 'Obter Código',
'retrievePassword.captchaSendSuccess': 'Código de verificação enviado com sucesso',
'retrievePassword.passwordUpdateSuccess': 'Senha redefinida com sucesso',
'retrievePassword.resetButton': 'Redefinir Senha',
'retrievePassword.goToLogin': 'Voltar ao Login',
// SM2 encryption related error messages
'sm2.publicKeyNotConfigured': 'Chave pública SM2 não configurada, por favor entre em contato com o administrador',
'sm2.encryptionFailed': 'Falha na criptografia da senha',
'sm2.keyGenerationFailed': 'Falha na geração do par de chaves',
'sm2.invalidPublicKey': 'Formato de chave pública inválido',
'sm2.encryptionError': 'Ocorreu um erro durante a criptografia',
'sm2.publicKeyRetry': 'Tentando obter a chave pública novamente...',
'sm2.publicKeyRetryFailed': 'Falha na nova tentativa de obter a chave pública',
// Voiceprint page
'voiceprint.noSelectedAgent': 'Nenhum agente selecionado',
'voiceprint.pleaseSelectAgent': 'Por favor, selecione um agente primeiro',
'voiceprint.fetchHistoryFailed': 'Falha ao buscar histórico de conversas',
'voiceprint.clickToSelectVector': 'Clique para selecionar vetor de impressão vocal',
'voiceprint.pleaseInputName': 'Por favor, insira o nome',
'voiceprint.pleaseSelectVector': 'Por favor, selecione o vetor de impressão vocal',
'voiceprint.addSuccess': 'Adicionado com sucesso',
'voiceprint.addFailed': 'Falha ao adicionar locutor',
'voiceprint.editSuccess': 'Editado com sucesso',
'voiceprint.editFailed': 'Falha ao editar locutor',
'voiceprint.deleteConfirmMsg': 'Tem certeza que deseja excluir este locutor?',
'voiceprint.deleteConfirmTitle': 'Confirmar Exclusão',
'voiceprint.deleteSuccess': 'Excluído com sucesso',
'voiceprint.loading': 'Carregando...',
'voiceprint.delete': 'Excluir',
'voiceprint.emptyTitle': 'Nenhum dado de impressão vocal',
'voiceprint.emptyDesc': 'Clique no botão + no canto inferior direito para adicionar seu primeiro locutor',
'voiceprint.addSpeaker': 'Adicionar Locutor',
'voiceprint.voiceVector': 'Vetor de Impressão Vocal',
'voiceprint.name': 'Nome',
'voiceprint.description': 'Descrição',
'voiceprint.pleaseInputDescription': 'Por favor, insira a descrição',
'voiceprint.cancel': 'Cancelar',
'voiceprint.save': 'Salvar',
'voiceprint.editSpeaker': 'Editar Locutor',
'voiceprint.selectVector': 'Selecionar Vetor de Impressão Vocal',
'voiceprint.voiceprintInterfaceNotConfigured': 'Interface de impressão vocal não configurada',
// Settings page
'settings.pageTitle': 'Configurações',
'settings.navigationTitle': 'Configurações',
'settings.networkSettings': 'Configurações de Rede',
'settings.serverApiUrl': 'URL da API do Servidor',
'settings.validServerUrl': 'Por favor, insira um endereço de servidor válido (começando com http ou https e terminando com /xiaozhi)',
'settings.saveSettings': 'Salvar Configurações',
'settings.resetDefault': 'Restaurar Padrão',
'settings.restartApp': 'Reiniciar App',
'settings.restartNow': 'Reiniciar Agora',
'settings.restartLater': 'Depois',
// About us
'settings.aboutApp': 'Sobre o Console XiaoZhi',
'settings.aboutContent': 'Console XiaoZhi\n\nUm aplicativo móvel de gerenciamento multiplataforma construído com Vue.js 3 + uni-app, oferecendo gerenciamento de dispositivos, configuração de agentes e outras funcionalidades para o hardware inteligente xiaozhi ESP32.\n\n© 2025 xiaozhi-esp32-server {version}',
'settings.restartSuccess': 'Salvo, você pode reiniciar o app manualmente depois',
'settings.serverUrlSavedAndCacheCleared': 'URL do servidor salva e cache limpo',
'settings.resetToDefaultAndCacheCleared': 'Restaurado ao padrão e cache limpo',
'settings.resetSuccess': 'Restauração bem-sucedida',
'settings.enterServerUrl': 'Por favor, insira a URL do servidor',
'settings.clearCacheFailed': 'Falha ao limpar o cache',
'settings.cacheManagement': 'Gerenciamento de Cache',
'settings.totalCacheSize': 'Tamanho Total do Cache',
'settings.appDataSize': 'Tamanho Total dos Dados do App',
'settings.cacheClear': 'Limpar Cache',
'settings.clearAllCache': 'Limpar todos os dados de cache',
'settings.clearCache': 'Limpar Cache',
'settings.modifyWillClearCache': 'Modificações irão limpar o cache',
'settings.appInfo': 'Informações do App',
'settings.aboutUs': 'Sobre Nós',
'settings.appVersion': 'Versão do App e Info da Equipe',
'settings.confirmClear': 'Confirmar Limpeza',
'settings.confirmClearMessage': 'Tem certeza que deseja limpar todo o cache? Isso excluirá todos os dados, incluindo o status de login, e exigirá novo login.',
'settings.cacheCleared': 'Cache limpo com sucesso, redirecionando para a página de login',
'settings.languageSettings': 'Configurações de Idioma',
'settings.language': 'Idioma',
'settings.selectLanguage': 'Selecionar Idioma',
'settings.languageChanged': 'Idioma alterado com sucesso',
// Messages
'message.loginSuccess': 'Login realizado com sucesso!',
'message.loginFail': 'Falha no login',
'message.registerSuccess': 'Cadastro realizado com sucesso',
'message.registerFail': 'Falha no cadastro',
'message.saveSuccess': 'Salvo com sucesso',
'message.saveFail': 'Falha ao salvar',
'message.deleteSuccess': 'Excluído com sucesso',
'message.deleteFail': 'Falha ao excluir',
'message.bindSuccess': 'Vinculação realizada com sucesso',
'message.bindFail': 'Falha na vinculação',
'message.unbindSuccess': 'Desvinculação realizada com sucesso',
'message.unbindFail': 'Falha na desvinculação',
'message.networkError': 'Erro de rede, por favor verifique sua conexão',
'message.serverError': 'Erro no servidor, por favor tente novamente mais tarde',
'message.invalidAddress': 'Endereço inválido, por favor verifique se o servidor está iniciado ou se a conexão de rede está normal',
'message.languageChanged': 'Idioma alterado',
'message.passwordError': 'Conta ou senha incorretos',
'message.phoneRegistered': 'Este número de telefone já está cadastrado',
// Agent Tools Page
'agent.tools.pageTitle': 'Ferramentas do Agente',
'agent.tools.unselected': 'Não selecionado',
'agent.tools.selected': 'Selecionado',
'agent.tools.noMorePlugins': 'Não há mais plugins',
'agent.tools.pleaseSelectPlugin': 'Por favor, selecione a função do plugin',
'agent.tools.builtInPlugins': 'Plugins Integrados',
'agent.tools.mcpAccessPoint': 'Ponto de Acesso MCP',
'agent.tools.copy': 'Copiar',
'agent.tools.noTools': 'Nenhuma ferramenta disponível',
'agent.tools.parameterConfig': 'Configuração de Parâmetros',
'agent.tools.noParamsNeeded': 'Nenhum parâmetro necessário',
'agent.tools.pleaseInput': 'Por favor, insira',
'agent.tools.inputOneItemPerLine': 'Insira um item por linha',
'agent.tools.pleaseInputValidJson': 'Por favor, insira um formato JSON válido',
'agent.tools.enableFunction': 'Ativar Função',
'agent.tools.toggleFunction': 'Ativar ou desativar esta função',
'agent.tools.jsonFormatError': 'Erro no formato JSON',
'agent.tools.noMcpAddressToCopy': 'Nenhum endereço MCP para copiar',
'agent.tools.mcpAddressCopied': 'Endereço MCP copiado para a área de transferência',
'agent.tools.copyFailed': 'Falha ao copiar, por favor tente novamente',
'agent.tools.defaultValue': 'Valor padrão',
'agent.tools.notSelected': 'Não selecionado',
'agent.tools.clickToConfigure': 'Clique para configurar',
'agent.tools.mcpEndpoint': 'Endpoint MCP',
'agent.tools.eachLineOneItem': 'Insira um item por linha',
// Device Config page
'deviceConfig.pageTitle': 'Configuração do Dispositivo',
'deviceConfig.wifiConfig': 'Configuração WiFi',
'deviceConfig.ultrasonicConfig': 'Configuração Ultrassônica',
'deviceConfig.selectConfigMethod': 'Selecionar Método de Configuração',
'deviceConfig.networkConfig': 'Configuração de Rede',
'deviceConfig.selectedNetwork': 'Rede Selecionada',
'deviceConfig.signal': 'Sinal',
'deviceConfig.openNetwork': 'Rede Aberta',
'deviceConfig.encryptedNetwork': 'Rede Criptografada',
'deviceConfig.password': 'Senha',
'deviceConfig.pleaseEnterPassword': 'Por favor, insira a senha do WiFi',
'deviceConfig.startConfig': 'Iniciar Configuração',
'deviceConfig.connectToXiaozhiHotspot': 'Por favor, conecte-se ao hotspot xiaozhi primeiro',
'deviceConfig.detecting': 'Detectando...',
'deviceConfig.reDetect': 'Detectar Novamente',
'deviceConfig.alreadyConnected': 'Conectado ao hotspot xiaozhi',
'deviceConfig.refreshStatus': 'Atualizar Status',
'deviceConfig.wifiNetworks': 'Redes WiFi',
'deviceConfig.selectWifiNetwork': 'Selecionar Rede WiFi',
'deviceConfig.refreshScan': 'Atualizar Busca',
'deviceConfig.noWifiNetworks': 'Nenhuma rede WiFi disponível',
'deviceConfig.clickToRefreshScan': 'Por favor, clique em Atualizar Busca',
'deviceConfig.signalStrong': 'Sinal Forte',
'deviceConfig.signalGood': 'Sinal Bom',
'deviceConfig.signalFair': 'Sinal Regular',
'deviceConfig.signalWeak': 'Sinal Fraco',
'deviceConfig.channel': 'Canal',
'deviceConfig.about': 'aproximadamente',
'deviceConfig.seconds': 'segundos',
'deviceConfig.generating': 'Gerando...',
'deviceConfig.playing': 'Reproduzindo...',
'deviceConfig.generateAndPlaySoundWave': 'Gerar e Reproduzir Onda Sonora',
'deviceConfig.playSoundWave': 'Reproduzir Onda Sonora',
'deviceConfig.stopPlaying': 'Parar Reprodução',
'deviceConfig.autoLoopPlaySoundWave': 'Reproduzir Onda Sonora em Loop Automático',
'deviceConfig.configAudioFile': 'Arquivo de Áudio de Configuração',
'deviceConfig.duration': 'Duração',
'deviceConfig.ultrasonicConfigInstructions': 'Instruções de Configuração Ultrassônica',
'deviceConfig.ensureWifiNetworkSelectedAndPasswordEntered': 'Certifique-se de que a rede WiFi foi selecionada e a senha foi inserida',
'deviceConfig.clickGenerateAndPlaySoundWave': 'Clique em Gerar e Reproduzir Onda Sonora, o sistema codificará as informações de configuração em áudio',
'deviceConfig.bringPhoneCloseToXiaozhiDevice': 'Aproxime o celular do dispositivo xiaozhi (distância de 1-2 metros)',
'deviceConfig.duringAudioPlaybackXiaozhiWillReceive': 'Durante a reprodução do áudio, o xiaozhi receberá e decodificará as informações de configuração',
'deviceConfig.afterConfigSuccessDeviceWillConnect': 'Após a configuração bem-sucedida, o dispositivo se conectará automaticamente à rede WiFi',
'deviceConfig.usesAfskModulation': 'Utiliza tecnologia de modulação AFSK, transmitindo dados pelas frequências de 1800Hz e 1500Hz',
'deviceConfig.ensureModeratePhoneVolume': 'Por favor, certifique-se de que o volume do celular está moderado para evitar interferência de ruídos ambientais',
'deviceConfig.generatingUltrasonicConfigAudio': 'Gerando áudio de configuração ultrassônica',
'deviceConfig.configData': 'Dados de configuração',
'deviceConfig.dataBytesLength': 'Comprimento dos bytes de dados',
'deviceConfig.bitStreamLength': 'Comprimento do fluxo de bits',
'deviceConfig.base64Length': 'Comprimento Base64',
'deviceConfig.audioFileTooLarge': 'Arquivo de áudio muito grande, por favor reduza o tamanho do SSID ou da senha',
'deviceConfig.audioGenerationSuccess': 'Áudio gerado com sucesso',
'deviceConfig.samplePoints': 'Pontos de amostra',
'deviceConfig.soundWaveGenerationSuccess': 'Onda sonora gerada com sucesso',
'deviceConfig.audioGenerationFailed': 'Falha na geração do áudio',
'deviceConfig.soundWaveGenerationFailed': 'Falha na geração da onda sonora',
'deviceConfig.pleaseGenerateAudioFirst': 'Por favor, gere o áudio primeiro',
'deviceConfig.startPlayingUltrasonicConfigAudio': 'Iniciando reprodução do áudio de configuração ultrassônica',
'deviceConfig.ultrasonicAudioStartedPlaying': 'Reprodução do áudio ultrassônico iniciada',
'deviceConfig.startPlayingConfigSoundWave': 'Reprodução da onda sonora de configuração iniciada',
'deviceConfig.ultrasonicAudioPlaybackEnded': 'Reprodução do áudio ultrassônico finalizada',
'deviceConfig.audioPlaybackFailed': 'Falha na reprodução do áudio',
'deviceConfig.audioResourceBusy': 'Recurso de áudio ocupado, por favor tente novamente mais tarde',
'deviceConfig.audioFormatNotSupported': 'Formato de áudio não suportado, pode ser um problema de URI de dados',
'deviceConfig.audioFileError': 'Erro no arquivo de áudio',
'deviceConfig.cleaningUpAudioContext': 'Limpando contexto de áudio',
'deviceConfig.cleaningUpAudioContextFailed': 'Falha ao limpar contexto de áudio',
'deviceConfig.stoppedPlayingUltrasonicAudio': 'Reprodução do áudio ultrassônico interrompida',
'deviceConfig.stoppedPlaying': 'Reprodução interrompida',
'deviceConfig.configMethod': 'Método de Configuração',
'deviceConfig.enterWifiPassword': 'Por favor, insira a senha do WiFi',
'deviceConfig.xiaozhi': 'xiaozhi',
'deviceConfig.connectXiaozhiHotspot': 'Por favor, conecte-se ao hotspot xiaozhi',
'deviceConfig.wifiScanResponse': 'Resposta da busca WiFi',
'deviceConfig.scanSuccess': 'Busca bem-sucedida',
'deviceConfig.networks': 'redes',
'deviceConfig.wifiScanFailed': 'Falha na busca WiFi',
'deviceConfig.scanFailedCheckConnection': 'Falha na busca, por favor verifique a conexão',
'deviceConfig.checking': 'Verificando',
'deviceConfig.reCheck': 'Verificar Novamente',
'deviceConfig.connectedXiaozhiHotspot': 'Conectado ao hotspot xiaozhi',
'deviceConfig.wifiNetwork': 'Rede WiFi',
'deviceConfig.scanning': 'Buscando',
'deviceConfig.cancel': 'Cancelar',
'deviceConfig.clickRefreshScan': 'Por favor, clique em Atualizar Busca',
'deviceConfig.esp32ConnectionCheckFailed': 'Falha na verificação de conexão ESP32',
'deviceConfig.startWifiConfig': 'Iniciando configuração WiFi',
'deviceConfig.configSuccess': 'Configuração bem-sucedida',
'deviceConfig.deviceWillConnectTo': 'O dispositivo se conectará a',
'deviceConfig.deviceWillRestart': 'O dispositivo será reiniciado',
'deviceConfig.pleaseDisconnectXiaozhiHotspot': 'Por favor, desconecte-se do hotspot xiaozhi',
'deviceConfig.configFailed': 'Falha na configuração',
'deviceConfig.wifiConfigFailed': 'Falha na configuração WiFi',
'deviceConfig.pleaseCheckNetworkConnection': 'Por favor, verifique a conexão de rede',
'deviceConfig.startWifiConfigButton': 'Iniciar Configuração',
'deviceConfig.wifiConfigInstructions': 'Instruções de Configuração WiFi',
'deviceConfig.phoneConnectXiaozhiHotspot': 'Conecte o celular ao hotspot xiaozhi',
'deviceConfig.selectTargetWifiNetwork': 'Selecione a rede WiFi de destino',
'deviceConfig.enterWifiPasswordIfNeeded': 'Insira a senha do WiFi se necessário',
'deviceConfig.clickStartConfigAndWait': 'Clique em Iniciar Configuração e aguarde',
'deviceConfig.afterConfigSuccessDeviceWillRestart': 'Após a configuração bem-sucedida, o dispositivo reiniciará automaticamente',
'deviceConfig.audioPlaybackError': 'Erro na reprodução de áudio',
'deviceConfig.playbackFailed': 'Falha na reprodução',
}
+1 -1
View File
@@ -2,7 +2,7 @@ import { ref } from 'vue'
import { defineStore } from 'pinia'
// 支持的语言类型
export type Language = 'zh_CN' | 'en' | 'zh_TW' | 'de' | 'vi'
export type Language = 'zh_CN' | 'en' | 'zh_TW' | 'de' | 'vi' | 'pt_BR'
export interface LangStore {
currentLang: Language
@@ -254,6 +254,8 @@ export default {
return this.$t("language.de");
case "vi":
return this.$t("language.vi");
case "pt_BR":
return this.$t("language.ptBR");
default:
return this.$t("language.zhCN");
}
@@ -272,6 +274,8 @@ export default {
return require("@/assets/xiaozhi-ai_de.png");
case "vi":
return require("@/assets/xiaozhi-ai_vi.png");
case "pt_BR":
return require("@/assets/xiaozhi-ai_en.png");
default:
return require("@/assets/xiaozhi-ai.png");
}
@@ -303,6 +307,10 @@ export default {
label: this.$t("language.vi"),
value: "vi",
},
{
label: this.$t("language.ptBR"),
value: "pt_BR",
},
],
},
{
+1
View File
@@ -663,6 +663,7 @@ export default {
'language.en': 'English',
'language.de': 'Deutsch',
'language.vi': 'Tiếng Việt',
'language.ptBR': 'Português (Brasil)',
// Home page text
'home.addAgent': 'Agent hinzufügen',
+1
View File
@@ -663,6 +663,7 @@ export default {
'language.en': 'English',
'language.de': 'Deutsch',
'language.vi': 'Tiếng Việt',
'language.ptBR': 'Português (Brasil)',
// Home page text
'home.addAgent': 'Add Agent',
+6 -1
View File
@@ -5,6 +5,7 @@ import zhTW from './zh_TW';
import en from './en';
import de from './de';
import vi from './vi';
import ptBR from './pt_BR';
Vue.use(VueI18n);
@@ -27,6 +28,9 @@ const getDefaultLanguage = () => {
if (browserLang.indexOf('vi') === 0) {
return 'vi';
}
if (browserLang === 'pt-BR' || browserLang === 'pt') {
return 'pt_BR';
}
return 'en';
};
@@ -38,7 +42,8 @@ const i18n = new VueI18n({
'zh_TW': zhTW,
'en': en,
'de': de,
'vi': vi
'vi': vi,
'pt_BR': ptBR
}
});
File diff suppressed because it is too large Load Diff
+1
View File
@@ -663,6 +663,7 @@ export default {
'language.en': 'English',
'language.de': 'Deutsch',
'language.vi': 'Tiếng Việt',
'language.ptBR': 'Português (Brasil)',
// Home page text
'home.addAgent': 'Thêm tác nhân',
+1
View File
@@ -663,6 +663,7 @@ export default {
'language.en': 'English',
'language.de': 'Deutsch',
'language.vi': 'Tiếng Việt',
'language.ptBR': 'Português (Brasil)',
// 首页文本
'home.addAgent': '添加智能体',
+1
View File
@@ -663,6 +663,7 @@ export default {
'language.en': 'English',
'language.de': 'Deutsch',
'language.vi': 'Tiếng Việt',
'language.ptBR': 'Português (Brasil)',
// 首頁文本
'home.addAgent': '添加智能體',
+5
View File
@@ -55,6 +55,9 @@
<el-dropdown-item @click.native="changeLanguage('vi')">
{{ $t("language.vi") }}
</el-dropdown-item>
<el-dropdown-item @click.native="changeLanguage('pt_BR')">
{{ $t("language.ptBR") }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
@@ -188,6 +191,8 @@ export default {
return this.$t("language.de");
case "vi":
return this.$t("language.vi");
case "pt_BR":
return this.$t("language.ptBR");
default:
return this.$t("language.zhCN");
}
@@ -137,6 +137,8 @@ export default {
return require("@/assets/xiaozhi-ai_de.png");
case "vi":
return require("@/assets/xiaozhi-ai_vi.png");
case "pt_BR":
return require("@/assets/xiaozhi-ai_en.png");
default:
return require("@/assets/xiaozhi-ai.png");
}
@@ -2,7 +2,8 @@
"permissions": {
"allow": [
"Bash(tree:*)",
"Bash(find:*)"
"Bash(find:*)",
"Bash(python3:*)"
],
"deny": [],
"ask": []
+8
View File
@@ -1118,6 +1118,14 @@ class ConnectionHandler:
async def close(self, ws=None):
"""资源清理方法"""
try:
# 清理 VAD 连接资源
if (
hasattr(self, "vad")
and self.vad
and hasattr(self.vad, "release_conn_resources")
):
self.vad.release_conn_resources(self)
# 清理音频缓冲区
if hasattr(self, "audio_buffer"):
self.audio_buffer.clear()
@@ -1,7 +1,8 @@
import time
import os
import numpy as np
import torch
import opuslib_next
import onnxruntime
from config.logger import setup_logging
from core.providers.vad.base import VADProviderBase
@@ -12,16 +13,17 @@ logger = setup_logging()
class VADProvider(VADProviderBase):
def __init__(self, config):
logger.bind(tag=TAG).info("SileroVAD", config)
self.model, _ = torch.hub.load(
repo_or_dir=config["model_dir"],
source="local",
model="silero_vad",
force_reload=False,
model_path = os.path.join(
config["model_dir"], "src", "silero_vad", "data", "silero_vad.onnx"
)
opts = onnxruntime.SessionOptions()
opts.inter_op_num_threads = 1
opts.intra_op_num_threads = 1
self.session = onnxruntime.InferenceSession(
model_path, providers=["CPUExecutionProvider"], sess_options=opts
)
self.decoder = opuslib_next.Decoder(16000, 1)
# 处理空字符串的情况
threshold = config.get("threshold", "0.5")
threshold_low = config.get("threshold_low", "0.2")
min_silence_duration_ms = config.get("min_silence_duration_ms", "1000")
@@ -33,40 +35,58 @@ class VADProvider(VADProviderBase):
int(min_silence_duration_ms) if min_silence_duration_ms else 1000
)
# 至少要多少帧才算有语音
self.frame_window_threshold = 3
def __del__(self):
if hasattr(self, 'decoder') and self.decoder is not None:
try:
del self.decoder
except Exception:
pass
def _init_connection_state(self, conn):
"""为连接初始化独立的 VAD 状态"""
if not hasattr(conn, "_vad_opus_decoder"):
conn._vad_opus_decoder = opuslib_next.Decoder(16000, 1)
if not hasattr(conn, "_vad_state"):
conn._vad_state = np.zeros((2, 1, 128), dtype=np.float32)
if not hasattr(conn, "_vad_context"):
conn._vad_context = np.zeros((1, 64), dtype=np.float32)
def release_conn_resources(self, conn):
"""释放连接的 VAD 资源(连接关闭时调用)"""
for attr in ("_vad_opus_decoder", "_vad_state", "_vad_context"):
if hasattr(conn, attr):
try:
delattr(conn, attr)
except Exception:
pass
def is_vad(self, conn, opus_packet):
# 手动模式:直接返回True,不进行实时VAD检测,所有音频都缓存
if conn.client_listen_mode == "manual":
return True
try:
pcm_frame = self.decoder.decode(opus_packet, 960)
conn.client_audio_buffer.extend(pcm_frame) # 将新数据加入缓冲区
# 处理缓冲区中的完整帧(每次处理512采样点)
try:
self._init_connection_state(conn)
pcm_frame = conn._vad_opus_decoder.decode(opus_packet, 960)
conn.client_audio_buffer.extend(pcm_frame)
client_have_voice = False
while len(conn.client_audio_buffer) >= 512 * 2:
# 提取前512个采样点(1024字节)
chunk = conn.client_audio_buffer[: 512 * 2]
conn.client_audio_buffer = conn.client_audio_buffer[512 * 2 :]
# 转换为模型需要的张量格式
audio_int16 = np.frombuffer(chunk, dtype=np.int16)
audio_float32 = audio_int16.astype(np.float32) / 32768.0
audio_tensor = torch.from_numpy(audio_float32)
audio_input = np.concatenate(
[conn._vad_context, audio_float32.reshape(1, -1)], axis=1
).astype(np.float32)
# 检测语音活动
with torch.no_grad():
speech_prob = self.model(audio_tensor, 16000).item()
ort_inputs = {
"input": audio_input,
"state": conn._vad_state,
"sr": np.array(16000, dtype=np.int64),
}
out, state = self.session.run(None, ort_inputs)
conn._vad_state = state
conn._vad_context = audio_input[:, -64:]
speech_prob = out.item()
# 双阈值判断
if speech_prob >= self.vad_threshold:
+120 -64
View File
@@ -1201,6 +1201,113 @@ body {
z-index: 999;
}
/* 滚动条样式 */
.chat-stream::-webkit-scrollbar,
.modal-body::-webkit-scrollbar,
.mcp-tools-list::-webkit-scrollbar {
width: 6px;
}
.chat-stream::-webkit-scrollbar-track,
.modal-body::-webkit-scrollbar-track,
.mcp-tools-list::-webkit-scrollbar-track {
background: #2f3136;
border-radius: 3px;
}
.chat-stream::-webkit-scrollbar-thumb,
.modal-body::-webkit-scrollbar-thumb,
.mcp-tools-list::-webkit-scrollbar-thumb {
background: #40444b;
border-radius: 3px;
}
.chat-stream::-webkit-scrollbar-thumb:hover,
.modal-body::-webkit-scrollbar-thumb:hover,
.mcp-tools-list::-webkit-scrollbar-thumb:hover {
background: #4f545c;
}
/* ==================== 摄像头显示区域样式 ==================== */
.camera-container {
position: fixed;
top: 20px;
left: 20px;
width: 180px;
height: 240px;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
z-index: 1000;
display: none;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
cursor: move;
}
.camera-container.active {
display: block;
}
.camera-container.dragging {
cursor: grabbing;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
border-color: rgba(88, 101, 242, 0.5);
}
@keyframes flipWithPosition {
0% {
transform: var(--original-transform) rotateY(0deg);
}
100% {
transform: var(--original-transform) rotateY(180deg);
}
}
.camera-container.flip {
animation: flipWithPosition 0.5s ease-in-out 1 forwards;
}
#cameraVideo {
width: 100%;
height: 100%;
object-fit: cover;
background: #1a1a1a;
pointer-events: none;
transform: scaleX(-1);
}
.camera-switch-mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.camera-switch-container {
position: absolute;
bottom: 10px;
right: 10px;
}
.camera-switch {
display: none;
height: 20px;
color: #fff;
}
.camera-switch .btn-icon {
margin-bottom: 0;
}
.camera-switch.active {
display: block;
}
/* ==================== 响应式设计 ==================== */
@media (max-width: 768px) {
.container {
@@ -1264,6 +1371,19 @@ body {
left: 10px;
bottom: 80px;
}
.chat-ipt input {
font-size: 12px;
}
.camera-container {
width: 130px;
height: 180px;
}
.status-indicator {
font-size: 12px;
}
}
@media (max-width: 480px) {
@@ -1343,68 +1463,4 @@ body {
width: 250px;
height: 80px;
}
}
/* 滚动条样式 */
.chat-stream::-webkit-scrollbar,
.modal-body::-webkit-scrollbar,
.mcp-tools-list::-webkit-scrollbar {
width: 6px;
}
.chat-stream::-webkit-scrollbar-track,
.modal-body::-webkit-scrollbar-track,
.mcp-tools-list::-webkit-scrollbar-track {
background: #2f3136;
border-radius: 3px;
}
.chat-stream::-webkit-scrollbar-thumb,
.modal-body::-webkit-scrollbar-thumb,
.mcp-tools-list::-webkit-scrollbar-thumb {
background: #40444b;
border-radius: 3px;
}
.chat-stream::-webkit-scrollbar-thumb:hover,
.modal-body::-webkit-scrollbar-thumb:hover,
.mcp-tools-list::-webkit-scrollbar-thumb:hover {
background: #4f545c;
}
/* ==================== 摄像头显示区域样式 ==================== */
.camera-container {
position: fixed;
top: 20px;
left: 20px;
width: 240px;
height: 180px;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
z-index: 1000;
display: none;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
cursor: move;
}
.camera-container.active {
display: block;
}
.camera-container.dragging {
cursor: grabbing;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
border-color: rgba(88, 101, 242, 0.5);
}
#cameraVideo {
width: 100%;
height: 100%;
object-fit: cover;
background: #1a1a1a;
pointer-events: none;
transform: scaleX(-1);
}
+44 -1
View File
@@ -25,6 +25,7 @@ class App {
this.audioPlayer = null;
this.live2dManager = null;
this.cameraStream = null;
this.currentFacingMode = 'user';
}
// 初始化应用
@@ -127,6 +128,9 @@ class App {
async initCamera() {
const cameraContainer = document.getElementById('cameraContainer');
const cameraVideo = document.getElementById('cameraVideo');
const cameraSwitch = document.getElementById('cameraSwitch');
const cameraSwitchMask = document.getElementById('cameraSwitchMask');
const dialBtn = document.getElementById('dialBtn');
if (!cameraContainer || !cameraVideo) {
log('摄像头元素未找到,跳过初始化', 'warning');
@@ -188,11 +192,24 @@ class App {
}
log('正在请求摄像头权限...', 'info');
this.cameraStream = await navigator.mediaDevices.getUserMedia({
video: { width: 320, height: 240, facingMode: 'user' },
video: { width: 320, height: 240, facingMode: this.currentFacingMode },
audio: false
});
cameraVideo.srcObject = this.cameraStream;
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(device => device.kind === 'videoinput');
if (videoDevices.length > 1) {
if (cameraSwitch) cameraSwitch.classList.add('active');
}
cameraContainer.classList.add('active');
// 切换时挂断情况
const hasActive = dialBtn.classList.contains('dial-active');
if (!hasActive) {
cameraContainer.classList.remove('active');
cameraSwitch.classList.remove('active');
window.stopCamera();
}
log('摄像头已启动', 'success');
return true;
} catch (error) {
@@ -217,6 +234,32 @@ class App {
}
};
window.switchCamera = async() => {
if (window.switchCameraTimer) return;
if (this.cameraStream) {
const currentTransform = window.getComputedStyle(cameraContainer).transform;
const originalTransform = currentTransform === 'none' ? 'translate(0px, 0px)' : currentTransform;
cameraContainer.style.setProperty('--original-transform', originalTransform);
cameraContainer.classList.add('flip');
if (cameraSwitchMask) cameraSwitchMask.style.opacity = 0;
this.currentFacingMode = this.currentFacingMode === 'user' ? 'environment' : 'user';
window.stopCamera();
window.startCamera();
window.switchCameraTimer = setTimeout(() => {
if (this.currentFacingMode === 'user') {
cameraVideo.style.transform = 'scaleX(-1)';
} else {
cameraVideo.style.transform = 'scaleX(1)';
}
window.switchCameraTimer = null;
cameraContainer.classList.remove('flip');
cameraContainer.style.removeProperty('--original-transform');
if (cameraSwitchMask) cameraSwitchMask.style.opacity = 1;
}, 500);
}
};
window.takePhoto = (question = '描述一下看到的物品') => {
return new Promise(async (resolve) => {
const canvas = document.createElement('canvas');
@@ -98,6 +98,18 @@ class UIController {
});
}
// Camera switch button
const cameraSwitch = document.getElementById('cameraSwitch');
const cameraSwitchMask = document.getElementById('cameraSwitchMask');
if (cameraSwitchMask) {
cameraSwitchMask.addEventListener('click', () => {
const isCameraActive = cameraSwitch.classList.contains('active');
if (isCameraActive) {
window.switchCamera();
}
})
}
// Dial button
const dialBtn = document.getElementById('dialBtn');
if (dialBtn) {
@@ -115,6 +127,7 @@ class UIController {
if (isConnected) {
wsHandler.disconnect();
this.updateDialButton(false);
if (cameraSwitch) cameraSwitch.classList.remove('active');
this.addChatMessage('Disconnected, see you next time~😊', false);
} else {
// Check if OTA URL is filled
@@ -153,6 +166,7 @@ class UIController {
if (isActive) {
// 关闭摄像头
if (typeof window.stopCamera === 'function') {
if (cameraSwitch) cameraSwitch.classList.remove('active');
window.stopCamera();
}
cameraContainer.classList.remove('active');
+9
View File
@@ -44,9 +44,18 @@
<!-- 主容器 -->
<div class="container">
<!-- 摄像头显示区域(可拖动) -->
<div class="camera-container" id="cameraContainer">
<video id="cameraVideo" autoplay playsinline muted></video>
<div class="camera-switch-mask" id="cameraSwitchMask">
<div class="camera-switch-container">
<!-- 摄像头切换按钮 -->
<div class="camera-switch" id="cameraSwitch">
<svg class="btn-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7319" width="32" height="32"><path d="M722.1 530.3c0-44.9-13.8-86.5-37.2-120.9-0.3-0.6-0.6-1.2-0.9-1.7-2.9-4.2-6.1-8.1-9.2-12-0.4-0.5-0.7-1-1.1-1.5-21.4-26.3-48.3-46.7-78.7-59.9-0.8-0.4-1.7-0.8-2.5-1.2-4.9-2-9.8-3.8-14.9-5.5-1.8-0.6-3.6-1.3-5.4-1.8-4.4-1.3-8.8-2.4-13.3-3.5-2.5-0.6-4.9-1.2-7.4-1.7-1.2-0.2-2.4-0.6-3.6-0.8-3.3-0.6-6.7-0.8-10-1.3-2.3-0.3-4.6-0.7-6.9-0.9-5.6-0.5-11.2-0.8-16.7-0.9-1 0-2-0.1-3-0.1-0.2 0-0.3 0.1-0.5 0.1-42.8 0-84.6 13.1-120.4 38.6-11.5 8.1-14.2 24.1-6.2 35.7 8 11.6 23.8 14.4 35.2 6.3 27.7-19.7 60-29.6 93.1-29.3 4.8 0 9.5 0.3 14.1 0.7 1.4 0.2 2.8 0.4 4.3 0.5 3.8 0.5 7.6 1 11.3 1.8 1.6 0.3 3.3 0.8 4.9 1.1 3.7 0.8 7.3 1.7 10.9 2.8 1.1 0.4 2.2 0.8 3.4 1.2 4.1 1.3 8.1 2.8 12 4.5 0.4 0.2 0.8 0.4 1.2 0.5 23.6 10.3 44.3 26.1 60.4 45.9 0.1 0.1 0.2 0.3 0.3 0.4 22.7 28 36.4 63.9 36.4 102.9h-42.2L697 632.9l67.5-102.6h-42.4z m-119 133c-27.9 19.8-60.5 29.7-93.9 29.3-4.4-0.1-8.8-0.3-13.1-0.7-1.8-0.2-3.5-0.5-5.3-0.7-3.4-0.5-6.8-0.9-10.1-1.6-2-0.4-4-0.9-6.1-1.4-3.3-0.8-6.5-1.5-9.7-2.5-1.5-0.5-3-1-4.6-1.5-3.7-1.2-7.3-2.5-10.8-4-0.8-0.3-1.6-0.7-2.4-1.1-4.1-1.9-8.2-3.8-12.2-6l-0.6-0.3c-13.4-7.5-25.7-16.8-36.4-27.7-0.2-0.2-0.3-0.4-0.5-0.6-3.3-3.4-6.5-6.9-9.5-10.7-0.6-0.8-1.3-1.6-1.9-2.5-21.9-27.8-35.1-63-35.1-101.2h42.2l-67.5-102.6-67.5 102.6h42.2c0 45 13.9 86.7 37.3 121.1 0.3 0.5 0.5 1 0.8 1.5 2.4 3.5 5.1 6.8 7.7 10.1 1 1.3 1.9 2.6 2.9 3.8 3.9 4.7 7.9 9.2 12.1 13.5 0.4 0.4 0.8 0.9 1.2 1.3 14.1 14.3 30.1 26.4 47.4 36 0.5 0.3 0.9 0.6 1.4 0.8 5 2.7 10.1 5.2 15.3 7.5l3.9 1.8c4.4 1.9 9 3.5 13.6 5.1 2.2 0.7 4.4 1.5 6.6 2.2 4 1.2 8.1 2.2 12.3 3.2 2.8 0.7 5.5 1.4 8.3 1.9 1.2 0.3 2.3 0.6 3.4 0.8 3.9 0.7 7.8 1.1 11.8 1.6l4.2 0.6c7 0.7 14.1 1.2 21.1 1.2 42.9 0 84.7-13.3 120.6-38.8 11.4-8.2 14.2-24.1 6.2-35.8-8-11.5-23.8-14.3-35.3-6.2z m271.3-421.6H763c-26.5 0-82.9-119.7-112.5-119.7H370.6c-29.6 0-85.9 119.7-112.4 119.7H147.5c-46.3 0-83.9 40.2-83.9 89.7v478.8c0 49.6 37.6 89.8 83.9 89.8h726.9c46.3 0 83.9-40.1 83.9-89.8V331.4c0-49.5-37.6-89.7-83.9-89.7z m28 538.6c0 33.1-25 59.8-55.9 59.8h-671c-30.9 0-55.9-26.8-55.9-59.8V361.4c0-33 25-59.8 55.9-59.8h111.2c25.8 0 82-119.7 111.6-119.7h223.2c29.5 0 85.6 119.7 111.5 119.7h113.5c30.9 0 55.9 26.8 55.9 59.8v418.9z" p-id="7320" fill="#ffffff"></path></svg>
</div>
</div>
</div>
</div>
<!-- 连接状态指示器 - 页面顶部中部 -->