Merge branch 'py_test_end' into fix-end

This commit is contained in:
Sakura-RanChen
2026-04-01 09:35:23 +08:00
committed by GitHub
117 changed files with 4857 additions and 1509 deletions
+1
View File
@@ -83,6 +83,7 @@ VAD:
9、[知识库ragflow集成指南](./ragflow-integration.md)<br/>
10、[如何部署上下文源](./context-provider-integration.md)<br/>
11、[如何集成PowerMem智能记忆](./powermem-integration.md)<br/>
12、[如何配置天气插件查询天气](./weather-integration.md)<br/>
### 11、语音克隆、本地语音部署相关教程
1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)<br/>
@@ -151,6 +151,11 @@ public interface Constant {
*/
String MEMORY_NO_MEM = "Memory_nomem";
/**
* 仅上报聊天记录(不总结记忆)
*/
String MEMORY_MEM_REPORT_ONLY = "Memory_mem_report_only";
/**
* 火山引擎双声道语音克隆
*/
@@ -304,7 +309,7 @@ public interface Constant {
/**
* 版本号
*/
public static final String VERSION = "0.9.1";
public static final String VERSION = "0.9.2";
/**
* 无效固件URL
@@ -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工具列表
}
@@ -12,28 +12,29 @@ import xiaozhi.modules.sys.service.SysParamsService;
* 封装了重复的SM2解密、验证码提取和验证逻辑
*/
public class Sm2DecryptUtil {
/**
* 验证码长度
*/
private static final int CAPTCHA_LENGTH = 5;
/**
* 解密SM2加密内容,提取验证码并验证
*
* @param encryptedPassword SM2加密的密码字符串
* @param captchaId 验证码ID
* @param captchaService 验证码服务
* @param sysParamsService 系统参数服务
* @param captchaId 验证码ID
* @param captchaService 验证码服务
* @param sysParamsService 系统参数服务
* @return 解密后的实际密码
*/
public static String decryptAndValidateCaptcha(String encryptedPassword, String captchaId,
CaptchaService captchaService, SysParamsService sysParamsService) {
public static String decryptAndValidateCaptcha(String encryptedPassword, String captchaId,
CaptchaService captchaService, SysParamsService sysParamsService) {
// 获取SM2私钥
String privateKeyStr = sysParamsService.getValue(Constant.SM2_PRIVATE_KEY, true);
if (StringUtils.isBlank(privateKeyStr)) {
throw new RenException(ErrorCode.SM2_KEY_NOT_CONFIGURED);
}
// 使用SM2私钥解密密码
String decryptedContent;
try {
@@ -41,19 +42,20 @@ public class Sm2DecryptUtil {
} catch (Exception e) {
throw new RenException(ErrorCode.SM2_DECRYPT_ERROR);
}
// 分离验证码和密码:前5位是验证码,后面是密码
if (decryptedContent.length() > CAPTCHA_LENGTH) {
String embeddedCaptcha = decryptedContent.substring(0, CAPTCHA_LENGTH);
String actualPassword = decryptedContent.substring(CAPTCHA_LENGTH);
// 验证嵌入的验证码是否正确
boolean embeddedCaptchaValid = captchaService.validate(captchaId, embeddedCaptcha, true);
if (!embeddedCaptchaValid) {
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
}
return actualPassword;
} else if (decryptedContent.length() > 0) {
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
} else {
throw new RenException(ErrorCode.SM2_DECRYPT_ERROR);
}
@@ -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<String>().error("没有权限查看该智能体的MCP接入点地址");
return new Result<String>().error(ErrorCode.MCP_ACCESS_POINT_ADDRESS_NO_PERMISSION);
}
String agentMcpAccessAddress = agentMcpAccessPointService.getAgentMcpAccessAddress(agentId);
if (agentMcpAccessAddress == null) {
return new Result<String>().ok("请联系管理员进入参数管理配置mcp接入点地址");
return new Result<String>().error(ErrorCode.MCP_ACCESS_POINT_ADDRESS_NOT_CONFIGURED);
}
return new Result<String>().ok(agentMcpAccessAddress);
}
@@ -58,7 +59,7 @@ public class AgentMcpAccessPointController {
// 检查权限
if (!agentService.checkAgentPermission(agentId, user.getId())) {
return new Result<List<String>>().error("没有权限查看该智能体的MCP工具列表");
return new Result<List<String>>().error(ErrorCode.MCP_ACCESS_POINT_TOOLS_LIST_NO_PERMISSION);
}
List<String> agentMcpToolsList = agentMcpAccessPointService.getAgentMcpToolsList(agentId);
return new Result<List<String>>().ok(agentMcpToolsList);
@@ -14,6 +14,7 @@ import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.RequiredArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
import xiaozhi.modules.agent.dto.AgentChatSummaryDTO;
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
@@ -90,21 +91,28 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
@Override
public boolean generateAndSaveChatSummary(String sessionId) {
try {
// 1. 生成总结
AgentChatSummaryDTO summaryDTO = generateChatSummary(sessionId);
if (!summaryDTO.isSuccess()) {
log.info("生成总结失败: {}", summaryDTO.getErrorMessage());
return false;
}
// 2. 获取设备信息(通过会话关联的设备)
// 1. 获取设备信息(通过会话关联的设备)
DeviceEntity device = getDeviceBySessionId(sessionId);
if (device == null) {
log.info("未找到与会话 {} 关联的设备", sessionId);
return false;
}
// 3. 更新智能体记忆
// 2. 检查记忆模型类型,如果是仅上报聊天记录模式则跳过总结
String memModelId = agentService.getAgentById(device.getAgentId()).getMemModelId();
if (memModelId != null && memModelId.equals(Constant.MEMORY_MEM_REPORT_ONLY)) {
log.info("会话 {} 使用仅上报聊天记录模式,跳过记忆总结", sessionId);
return true;
}
// 3. 生成总结
AgentChatSummaryDTO summaryDTO = generateChatSummary(sessionId);
if (!summaryDTO.isSuccess()) {
log.info("生成总结失败: {}", summaryDTO.getErrorMessage());
return false;
}
// 4. 更新智能体记忆
AgentMemoryDTO memoryDTO = new AgentMemoryDTO();
memoryDTO.setSummaryMemory(summaryDTO.getSummary());
@@ -57,7 +57,8 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
.eq("model_type", modelType)
.eq("is_enabled", 1)
.like(StringUtils.isNotBlank(modelName), "model_name", modelName)
.select("id", "model_name"));
.select("id", "model_name")
.orderByAsc("sort"));
return ConvertUtils.sourceToTarget(entities, ModelBasicInfoDTO.class);
}
@@ -95,6 +95,7 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
QueryWrapper<ModelProviderEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("model_type", StringUtils.isBlank(modelType) ? "" : modelType);
queryWrapper.orderByAsc("sort");
List<ModelProviderEntity> providerEntities = modelProviderDao.selectList(queryWrapper);
return ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class);
}
@@ -147,7 +148,8 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
UserDetail user = SecurityUser.getUser();
modelProviderDTO.setUpdater(user.getId());
modelProviderDTO.setUpdateDate(new Date());
if (modelProviderDao.updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) {
if (modelProviderDao
.updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) {
throw new RenException(ErrorCode.UPDATE_DATA_FAILED);
}
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
@@ -17,4 +17,7 @@ public class VoiceCloneDTO {
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "语言")
private String languages;
}
@@ -28,6 +28,9 @@ public class VoiceCloneResponseDTO {
@Schema(description = "声音id")
private String voiceId;
@Schema(description = "语言")
private String languages;
@Schema(description = "用户ID(关联用户表)")
private Long userId;
@@ -31,6 +31,9 @@ public class VoiceCloneEntity {
@Schema(description = "声音id")
private String voiceId;
@Schema(description = "语言")
private String languages;
@Schema(description = "用户 ID(关联用户表)")
private Long userId;
@@ -116,6 +116,7 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl<VoiceCloneDao, VoiceC
entity.setVoiceId(voiceId);
entity.setName(namePrefix + "_" + index);
entity.setUserId(dto.getUserId());
entity.setLanguages(dto.getLanguages());
entity.setTrainStatus(0); // 默认训练中
batchInsertList.add(entity);
}
@@ -0,0 +1,4 @@
-- 给声音克隆表添加语言字段
SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ai_voice_clone' AND COLUMN_NAME = 'languages');
SET @sql = IF(@col_exists = 0, 'ALTER TABLE `ai_voice_clone` ADD COLUMN `languages` VARCHAR(50) DEFAULT NULL COMMENT ''语言'' AFTER `voice_id`', 'SELECT ''Column languages already exists'' AS msg');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
@@ -0,0 +1,7 @@
-- 新增仅上报聊天记录记忆模型供应器
delete from `ai_model_provider` where `id` = 'SYSTEM_Memory_mem_report_only';
delete from `ai_model_config` where `id` = 'Memory_mem_report_only';
INSERT INTO `ai_model_provider` VALUES ('SYSTEM_Memory_mem_report_only', 'Memory', 'mem_report_only', '仅上报聊天记录', '[]', 4, 1, NOW(), 1, NOW());
INSERT INTO `ai_model_config` VALUES ('Memory_mem_report_only', 'Memory', 'mem_report_only', '仅上报聊天记录', 0, 1, '{"type": "mem_report_only"}', NULL, '仅上报聊天记录,不总结记忆', 3, NULL, NULL, NULL, NULL);
@@ -0,0 +1,2 @@
-- 修改聊天内容字段类型
ALTER TABLE ai_agent_chat_history MODIFY COLUMN content TEXT COMMENT '聊天内容';
@@ -557,6 +557,13 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202603091051.sql
- changeSet:
id: 202603101723
author: RanChen
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202603101723.sql
- changeSet:
id: 202603111131
author: DaGou12138
@@ -564,4 +571,18 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202603111131.sql
- changeSet:
id: 202603231037
author: rainv123
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202603231037.sql
- changeSet:
id: 202603311200
author: cgd
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202603311200.sql
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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}
@@ -3,7 +3,7 @@
<mapper namespace="xiaozhi.modules.voiceclone.dao.VoiceCloneDao">
<select id="getTrainSuccess" resultType="xiaozhi.modules.model.dto.VoiceDTO">
select id, name, voice_id as voiceDemo
select id, name, voice_id as voiceDemo, languages
from ai_voice_clone
where model_id = #{modelId} and user_id = #{userId} and train_status = 2
</select>
+3 -5
View File
@@ -131,6 +131,7 @@ export function getMcpAddress(agentId: string) {
meta: {
ignoreAuth: false,
toast: false,
isExposeError: true,
},
})
}
@@ -208,11 +209,8 @@ export function updateAgentTags(agentId: string, data) {
}
// 获取所有语言
export function getAllLanguage(modelId, voiceName) {
const queryParams = new URLSearchParams({
voiceName: voiceName || '',
}).toString()
return http.Get(`/models/${modelId}/voices?${queryParams}`, {
export function getAllLanguage(modelId: string) {
return http.Get<{ id: string, name: string, languages: string }[]>(`/models/${modelId}/voices`, {
meta: {
ignoreAuth: false,
toast: false,
@@ -48,6 +48,15 @@ export interface AgentDetail {
ttsRate: number
ttsPitch: number
functions: AgentFunction[]
contextProviders: Providers[]
}
export interface Providers {
url: string
headers: Array<{
key: string
value: string
}>
}
export interface AgentFunction {
@@ -33,6 +33,22 @@ export function bindDevice(agentId: string, code: string) {
return http.Post(`/device/bind/${agentId}/${code}`, null)
}
/**
* 手动添加设备
* @param agentId 智能体ID
* @param board 设备类型
* @param appVersion 固件版本
* @param macAddress MAC地址
*/
export function bindDeviceManual(data: {
agentId: string
board: string
appVersion: string
macAddress: string
}) {
return http.Post('/device/manual-add', data)
}
/**
* 设置设备OTA升级开关
* @param deviceId 设备ID (MAC地址)
@@ -60,3 +60,13 @@ export function updateVoicePrint(data: VoicePrint) {
},
})
}
// 获取音频下载ID
export function getAudioDownloadId(audioId: string) {
return http.Post<string>(`/agent/audio/${audioId}`, {}, {
meta: {
ignoreAuth: false,
toast: false,
},
})
}
@@ -1,467 +0,0 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { t } from '@/i18n'
interface Props {
visible?: boolean
providers?: Array<{
url: string
headers: Record<string, string>
}>
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
providers: () => [],
})
const emit = defineEmits<{
'update:visible': [value: boolean]
'confirm': [value: Array<{
url: string
headers: Record<string, string>
}>]
}>()
const localProviders = ref<Array<{
url: string
headers: Array<{
key: string
value: string
}>
}>>([])
function initLocalData() {
localProviders.value = props.providers.map((p) => {
const headers = p.headers || {}
return {
url: p.url || '',
headers: Object.entries(headers).map(([key, value]) => ({ key, value })),
}
})
if (localProviders.value.length === 0) {
localProviders.value.push({ url: '', headers: [{ key: '', value: '' }] })
}
}
function addProvider(index: number) {
localProviders.value.splice(index, 0, {
url: '',
headers: [{ key: '', value: '' }],
})
}
function removeProvider(index: number) {
localProviders.value.splice(index, 1)
}
function addHeader(pIndex: number, hIndex: number) {
localProviders.value[pIndex].headers.splice(hIndex, 0, { key: '', value: '' })
}
function removeHeader(pIndex: number, hIndex: number) {
localProviders.value[pIndex].headers.splice(hIndex, 1)
}
function handleConfirm() {
const result = localProviders.value
.filter(p => p.url.trim() !== '')
.map((p) => {
const headersObj: Record<string, string> = {}
p.headers.forEach((h) => {
if (h.key.trim()) {
headersObj[h.key.trim()] = h.value
}
})
return {
url: p.url.trim(),
headers: headersObj,
}
})
emit('confirm', result)
emit('update:visible', false)
}
function handleClose() {
emit('update:visible', false)
}
watch(() => props.visible, (val) => {
if (val) {
initLocalData()
}
})
</script>
<template>
<view v-if="visible" class="context-provider-dialog-mask" @click="handleClose">
<view class="context-provider-dialog" @click.stop>
<view class="dialog-header">
<text class="dialog-title">
{{ t('contextProviderDialog.title') }}
</text>
</view>
<view class="dialog-content">
<view v-if="localProviders.length === 0" class="empty-container">
<text class="empty-text">
{{ t('contextProviderDialog.noContextApi') }}
</text>
<wd-button type="primary" size="small" @click="addProvider(0)">
{{ t('contextProviderDialog.add') }}
</wd-button>
</view>
<view v-else class="providers-list">
<view v-for="(provider, pIndex) in localProviders" :key="pIndex" class="provider-item">
<view class="provider-card">
<view class="card-header">
<view class="card-controls">
<wd-button
type="icon"
icon="add"
size="small"
class="!h-[60rpx] !w-[60rpx] !bg-[#66b1ff] !text-[#fff]"
@click="addProvider(pIndex + 1)"
/>
<wd-button
type="icon"
icon="decrease"
size="small"
class="!h-[60rpx] !w-[60rpx] !bg-[#F56C6C] !text-[#fff]"
@click="removeProvider(pIndex)"
/>
</view>
</view>
<view class="input-row">
<text class="label-text">
{{ t('contextProviderDialog.apiUrl') }}
</text>
<wd-input
v-model="provider.url"
class="flex-1"
:placeholder="t('contextProviderDialog.apiUrlPlaceholder')"
/>
</view>
<view class="headers-section">
<view class="label-text" style="margin-top: 6px;">
{{ t('contextProviderDialog.requestHeaders') }}
</view>
<view class="headers-list">
<view
v-for="(header, hIndex) in provider.headers"
:key="hIndex"
class="header-row-vertical"
>
<view class="header-input-group">
<wd-input
v-model="header.key"
placeholder="key"
class="header-input-full"
/>
</view>
<view class="header-input-group">
<wd-input
v-model="header.value"
placeholder="value"
class="header-input-full"
/>
</view>
<view class="header-controls">
<wd-button
type="icon"
icon="add"
size="small"
class="!h-[50rpx] !w-[50rpx] !border-[1rpx] !border-solid !bg-[#ecf5ff] !text-[#b3d8ff]"
@click="addHeader(pIndex, hIndex + 1)"
/>
<wd-button
type="icon"
icon="decrease"
size="small"
class="!h-[50rpx] !w-[50rpx] !border-[1rpx] !border-solid !bg-[#fef0f0] !text-[#F56C6C]"
@click="removeHeader(pIndex, hIndex)"
/>
</view>
</view>
<view v-if="provider.headers.length === 0" class="header-row empty-header">
<text class="no-header-text">
{{ t('contextProviderDialog.noHeaders') }}
</text>
<wd-button type="text" size="small" @click="addHeader(pIndex, 0)">
{{ t('contextProviderDialog.addHeader') }}
</wd-button>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="dialog-footer">
<wd-button class="cancel-btn" @click="handleClose">
{{ t('contextProviderDialog.cancel') }}
</wd-button>
<wd-button type="primary" class="confirm-btn" @click="handleConfirm">
{{ t('contextProviderDialog.confirm') }}
</wd-button>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
.context-provider-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.context-provider-dialog {
background: #fff;
// border-radius: 20rpx;
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
border-bottom: 1px solid #eee;
}
.dialog-title {
font-size: 32rpx;
font-weight: 600;
color: #232338;
}
.close-icon {
font-size: 40rpx;
color: #9d9ea3;
cursor: pointer;
}
.dialog-content {
flex: 1;
overflow-y: auto;
padding: 20rpx;
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
gap: 30rpx;
}
.empty-text {
font-size: 28rpx;
color: #9d9ea3;
}
.providers-list {
display: flex;
flex-direction: column;
}
.provider-item {
margin-bottom: 30rpx;
}
.provider-card {
flex: 1;
background: #fff;
border-radius: 16rpx;
border: 1px solid #eee;
border-left: 6rpx solid #336cff;
padding: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.card-title {
font-size: 30rpx;
font-weight: 600;
color: #232338;
}
.card-controls {
display: flex;
gap: 16rpx;
}
.input-row {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 40rpx;
}
.headers-section {
display: flex;
gap: 20rpx;
align-items: flex-start;
}
.headers-list {
flex: 1;
display: flex;
flex-direction: column;
gap: 20rpx;
background: #fcfcfc;
padding: 4rpx;
border-radius: 12rpx;
border: 1px dashed #dcdfe6;
}
.header-row-vertical {
display: flex;
flex-direction: column;
gap: 16rpx;
background: #fff;
// padding: 20rpx;
border-radius: 12rpx;
// border: 1px solid #e8e8e8;
}
.header-input-group {
display: flex;
align-items: center;
gap: 8rpx;
}
.header-key-label,
.header-value-label {
font-size: 24rpx;
font-weight: 500;
color: #606266;
}
.header-input-full {
width: 100%;
}
.header-controls {
display: flex;
gap: 12rpx;
// margin-top: 8rpx;
align-self: flex-start;
}
.block-controls {
display: flex;
flex-direction: column;
gap: 16rpx;
padding-top: 10rpx;
}
.input-row {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 20rpx;
}
.label-text {
font-size: 28rpx;
font-weight: 600;
color: #606266;
width: 140rpx;
}
.headers-section {
display: flex;
gap: 20rpx;
align-items: flex-start;
}
.headers-list {
flex: 1;
display: flex;
flex-direction: column;
gap: 20rpx;
background: #fcfcfc;
padding: 16rpx;
border-radius: 12rpx;
border: 1px dashed #dcdfe6;
}
.header-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.header-input {
width: 200rpx;
}
.separator {
color: #909399;
font-weight: bold;
font-size: 32rpx;
}
.row-controls {
display: flex;
gap: 12rpx;
margin-left: 8rpx;
flex-shrink: 0;
}
.empty-header {
justify-content: center;
padding: 20rpx;
color: #909399;
font-size: 26rpx;
}
.no-header-text {
margin-right: 16rpx;
}
.flex-1 {
flex: 1;
}
.dialog-footer {
display: flex;
gap: 20rpx;
padding: 30rpx;
border-top: 1px solid #eee;
}
.cancel-btn,
.confirm-btn {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
font-size: 28rpx;
}
</style>
@@ -1,270 +0,0 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { t } from '@/i18n'
interface Props {
visible?: boolean
settings?: {
volume: number
speed: number
pitch: number
}
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
settings: () => ({
volume: 0,
speed: 0,
pitch: 0,
}),
})
const emit = defineEmits<{
'update:visible': [value: boolean]
'confirm': [value: {
volume: number
speed: number
pitch: number
}]
}>()
const localSettings = ref({
volume: 0,
speed: 0,
pitch: 0,
})
function initLocalData() {
localSettings.value = {
volume: props.settings.volume || 0,
speed: props.settings.speed || 0,
pitch: props.settings.pitch || 0,
}
}
function handleConfirm() {
emit('confirm', localSettings.value)
emit('update:visible', false)
}
function handleClose() {
emit('update:visible', false)
}
watch(() => props.visible, (val) => {
if (val) {
initLocalData()
}
})
</script>
<template>
<view v-if="visible" class="voice-settings-dialog-mask" @click="handleClose">
<view class="voice-settings-dialog" @click.stop>
<view class="dialog-header">
<text class="dialog-title">
{{ t('agent.languageConfig') }}
</text>
</view>
<view class="dialog-content">
<!-- 音量调节 -->
<view class="setting-item">
<text class="setting-label">
{{ t('agent.ttsVolume') }}
</text>
<view class="slider-container">
<wd-slider
v-model="localSettings.volume"
:min="-100"
:max="100"
:step="1"
:show-value="false"
custom-class="voice-slider"
/>
<text class="slider-value">
{{ localSettings.volume }}
</text>
</view>
<text class="setting-desc">
{{ t('agent.volumeHint') }}
</text>
</view>
<!-- 语速调节 -->
<view class="setting-item">
<text class="setting-label">
{{ t('agent.ttsRate') }}
</text>
<view class="slider-container">
<wd-slider
v-model="localSettings.speed"
:min="-100"
:max="100"
:step="1"
:show-value="false"
custom-class="voice-slider"
/>
<text class="slider-value">
{{ localSettings.speed }}
</text>
</view>
<text class="setting-desc">
{{ t('agent.speedHint') }}
</text>
</view>
<!-- 音调调节 -->
<view class="setting-item">
<text class="setting-label">
{{ t('agent.ttsPitch') }}
</text>
<view class="slider-container">
<wd-slider
v-model="localSettings.pitch"
:min="-100"
:max="100"
:step="1"
:show-value="false"
custom-class="voice-slider"
/>
<text class="slider-value">
{{ localSettings.pitch }}
</text>
</view>
<text class="setting-desc">
{{ t('agent.pitchHint') }}
</text>
</view>
</view>
<view class="dialog-footer">
<wd-button class="cancel-btn" @click="handleClose">
{{ t('agent.cancel') }}
</wd-button>
<wd-button type="primary" class="confirm-btn" @click="handleConfirm">
{{ t('agent.save') }}
</wd-button>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
.voice-settings-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.voice-settings-dialog {
background: #fff;
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1px solid #eee;
}
.dialog-title {
font-size: 32rpx;
font-weight: 600;
color: #232338;
}
.close-icon {
font-size: 40rpx;
color: #9d9ea3;
cursor: pointer;
}
.dialog-content {
flex: 1;
overflow-y: auto;
padding: 40rpx;
display: flex;
flex-direction: column;
gap: 50rpx;
}
.setting-item {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.setting-label {
font-size: 30rpx;
font-weight: 600;
color: #232338;
}
.slider-container {
display: flex;
align-items: center;
gap: 20rpx;
}
.voice-slider {
flex: 1;
}
.slider-value {
font-size: 28rpx;
color: #336cff;
font-weight: 500;
min-width: 80rpx;
text-align: right;
}
.setting-desc {
font-size: 24rpx;
color: #9d9ea3;
margin-top: 10rpx;
}
.dialog-footer {
display: flex;
gap: 20rpx;
padding: 30rpx;
border-top: 1px solid #eee;
}
.cancel-btn,
.confirm-btn {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
font-size: 28rpx;
}
.confirm-btn {
background-color: #336cff !important;
}
/* 自定义滑块样式 */
:deep(.wd-slider) {
--wd-slider-bar-background: #e6ebff;
--wd-slider-bar-active-background: #336cff;
--wd-slider-thumb-border-color: #336cff;
--wd-slider-thumb-background: #336cff;
--wd-slider-thumb-size: 32rpx;
}
</style>
+30 -3
View File
@@ -1,5 +1,6 @@
import type { uniappRequestAdapter } from '@alova/adapter-uniapp'
import type { IResponse } from './types'
import type { Language } from '@/store/lang'
import AdapterUniapp from '@alova/adapter-uniapp'
import { createAlova } from 'alova'
import { createServerTokenAuthentication } from 'alova/client'
@@ -8,6 +9,16 @@ import { getEnvBaseUrl } from '@/utils'
import { toast } from '@/utils/toast'
import { ContentTypeEnum, ResultEnum, ShowMessage } from './enum'
// 语言映射, 用于设置 Accept-language 头
const langMap: Record<Language, string> = {
zh_CN: 'zh-CN',
en: 'en-US',
zh_TW: 'zh-TW',
de: 'de',
vi: 'vi',
pt_BR: 'pt-BR',
}
/**
* 创建请求实例
*/
@@ -42,10 +53,26 @@ const alovaInstance = createAlova({
statesHook: VueHook,
beforeRequest: onAuthRequired((method) => {
// h5动态获取最新的 baseURL,确保使用用户设置的服务器地址
const currentBaseUrl = getEnvBaseUrl()
if (currentBaseUrl !== method.baseURL) {
method.baseURL = currentBaseUrl
}
// 检查混合内容错误(HTTPS页面请求HTTP接口)
const currentProtocol = typeof window !== 'undefined' && window.location.protocol
const requestProtocol = method.baseURL?.split(':')[0]
const currentLang = langMap[uni.getStorageSync('app_language') as Language || 'zh_CN']
if (currentProtocol === 'https:' && requestProtocol === 'http') {
const errorMessage = '无法配置http协议地址,请检查接口地址'
throw new Error(errorMessage)
}
// 设置默认 Content-Type
method.config.headers = {
'Content-Type': ContentTypeEnum.JSON,
'Accept': 'application/json, text/plain, */*',
'Accept-language': currentLang,
...method.config.headers,
}
@@ -55,14 +82,14 @@ const alovaInstance = createAlova({
// 处理认证信息
if (!ignoreAuth) {
const token = uni.getStorageSync('token')
if (!token) {
const authInfo = JSON.parse(uni.getStorageSync('token') || '{}')
if (!authInfo.token) {
// 跳转到登录页
uni.reLaunch({ url: '/pages/login/index' })
throw new Error('[请求错误]:未登录')
}
// 添加 Authorization 头
method.config.headers.Authorization = `Bearer ${token}`
method.config.headers.Authorization = `Bearer ${authInfo.token}`
}
// 处理动态域名
@@ -12,6 +12,7 @@ export enum ResultEnum {
ServiceUnavailable = 503, // 服务不可用(原为serviceUnavailable
GatewayTimeout = 504, // 网关超时(原为gatewayTimeout
HttpVersionNotSupported = 505, // HTTP版本不支持(原为httpVersionNotSupported
MixedContent = 600, // 混合内容错误(HTTPS页面请求HTTP接口)
}
export enum ContentTypeEnum {
JSON = 'application/json;charset=UTF-8',
@@ -59,6 +60,9 @@ export function ShowMessage(status: number | string): string {
case 505:
message = 'HTTP版本不受支持(505)'
break
case 600:
message = '混合内容错误(600)'
break
default:
message = `连接出错(${status})!`
}
+32 -6
View File
@@ -11,7 +11,7 @@ export default {
'login.navigationTitle': 'Anmelden',
'login.fetchConfigError': 'Konfiguration konnte nicht abgerufen werden:',
'login.selectLanguage': 'Sprache auswählen',
'login.selectLanguageTip': 'Vi',
'login.selectLanguageTip': 'De',
'login.welcomeBack': 'Willkommen zurück',
'login.pleaseLogin': 'Bitte melden Sie sich an',
'login.enterUsername': 'Bitte Benutzernamen eingeben',
@@ -30,6 +30,8 @@ export default {
'login.requiredMobile': 'Bitte gültige Handynummer eingeben',
'login.captchaError': 'Grafischer Bestätigungscode Fehler',
'login.forgotPassword': 'Passwort vergessen',
'login.userAgreement': 'Nutzungsbedingungen',
'login.privacyPolicy': 'Datenschutzrichtlinie',
// Registrierungsseite
'register.pageTitle': 'Registrieren',
@@ -67,7 +69,7 @@ export default {
'home.createFirstAgent': 'Klicken Sie auf die + Schaltfläche unten rechts, um Ihren ersten Agenten zu erstellen',
'home.dialogTitle': 'Agent erstellen',
'home.inputPlaceholder': 'z.B. Kundenservice-Assistent, Sprachassistent, Wissens-F&A',
'home.createError': 'Bitte Agenten-Namen eingeben',
'home.createError': 'Der Name muss zwischen 1 und 64 Zeichen lang sein.',
'home.createNow': 'Jetzt erstellen',
'home.justNow': 'Gerade eben',
'home.minutesAgo': 'Minuten her',
@@ -83,7 +85,7 @@ export default {
'agent.chatHistory': 'Chat-Verlauf',
'agent.voiceprintManagement': 'Stimmabdruckverwaltung',
'agent.editTitle': 'Agent bearbeiten',
'agent.toolsTitle': 'Funktionen bearbeiten',
'agent.toolsTitle': 'Bearbeiten',
'agent.voiceActivityDetection': 'Sprachaktivitätserkennung',
'agent.speechRecognition': 'Spracherkennung',
'agent.largeLanguageModel': 'Großes Sprachmodell',
@@ -114,7 +116,7 @@ export default {
'agent.tts': 'Text-zu-Sprache',
'agent.voiceprint': 'Agenten-Stimme',
'agent.plugins': 'Plugins',
'agent.editFunctions': 'Funktionen bearbeiten',
'agent.editFunctions': 'Bearbeiten',
'agent.historyMemory': 'Verlaufsspeicher',
'agent.memoryContent': 'Speicherinhalt',
'agent.saving': 'Wird gespeichert...',
@@ -150,6 +152,25 @@ export default {
'contextProviderDialog.cancel': 'Abbrechen',
'contextProviderDialog.confirm': 'Bestätigen',
// Manual add device dialog related
'manualAddDeviceDialog.title': 'Manuell Gerät hinzufügen',
'manualAddDeviceDialog.deviceType': 'Gerätetyp',
'manualAddDeviceDialog.deviceTypePlaceholder': 'Bitte Gerätetyp auswählen',
'manualAddDeviceDialog.firmwareVersion': 'Firmware-Version',
'manualAddDeviceDialog.firmwareVersionPlaceholder': 'Bitte Firmware-Version eingeben',
'manualAddDeviceDialog.macAddress': 'Mac-Adresse',
'manualAddDeviceDialog.macAddressPlaceholder': 'Bitte Mac-Adresse eingeben',
'manualAddDeviceDialog.confirm': 'Bestätigen',
'manualAddDeviceDialog.cancel': 'Abbrechen',
'manualAddDeviceDialog.requiredMacAddress': 'Bitte Mac-Adresse eingeben',
'manualAddDeviceDialog.invalidMacAddress': 'Bitte korrektes Mac-Adressformat eingeben, z.B.: 00:1A:2B:3C:4D:5E',
'manualAddDeviceDialog.requiredDeviceType': 'Bitte Gerätetyp auswählen',
'manualAddDeviceDialog.requiredFirmwareVersion': 'Bitte Firmware-Version eingeben',
'manualAddDeviceDialog.getFirmwareTypeFailed': 'Firmware-Typ konnte nicht abgerufen werden',
'manualAddDeviceDialog.addSuccess': 'Gerät erfolgreich hinzugefügt',
'manualAddDeviceDialog.addFailed': 'Hinzufügen fehlgeschlagen',
'manualAddDeviceDialog.bindWithCode': 'Mit 6-stelligem Code binden',
// Chat-Verlauf Seite
'chatHistory.getChatSessions': 'Chat-Sitzungsliste abrufen',
'chatHistory.noSelectedAgent': 'Kein Agent ausgewählt',
@@ -335,7 +356,7 @@ export default {
'message.unbindFail': 'Entbinden fehlgeschlagen',
'message.networkError': 'Netzwerkfehler, bitte Verbindung prüfen',
'message.serverError': 'Serverfehler, bitte später erneut versuchen',
'message.invalidAddress': 'Ungültige Adresse, bitte prüfen ob Server gestartet oder Netzwerkverbindung normal ist',
'message.invalidAddress': 'Die Adresse kann ungültig sein. Bitte überprüfen Sie, ob der Server gestartet ist oder ob die Netzwerkverbindung funktioniert. Es kann auch sein, dass die Anfrage aufgrund eines Problems mit dem HTTPS-Protokoll nicht gesendet werden kann.',
'message.languageChanged': 'Sprache geändert',
'message.passwordError': 'Konto oder Passwort Fehler',
'message.phoneRegistered': 'Diese Handynummer wurde bereits registriert',
@@ -350,7 +371,7 @@ export default {
'agent.tools.mcpAccessPoint': 'MCP-Zugangspunkt',
'agent.tools.copy': 'Kopieren',
'agent.tools.noTools': 'Keine Werkzeuge verfügbar',
'agent.tools.parameterConfig': 'Parameterkonfiguration',
'agent.tools.parameterConfig': 'Konfig',
'agent.tools.noParamsNeeded': 'Keine Parameter benötigt',
'agent.tools.pleaseInput': 'Bitte eingeben',
'agent.tools.inputOneItemPerLine': 'Ein Element pro Zeile eingeben',
@@ -471,4 +492,9 @@ export default {
'deviceConfig.afterConfigSuccessDeviceWillRestart': 'Nach erfolgreicher Konfiguration startet Gerät automatisch neu',
'deviceConfig.audioPlaybackError': 'Audio-Wiedergabe-Fehler',
'deviceConfig.playbackFailed': 'Wiedergabe fehlgeschlagen',
// Voiceprint page
'voiceprint.audioNotExist': 'Audio existiert nicht',
'voiceprint.getAudioFailed': 'Audio konnte nicht abgerufen werden',
'voiceprint.audioPlayFailed': 'Audio-Wiedergabe fehlgeschlagen',
}
+30 -4
View File
@@ -11,7 +11,7 @@ export default {
'login.navigationTitle': 'Login',
'login.fetchConfigError': 'Failed to fetch configuration:',
'login.selectLanguage': 'Select Language',
'login.selectLanguageTip': '中文',
'login.selectLanguageTip': 'En',
'login.welcomeBack': 'Welcome Back',
'login.pleaseLogin': 'Please log in to your account',
'login.enterUsername': 'Please enter username',
@@ -30,6 +30,8 @@ export default {
'login.requiredMobile': 'Please enter a valid phone number',
'login.captchaError': 'Graphic verification code error',
'login.forgotPassword': 'Forgot Password',
'login.userAgreement': 'User Agreement',
'login.privacyPolicy': 'Privacy Policy',
// Register page
'register.pageTitle': 'Register',
@@ -67,7 +69,7 @@ export default {
'home.createFirstAgent': 'Click the + button in the lower right corner to create your first agent',
'home.dialogTitle': 'Create Agent',
'home.inputPlaceholder': 'e.g. Customer Service Assistant, Voice Assistant, Knowledge Q&A',
'home.createError': 'Please input agent name',
'home.createError': 'The name length must be between 1 and 64 characters',
'home.createNow': 'Create Now',
'home.justNow': 'Just now',
'home.minutesAgo': 'minutes ago',
@@ -150,6 +152,25 @@ export default {
'contextProviderDialog.cancel': 'Cancel',
'contextProviderDialog.confirm': 'Confirm',
// Manual add device dialog related
'manualAddDeviceDialog.title': 'Manual Add Device',
'manualAddDeviceDialog.deviceType': 'Device Type',
'manualAddDeviceDialog.deviceTypePlaceholder': 'Please select device type',
'manualAddDeviceDialog.firmwareVersion': 'Firmware Version',
'manualAddDeviceDialog.firmwareVersionPlaceholder': 'Please enter firmware version',
'manualAddDeviceDialog.macAddress': 'Mac Address',
'manualAddDeviceDialog.macAddressPlaceholder': 'Please enter Mac address',
'manualAddDeviceDialog.confirm': 'Confirm',
'manualAddDeviceDialog.cancel': 'Cancel',
'manualAddDeviceDialog.requiredMacAddress': 'Please enter Mac address',
'manualAddDeviceDialog.invalidMacAddress': 'Please enter correct Mac address format, e.g.: 00:1A:2B:3C:4D:5E',
'manualAddDeviceDialog.requiredDeviceType': 'Please select device type',
'manualAddDeviceDialog.requiredFirmwareVersion': 'Please enter firmware version',
'manualAddDeviceDialog.getFirmwareTypeFailed': 'Failed to get firmware type',
'manualAddDeviceDialog.addSuccess': 'Device added successfully',
'manualAddDeviceDialog.addFailed': 'Failed to add',
'manualAddDeviceDialog.bindWithCode': 'Bind with 6-digit code',
// Chat History Page
'chatHistory.getChatSessions': 'Get chat session list',
'chatHistory.noSelectedAgent': 'No agent selected',
@@ -335,7 +356,7 @@ export default {
'message.unbindFail': 'Unbinding failed',
'message.networkError': 'Network error, please check your connection',
'message.serverError': 'Server error, please try again later',
'message.invalidAddress': 'Invalid address, please check if the server is started or network connection is normal',
'message.invalidAddress': 'The address may be invalid. Please check if the server is started or if the network connection is normal; It is also possible that requests cannot be sent due to HTTPS protocol issues',
'message.languageChanged': 'Language changed',
'message.passwordError': 'Account or password error',
'message.phoneRegistered': 'This phone number has been registered',
@@ -350,7 +371,7 @@ export default {
'agent.tools.mcpAccessPoint': 'MCP Access Point',
'agent.tools.copy': 'Copy',
'agent.tools.noTools': 'No tools available',
'agent.tools.parameterConfig': 'Parameter Configuration',
'agent.tools.parameterConfig': 'Param Config',
'agent.tools.noParamsNeeded': 'No parameters needed',
'agent.tools.pleaseInput': 'Please input',
'agent.tools.inputOneItemPerLine': 'Input one item per line',
@@ -471,4 +492,9 @@ export default {
'deviceConfig.afterConfigSuccessDeviceWillRestart': 'After successful configuration, device will automatically restart',
'deviceConfig.audioPlaybackError': 'Audio playback error',
'deviceConfig.playbackFailed': 'Playback failed',
// Voiceprint page
'voiceprint.audioNotExist': 'Audio does not exist',
'voiceprint.getAudioFailed': 'Failed to get audio',
'voiceprint.audioPlayFailed': 'Audio playback failed',
}
+31 -5
View File
@@ -11,7 +11,7 @@ export default {
'login.navigationTitle': 'Entrar',
'login.fetchConfigError': 'Falha ao buscar configuração:',
'login.selectLanguage': 'Selecionar Idioma',
'login.selectLanguageTip': '中文',
'login.selectLanguageTip': 'Pt',
'login.welcomeBack': 'Bem-vindo de Volta',
'login.pleaseLogin': 'Por favor, entre na sua conta',
'login.enterUsername': 'Por favor, insira o nome de usuário',
@@ -29,7 +29,9 @@ export default {
'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',
'login.forgotPassword': 'Esqueceu a Senha',
'login.userAgreement': 'Termos de Uso',
'login.privacyPolicy': 'Política de Privacidade',
// Register page
'register.pageTitle': 'Cadastro',
@@ -67,7 +69,7 @@ export default {
'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.createError': 'O comprimento do nome deve estar entre 1 e 64 caracteres',
'home.createNow': 'Criar Agora',
'home.justNow': 'Agora mesmo',
'home.minutesAgo': 'minutos atrás',
@@ -150,6 +152,25 @@ export default {
'contextProviderDialog.cancel': 'Cancelar',
'contextProviderDialog.confirm': 'Confirmar',
// Diálogo de adição manual de dispositivo
'manualAddDeviceDialog.title': 'Adicionar Dispositivo Manualmente',
'manualAddDeviceDialog.deviceType': 'Tipo de Dispositivo',
'manualAddDeviceDialog.deviceTypePlaceholder': 'Por favor, selecione o tipo de dispositivo',
'manualAddDeviceDialog.firmwareVersion': 'Versão do Firmware',
'manualAddDeviceDialog.firmwareVersionPlaceholder': 'Por favor, insira a versão do firmware',
'manualAddDeviceDialog.macAddress': 'Endereço MAC',
'manualAddDeviceDialog.macAddressPlaceholder': 'Por favor, insira o endereço MAC',
'manualAddDeviceDialog.confirm': 'Confirmar',
'manualAddDeviceDialog.cancel': 'Cancelar',
'manualAddDeviceDialog.requiredMacAddress': 'Por favor, insira o endereço MAC',
'manualAddDeviceDialog.invalidMacAddress': 'Por favor, insira o formato correto de endereço MAC, ex.: 00:1A:2B:3C:4D:5E',
'manualAddDeviceDialog.requiredDeviceType': 'Por favor, selecione o tipo de dispositivo',
'manualAddDeviceDialog.requiredFirmwareVersion': 'Por favor, insira a versão do firmware',
'manualAddDeviceDialog.getFirmwareTypeFailed': 'Falha ao obter tipo de firmware',
'manualAddDeviceDialog.addSuccess': 'Dispositivo adicionado com sucesso',
'manualAddDeviceDialog.addFailed': 'Falha ao adicionar',
'manualAddDeviceDialog.bindWithCode': 'Vincular com Código de 6 Dígitos',
// Chat History Page
'chatHistory.getChatSessions': 'Obter lista de sessões de conversa',
'chatHistory.noSelectedAgent': 'Nenhum agente selecionado',
@@ -335,7 +356,7 @@ export default {
'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.invalidAddress': 'O endereço pode ser inválido, verifique se o servidor está iniciado ou se a conexão de rede está correta; Também pode ser impossível enviar solicitações devido a problemas com o protocolo HTTPS',
'message.languageChanged': 'Idioma alterado',
'message.passwordError': 'Conta ou senha incorretos',
'message.phoneRegistered': 'Este número de telefone já está cadastrado',
@@ -350,7 +371,7 @@ export default {
'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.parameterConfig': 'Configuração',
'agent.tools.noParamsNeeded': 'Nenhum parâmetro necessário',
'agent.tools.pleaseInput': 'Por favor, insira',
'agent.tools.inputOneItemPerLine': 'Insira um item por linha',
@@ -471,4 +492,9 @@ export default {
'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',
// Voiceprint page
'voiceprint.audioNotExist': 'O áudio não existe',
'voiceprint.getAudioFailed': 'Falha ao obter áudio',
'voiceprint.audioPlayFailed': 'Falha na reprodução de áudio',
}
+30 -4
View File
@@ -11,7 +11,7 @@ export default {
'login.navigationTitle': 'Đăng nhập',
'login.fetchConfigError': 'Không thể tải cấu hình:',
'login.selectLanguage': 'Chọn ngôn ngữ',
'login.selectLanguageTip': 'de',
'login.selectLanguageTip': 'Vi',
'login.welcomeBack': 'Chào mừng trở lại',
'login.pleaseLogin': 'Vui lòng đăng nhập vào tài khoản của bạn',
'login.enterUsername': 'Vui lòng nhập tên đăng nhập',
@@ -30,6 +30,8 @@ export default {
'login.requiredMobile': 'Vui lòng nhập số điện thoại hợp lệ',
'login.captchaError': 'Lỗi mã xác minh đồ họa',
'login.forgotPassword': 'Quên mật khẩu',
'login.userAgreement': 'Thỏa thuận người dùng',
'login.privacyPolicy': 'Chính sách bảo mật',
// Trang đăng ký
'register.pageTitle': 'Đăng ký',
@@ -67,7 +69,7 @@ export default {
'home.createFirstAgent': 'Nhấp vào nút + ở góc dưới bên phải để tạo đại lý đầu tiên của bạn',
'home.dialogTitle': 'Tạo đại lý',
'home.inputPlaceholder': 'ví dụ: Trợ lý chăm sóc khách hàng, Trợ lý giọng nói, Hỏi đáp kiến thức',
'home.createError': 'Vui lòng nhập tên đại lý',
'home.createError': 'Độ dài tên phải từ 1 đến 64 ký tự',
'home.createNow': 'Tạo ngay',
'home.justNow': 'Vừa xong',
'home.minutesAgo': 'phút trước',
@@ -150,6 +152,25 @@ export default {
'contextProviderDialog.cancel': 'Hủy bỏ',
'contextProviderDialog.confirm': 'Xác nhận',
// Manual add device dialog related
'manualAddDeviceDialog.title': 'Thêm thiết bị thủ công',
'manualAddDeviceDialog.deviceType': 'Loại thiết bị',
'manualAddDeviceDialog.deviceTypePlaceholder': 'Vui lòng chọn loại thiết bị',
'manualAddDeviceDialog.firmwareVersion': 'Phiên bản firmware',
'manualAddDeviceDialog.firmwareVersionPlaceholder': 'Vui lòng nhập phiên bản firmware',
'manualAddDeviceDialog.macAddress': 'Địa chỉ Mac',
'manualAddDeviceDialog.macAddressPlaceholder': 'Vui lòng nhập địa chỉ Mac',
'manualAddDeviceDialog.confirm': 'Xác nhận',
'manualAddDeviceDialog.cancel': 'Hủy bỏ',
'manualAddDeviceDialog.requiredMacAddress': 'Vui lòng nhập địa chỉ Mac',
'manualAddDeviceDialog.invalidMacAddress': 'Vui lòng nhập đúng định dạng địa chỉ Mac, ví dụ: 00:1A:2B:3C:4D:5E',
'manualAddDeviceDialog.requiredDeviceType': 'Vui lòng chọn loại thiết bị',
'manualAddDeviceDialog.requiredFirmwareVersion': 'Vui lòng nhập phiên bản firmware',
'manualAddDeviceDialog.getFirmwareTypeFailed': 'Không thể lấy loại firmware',
'manualAddDeviceDialog.addSuccess': 'Đã thêm thiết bị thành công',
'manualAddDeviceDialog.addFailed': 'Thêm thất bại',
'manualAddDeviceDialog.bindWithCode': 'Liên kết bằng mã xác minh 6 chữ số',
// Trang lịch sử trò chuyện
'chatHistory.getChatSessions': 'Lấy danh sách phiên trò chuyện',
'chatHistory.noSelectedAgent': 'Chưa chọn đại lý',
@@ -335,7 +356,7 @@ export default {
'message.unbindFail': 'Hủy liên kết thất bại',
'message.networkError': 'Lỗi mạng, vui lòng kiểm tra kết nối',
'message.serverError': 'Lỗi máy chủ, vui lòng thử lại sau',
'message.invalidAddress': 'Địa chỉ không hợp lệ, vui lòng kiểm tra máy chủ đã khởi động hoặc kết nối mạng bình thường',
'message.invalidAddress': 'Địa chỉ có thể không hợp lệ. Vui lòng kiểm tra xem máy chủ đã khởi động hay kết nối mạng chưa; Có thể không gửi được yêu cầu do lỗi giao thức HTTPS',
'message.languageChanged': 'Đã thay đổi ngôn ngữ',
'message.passwordError': 'Lỗi tài khoản hoặc mật khẩu',
'message.phoneRegistered': 'Số điện thoại này đã được đăng ký',
@@ -350,7 +371,7 @@ export default {
'agent.tools.mcpAccessPoint': 'Điểm truy cập MCP',
'agent.tools.copy': 'Sao chép',
'agent.tools.noTools': 'Không có công cụ nào',
'agent.tools.parameterConfig': 'Cấu hình tham số',
'agent.tools.parameterConfig': 'Cấu hình',
'agent.tools.noParamsNeeded': 'Không cần tham số',
'agent.tools.pleaseInput': 'Vui lòng nhập',
'agent.tools.inputOneItemPerLine': 'Nhập một mục mỗi dòng',
@@ -471,4 +492,9 @@ export default {
'deviceConfig.afterConfigSuccessDeviceWillRestart': 'Sau khi cấu hình thành công, thiết bị sẽ tự động khởi động lại',
'deviceConfig.audioPlaybackError': 'Lỗi phát âm thanh',
'deviceConfig.playbackFailed': 'Phát thất bại',
// Voiceprint page
'voiceprint.audioNotExist': 'Âm thanh không tồn tại',
'voiceprint.getAudioFailed': 'Không thể lấy âm thanh',
'voiceprint.audioPlayFailed': 'Phát âm thanh thất bại',
}
+29 -3
View File
@@ -11,7 +11,7 @@ export default {
'login.navigationTitle': '登录',
'login.fetchConfigError': '获取配置失败:',
'login.selectLanguage': '选择语言',
'login.selectLanguageTip': 'En',
'login.selectLanguageTip': '中文',
'login.welcomeBack': '欢迎回来',
'login.pleaseLogin': '请登录您的账户',
'login.enterUsername': '请输入用户名',
@@ -30,6 +30,8 @@ export default {
'login.requiredMobile': '请输入正确的手机号码',
'login.captchaError': '图形验证码错误',
'login.forgotPassword': '忘记密码',
'login.userAgreement': '用户协议',
'login.privacyPolicy': '隐私政策',
// 注册页面
'register.pageTitle': '注册',
@@ -67,7 +69,7 @@ export default {
'home.createFirstAgent': '点击右下角 + 号创建您的第一个智能体',
'home.dialogTitle': '创建智能体',
'home.inputPlaceholder': '例如:客服助手、语音助理、知识问答',
'home.createError': '请输入智能体名称',
'home.createError': '名称长度必须在 1 到 64 个字符之间',
'home.createNow': '立即创建',
'home.justNow': '刚刚',
'home.minutesAgo': '分钟前',
@@ -150,6 +152,25 @@ export default {
'contextProviderDialog.cancel': '取消',
'contextProviderDialog.confirm': '确定',
// 手動添加設備對話框相關
'manualAddDeviceDialog.title': '手动添加设备',
'manualAddDeviceDialog.deviceType': '设备型号',
'manualAddDeviceDialog.deviceTypePlaceholder': '请选择设备型号',
'manualAddDeviceDialog.firmwareVersion': '固件版本',
'manualAddDeviceDialog.firmwareVersionPlaceholder': '请输入固件版本',
'manualAddDeviceDialog.macAddress': 'Mac地址',
'manualAddDeviceDialog.macAddressPlaceholder': '请输入Mac地址',
'manualAddDeviceDialog.confirm': '确定',
'manualAddDeviceDialog.cancel': '取消',
'manualAddDeviceDialog.requiredMacAddress': '请输入Mac地址',
'manualAddDeviceDialog.invalidMacAddress': '请输入正确的Mac地址格式,例如:00:1A:2B:3C:4D:5E',
'manualAddDeviceDialog.requiredDeviceType': '请选择设备型号',
'manualAddDeviceDialog.requiredFirmwareVersion': '请输入固件版本',
'manualAddDeviceDialog.getFirmwareTypeFailed': '获取固件类型失败',
'manualAddDeviceDialog.addSuccess': '设备添加成功',
'manualAddDeviceDialog.addFailed': '添加失败',
'manualAddDeviceDialog.bindWithCode': '6位验证码绑定',
// 聊天历史页面
'chatHistory.getChatSessions': '获取聊天会话列表',
'chatHistory.noSelectedAgent': '没有选中的智能体',
@@ -335,7 +356,7 @@ export default {
'message.unbindFail': '解绑失败',
'message.networkError': '网络错误,请检查网络连接',
'message.serverError': '服务器错误,请稍后再试',
'message.invalidAddress': '无效地址,请检查服务端是否启动或网络连接是否正常',
'message.invalidAddress': '地址可能无效,请检查服务端是否启动或网络连接是否正常;也可能因 HTTPS 协议问题导致无法发送请求',
'message.languageChanged': '语言已切换',
'message.passwordError': '账号或密码错误',
'message.phoneRegistered': '此手机号码已经注册过',
@@ -471,4 +492,9 @@ export default {
'deviceConfig.afterConfigSuccessDeviceWillRestart': '配网成功后设备将自动重启',
'deviceConfig.audioPlaybackError': '音频播放错误',
'deviceConfig.playbackFailed': '播放失败',
// Voiceprint page
'voiceprint.audioNotExist': '该音频不存在',
'voiceprint.getAudioFailed': '获取音频失败',
'voiceprint.audioPlayFailed': '音频播放失败',
}
+30 -4
View File
@@ -11,7 +11,7 @@ export default {
'login.navigationTitle': '登錄',
'login.fetchConfigError': '獲取配置失敗:',
'login.selectLanguage': '選擇語言',
'login.selectLanguageTip': '简体',
'login.selectLanguageTip': '繁體',
'login.welcomeBack': '歡迎回來',
'login.pleaseLogin': '請登錄您的賬戶',
'login.enterUsername': '請輸入用戶名',
@@ -29,7 +29,9 @@ export default {
'login.requiredCaptcha': '驗證碼不能為空',
'login.requiredMobile': '請輸入正確的手機號碼',
'login.captchaError': '圖形驗證碼錯誤',
'login.forgotPassword': '忘記密碼',
'login.forgotPassword': '忘記密碼',
'login.userAgreement': '用戶協議',
'login.privacyPolicy': '隱私政策',
// 忘記密碼頁面
'retrievePassword.title': '重置密碼',
@@ -88,7 +90,7 @@ export default {
'home.createFirstAgent': '點擊右下角 + 號創建您的第一個智能體',
'home.dialogTitle': '創建智能體',
'home.inputPlaceholder': '例如:客服助手、語音助理、知識問答',
'home.createError': '請輸入智能體暱稱',
'home.createError': '暱稱長度必須在 1 到 64 個字元之間。',
'home.createNow': '立即創建',
'home.justNow': '剛剛',
'home.minutesAgo': '分鐘前',
@@ -171,6 +173,25 @@ export default {
'contextProviderDialog.cancel': '取消',
'contextProviderDialog.confirm': '確定',
// 手動添加設備對話框相關
'manualAddDeviceDialog.title': '手動添加設備',
'manualAddDeviceDialog.deviceType': '設備型號',
'manualAddDeviceDialog.deviceTypePlaceholder': '請選擇設備型號',
'manualAddDeviceDialog.firmwareVersion': '固件版本',
'manualAddDeviceDialog.firmwareVersionPlaceholder': '請輸入固件版本',
'manualAddDeviceDialog.macAddress': 'Mac地址',
'manualAddDeviceDialog.macAddressPlaceholder': '請輸入Mac地址',
'manualAddDeviceDialog.confirm': '確定',
'manualAddDeviceDialog.cancel': '取消',
'manualAddDeviceDialog.requiredMacAddress': '請輸入Mac地址',
'manualAddDeviceDialog.invalidMacAddress': '請輸入正確的Mac地址格式,例如:00:1A:2B:3C:4D:5E',
'manualAddDeviceDialog.requiredDeviceType': '請選擇設備型號',
'manualAddDeviceDialog.requiredFirmwareVersion': '請輸入固件版本',
'manualAddDeviceDialog.getFirmwareTypeFailed': '獲取固件類型失敗',
'manualAddDeviceDialog.addSuccess': '設備添加成功',
'manualAddDeviceDialog.addFailed': '添加失敗',
'manualAddDeviceDialog.bindWithCode': '6位驗證碼綁定',
// 聊天歷史頁面
'chatHistory.getChatSessions': '獲取聊天會話列表',
'chatHistory.noSelectedAgent': '沒有選中的智能體',
@@ -335,7 +356,7 @@ export default {
'message.unbindFail': '解除綁定失敗',
'message.networkError': '網絡錯誤,請檢查網絡連接',
'message.serverError': '服務器錯誤,請稍後再試',
'message.invalidAddress': '無效地址,請檢查服務端是否啟動或網絡連接是否正常',
'message.invalidAddress': '地址可能無效,請檢查服務端是否啟動或網絡連接是否正常; 也可能因HTTPS協定問題導致無法發送請求常',
'message.languageChanged': '語言已切換',
'message.passwordError': '帳號或密碼錯誤',
'message.phoneRegistered': '此手機號已經註冊過',
@@ -471,4 +492,9 @@ export default {
'deviceConfig.afterConfigSuccessDeviceWillRestart': '配網成功後設備將自動重啟',
'deviceConfig.audioPlaybackError': '音頻播放錯誤',
'deviceConfig.playbackFailed': '播放失敗',
// Voiceprint page
'voiceprint.audioNotExist': '該音頻不存在',
'voiceprint.getAudioFailed': '獲取音頻失敗',
'voiceprint.audioPlayFailed': '音頻播放失敗',
}
+150 -88
View File
@@ -2,10 +2,8 @@
import type { AgentDetail, ModelOption, PluginDefinition, RoleTemplate } from '@/api/agent/types'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { getAgentDetail, getAgentTags, getAllLanguage, getModelOptions, getPluginFunctions, getRoleTemplates, updateAgent, updateAgentTags } from '@/api/agent/agent'
import ContextProviderDialog from '@/components/ContextProviderDialog.vue'
import VoiceSettingsDialog from '@/components/VoiceSettingsDialog.vue'
import { t } from '@/i18n'
import { usePluginStore } from '@/store'
import { usePluginStore, useProvider, useSpeedPitch } from '@/store'
import { toast } from '@/utils/toast'
defineOptions({
@@ -105,29 +103,21 @@ const pickerShow = ref<{
report: false,
})
const ttsSettings = ref({
volume: 0,
speed: 0,
pitch: 0,
})
const allFunctions = ref<PluginDefinition[]>([])
const dynamicTags = ref([])
const inputValue = ref('')
const inputVisible = ref(false)
const showContextProviderDialog = ref(false)
const currentContextProviders = ref([])
const showVoiceSettingsDialog = ref(false)
const voiceSettings = ref({
volume: 0,
speed: 0,
pitch: 0,
})
const languageOptions = ref([])
const isVisibleReport = ref(false)
// 音频播放相关
const audioRef = ref<UniApp.InnerAudioContext | null>(null)
const playingVoiceId = ref<string>('')
// 使用插件store
const pluginStore = usePluginStore()
const speedPitchStore = useSpeedPitch()
const providerStore = useProvider()
// tabs
const tabList = [
@@ -172,29 +162,20 @@ function handleInputConfirm() {
inputVisible.value = false
}
// 是否禁用历史记忆输入框
const isMemoryDisabled = computed(() => formData.value.memModelId !== 'Memory_mem_local_short')
// 打开上下文源编辑弹窗
function openContextProviderDialog() {
showContextProviderDialog.value = true
}
// 处理上下文源更新
function handleUpdateContext(providers: any[]) {
currentContextProviders.value = providers
}
function openVoiceSettingsDialog() {
showVoiceSettingsDialog.value = true
}
function handleUpdateVoiceSettings(settings: any) {
ttsSettings.value = settings
formData.value.ttsVolume = settings.volume
formData.value.ttsRate = settings.speed
formData.value.ttsPitch = settings.pitch
uni.navigateTo({
url: '/pages/agent/provider',
})
}
function handleRegulate() {
openVoiceSettingsDialog()
uni.navigateTo({
url: '/pages/agent/speedPitch',
})
}
// 加载智能体详情
@@ -211,14 +192,15 @@ async function loadAgentDetail() {
pluginStore.setCurrentAgentId(agentId.value)
pluginStore.setCurrentFunctions(detail.functions || [])
// 更新语速音调
speedPitchStore.updateSpeedPitch({
ttsVolume: detail.ttsVolume || 0,
ttsRate: detail.ttsRate || 0,
ttsPitch: detail.ttsPitch || 0,
})
// 加载上下文配置
currentContextProviders.value = (detail as any).contextProviders || []
// 加载语音设置
voiceSettings.value = {
volume: (detail as any).volume || 0,
speed: (detail as any).speed || 0,
pitch: (detail as any).pitch || 0,
}
providerStore.updateProviders(detail.contextProviders || [])
// 如果有TTS模型,加载对应的音色选项
if (detail.ttsModelId) {
@@ -373,17 +355,17 @@ function filterVoicesByLanguage() {
}
// 同步到ttsSettings(如果值为null,使用0作为显示默认值,但不修改form中的值)
ttsSettings.value = {
volume: formData.value.ttsVolume !== null && formData.value.ttsVolume !== undefined ? formData.value.ttsVolume : 0,
speed: formData.value.ttsRate !== null && formData.value.ttsRate !== undefined ? formData.value.ttsRate : 0,
pitch: formData.value.ttsPitch !== null && formData.value.ttsPitch !== undefined ? formData.value.ttsPitch : 0,
}
speedPitchStore.updateSpeedPitch({
ttsVolume: formData.value.ttsVolume !== null && formData.value.ttsVolume !== undefined ? formData.value.ttsVolume : 0,
ttsRate: formData.value.ttsRate !== null && formData.value.ttsRate !== undefined ? formData.value.ttsRate : 0,
ttsPitch: formData.value.ttsPitch !== null && formData.value.ttsPitch !== undefined ? formData.value.ttsPitch : 0,
})
}
// 根据语音合成模型加载语言
async function fetchAllLanguag(ttsModelId: string) {
try {
const res = await getAllLanguage(ttsModelId, '') as any[]
const res = await getAllLanguage(ttsModelId)
// 保存完整的音色信息
voiceDetails.value = res.reduce((acc, voice) => {
acc[voice.id] = voice
@@ -447,15 +429,24 @@ function selectRoleTemplate(templateId: string) {
selectedTemplateId.value = templateId
const template = roleTemplates.value.find(t => t.id === templateId)
if (template) {
formData.value.systemPrompt = template.systemPrompt
formData.value.vadModelId = template.vadModelId
formData.value.asrModelId = template.asrModelId
formData.value.llmModelId = template.llmModelId
formData.value.vllmModelId = template.vllmModelId
formData.value.intentModelId = template.intentModelId
formData.value.memModelId = template.memModelId
formData.value.ttsModelId = template.ttsModelId
formData.value.ttsVoiceId = template.ttsVoiceId
formData.value = {
...formData.value,
systemPrompt: template.systemPrompt || formData.value.systemPrompt,
vadModelId: template.vadModelId || formData.value.vadModelId,
asrModelId: template.asrModelId || formData.value.asrModelId,
llmModelId: template.llmModelId || formData.value.llmModelId,
vllmModelId: template.vllmModelId || formData.value.vllmModelId,
intentModelId: template.intentModelId || formData.value.intentModelId,
memModelId: template.memModelId || formData.value.memModelId,
ttsModelId: template.ttsModelId || formData.value.ttsModelId,
ttsVoiceId: template.ttsVoiceId || formData.value.ttsVoiceId,
agentName: template.agentName || formData.value.agentName,
chatHistoryConf: template.chatHistoryConf || formData.value.chatHistoryConf,
summaryMemory: template.summaryMemory || formData.value.summaryMemory,
langCode: template.langCode || formData.value.langCode,
}
fetchAllLanguag(template.ttsModelId || formData.value.ttsModelId)
updateDisplayNames()
}
}
@@ -518,6 +509,57 @@ async function onPickerConfirm(type: string, value: any, name: string) {
// 选择器取消
function onPickerCancel(type: string) {
pickerShow.value[type] = false
// 关闭时停止播放
if (type === 'voiceprint') {
stopAudio()
}
}
// 播放音频
function playAudio(voiceDemo: string, voiceId: string, event: Event) {
event.stopPropagation() // 阻止事件冒泡,防止关闭下拉框
if (!voiceDemo) {
return
}
// 如果正在播放同一个音频,则停止
if (playingVoiceId.value === voiceId) {
stopAudio()
return
}
// 停止之前的音频
stopAudio()
// 创建新的音频实例
audioRef.value = uni.createInnerAudioContext()
audioRef.value.src = voiceDemo
playingVoiceId.value = voiceId
// 监听播放结束
audioRef.value.onEnded(() => {
playingVoiceId.value = ''
})
// 监听播放错误
audioRef.value.onError(() => {
toast.error('音频播放失败')
playingVoiceId.value = ''
})
// 播放音频
audioRef.value.play()
}
// 停止音频
function stopAudio() {
if (audioRef.value) {
audioRef.value.stop()
audioRef.value.destroy()
audioRef.value = null
}
playingVoiceId.value = ''
}
// 获取模型显示名称
@@ -564,8 +606,9 @@ async function saveAgent() {
// 构建保存数据,包含上下文配置和语音设置
const saveData = {
...formData.value,
...speedPitchStore.speedPitch,
ttsLanguage: formData.value.language,
contextProviders: currentContextProviders.value,
contextProviders: providerStore.providers,
}
await updateAgent(agentId.value, saveData)
loadAgentDetail()
@@ -611,15 +654,6 @@ function handleTools() {
})
}
// 监听插件配置更新
function watchPluginUpdates() {
// 监听store中的插件配置变化
watch(() => pluginStore.currentFunctions, (newFunctions) => {
console.log('插件配置已更新:', newFunctions)
formData.value.functions = newFunctions
}, { deep: true })
}
// 获取智能体标签
async function loadAgentTags() {
try {
@@ -635,9 +669,12 @@ async function handleUpdateAgentTags() {
await updateAgentTags(agentId.value, { tagNames })
}
// 监听store中的插件配置变化
watch(() => pluginStore.currentFunctions, (newFunctions) => {
formData.value.functions = newFunctions
}, { deep: true })
onMounted(async () => {
// 初始化插件配置监听
watchPluginUpdates()
loadAgentTags()
// 先加载模型选项和角色模板
@@ -726,7 +763,7 @@ onMounted(async () => {
</text>
<view class="mt-0 flex flex-wrap items-center gap-[12rpx]">
<text class="text-[26rpx] text-[#65686f]">
{{ t('agent.contextProviderSuccess', { count: currentContextProviders.length }) }}
{{ t('agent.contextProviderSuccess', { count: providerStore.providers.length }) }}
</text>
<a class="text-[26rpx] text-[#5778ff] no-underline" href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/context-provider-integration.md" target="_blank">
{{ t('agent.contextProviderDocLink') }}
@@ -908,8 +945,9 @@ onMounted(async () => {
<textarea
v-model="formData.summaryMemory"
:placeholder="t('agent.memoryContent')"
disabled
class="box-border h-[500rpx] w-full resize-none break-words break-all border border-[#eeeeee] rounded-[12rpx] bg-[#f0f0f0] p-[20rpx] text-[26rpx] text-[#65686f] leading-[1.6] opacity-80 outline-none"
:disabled="isMemoryDisabled"
:style="isMemoryDisabled ? 'background: #f0f0f0' : ''"
class="box-border h-[500rpx] w-full resize-none break-words break-all border border-[#eeeeee] rounded-[12rpx] p-[20rpx] text-[26rpx] leading-[1.6] opacity-80 outline-none"
/>
</view>
</view>
@@ -972,16 +1010,35 @@ onMounted(async () => {
<wd-action-sheet
v-model="pickerShow.tts"
:actions="modelOptions.TTS && modelOptions.TTS.map(item => ({ name: item.modelName, value: item.id }))"
class="custom-sheet-tts"
@close="onPickerCancel('tts')"
@select="({ item }) => onPickerConfirm('tts', item.value, item.name)"
/>
<wd-action-sheet
v-model="pickerShow.voiceprint"
:actions="voiceOptions"
@close="onPickerCancel('voiceprint')"
@select="({ item }) => onPickerConfirm('voiceprint', item.value, item.name)"
/>
<!-- 自定义语音选择弹出层 -->
<wd-popup v-model="pickerShow.voiceprint" class="custom-popup" position="bottom" @close="onPickerCancel('voiceprint')">
<view class="overflow-hidden rounded-[20rpx] bg-white pb-[20rpx] pt-[20rpx]">
<view class="max-h-[600rpx] overflow-y-auto">
<view
v-for="voice in voiceOptions"
:key="voice.value"
class="flex items-center justify-between border-b border-[#f5f5f5] p-[32rpx] transition-all active:bg-[#f5f7fb]"
@click="onPickerConfirm('voiceprint', voice.value, voice.name)"
>
<text :class="`flex-1 text-[28rpx] text-[#232338] ${(voice.voiceDemo || voice.voice_demo) ? '' : 'text-center'}`">
{{ voice.name }}
</text>
<view v-if="voice.voiceDemo || voice.voice_demo" class="ml-[20rpx]" @click.stop="playAudio(voice.voiceDemo || voice.voice_demo, voice.value, $event)">
<wd-icon
:name="playingVoiceId === voice.value ? 'pause-circle' : 'play-circle'"
size="24px"
:custom-class="playingVoiceId === voice.value ? 'text-[#336cff]' : 'text-[#9d9ea3]'"
/>
</view>
</view>
</view>
</view>
</wd-popup>
<wd-action-sheet
v-model="pickerShow.language"
:actions="languageOptions"
@@ -994,16 +1051,6 @@ onMounted(async () => {
@close="onPickerCancel('report')"
@select="({ item }) => onPickerConfirm('report', item.value, item.name)"
/>
<ContextProviderDialog
v-model:visible="showContextProviderDialog"
:providers="currentContextProviders"
@confirm="handleUpdateContext"
/>
<VoiceSettingsDialog
v-model:visible="showVoiceSettingsDialog"
:settings="ttsSettings"
@confirm="handleUpdateVoiceSettings"
/>
</view>
</template>
@@ -1011,4 +1058,19 @@ onMounted(async () => {
::v-deep .wd-tag__close {
color: #336cff !important;
}
::v-deep .custom-popup {
.wd-popup {
padding: 20rpx !important;
background: transparent !important;
}
}
::v-deep .custom-sheet-tts {
.wd-action-sheet {
padding: 8px 0 !important;
overflow: hidden;
}
.wd-action-sheet__actions {
padding: 0 !important;
}
}
</style>
+11 -5
View File
@@ -51,17 +51,18 @@ const currentTab = ref('agent-config')
// 刷新和加载状态
const refreshing = ref(false)
// 计算是否启用下拉刷新(角色编辑页面不启用)
const refresherEnabled = computed(() => {
return currentTab.value !== 'agent-config'
})
const refresherEnabled = ref(false)
// 子组件引用
const deviceRef = ref()
const chatRef = ref()
const voiceprintRef = ref()
// 更新刷新器状态
function updateRefresherEnabled(value: boolean) {
refresherEnabled.value = value
}
// Tab 配置
const tabList = [
{
@@ -144,6 +145,10 @@ async function onLoadMore() {
}
}
watch(() => currentTab.value, (newTab) => {
updateRefresherEnabled(newTab !== 'agent-config')
})
// 接收页面参数
onLoad((options) => {
if (options?.agentId) {
@@ -204,6 +209,7 @@ onMounted(async () => {
v-else-if="currentTab === 'voiceprint-management'"
ref="voiceprintRef"
:agent-id="currentAgentId"
@update-refresher-enabled="updateRefresherEnabled"
/>
</view>
</scroll-view>
@@ -0,0 +1,208 @@
<route lang="jsonc" type="page">
{
"layout": "default",
"style": {
"navigationBarTitleText": "编辑源",
"navigationStyle": "custom"
}
}
</route>
<script lang="ts" setup>
import type { Providers } from '@/api/agent/types'
import { onMounted, ref } from 'vue'
import { t } from '@/i18n'
import { useProvider } from '@/store/provider'
defineOptions({
name: 'Provider',
})
const providerStore = useProvider()
const localProviders = ref<Providers[]>([])
function initLocalData() {
localProviders.value = providerStore.providers.map((p) => {
const headers = p.headers || {}
return {
url: p.url,
headers: Object.entries(headers).map(([key, value]: [string, string]) => ({ key, value })),
}
})
if (localProviders.value.length === 0) {
localProviders.value.push({ url: '', headers: [{ key: '', value: '' }] })
}
}
function addProvider(index: number) {
localProviders.value.splice(index, 0, {
url: '',
headers: [{ key: '', value: '' }],
})
}
function removeProvider(index: number) {
localProviders.value.splice(index, 1)
}
function addHeader(pIndex: number, hIndex: number) {
localProviders.value[pIndex].headers.splice(hIndex, 0, { key: '', value: '' })
}
function removeHeader(pIndex: number, hIndex: number) {
localProviders.value[pIndex].headers.splice(hIndex, 1)
}
function handleConfirm() {
const result = localProviders.value
.filter(p => p.url.trim() !== '')
.map((p) => {
const headersObj = {}
p.headers.forEach((h) => {
if (h.key.trim()) {
headersObj[h.key.trim()] = h.value
}
})
return {
url: p.url.trim(),
headers: headersObj,
}
})
providerStore.updateProviders(result as Providers[])
goBack()
}
function goBack() {
uni.navigateBack()
}
onMounted(() => {
initLocalData()
})
</script>
<template>
<view class="h-screen flex flex-col bg-[#f5f7fb]">
<!-- 头部导航 -->
<wd-navbar
:title="t('contextProviderDialog.title')"
safe-area-inset-top
left-arrow
:bordered="false"
@click-left="goBack"
>
<template #left>
<wd-icon name="arrow-left" size="18" />
</template>
</wd-navbar>
<view class="flex-1 overflow-y-auto px-[20rpx] pt-[20rpx]">
<view v-if="localProviders.length === 0" class="flex flex-col items-center justify-center gap-[30rpx] py-[100rpx]">
<text class="text-[28rpx] text-[#9d9ea3]">
{{ t('contextProviderDialog.noContextApi') }}
</text>
<wd-button type="primary" size="small" @click="addProvider(0)">
{{ t('contextProviderDialog.add') }}
</wd-button>
</view>
<view v-else class="flex flex-col">
<view v-for="(provider, pIndex) in localProviders" :key="pIndex" class="mb-[30rpx]">
<view class="flex-1 border border-l-[6rpx] border-[#eee] border-l-[#336cff] rounded-[16rpx] bg-[#fff] p-[20rpx] shadow-[0_4rpx_16rpx_rgba(0,0,0,0.05)]">
<view class="mb-[30rpx] flex items-center justify-between">
<view class="flex gap-[16rpx]">
<wd-button
type="icon"
icon="add"
size="small"
class="!h-[60rpx] !w-[60rpx] !bg-[#66b1ff] !text-[#fff]"
@click="addProvider(pIndex + 1)"
/>
<wd-button
type="icon"
icon="decrease"
size="small"
class="!h-[60rpx] !w-[60rpx] !bg-[#F56C6C] !text-[#fff]"
@click="removeProvider(pIndex)"
/>
</view>
</view>
<view class="mb-[40rpx] flex items-center gap-[20rpx]">
<text class="w-[140rpx] text-[28rpx] text-[#606266] font-semibold">
{{ t('contextProviderDialog.apiUrl') }}
</text>
<wd-input
v-model="provider.url"
class="flex-1"
:placeholder="t('contextProviderDialog.apiUrlPlaceholder')"
/>
</view>
<view class="flex items-start gap-[20rpx]">
<view class="mt-[6rpx] w-[140rpx] text-[28rpx] text-[#606266] font-semibold">
{{ t('contextProviderDialog.requestHeaders') }}
</view>
<view class="flex flex-1 flex-col gap-[20rpx] border border-[#dcdfe6] rounded-[12rpx] border-dashed bg-[#fcfcfc] p-[4rpx]">
<view
v-for="(header, hIndex) in provider.headers"
:key="hIndex"
class="flex flex-col gap-[16rpx] rounded-[12rpx] bg-[#fff]"
>
<view class="flex items-center gap-[8rpx]">
<wd-input
v-model="header.key"
placeholder="key"
class="w-full"
/>
</view>
<view class="flex items-center gap-[8rpx]">
<wd-input
v-model="header.value"
placeholder="value"
class="w-full"
/>
</view>
<view class="flex self-start gap-[12rpx]">
<wd-button
type="icon"
icon="add"
size="small"
class="!h-[50rpx] !w-[50rpx] !border-[1rpx] !border-solid !bg-[#ecf5ff] !text-[#b3d8ff]"
@click="addHeader(pIndex, hIndex + 1)"
/>
<wd-button
type="icon"
icon="decrease"
size="small"
class="!h-[50rpx] !w-[50rpx] !border-[1rpx] !border-solid !bg-[#fef0f0] !text-[#F56C6C]"
@click="removeHeader(pIndex, hIndex)"
/>
</view>
</view>
<view v-if="provider.headers.length === 0" class="flex items-center justify-center py-[20rpx] text-[26rpx] text-[#909399]">
<text class="mr-[16rpx]">
{{ t('contextProviderDialog.noHeaders') }}
</text>
<wd-button type="text" size="small" @click="addHeader(pIndex, 0)">
{{ t('contextProviderDialog.addHeader') }}
</wd-button>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="flex gap-[20rpx] border-t border-[#eee] px-[30rpx] py-[30rpx]">
<wd-button type="primary" class="h-[80rpx] flex-1 rounded-[12rpx] text-[28rpx]" @click="handleConfirm">
{{ t('contextProviderDialog.confirm') }}
</wd-button>
</view>
</view>
</template>
<style scoped lang="scss">
</style>
@@ -0,0 +1,153 @@
<route lang="jsonc" type="page">
{
"layout": "default",
"style": {
"navigationBarTitleText": "语音设置",
"navigationStyle": "custom"
}
}
</route>
<script lang="ts" setup>
import { t } from '@/i18n'
import { useSpeedPitch } from '@/store'
defineOptions({
name: 'SpeedPitch',
})
const speedPitchStore = useSpeedPitch()
const localSettings = ref({
ttsVolume: 0,
ttsRate: 0,
ttsPitch: 0,
})
function handleConfirm() {
speedPitchStore.updateSpeedPitch(localSettings.value)
goBack()
}
// 返回上一页并更新配置
function goBack() {
uni.navigateBack()
}
onMounted(() => {
localSettings.value = {
ttsVolume: speedPitchStore.speedPitch.ttsVolume,
ttsRate: speedPitchStore.speedPitch.ttsRate,
ttsPitch: speedPitchStore.speedPitch.ttsPitch,
}
})
</script>
<template>
<view class="h-screen flex flex-col bg-[#f5f7fb]">
<!-- 头部导航 -->
<wd-navbar
:title="t('agent.languageConfig')"
safe-area-inset-top
left-arrow
:bordered="false"
@click-left="goBack"
>
<template #left>
<wd-icon name="arrow-left" size="18" />
</template>
</wd-navbar>
<view class="flex flex-1 flex-col overflow-hidden">
<view class="flex flex-1 flex-col gap-[50rpx] overflow-y-auto px-[40rpx] py-[50rpx]">
<!-- 音量调节 -->
<view class="flex flex-col gap-[20rpx]">
<text class="text-[30rpx] text-[#232338] font-semibold">
{{ t('agent.ttsVolume') }}
</text>
<view class="flex items-center gap-[20rpx]">
<wd-slider
v-model="localSettings.ttsVolume"
:min="-100"
:max="100"
:step="1"
:show-value="false"
custom-class="voice-slider"
class="flex-1"
/>
<text class="min-w-[80rpx] text-right text-[28rpx] text-[#336cff] font-medium">
{{ localSettings.ttsVolume }}
</text>
</view>
<text class="mt-[10rpx] text-[24rpx] text-[#9d9ea3]">
{{ t('agent.volumeHint') }}
</text>
</view>
<!-- 语速调节 -->
<view class="flex flex-col gap-[20rpx]">
<text class="text-[30rpx] text-[#232338] font-semibold">
{{ t('agent.ttsRate') }}
</text>
<view class="flex items-center gap-[20rpx]">
<wd-slider
v-model="localSettings.ttsRate"
:min="-100"
:max="100"
:step="1"
:show-value="false"
custom-class="voice-slider"
class="flex-1"
/>
<text class="min-w-[80rpx] text-right text-[28rpx] text-[#336cff] font-medium">
{{ localSettings.ttsRate }}
</text>
</view>
<text class="mt-[10rpx] text-[24rpx] text-[#9d9ea3]">
{{ t('agent.speedHint') }}
</text>
</view>
<!-- 音调调节 -->
<view class="flex flex-col gap-[20rpx]">
<text class="text-[30rpx] text-[#232338] font-semibold">
{{ t('agent.ttsPitch') }}
</text>
<view class="flex items-center gap-[20rpx]">
<wd-slider
v-model="localSettings.ttsPitch"
:min="-100"
:max="100"
:step="1"
:show-value="false"
custom-class="voice-slider"
class="flex-1"
/>
<text class="min-w-[80rpx] text-right text-[28rpx] text-[#336cff] font-medium">
{{ localSettings.ttsPitch }}
</text>
</view>
<text class="mt-[10rpx] text-[24rpx] text-[#9d9ea3]">
{{ t('agent.pitchHint') }}
</text>
</view>
</view>
<view class="flex gap-[20rpx] border-t border-[#eee] px-[30rpx] py-[30rpx]">
<wd-button type="primary" class="h-[80rpx] flex-1 rounded-[12rpx] text-[28rpx] !bg-[#336cff]" @click="handleConfirm">
{{ t('agent.save') }}
</wd-button>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
/* 自定义滑块样式 */
:deep(.wd-slider) {
--wd-slider-bar-background: #e6ebff;
--wd-slider-bar-active-background: #336cff;
--wd-slider-thumb-border-color: #336cff;
--wd-slider-thumb-background: #336cff;
--wd-slider-thumb-size: 32rpx;
}
</style>
+39 -30
View File
@@ -11,8 +11,8 @@
<script lang="ts" setup>
import { useMessage } from 'wot-design-uni'
import { getMcpAddress, getMcpTools } from '@/api/agent/agent'
import { usePluginStore } from '@/store'
import { t } from '@/i18n'
import { usePluginStore } from '@/store'
const message = useMessage()
const pluginStore = usePluginStore()
@@ -30,8 +30,8 @@ const mcpAddress = ref('')
const mcpTools = ref<string[]>([])
// 初始化时从本地存储加载MCP地址
if (uni.getStorageSync('cachedMcpAddress_' + agentId.value)) {
mcpAddress.value = uni.getStorageSync('cachedMcpAddress_' + agentId.value)
if (uni.getStorageSync(`cachedMcpAddress_${agentId.value}`)) {
mcpAddress.value = uni.getStorageSync(`cachedMcpAddress_${agentId.value}`)
}
// 参数编辑相关
@@ -67,16 +67,19 @@ async function mergeFunctions() {
const address = await getMcpAddress(agentId.value)
mcpAddress.value = address
// 缓存到本地存储,下次打开页面可以立即显示
uni.setStorageSync('cachedMcpAddress_' + agentId.value, address)
} catch (error) {
uni.setStorageSync(`cachedMcpAddress_${agentId.value}`, address)
}
catch (error) {
mcpAddress.value = error
console.error('获取MCP地址失败:', error)
}
// 异步获取MCP工具列表,不阻塞UI显示
try {
const tools = await getMcpTools(agentId.value)
mcpTools.value = tools || []
} catch (error) {
}
catch (error) {
console.error('获取MCP工具列表失败:', error)
}
}
@@ -85,12 +88,12 @@ async function mergeFunctions() {
// 添加插件到已选
function selectFunction(func: any) {
// 添加到已选列表
selectedList.value.push({
selectedList.value = [...selectedList.value, {
id: func.id,
name: func.name,
params: { ...func.params },
fieldsMeta: func.fieldsMeta,
})
}]
// 从未选列表中移除
notSelectedList.value = notSelectedList.value.filter(
@@ -206,15 +209,6 @@ function closeParamEdit() {
// 返回上一页并更新配置
function goBack() {
const finalFunctions = selectedList.value.map(f => ({
pluginId: f.id,
paramInfo: f.params,
}))
// 更新到store中
pluginStore.updateFunctions(finalFunctions)
// 直接返回
uni.navigateBack()
}
@@ -229,11 +223,11 @@ function copyMcpAddress() {
data: mcpAddress.value,
showToast: false,
success: () => {
message.alert(t('agent.tools.mcpAddressCopied'))
},
message.alert(t('agent.tools.mcpAddressCopied'))
},
fail: () => {
message.alert(t('agent.tools.copyFailed'))
},
message.alert(t('agent.tools.copyFailed'))
},
})
}
@@ -254,6 +248,15 @@ function getFieldRemark(field: any) {
return description
}
// 监听已选列表变化,实时更新插件配置,避免用户不点击返回按钮导致配置丢失
watch(() => selectedList.value, (newSelectedList) => {
const finalFunctions = newSelectedList.map(f => ({
pluginId: f.id,
paramInfo: f.params,
}))
pluginStore.updateFunctions(finalFunctions)
})
onMounted(async () => {
// 直接从store获取数据并合并
await mergeFunctions()
@@ -308,7 +311,7 @@ onMounted(async () => {
v-if="notSelectedList.length === 0"
class="h-[400rpx] flex items-center justify-center"
>
<wd-status-tip image="content" tip="{{ t('agent.tools.noMorePlugins') }}" />
<wd-status-tip image="content" :tip="t('agent.tools.noMorePlugins')" />
</view>
<view v-else class="p-[20rpx] space-y-[20rpx]">
<view
@@ -344,7 +347,7 @@ onMounted(async () => {
v-if="selectedList.length === 0"
class="h-[400rpx] flex items-center justify-center"
>
<wd-status-tip image="content" tip="{{ t('agent.tools.pleaseSelectPlugin') }}" />
<wd-status-tip image="content" :tip="t('agent.tools.pleaseSelectPlugin')" />
</view>
<view v-else class="p-[20rpx] space-y-[20rpx]">
<view
@@ -407,11 +410,11 @@ onMounted(async () => {
class="flex-1 rounded-[10rpx] bg-[#f5f7fb] p-[20rpx]"
>
<view
class="ml-[20rpx] h-[70rpx] flex items-center justify-center rounded-[10rpx] bg-[#1677ff] px-[20rpx] text-[24rpx] text-white"
@click="copyMcpAddress"
>
{{ t('agent.tools.copy') }}
</view>
class="ml-[20rpx] h-[70rpx] flex items-center justify-center rounded-[10rpx] bg-[#1677ff] px-[20rpx] text-[24rpx] text-white"
@click="copyMcpAddress"
>
{{ t('agent.tools.copy') }}
</view>
</view>
<!-- 工具列表 -->
<view class="mt-[20rpx] flex-1 overflow-hidden">
@@ -442,7 +445,7 @@ onMounted(async () => {
<!-- 参数编辑弹窗 -->
<wd-action-sheet
v-model="showParamDialog"
:title="`${t('agent.tools.paramConfiguration')} - ${currentFunction?.name || ''}`"
:title="`${t('agent.tools.parameterConfig')} - ${currentFunction?.name || ''}`"
custom-header-class="h-[75vh]"
@close="closeParamEdit"
>
@@ -583,3 +586,9 @@ onMounted(async () => {
</wd-action-sheet>
</view>
</template>
<style scoped lang="scss">
::v-deep .wd-action-sheet__header {
padding-right: 30rpx;
}
</style>
@@ -13,9 +13,9 @@ import type { ChatMessage, UserMessageContent } from '@/api/chat-history/types'
import { onLoad, onUnload } from '@dcloudio/uni-app'
import { computed, ref } from 'vue'
import { getAudioId, getChatHistory } from '@/api/chat-history/chat-history'
import { getEnvBaseUrl } from '@/utils'
import { toast } from '@/utils/toast'
import { t } from '@/i18n'
import { debounce, getEnvBaseUrl } from '@/utils'
import { toast } from '@/utils/toast'
defineOptions({
name: 'ChatDetail',
@@ -129,7 +129,7 @@ function formatTime(timeStr: string) {
}
// 播放音频
async function playAudio(audioId: string) {
const playAudio = debounce(async (audioId: string) => {
if (!audioId) {
toast.error(t('chatHistory.invalidAudioId'))
return
@@ -139,8 +139,11 @@ async function playAudio(audioId: string) {
// 如果正在播放其他音频,先停止
if (audioContext.value) {
audioContext.value.stop()
audioContext.value.destroy()
audioContext.value = null
}
// 如果当前音频ID与请求ID相同暂停播放
if (playingAudioId.value === audioId) {
playingAudioId.value = null
return
}
// 获取音频下载ID
@@ -151,7 +154,9 @@ async function playAudio(audioId: string) {
const audioUrl = `${baseUrl}/agent/play/${downloadId}`
// 创建音频上下文
audioContext.value = uni.createInnerAudioContext()
if (!audioContext.value) {
audioContext.value = uni.createInnerAudioContext()
}
audioContext.value.src = audioUrl
// 设置播放状态
@@ -185,7 +190,7 @@ async function playAudio(audioId: string) {
toast.error(t('chatHistory.playAudioFailed'))
playingAudioId.value = null
}
}
}, 400)
onLoad((options) => {
if (options?.sessionId && options?.agentId) {
+317 -20
View File
@@ -2,23 +2,28 @@
import type { Device, FirmwareType } from '@/api/device'
import { computed, onMounted, ref } from 'vue'
import { useMessage } from 'wot-design-uni'
import { bindDevice, getBindDevices, getFirmwareTypes, unbindDevice, updateDeviceAutoUpdate } from '@/api/device'
import { toast } from '@/utils/toast'
import { bindDevice, bindDeviceManual, getBindDevices, getFirmwareTypes, unbindDevice, updateDeviceAutoUpdate } from '@/api/device'
import { t } from '@/i18n'
import { toast } from '@/utils/toast'
defineOptions({
name: 'DeviceManage',
})
const props = withDefaults(defineProps<Props>(), {
agentId: 'default',
})
const actions = [
{ key: 'code', name: t('manualAddDeviceDialog.bindWithCode') },
{ key: 'manual', name: t('manualAddDeviceDialog.title') },
]
// 接收props
interface Props {
agentId?: string
}
const props = withDefaults(defineProps<Props>(), {
agentId: 'default'
})
// 获取屏幕边界到安全区域距离
let safeAreaInsets: any
let systemInfo: any
@@ -44,6 +49,45 @@ safeAreaInsets = systemInfo.safeAreaInsets
const deviceList = ref<Device[]>([])
const firmwareTypes = ref<FirmwareType[]>([])
const loading = ref(false)
const isBindDevice = ref(false)
// 手动绑定弹窗
const isManualBindDialog = ref(false)
const manualBindForm = ref({
board: '',
appVersion: '',
macAddress: '',
})
// 表单校验错误提示
const formErrors = ref({
board: '',
appVersion: '',
macAddress: '',
})
// MAC地址正则校验
const macRegex = /^(?:[0-9A-F]{2}[:-]){5}[0-9A-F]{2}$/i
function selectBindMode(row) {
if (row.item.key === 'code') {
openBindDialog()
}
else if (row.item.key === 'manual') {
// 打开弹窗前重置表单和错误提示
manualBindForm.value = {
board: '',
appVersion: '',
macAddress: '',
}
formErrors.value = {
board: '',
appVersion: '',
macAddress: '',
}
isManualBindDialog.value = true
}
}
// 消息组件
const message = useMessage()
@@ -56,11 +100,8 @@ const currentAgentId = computed(() => {
// 获取设备列表
async function loadDeviceList() {
try {
console.log('获取设备列表')
// 检查是否有当前选中的智能体
if (!currentAgentId.value) {
console.warn('没有选中的智能体')
deviceList.value = []
return
}
@@ -164,8 +205,8 @@ async function handleBindDevice(code: string) {
}
catch (error: any) {
console.error('绑定设备失败:', error)
const errorMessage = error?.message || '绑定失败,请检查验证码是否正确'
toast.error(errorMessage || t('device.bindFailed'))
const errorMessage = error?.message || t('device.bindFailed')
toast.error(errorMessage)
}
}
@@ -190,6 +231,128 @@ function openBindDialog() {
})
}
// 手动绑定设备
async function handleManualBind() {
try {
// 先校验整个表单
const isValid = validateForm()
if (!isValid) {
return
}
if (!currentAgentId.value) {
toast.error(t('device.pleaseSelectAgent'))
return
}
await bindDeviceManual({
agentId: currentAgentId.value,
board: manualBindForm.value.board,
appVersion: manualBindForm.value.appVersion,
macAddress: manualBindForm.value.macAddress,
})
await loadDeviceList()
toast.success(t('manualAddDeviceDialog.addSuccess'))
isManualBindDialog.value = false
// 重置表单和错误提示
manualBindForm.value = {
board: '',
appVersion: '',
macAddress: '',
}
formErrors.value = {
board: '',
appVersion: '',
macAddress: '',
}
}
catch (error: any) {
const errorMessage = error?.message || t('manualAddDeviceDialog.addFailed')
toast.error(errorMessage)
}
}
// 校验单个字段
function validateField(field: string) {
switch (field) {
case 'board':
if (!manualBindForm.value.board) {
formErrors.value.board = t('manualAddDeviceDialog.deviceTypePlaceholder')
}
else {
formErrors.value.board = ''
}
break
case 'appVersion':
if (!manualBindForm.value.appVersion) {
formErrors.value.appVersion = t('manualAddDeviceDialog.firmwareVersionPlaceholder')
}
else {
formErrors.value.appVersion = ''
}
break
case 'macAddress':
if (!manualBindForm.value.macAddress) {
formErrors.value.macAddress = t('manualAddDeviceDialog.macAddressPlaceholder')
}
else if (!macRegex.test(manualBindForm.value.macAddress)) {
formErrors.value.macAddress = t('manualAddDeviceDialog.invalidMacAddress')
}
else {
formErrors.value.macAddress = ''
}
break
}
}
// 清除字段错误提示
function clearFieldError(field: string) {
formErrors.value[field] = ''
}
// 处理选择器变化
function handlePickerChange() {
clearFieldError('board')
}
// 校验整个表单
function validateForm(): boolean {
let isValid = true
// 校验设备类型
if (!manualBindForm.value.board) {
formErrors.value.board = t('manualAddDeviceDialog.deviceTypePlaceholder')
isValid = false
}
else {
formErrors.value.board = ''
}
// 校验固件版本
if (!manualBindForm.value.appVersion) {
formErrors.value.appVersion = t('manualAddDeviceDialog.firmwareVersionPlaceholder')
isValid = false
}
else {
formErrors.value.appVersion = ''
}
// 校验MAC地址
if (!manualBindForm.value.macAddress) {
formErrors.value.macAddress = t('manualAddDeviceDialog.macAddressPlaceholder')
isValid = false
}
else if (!macRegex.test(manualBindForm.value.macAddress)) {
formErrors.value.macAddress = t('manualAddDeviceDialog.invalidMacAddress')
isValid = false
}
else {
formErrors.value.macAddress = ''
}
return isValid
}
// 获取设备类型列表
async function loadFirmwareTypes() {
try {
@@ -241,14 +404,14 @@ defineExpose({
<view class="mb-[20rpx]">
<text class="mb-[12rpx] block text-[28rpx] text-[#65686f] leading-[1.4]">
{{ t('device.macAddress') }}{{ device.macAddress }}
</text>
<text class="mb-[12rpx] block text-[28rpx] text-[#65686f] leading-[1.4]">
{{ t('device.firmwareVersion') }}{{ device.appVersion }}
</text>
<text class="block text-[28rpx] text-[#65686f] leading-[1.4]">
{{ t('device.lastConnection') }}{{ formatTime(device.lastConnectedAt) }}
</text>
{{ t('device.macAddress') }}{{ device.macAddress }}
</text>
<text class="mb-[12rpx] block text-[28rpx] text-[#65686f] leading-[1.4]">
{{ t('device.firmwareVersion') }}{{ device.appVersion }}
</text>
<text class="block text-[28rpx] text-[#65686f] leading-[1.4]">
{{ t('device.lastConnection') }}{{ formatTime(device.lastConnectedAt) }}
</text>
</view>
<view class="flex items-center justify-between border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx]">
@@ -295,10 +458,88 @@ defineExpose({
</view>
<!-- FAB 绑定设备按钮 -->
<wd-fab type="primary" size="small" icon="add" :draggable="true" :expandable="false" @click="openBindDialog" />
<wd-fab type="primary" size="small" icon="add" :draggable="true" :expandable="false" @click="isBindDevice = true" />
<!-- MessageBox 组件 -->
<wd-message-box />
<wd-action-sheet v-model="isBindDevice" :actions="actions" @close="isBindDevice = false" @select="selectBindMode" />
<!-- 手动绑定设备弹窗 -->
<wd-popup v-model="isManualBindDialog" position="bottom" :close-on-click-modal="false" custom-style="border-radius: 24rpx 24rpx 0 0;">
<view class="manual-bind-dialog">
<view class="dialog-header">
<text class="dialog-title">
{{ t('manualAddDeviceDialog.title') }}
</text>
<wd-icon name="close" size="20" @click="isManualBindDialog = false" />
</view>
<view class="dialog-content">
<view class="form-item">
<text class="form-label">
{{ t('manualAddDeviceDialog.deviceType') }}
<text class="required">
*
</text>
</text>
<wd-picker
v-model="manualBindForm.board"
class="custom-wd-picker"
:columns="firmwareTypes.map(item => ({ value: item.key, label: item.name }))"
:placeholder="t('manualAddDeviceDialog.deviceTypePlaceholder')"
:cancel-button-text="t('common.cancel')"
:confirm-button-text="t('common.confirm')"
@confirm="handlePickerChange"
/>
<text v-if="formErrors.board" class="error-text">
{{ formErrors.board }}
</text>
</view>
<view class="form-item">
<text class="form-label">
{{ t('manualAddDeviceDialog.firmwareVersion') }}
<text class="required">
*
</text>
</text>
<wd-input
v-model="manualBindForm.appVersion"
:placeholder="t('manualAddDeviceDialog.firmwareVersionPlaceholder')"
@input="clearFieldError('appVersion')"
@blur="validateField('appVersion')"
/>
<text v-if="formErrors.appVersion" class="error-text">
{{ formErrors.appVersion }}
</text>
</view>
<view class="form-item">
<text class="form-label">
{{ t('manualAddDeviceDialog.macAddress') }}
<text class="required">
*
</text>
</text>
<wd-input
v-model="manualBindForm.macAddress"
:placeholder="t('manualAddDeviceDialog.macAddressPlaceholder')"
@input="validateField('macAddress')"
@blur="validateField('macAddress')"
/>
<text v-if="formErrors.macAddress" class="error-text">
{{ formErrors.macAddress }}
</text>
</view>
</view>
<view class="dialog-footer">
<wd-button block type="primary" @click="handleManualBind">
{{ t('manualAddDeviceDialog.confirm') }}
</wd-button>
</view>
</view>
</wd-popup>
</view>
</template>
@@ -327,8 +568,64 @@ defineExpose({
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
border: 1rpx solid #eeeeee;
}
::v-deep .wd-action-sheet__popup,
::v-deep .wd-popup {
z-index: 100 !important;
}
.custom-wd-picker ::v-deep .wd-picker__cell {
padding-left: 0 !important;
}
:deep(.wd-icon) {
font-size: 32rpx;
}
.manual-bind-dialog {
padding: 32rpx;
background: #ffffff;
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32rpx;
}
.dialog-title {
font-size: 36rpx;
font-weight: 600;
color: #232338;
}
.dialog-content {
margin-bottom: 32rpx;
}
.form-item {
margin-bottom: 24rpx;
}
.form-label {
display: block;
font-size: 28rpx;
color: #65686f;
margin-bottom: 12rpx;
}
.required {
color: #ff4d4f;
margin-left: 4rpx;
}
.error-text {
display: block;
font-size: 24rpx;
color: #ff4d4f;
margin-top: 8rpx;
}
.dialog-footer {
padding-top: 16rpx;
}
</style>
@@ -125,7 +125,7 @@ function openCreateDialog() {
msg: '',
inputPlaceholder: t('home.inputPlaceholder'),
inputValue: '',
inputPattern: /^[\u4E00-\u9FA5a-z0-9\s]{1,50}$/i,
inputPattern: /^.{1,64}$/i,
inputError: t('home.createError'),
confirmButtonText: t('home.createNow'),
cancelButtonText: t('common.cancel'),
@@ -159,7 +159,7 @@ function formatTime(timeStr: string) {
onShow(() => {
console.log('首页 onShow,刷新智能体列表')
if (pagingRef.value) {
pagingRef.value.reload()
pagingRef.value.refresh()
}
})
+93 -54
View File
@@ -9,17 +9,16 @@
</route>
<script lang="ts" setup>
import type { LoginData } from '@/api/auth';
import { computed, onMounted, ref } from 'vue';
import { login } from '@/api/auth';
import { useConfigStore } from '@/store';
import { getEnvBaseUrl } from '@/utils';
import { toast } from '@/utils/toast';
import type { LoginData } from '@/api/auth'
import type { Language } from '@/store/lang'
import { computed, onMounted, ref } from 'vue'
import { login } from '@/api/auth'
// 导入国际化相关功能
import { t, changeLanguage, getSupportedLanguages, initI18n } from '@/i18n';
import type { Language } from '@/store/lang';
import { changeLanguage, getCurrentLanguage, getSupportedLanguages, initI18n, t } from '@/i18n'
import { useConfigStore, useUserStore } from '@/store'
// 导入SM2加密工具
import { sm2Encrypt } from '@/utils'
import { getEnvBaseUrl, sm2Encrypt } from '@/utils'
import { toast } from '@/utils/toast'
// 获取屏幕边界到安全区域距离
let safeAreaInsets
@@ -62,6 +61,7 @@ const loginType = ref<'username' | 'mobile'>('username')
// 获取配置store
const configStore = useConfigStore()
const userStore = useUserStore()
// 区号选择相关
const showAreaCodeSheet = ref(false)
@@ -78,11 +78,6 @@ const areaCodeList = computed(() => {
return configStore.config.mobileAreaList || [{ name: '中国大陆', key: '+86' }]
})
// SM2公钥
const sm2PublicKey = computed(() => {
return configStore.config.sm2PublicKey
})
// 切换登录方式
function toggleLoginType() {
loginType.value = loginType.value === 'username' ? 'mobile' : 'username'
@@ -123,6 +118,22 @@ function goToForgotPassword() {
})
}
// 跳转到用户协议
function goToUserAgreement() {
const lang = getCurrentLanguage() === 'zh_CN' ? 'zh' : 'en'
uni.navigateTo({
url: `/pages/login/user-agreement-${lang}`,
})
}
// 跳转到隐私政策
function goToPrivacyPolicy() {
const lang = getCurrentLanguage() === 'zh_CN' ? 'zh' : 'en'
uni.navigateTo({
url: `/pages/login/privacy-policy-${lang}`,
})
}
// 生成UUID
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
@@ -132,9 +143,7 @@ function generateUUID() {
})
}
let skipReLaunch = false // 全局或组件作用域
//跳转至服务端设置页面
// 跳转至服务端设置页面
function goToServerSetting() {
uni.switchTab({
url: '/pages/settings/index',
@@ -143,9 +152,9 @@ function goToServerSetting() {
// 获取验证码
async function refreshCaptcha() {
const uuid = generateUUID();
formData.value.captchaId = uuid;
captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}`;
const uuid = generateUUID()
formData.value.captchaId = uuid
captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}`
}
// 登录
@@ -179,7 +188,8 @@ async function handleLogin() {
}
// 检查SM2公钥是否配置
if (!sm2PublicKey.value) {
const sm2PublicKey = configStore.config.sm2PublicKey
if (!sm2PublicKey) {
toast.warning(t('sm2.publicKeyNotConfigured'))
return
}
@@ -192,8 +202,9 @@ async function handleLogin() {
try {
// 拼接验证码和密码
const captchaAndPassword = formData.value.captcha + formData.value.password
encryptedPassword = sm2Encrypt(sm2PublicKey.value, captchaAndPassword)
} catch (error) {
encryptedPassword = sm2Encrypt(sm2PublicKey, captchaAndPassword)
}
catch (error) {
console.error('密码加密失败:', error)
toast.warning(t('sm2.encryptionFailed'))
return
@@ -203,20 +214,21 @@ async function handleLogin() {
const loginData: LoginData = {
username: '',
password: encryptedPassword,
captchaId: formData.value.captchaId
captchaId: formData.value.captchaId,
}
// 如果是手机号登录,将区号+手机号拼接到username字段
if (loginType.value === 'mobile') {
loginData.username = `${selectedAreaCode.value}${formData.value.mobile}`
} else {
}
else {
loginData.username = formData.value.username
}
const response = await login(loginData)
// 存储token
uni.setStorageSync('token', response.token)
uni.setStorageSync('expire', response.expire)
uni.setStorageSync('token', JSON.stringify(response))
await userStore.getUserInfo()
toast.success(t('message.loginSuccess'))
@@ -230,14 +242,6 @@ async function handleLogin() {
catch (error: any) {
// 登录失败重新获取验证码
refreshCaptcha()
// 处理验证码错误 - 从error.message中解析错误码
if (error.message.includes('请求错误[10067]')) {
toast.warning(t('login.captchaError'))
}
// 处理账号或密码错误
else if (error.message.includes('请求错误[10004]')) {
toast.warning(t('message.passwordError'))
}
}
finally {
loading.value = false
@@ -267,7 +271,8 @@ onMounted(async () => {
if (!configStore.config.name) {
try {
await configStore.fetchPublicConfig()
} catch (error) {
}
catch (error) {
console.error(t('login.fetchConfigError'), error)
}
}
@@ -287,19 +292,21 @@ onMounted(async () => {
</text>
</view>
</view>
<!-- 右上角按钮组 -->
<view class="top-right-buttons" :style="{ top: `${safeAreaInsets?.top + 10}px` }">
<!-- 语言切换按钮 -->
<view class="lang-btn" @click="showLanguageSheet = true">
<text class="lang-text-icon">{{ t('login.selectLanguageTip') }}</text>
</view>
<!-- 服务端设置按钮 -->
<view class="server-btn" @click="goToServerSetting">
<wd-icon name="setting" custom-class="server-icon" />
</view>
</view>
<!-- 右上角按钮组 -->
<view class="top-right-buttons" :style="{ top: `${safeAreaInsets?.top + 10}px` }">
<!-- 语言切换按钮 -->
<view class="lang-btn" @click="showLanguageSheet = true">
<text class="lang-text-icon">
{{ t('login.selectLanguageTip') }}
</text>
</view>
<!-- 服务端设置按钮 -->
<view class="server-btn" @click="goToServerSetting">
<wd-icon name="setting" custom-class="server-icon" />
</view>
</view>
<view class="form-container">
<view class="form">
@@ -390,6 +397,16 @@ onMounted(async () => {
</view>
</view>
<view class="policy-links">
<text class="policy-link" @click="goToUserAgreement">
{{ t('login.userAgreement') }}
</text>
<text class="policy-divider">|</text>
<text class="policy-link" @click="goToPrivacyPolicy">
{{ t('login.privacyPolicy') }}
</text>
</view>
<!-- 登录方式切换 -->
<view v-if="enableMobileLogin" class="login-type-switch">
<view class="switch-tabs">
@@ -566,7 +583,6 @@ onMounted(async () => {
.input-wrapper {
position: relative;
background: #f8f9fa;
border-radius: 16rpx;
padding: 20rpx 16rpx;
border: 2rpx solid #e9ecef;
@@ -684,6 +700,29 @@ onMounted(async () => {
margin-bottom: 30rpx;
}
.policy-links {
display: flex;
justify-content: center;
align-items: center;
gap: 20rpx;
margin-bottom: 30rpx;
.policy-link {
color: #667eea;
font-size: 26rpx;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.policy-divider {
color: #999999;
font-size: 26rpx;
}
}
.forgot-password {
.forgot-text {
color: #667eea;
@@ -893,7 +932,7 @@ onMounted(async () => {
cursor: pointer;
background: rgba(255, 255, 255, 0.15);
border-radius: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.2);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
&:active {
transform: scale(0.95);
@@ -901,7 +940,7 @@ onMounted(async () => {
.lang-text-icon {
font-size: 18rpx;
color: #FFFFFF;
color: #ffffff;
}
&:hover {
@@ -919,7 +958,7 @@ onMounted(async () => {
cursor: pointer;
background: rgba(255, 255, 255, 0.15);
border-radius: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.2);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
&:active {
transform: scale(0.95);
@@ -927,7 +966,7 @@ onMounted(async () => {
.server-icon {
font-size: 28rpx;
color: #FFFFFF;
color: #ffffff;
}
&:hover {
@@ -0,0 +1,402 @@
<route lang="jsonc" type="page">
{
"layout": "default",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "Privacy Policy"
}
}
</route>
<script lang="ts" setup>
import { t } from '@/i18n'
function goBack() {
uni.navigateBack()
}
</script>
<template>
<view class="h-screen flex flex-col bg-[#f5f8fd]">
<wd-navbar
:title="t('login.privacyPolicy')"
safe-area-inset-top
left-arrow
:bordered="false"
@click-left="goBack"
>
<template #left>
<wd-icon name="arrow-left" size="18" />
</template>
</wd-navbar>
<scroll-view scroll-y class="box-border flex-1 px-[30rpx] py-[30rpx]">
<view class="rounded-[16rpx] bg-white px-[30rpx] py-[40rpx] shadow-[0_4rpx_20rpx_rgba(0,0,0,0.05)]">
<view class="mb-[40rpx] text-center">
<text class="text-[36rpx] text-[#1a1a1a] font-bold">
Privacy Policy
</text>
</view>
<view class="mb-[40rpx] text-center">
<text class="text-[24rpx] text-[#666666]">
Last Updated: March 10, 2026
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
Introduction
</text>
</view>
<view class="mb-[24rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
Welcome to use the XiaoZhi Backend Service (hereinafter referred to as "the Service"). The operator of this Service is the actual deployer and administrator of the Service (hereinafter referred to as "Operator" or "we"). We fully understand the importance of your personal information and will do our best to protect your personal information security.
</text>
</view>
<view class="mb-[24rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
Please carefully read and fully understand all contents of this Privacy Policy before using the Service. Once you start using the Service, it signifies that you have read and agreed to this Privacy Policy.
</text>
</view>
<view class="mb-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
This Privacy Policy applies to our collection, storage, use, sharing, and protection of your personal information when you use the Service through the Admin Console (management backend), API interfaces, and other means.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
I. Information We Collect
</text>
</view>
<view class="mb-[24rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
To provide services to you, we may need to collect the following information:
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.1 Account Registration Information: When you register an account, we collect your phone number or username, password, and other information to create and verify your account.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.2 Device Information: When you bind hardware devices, we collect device identification information (such as device code, MAC address), device model, firmware version, etc., for device management and service provision.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.3 Agent Configuration Information: When you create and configure Agents, we collect role templates, language model selections, voice parameter configurations, etc., to provide personalized AI interaction services.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.4 Voice Interaction Data: When you use the voice interaction function, the Service uses Voice Activity Detection (VAD) to determine the start and end of your voice, processes your voice input data, and transmits it to third-party Automatic Speech Recognition (ASR) and Large Language Model (LLM) services for processing to achieve voice interaction capabilities.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.5 Image Data: When you use the vision model function, we may process image data captured through the device camera and transmit it to third-party vision model services for analysis and understanding to achieve image recognition, scene understanding, and other functions.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.6 Conversation Memory Data: When you enable the memory function, the Service stores summaries of your interaction history with the Agent to provide more coherent and personalized interaction experiences in subsequent conversations.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.7 Knowledge Base Data: When you use the knowledge base function, we collect and store documents, texts, and other knowledge base content you upload for Agents to perform knowledge retrieval and Q&A during conversations.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.8 Voice Print Data: When you use the voice print recognition function, we collect and store your voice print characteristic samples for speaker identity verification and personalized services.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.9 Chat History Data: We store conversation history records between you and the Agents, including text conversation content, conversation time, interaction context, etc., to provide continuous conversation experience and history query functions.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.10 Conversation Audio Data: When you use the voice interaction function, we may store audio data of your interactions with the Agents to improve voice interaction quality and enable retrieval queries.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.11 Agent Configuration Data: When you configure Agents, we collect tags, plugin configurations, context provider settings, etc., to provide Agent customization services.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.12 Device Firmware Data: When you use the OTA upgrade function, we record device firmware version, upgrade history, etc., for device management and firmware traceability.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.13 Log Information: When you use the Service, we automatically collect service log information, including but not limited to access time, IP address, browser type, operation records, etc., for service operation and security assurance.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.14 Verification Code Information: When you use phone number login, we send verification codes via SMS service for identity verification.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
II. How We Use the Collected Information
</text>
</view>
<view class="mb-[24rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
The information we collect will be used for the following purposes:
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(1) Providing, maintaining, and improving the Service, including core functions such as account management, device management, and Agent configuration.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(2) Processing your voice interaction requests, using Voice Activity Detection (VAD) to identify voice input, calling third-party AI model services to complete speech recognition, intent recognition, semantic understanding, and speech synthesis.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(3) Processing your image data, calling third-party vision model services to complete image recognition and scene understanding.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(4) Storing and managing your conversation memory data to provide more coherent service experiences in subsequent interactions.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(5) Storing and retrieving knowledge base content you upload so that Agents can provide more accurate knowledge Q&A during conversations.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(6) Ensuring service security and stability, including identity verification, security protection, troubleshooting, etc.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(7) Complying with applicable laws, regulations, and regulatory requirements.
</text>
</view>
<view class="mb-[24rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
We will not use your personal information for purposes unrelated to the above. If we need to use information for other purposes not stated in this Privacy Policy, we will obtain your consent in advance.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
III. Sharing and Disclosure of Information
</text>
</view>
<view class="mb-[24rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
We will not sell your personal information to third parties. Only in the following circumstances may we share or disclose your information:
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.1 Third-party Service Calls: To achieve voice interaction, visual understanding, and other functions, your voice data, image data, and text content may be transmitted to third-party AI service providers for processing.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.2 Legal Requirements: According to applicable laws, regulations, legal procedures, or requirements from government authorities, we may need to disclose your personal information.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.3 Security Assurance: To protect the Operator, other users, or the public from damage, we may use or disclose personal information within a reasonably necessary scope.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.4 With Consent: We may share your personal information with third parties if we obtain your explicit consent.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
IV. Storage and Protection of Information
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.1 Storage Location: Your personal information will be stored on the server where the Operator deploys the Service, including but not limited to databases (MySQL/PostgreSQL) and cache services (Redis).
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.2 Storage Period: We will retain your personal information for the period necessary to provide you with services. After you cancel your account, we will delete or anonymize your personal information within a reasonable time.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.3 Security Measures: We take reasonable technical and management measures to protect the security of your personal information, including but not limited to data encryption, access control, security auditing, etc.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.4 Security Incident Handling: If a personal information security incident occurs, we will promptly inform you of the basic situation of the security incident, possible impacts, and measures taken or to be taken.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.5 Special Note for Open-source Projects: The Service is an open-source project with two operation modes:
</text>
</view>
<view class="mb-[8rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(1) Self-deployment: If you self-deploy the Service, the Operator is only the code provider, and actual data storage and processing are your responsibility.
</text>
</view>
<view class="mb-[8rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(2) Test Platform: If you use the test platform deployed by the Operator, your data will be managed and protected by the Operator.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.6 Cross-border Data Transfer: When providing intelligent interaction functions, the Service may need to call interfaces from third-party AI service providers outside mainland China.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
V. Your Rights
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5.1 Query and Access: You can view and manage your personal information such as account information, device information, and Agent configuration through the Admin Console.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5.2 Correction and Modification: When you find that your personal information is incorrect, you can correct it through the Admin Console or contact the Operator for assistance.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5.3 Deletion: Under certain circumstances, you may request us to delete your personal information.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5.4 Account Cancellation: You can cancel your account through the account settings in the Admin Console or contact the Operator for processing.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5.5 Withdrawal of Consent: You may withdraw your consent granted to us at any time.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
VI. Protection of Minor Information
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
6.1 We attach great importance to the protection of minors' personal information. If you are a minor under 18 years of age, please use the Service under the guidance and consent of your guardian.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
6.2 If a guardian discovers that a minor has provided personal information to us without consent, please contact the Operator. We will delete the relevant information as soon as possible.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
6.3 For minors who use the Service with guardian consent, we will provide stricter protection for their personal information in accordance with laws and regulations.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
VII. Third-party Services Description
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
7.1 When providing intelligent interaction functions, the Service needs to call third-party services, including but not limited to: Voice Activity Detection Service (VAD), Automatic Speech Recognition Service (ASR), Large Language Model Service (LLM), Text-to-Speech Service (TTS), Vision Model Service, SMS Verification Code Service, MQTT Message Broker Service, Database Service.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
7.2 The above third-party service providers will process data according to their respective privacy policies. We recommend that you understand the privacy policies of relevant third-party service providers before using the Service.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
VIII. Use of Cookies and Similar Technologies
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
8.1 The Service may use Cookies and similar technologies to save your login status, record your preference settings, etc., to provide you with a better user experience.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
8.2 You can manage Cookies through browser settings. However, please note that disabling Cookies may affect some functions of the Service.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
IX. Changes to the Privacy Policy
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
9.1 We may revise this Privacy Policy based on business adjustments, changes in laws and regulations, and other reasons. The revised Privacy Policy will be published through the Service's announcement mechanisms.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
9.2 If you continue to use the Service after the Privacy Policy revision, it shall be deemed that you have accepted the revised Privacy Policy.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
X. Information Security Warning
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
10.1 Please do not disclose sensitive personal information such as your property account numbers, bank card numbers, credit card numbers, passwords, ID card numbers, etc., in voice or text interactions with AI. Any losses resulting from this shall be borne by you.
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
10.2 Please properly keep your account and password secure, and do not share your account information with others or share accounts with others.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
10.3 If you discover that personal information may have been leaked, please contact the Operator promptly to take measures.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
XI. Applicable Law
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
11.1 The formulation, interpretation, execution, and dispute resolution of this Privacy Policy shall be governed by the laws of the People's Republic of China (excluding the laws of Hong Kong, Macau, and Taiwan for the purposes of this Privacy Policy).
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
11.2 If any provision of this Privacy Policy conflicts with the current laws and regulations of the People's Republic of China, the provisions of laws and regulations shall prevail, and the remaining provisions shall remain valid.
</text>
</view>
</view>
</scroll-view>
</view>
</template>
@@ -0,0 +1,402 @@
<route lang="jsonc" type="page">
{
"layout": "default",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "隐私政策"
}
}
</route>
<script lang="ts" setup>
import { t } from '@/i18n'
function goBack() {
uni.navigateBack()
}
</script>
<template>
<view class="h-screen flex flex-col bg-[#f5f8fd]">
<wd-navbar
:title="t('login.privacyPolicy')"
safe-area-inset-top
left-arrow
:bordered="false"
@click-left="goBack"
>
<template #left>
<wd-icon name="arrow-left" size="18" />
</template>
</wd-navbar>
<scroll-view scroll-y class="box-border flex-1 px-[20rpx] py-[30rpx]" :scroll-with-animation="true">
<view class="rounded-[16rpx] bg-white px-[30rpx] py-[40rpx] shadow-[0_4rpx_20rpx_rgba(0,0,0,0.05)]">
<view class="mb-[40rpx] text-center">
<text class="text-[36rpx] text-[#1a1a1a] font-bold">
{{ t('login.privacyPolicy') }}
</text>
</view>
<view class="mb-[40rpx] text-center">
<text class="text-[24rpx] text-[#666666]">
更新日期2026年3月10日
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
引言
</text>
</view>
<view class="mb-[24rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
欢迎您使用小智后端服务以下简称"本服务"本服务的运营方为本服务的实际部署者和管理者以下简称"运营者""我们"我们深知个人信息对您的重要性将尽全力保护您的个人信息安全
</text>
</view>
<view class="mb-[24rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
请您在使用本服务前仔细阅读并充分理解本隐私政策的全部内容一旦您开始使用本服务即表示您已阅读并同意本隐私政策
</text>
</view>
<view class="mb-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
本隐私政策适用于您通过智控台管理后台API接口及其他方式使用本服务时我们对您个人信息的收集存储使用共享保护等行为
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
我们收集的信息
</text>
</view>
<view class="mb-[24rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
为向您提供服务我们可能需要收集以下信息
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.1 账号注册信息当您注册账号时我们会收集您的手机号码或用户名密码等信息用于创建和验证您的账号
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.2 设备信息当您绑定硬件设备时我们会收集设备的标识信息如设备编码MAC地址等设备型号固件版本等用于设备管理和服务提供
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.3 智能体配置信息当您创建和配置智能体时我们会收集您设定的角色模板语言模型选择语音参数配置等信息用于提供个性化的AI交互服务
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.4 语音交互数据在您使用语音交互功能时本服务会通过语音活动检测VAD判断您的语音起止状态并处理您的语音输入数据将其传输至第三方语音识别ASR和语言模型LLM服务进行处理以实现语音交互功能
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.5 图像数据当您使用视觉模型功能时我们可能会处理您通过设备摄像头采集的图像数据并将其传输至第三方视觉模型服务进行分析和理解用于实现图像识别场景理解等功能
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.6 对话记忆数据当您开启记忆功能时本服务会存储您与智能体的交互历史摘要用于在后续对话中提供更连贯个性化的交互体验
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.7 知识库数据当您使用知识库功能时我们会收集和存储您上传的文档文本等知识库内容用于智能体在对话中进行知识检索和问答
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.8 声纹数据当您使用声纹识别功能时我们会收集和存储您的声纹特征样本用于说话人身份验证和个性化服务
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.9 聊天历史数据我们会存储您与智能体的对话历史记录包括文本对话内容对话时间交互上下文等信息用于提供连续性对话体验和历史查询功能
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.10 对话音频数据在您使用语音交互功能时我们可能会存储您与智能体交互的音频数据用于优化语音交互质量和回溯查询
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.11 智能体配置数据当您配置智能体时我们会收集您设定的标签插件配置上下文提供者设置等信息用于提供智能体定制化服务
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.12 设备固件数据当您使用OTA升级功能时我们会记录设备固件版本升级历史等信息用于设备管理和固件追溯
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.13 日志信息当您使用本服务时我们会自动收集服务日志信息包括但不限于访问时间IP地址浏览器类型操作记录等用于服务运维和安全保障
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1.14 验证码信息当您使用手机号登录时我们会通过短信服务向您发送验证码用于身份验证
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
我们如何使用收集的信息
</text>
</view>
<view class="mb-[24rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
我们收集的信息将用于以下目的
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1提供维护和改进本服务包括账号管理设备管理智能体配置等核心功能
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
2处理您的语音交互请求通过语音活动检测VAD识别语音输入调用第三方AI模型服务完成语音识别意图识别语义理解和语音合成
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3处理您的图像数据调用第三方视觉模型服务完成图像识别和场景理解
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4存储和管理您的对话记忆数据以便在后续交互中提供更连贯的服务体验
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5存储和检索您上传的知识库内容以便智能体在对话中为您提供更准确的知识问答
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
6保障服务的安全性和稳定性包括身份验证安全防护故障排查等
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
7遵守适用的法律法规和监管要求
</text>
</view>
<view class="mb-[24rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
我们不会将您的个人信息用于与上述目的无关的其他用途如需将信息用于本隐私政策未载明的其他目的我们会事先征得您的同意
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
信息的共享与披露
</text>
</view>
<view class="mb-[24rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
我们不会向第三方出售您的个人信息仅在以下情形下我们可能会共享或披露您的信息
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.1 第三方服务调用为实现语音交互视觉理解等功能您的语音数据图像数据和文本内容可能会被传输至第三方AI服务提供商如语言模型语音识别语音合成视觉模型服务商进行处理我们会选择具备合理安全保障能力的服务商但请您知悉第三方服务商将按照其自身的隐私政策处理相关数据
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.2 法律要求根据适用的法律法规法律程序或政府主管部门的要求我们可能需要披露您的个人信息
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.3 安全保障为保护运营者其他用户或公众的权益财产或安全免遭损害在合理必要的范围内使用或披露个人信息
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.4 征得同意在获得您明确同意的前提下我们可能会与第三方共享您的个人信息
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
信息的存储与保护
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.1 存储地点您的个人信息将存储在运营者部署本服务的服务器上包括但不限于数据库MySQL/PostgreSQL和缓存服务Redis
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.2 存储期限我们将在为您提供服务所必需的期限内保留您的个人信息当您注销账号后我们将在合理时间内删除或匿名化处理您的个人信息法律法规另有规定的除外
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.3 安全措施我们采取合理的技术和管理措施保护您的个人信息安全包括但不限于数据加密访问控制安全审计等但请您理解互联网并非绝对安全的环境我们无法保证信息传输和存储的绝对安全性
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.4 安全事件处理如发生个人信息安全事件我们将按照法律法规的要求及时向您告知安全事件的基本情况可能的影响已采取或将要采取的处置措施等
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.5 开源项目特别说明本服务为开源项目存在两种运营模式
</text>
</view>
<view class="mb-[8rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1自行部署如您自行部署本服务运营者仅为代码提供方实际的数据存储和处理由您自行负责
</text>
</view>
<view class="mb-[8rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
2测试平台如您使用运营者部署的测试平台您的数据将由运营人员负责管理和保护
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.6 数据跨境传输本服务在提供智能交互功能时可能需要调用境外第三方AI服务提供商的接口
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
您的权利
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5.1 查询与访问您可通过智控台查看和管理您的账号信息设备信息智能体配置等个人信息
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5.2 更正与修改当您发现个人信息有误时您可通过智控台自行更正或联系运营者协助处理
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5.3 删除在以下情形下您可请求我们删除您的个人信息1处理目的已实现无法实现或不再必要2我们违反法律法规或与您的约定收集使用个人信息3注销账号
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5.4 账号注销您可通过智控台的账号设置功能注销账号或联系运营者进行处理
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5.5 撤回同意您可随时撤回您此前向我们作出的同意授权
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
未成年人信息保护
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
6.1 我们高度重视未成年人个人信息的保护若您是未满18周岁的未成年人请在监护人的指导和同意下使用本服务
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
6.2 如果监护人发现未成年人未经其同意向我们提供了个人信息请联系运营者我们将尽快删除相关信息
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
6.3 对于经监护人同意使用本服务的未成年人我们将按照法律法规的规定给予其个人信息更严格的保护
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
第三方服务说明
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
7.1 本服务在提供智能交互功能时需要调用第三方服务包括但不限于语音活动检测服务VAD语音识别服务ASR大语言模型服务LLM语音合成服务TTS视觉模型服务短信验证码服务MQTT消息代理服务数据库服务
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
7.2 上述第三方服务提供商将按照其各自的隐私政策对数据进行处理我们建议您在使用本服务前了解相关第三方服务商的隐私政策
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
Cookie及类似技术的使用
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
8.1 本服务可能使用Cookie及类似技术来保存您的登录状态记录您的偏好设置等以便为您提供更好的使用体验
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
8.2 您可以通过浏览器设置管理Cookie但请注意如果禁用Cookie可能会影响本服务的部分功能
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
隐私政策的变更
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
9.1 我们可能会根据业务调整法律法规变化等原因对本隐私政策进行修订修订后的隐私政策将通过本平台公告等方式予以发布
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
9.2 如您在隐私政策修订后继续使用本服务则视为您已接受修订后的隐私政策
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
信息安全提示
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
10.1 请勿在与AI的语音或文字交互中透露您的财产账户银行卡号信用卡号密码身份证号等敏感个人信息否则由此带来的损失由您自行承担
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
10.2 请妥善保管您的账号和密码不要将账号信息告知他人或与他人共享账号
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
10.3 如您发现个人信息可能被泄露请及时联系运营者采取措施
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[30rpx] text-[#1a1a1a] font-bold">
十一适用法律
</text>
</view>
<view class="mb-[16rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
11.1 本隐私政策的制定解释执行及争议解决均适用中华人民共和国法律法规为本隐私政策之目的不包括港澳台地区法律
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
11.2 如本隐私政策的任何条款与中华人民共和国现行法律法规相抵触以法律法规的规定为准其余条款仍然有效
</text>
</view>
</view>
</scroll-view>
</view>
</template>
@@ -0,0 +1,539 @@
<route lang="jsonc" type="page">
{
"layout": "default",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "User Agreement"
}
}
</route>
<script lang="ts" setup>
import { t } from '@/i18n'
function goBack() {
uni.navigateBack()
}
</script>
<template>
<view class="h-screen flex flex-col bg-[#f5f8fd]">
<wd-navbar
:title="t('login.userAgreement')"
safe-area-inset-top
left-arrow
:bordered="false"
@click-left="goBack"
>
<template #left>
<wd-icon name="arrow-left" size="18" />
</template>
</wd-navbar>
<scroll-view scroll-y class="box-border flex-1 px-[30rpx] py-[30rpx]">
<view class="rounded-[16rpx] bg-white px-[30rpx] py-[40rpx] shadow-[0_4rpx_20rpx_rgba(0,0,0,0.05)]">
<view class="mb-[40rpx] text-center">
<text class="text-[36rpx] text-[#1a1a1a] font-bold">
User Agreement
</text>
</view>
<view class="mb-[40rpx] text-center">
<text class="text-[24rpx] text-[#666666]">
Last Updated: March 10, 2026
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
Important Notice
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
Welcome to use the XiaoZhi Backend Service (hereinafter referred to as "the Service"). The Service is designed to provide backend service support for XiaoZhi AI hardware devices, including but not limited to intelligent voice interaction, visual understanding, intent recognition, conversation memory, knowledge base Q&A, device management, and agent management. The operator of this Service is the actual deployer and administrator of the Service (hereinafter referred to as "Operator" or "we").
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
Before registering, logging in, or using the Service, please carefully read and fully understand all terms of this Agreement, especially the terms that are highlighted in bold and may have significant impact on your interests, including but not limited to disclaimers and liability limitations.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
When you complete registration, login, or otherwise use the Service following the on-screen instructions, it signifies that you have fully read, understood, and accepted all contents of this Agreement. If you disagree with this Agreement or any of its terms, please stop using the Service immediately.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
This Agreement may be updated and revised based on actual circumstances. The revised Agreement will be published through the Service's announcement mechanisms. The revised Agreement shall take effect from the date of publication. If you continue to use the Service after the Agreement revision, it shall be deemed that you have accepted the revised Agreement.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
I. Definitions
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
XiaoZhi Backend Service: refers to the XiaoZhi Backend Service and its backend service system, including but not limited to the Admin Console (management backend), API interfaces, WebSocket communication services, etc.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
Admin Console: refers to the web management interface provided by the Service for device management, agent configuration, user management, and other functions.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
User: refers to natural persons, legal persons, or other organizations who register or otherwise use the Service, hereinafter referred to as "you".
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
Intelligent Agent (Agent): refers to an AI character created and configured within the Service, containing a collection of capabilities such as language models, speech recognition, speech synthesis, visual understanding, intent recognition, conversation memory, and knowledge base.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
Device: refers to ESP32 and other hardware devices connected and managed through the Service.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
II. Scope of Agreement
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
This Agreement is a legal agreement between you and the Operator regarding your use of the Service. This Agreement applies to all your actions when using the Service through the Admin Console or other means.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
The source code of the Service follows the corresponding open-source license. This Agreement governs your use of the Service and does not affect the rights granted to you by the open-source license itself.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
III. Account Registration and Use
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.1 User Eligibility: You confirm that before using the Service, you should have the capacity for civil conduct as required by the laws of the People's Republic of China. If you are a minor, you should use the Service under the company and guidance of your legal guardian.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.2 Account Registration: You may register and log in to the Service through mobile phone verification code, username/password, or other methods. You shall provide true, accurate, and complete registration information and update it promptly.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.3 Account Security: You shall properly keep your account and password safe. All consequences arising from account leakage or unauthorized use by others due to your reasons shall be borne by you. If you discover any security risks with your account, please contact the Operator immediately.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.4 Account Usage Restrictions: Your account is for your personal use only. Without the Operator's consent, you shall not transfer, rent, or lend your account to any third party in any way.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.5 Account Cancellation: If you need to cancel your account, you may do so through the account settings in the Admin Console or contact the Operator for processing. After account cancellation, related data will be deleted and cannot be recovered. Please proceed with caution.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
IV. Service Content
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.1 Service Overview: Relying on artificial intelligence technology, the Service provides intelligent interaction services for connected hardware devices through API calls to third-party generative AI models, including but not limited to Voice Activity Detection (VAD), Automatic Speech Recognition (ASR), Large Language Model (LLM), Text-to-Speech (TTS), Vision Large Language Model (VLLM), Intent Recognition, Conversation Memory, and Retrieval-Augmented Generation (RAG).
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.2 Agent Management: You can create and configure Agents through the Admin Console, including setting role templates, selecting language models, configuring voice parameters, setting intent recognition rules, managing knowledge bases, enabling conversation memory, configuring Agent plugins, etc. The Agent settings shall not violate national laws and regulations.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.3 Device Management: You can bind and manage your hardware devices through the Admin Console, perform device configuration, firmware updates, OTA remote upgrades, etc. You shall ensure that the devices you manage are lawfully owned or authorized by you.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.4 Visual Understanding: The Service supports image capture through device cameras and uses third-party vision models for image recognition and scene understanding. You shall ensure that image capture complies with relevant laws and regulations and does not infringe upon others' privacy.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.5 Intent Recognition: The Service can understand the intent of user commands through intent recognition and trigger corresponding operations or services, such as controlling smart devices, querying information, etc.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.6 Conversation Memory: The Service can record and store summaries of your interaction history with the Agent to provide more coherent and personalized interaction experiences in subsequent conversations. You can manage or clear memory data in the Admin Console.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.7 Knowledge Base: The Service supports uploading documents, texts, and other content to build knowledge bases. Agents can retrieve knowledge base content during conversations to provide Q&A services. You shall ensure that uploaded knowledge base content does not infringe upon third-party intellectual property rights and does not contain content that violates laws and regulations.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.8 Voice Print Recognition: The Service supports voice print recognition. You can register voice print samples to achieve speaker identity verification and personalized services. Voice print data will be used for identity verification purposes.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.9 Voice Cloning: The Service supports voice cloning. You can clone custom voice timbres by providing audio samples. Cloned voices are for your personal use only and shall not be used to impersonate others or engage in illegal activities.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.10 MCP Endpoints: the Service supports MCP (Model Context Protocol) and can serve as an MCP Server to provide tool calling capabilities to external systems, or as an MCP Client to call external MCP services, achieving standardized integration with external systems.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.11 Context Providers: The Service supports Context Provider functionality, which can obtain real-time information from external data sources, such as weather, news, stocks, etc., providing Agents with richer knowledge sources.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.12 MQTT Protocol: The Service supports MQTT (Message Queuing Telemetry Transport) protocol for message communication between IoT devices. You can use the MQTT protocol to send device control commands, monitor device status, etc.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.13 Service Disclaimer: The conversations and responses generated by third-party AI models that the Service depends on are for reference only and do not constitute professional advice in any field. You shall not use the output content as professional advice in medical, legal, financial, or other professional fields. Any judgments or decisions you make based on the output content shall be entirely at your own responsibility.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.14 Service Fees: The Service is a free open-source project and does not charge any usage fees from users. It only provides platform capabilities for calling third-party services and does not involve any paid functions or value-added services.
</text>
</view>
<view class="mb-[24rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
The Service supports multiple AI service providers with different billing models as follows:
</text>
</view>
<view class="mb-[16rpx] ml-[60rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(1) Free Services: Some service providers offer free quotas or completely free services, such as Zhipu AI (free models like glm-4-flash), Microsoft EdgeTTS voice synthesis, etc.
</text>
</view>
<view class="mb-[16rpx] ml-[60rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(2) Pay-as-you-go: Some service providers charge based on API usage, such as OpenAI, Alibaba Cloud Intelligent Speech, Baidu Wenxin, iFlytek, etc. You need to pay the corresponding fees to third-party service providers yourself. Such fees are unrelated to this Service.
</text>
</view>
<view class="mb-[40rpx] ml-[60rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(3) Local Deployment: The Service supports fully local AI deployment solutions, such as FunASR speech recognition, FishSpeech voice synthesis, Ollama local large models, etc. Local deployment does not require network fees or API usage fees but requires sufficient local computing resources.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.15 Service Changes: The Operator reserves the right to adjust, upgrade, or terminate the Service content as actual circumstances require, and will provide notice as far in advance as possible.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
V. User Conduct Standards
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5.1 Legal Use: When using the Service, you shall comply with the laws and regulations of the People's Republic of China and relevant international treaties, and shall not use the Service for any illegal activities.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5.2 Prohibited Conduct: You promise not to engage in the following behaviors when using the Service:
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(1) Publishing, transmitting, or storing content that endangers national security or social stability, including but not limited to content involving subverting state power, damaging national honor and interests, or inciting ethnic hatred.
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(2) Publishing, transmitting, or storing content that infringes upon the legitimate rights and interests of others, including but not limited to infringing upon intellectual property rights, privacy rights, reputation rights, etc.
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(3) Publishing, transmitting, or storing obscene, pornographic, violent, terrorist, or crime-inciting content.
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(4) Publishing false information, spreading rumors, or engaging in fraudulent activities.
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(5) Interfering with the normal operation of the Service in any way, including but not limited to attacking servers, spreading malicious programs, or malicious consuming system resources.
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(6) Unauthorized reverse engineering, decompiling, or attempting to obtain system source code of the Service (this clause does not limit your lawful use of open-source code according to the open-source license).
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(7) Using the Service for automated batch operations, malicious consuming API call quotas or other system resources.
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(8) Entering content that violates laws, regulations, or public order and good customs in Agent settings.
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(9) Using the Service to harm or attempt to harm minors.
</text>
</view>
<view class="mb-[40rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(10) Other behaviors that violate laws, regulations, this Agreement, or may harm the legitimate rights and interests of the Operator or third parties.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5.3 Handling of Violations: If you violate the above conduct standards, the Operator has the right to take measures such as warnings, feature restrictions, service suspension, or account banning based on the severity of the situation, and reserves the right to pursue legal liability.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
VI. Intellectual Property
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
6.1 Open-source License: The Service is an open-source project, and its source code follows the corresponding open-source license. You may use the relevant source code in compliance with the license terms.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
6.2 Service Content Ownership: Except for open-source code, the intellectual property rights of interface designs, icons, copy, and other independently created content in the Service belong to the Operator.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
6.3 User Content: The intellectual property rights of content you input, upload, or generate through the Service shall be determined according to relevant laws and regulations. You grant the Operator the right to store and process your input content for the purpose of maintaining and improving the Service.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
6.4 Third-party Rights: When using the Service, you should respect the intellectual property rights and other legitimate rights of third parties. You shall bear all responsibilities for any disputes arising from your infringement of third-party rights.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
VII. Privacy Protection
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
7.1 Information Collection: To provide the Service, the Operator may collect your registration information (such as phone number, username), device information, usage logs, etc. For specific personal information collection and usage rules, please refer to the Privacy Policy.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
7.2 Information Protection: The Operator will take reasonable technical and management measures to protect the security of your personal information. However, due to the openness of the Internet, the Operator cannot absolutely guarantee information security. Please pay attention to protecting your personal sensitive information.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
7.3 Information Security Warning: Please do not disclose your sensitive personal information such as property accounts, bank card numbers, passwords, etc. to AI during your use of the Service. Any losses resulting from this shall be borne by you.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
VIII. Disclaimer
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
8.1 AI-Generated Content: The Service relies on third-party AI models to provide intelligent interaction capabilities. The content generated by AI may be inaccurate, incomplete, or inappropriate. The Operator does not make any guarantees regarding the authenticity, accuracy, or completeness of AI-generated content.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
8.2 Service Interruption: The Operator shall not be responsible for service interruptions or abnormalities caused by the following reasons:
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(1) Force majeure factors, such as natural disasters, pandemics, wars, etc.
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(2) Public service factors such as power supply failures or communication network failures.
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(3) Service failures or policy adjustments of third-party AI model service providers.
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(4) Temporary service interruptions caused by system maintenance or upgrades.
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(5) Network security incidents such as hacker attacks or computer viruses.
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(6) Service adjustments due to laws, regulations, or government controls.
</text>
</view>
<view class="mb-[40rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(7) Other situations not caused by the Operator's fault.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
8.3 Third-party Services: The Service may involve third-party provided language models, speech recognition, speech synthesis, vision models, intent recognition, and other services. You should also comply with the terms of service of third parties when using them. For issues arising from third-party services, please contact the third party directly.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
8.4 Open-source Statement: The Service is an open-source project provided "as is". To the maximum extent permitted by law, the Operator does not make any express or implied guarantees regarding the merchantability, fitness for a particular purpose, or non-infringement of the Service.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
8.5 Special Open-source Software Statement: This project is open-source software. This Service has no commercial cooperation relationship with any third-party API service providers it connects to (including but not limited to platforms such as speech recognition, large language models, speech synthesis, etc.) and does not provide any form of guarantee for their service quality and capital security. This software does not host any account keys, does not participate in capital flow, and does not bear the risk of recharge capital losses.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
8.6 Security Warning: This project functions are not fully developed and have not passed network security assessment. Please do not use it in production environments. If you deploy this project for learning in a public network environment, you must take necessary protective measures, including but not limited to setting strong passwords, restricting access permissions, enabling HTTPS encrypted transmission, etc.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
IX. Protection of Minors
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
9.1 If you are a minor under 18 years of age, you should use the Service under the guidance and consent of your guardian.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
9.2 Guardians should strengthen supervision of minors using the Service, guide minors to use it reasonably, and avoid excessive reliance.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
9.3 Minors should pay attention to personal information protection when using the Service and avoid uploading or disclosing personal sensitive information.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
X. Agreement Termination
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
10.1 User Termination: You have the right to stop using the Service and cancel your account at any time.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
10.2 Operator Termination: The Operator has the right to terminate providing services to you under the following circumstances:
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(1) You violate the terms of this Agreement.
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(2) You use the Service for illegal activities.
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(3) As required by laws, regulations, or policies.
</text>
</view>
<view class="mb-[40rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
(4) The Operator decides to stop operating the Service.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
10.3 Post-termination Handling: After the Agreement is terminated, the Operator is not obligated to retain your account information and related data. The Operator still has the right to pursue your breach of contract liability during the validity period of the Agreement.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
XI. Governing Law and Dispute Resolution
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
11.1 The formation, effectiveness, interpretation, revision, termination, and dispute resolution of this Agreement are governed by the laws of the People's Republic of China.
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
11.2 Any disputes arising from this Agreement or the Service shall be resolved through friendly negotiation between both parties; if negotiation fails, either party has the right to bring a lawsuit to the people's court with jurisdiction at the Operator's location.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
11.3 If any provision of this Agreement is determined to be invalid or unenforceable, it does not affect the validity of the remaining provisions.
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
XII. Miscellaneous
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
12.1 This Agreement constitutes the complete agreement between you and the Operator regarding your use of the Service.
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
12.2 The Operator's failure to exercise or delay in exercising any rights under this Agreement does not constitute a waiver of such rights.
</text>
</view>
</view>
</scroll-view>
</view>
</template>
@@ -0,0 +1,542 @@
<route lang="jsonc" type="page">
{
"layout": "default",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "用户协议"
}
}
</route>
<script lang="ts" setup>
import { t } from '@/i18n'
function goBack() {
uni.navigateBack()
}
</script>
<template>
<view class="h-screen flex flex-col bg-[#f5f8fd]">
<wd-navbar
:title="t('login.userAgreement')"
safe-area-inset-top
left-arrow
:bordered="false"
@click-left="goBack"
>
<template #left>
<wd-icon name="arrow-left" size="18" />
</template>
</wd-navbar>
<scroll-view scroll-y class="box-border flex-1 px-[30rpx] py-[30rpx]">
<view class="rounded-[16rpx] bg-white px-[30rpx] py-[40rpx] shadow-[0_4rpx_20rpx_rgba(0,0,0,0.05)]">
<view class="mb-[40rpx] text-center">
<text class="text-[36rpx] text-[#1a1a1a] font-bold">
{{ t('login.userAgreement') }}
</text>
</view>
<view class="mb-[40rpx] text-center">
<text class="text-[24rpx] text-[#666666]">
更新日期2026年3月10日
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
提示条款
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
欢迎您使用小智后端服务以下简称"本服务"本服务旨在为小智AI硬件设备提供后端服务支持包括但不限于智能语音交互视觉理解意图识别对话记忆知识库问答设备管理智能体管理等功能本服务的运营方为本服务的实际部署者和管理者以下简称"运营者""我们"
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
在您注册登录或使用本服务之前请您务必审慎阅读充分理解本协议各条款内容特别是以加粗形式提示您注意的可能与您利益有重大关系的条款包括但不限于免责声明责任限制等条款
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
当您按照页面提示完成注册登录或以其他方式使用本服务时即表示您已充分阅读理解并接受本协议的全部内容如果您不同意本协议或其中任何条款请立即停止使用本服务
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
本协议可能根据实际情况进行更新和修订修订后的协议将通过本平台公告等方式予以公示修订后的协议自公示之日起生效如您在协议修订后继续使用本服务则视为您已接受修订后的协议
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
定义
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
小智后端服务指小智后端服务及其部署运行的后端服务系统包括但不限于智控台管理后台API接口WebSocket通信服务等
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
智控台指本服务提供的Web管理界面用于设备管理智能体配置用户管理等功能
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
用户指通过注册或其他方式使用本服务的自然人法人或其他组织以下简称"您"
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
智能体指在本服务中创建和配置的AI角色包含语言模型语音识别语音合成视觉理解意图识别对话记忆知识库等能力的集合
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
设备指通过本服务进行连接和管理的ESP32等硬件设备
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
协议范围
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
本协议是您与运营者之间关于使用本服务的法律协议本协议适用于您通过智控台或其他方式使用本服务的全部行为
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
本服务的源代码遵循相应的开源许可证本协议约束您对本服务的使用行为不影响开源许可证本身赋予您的权利
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
账号注册与使用
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.1 用户资格您确认在使用本服务前您应当具备中华人民共和国法律规定的与您行为相适应的民事行为能力若您为未成年人应在法定监护人的陪同和指导下使用本服务
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.2 账号注册您可通过手机号验证码或用户名密码等方式注册和登录本服务您应当提供真实准确完整的注册信息并及时更新
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.3 账号安全您应妥善保管您的账号和密码因您的原因导致账号泄露或被他人冒用所产生的一切后果由您自行承担如发现账号存在安全隐患请立即联系运营者
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.4 账号使用限制您的账号仅限您本人使用未经运营者同意不得以任何方式将账号转让出租或借给第三方使用
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3.5 账号注销如您需要注销账号可通过智控台的账号设置功能进行操作或联系运营者进行处理账号注销后相关数据将被删除且无法恢复请谨慎操作
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
服务内容
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.1 服务概述本服务依托人工智能技术通过API接口调用第三方生成式人工智能模型为连接的硬件设备提供智能交互服务包括但不限于语音活动检测VAD语音识别ASR语义理解LLM语音合成TTS视觉理解VLLM意图识别Intent对话记忆Memory知识库RAG
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.2 智能体管理您可通过智控台创建和配置智能体包括设定角色模板选择语言模型配置语音参数设置意图识别规则管理知识库开启对话记忆配置智能体插件等智能体的设定内容不得违反国家法律法规
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.3 设备管理您可通过智控台绑定和管理您的硬件设备进行设备配置固件更新OTA远程升级等操作您应确保所管理的设备为您合法拥有或已获得授权
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.4 视觉理解本服务支持通过设备摄像头采集图像调用第三方视觉模型进行图像识别和场景理解您应确保采集图像的行为符合相关法律法规不得侵犯他人隐私
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.5 意图识别本服务可通过意图识别功能理解用户指令的意图并据此触发相应的操作或服务如控制智能设备查询信息等
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.6 对话记忆本服务可记录和存储您与智能体的交互历史摘要以便在后续对话中提供更连贯个性化的交互体验您可在智控台管理或清除记忆数据
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.7 知识库本服务支持您上传文档文本等内容构建知识库智能体可在对话中检索知识库内容为您提供问答服务您应确保上传的知识库内容不侵犯第三方知识产权且不包含违反法律法规的内容
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.8 声纹识别本服务支持声纹识别功能您可注册声纹样本以实现说话人身份验证和个性化服务声纹数据将用于身份验证目的
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.9 语音克隆本服务支持语音克隆功能您可通过提供音频样本克隆自定义音色克隆音色仅供您本人使用不得用于仿冒他人或从事违法违规活动
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.10 MCP接入点本服务支持MCPModel Context Protocol协议可作为MCP Server向外部提供工具调用能力也可作为MCP Client调用外部MCP服务实现与外部系统的标准化集成
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.11 上下文提供者本服务支持上下文提供者Context Provider功能可从外部数据源获取实时信息如天气新闻股票等为智能体提供更丰富的知识来源
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.12 MQTT协议本服务支持MQTTMessage Queuing Telemetry Transport协议用于与物联网设备之间的消息通信您可通过MQTT协议实现设备控制指令下发设备状态监控等功能
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.13 服务说明本服务所依赖的第三方AI模型生成的对话答复内容仅供参考不构成任何专业建议您不得将输出内容作为医疗法律金融等领域的专业建议您根据输出内容所作的任何判断或决策由您自行承担全部责任
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.14 服务费用本服务为免费开源项目不向用户收取任何使用费用仅提供调用第三方服务的平台能力不涉及任何付费功能或增值服务
</text>
</view>
<view class="mb-[24rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
本服务支持多种AI服务提供商不同提供商的收费模式如下
</text>
</view>
<view class="mb-[16rpx] ml-[60rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1免费服务部分服务商提供免费额度或完全免费的服务如智谱AIglm-4-flash等免费模型微软EdgeTTS语音合成等
</text>
</view>
<view class="mb-[16rpx] ml-[60rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
2按量付费服务部分服务商按API调用量计费如OpenAI阿里云智能语音百度文心讯飞等您需要自行向第三方服务商支付相应费用该等费用与本服务无关
</text>
</view>
<view class="mb-[40rpx] ml-[60rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3本地部署方案本服务支持完全本地部署的AI方案如FunASR语音识别FishSpeech语音合成Ollama本地大模型等本地部署无需网络费用和API调用费用但需要本地具备足够的计算资源
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4.15 服务变更运营者保留根据实际情况对服务内容进行调整升级或终止的权利并将尽可能提前予以通知
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
用户行为规范
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5.1 合法使用您在使用本服务时应遵守中华人民共和国的法律法规及相关国际条约不得利用本服务从事任何违法违规活动
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5.2 禁止行为您承诺在使用本服务时不得实施以下行为
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1发布传输存储危害国家安全社会稳定的内容包括但不限于涉及颠覆国家政权损害国家荣誉和利益煽动民族仇恨等内容
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
2发布传输存储侵犯他人合法权益的内容包括但不限于侵犯知识产权隐私权名誉权等
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3发布传输存储淫秽色情暴力恐怖或教唆犯罪的内容
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4发布虚假信息散布谣言或从事欺诈行为
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5以任何方式干扰本服务的正常运行包括但不限于攻击服务器传播恶意程序恶意消耗系统资源等
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
6未经授权对本服务进行反向工程反编译或其他试图获取系统源代码的行为本条不限制您依据开源许可证对开源代码的合法使用
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
7利用本服务进行自动化批量操作恶意消耗API调用配额或其他系统资源
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
8在智能体设定中输入违反法律法规或社会公序良俗的内容
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
9利用本服务伤害或企图伤害未成年人
</text>
</view>
<view class="mb-[40rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
10其他违反法律法规本协议约定或可能损害运营者及第三方合法权益的行为
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5.3 违规处理如您违反上述行为规范运营者有权视情节严重程度采取警告限制功能暂停服务封禁账号等措施并保留追究法律责任的权利
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
知识产权
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
6.1 开源许可本服务为开源项目其源代码遵循相应的开源许可证您可以在遵守该许可证条款的前提下使用相关源代码
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
6.2 服务内容权属除开源代码外本服务中的界面设计图标文案及其他运营者独立创作的内容其知识产权归运营者所有
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
6.3 用户内容您通过本服务输入上传或生成的内容其知识产权归属依照相关法律法规确定您授予运营者为维护和改进服务之目的对您输入内容进行存储和必要处理的权利
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
6.4 第三方权益您在使用本服务时应尊重第三方的知识产权及其他合法权益因您侵犯第三方权益而引发的纠纷由您自行承担全部责任
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
隐私保护
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
7.1 信息收集为提供本服务运营者可能会收集您的注册信息如手机号用户名设备信息使用日志等具体的个人信息收集和使用规则请参阅隐私政策
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
7.2 信息保护运营者将采取合理的技术和管理措施保护您的个人信息安全但由于互联网的开放性运营者不能绝对保证信息安全请您注意保护个人敏感信息
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
7.3 信息安全提示请勿在使用本服务过程中向AI透露您的财产账户银行卡密码等敏感个人信息否则由此带来的损失由您自行承担
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
免责声明
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
8.1 AI生成内容本服务依赖第三方AI模型提供智能交互能力AI生成的内容可能存在不准确不完整或不恰当之处运营者不对AI生成内容的真实性准确性完整性作任何保证
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
8.2 服务中断因以下原因导致服务中断或异常运营者不承担责任
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1不可抗力因素如自然灾害疫情战争等
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
2电力供应故障通信网络故障等公共服务因素
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3第三方AI模型服务商的服务故障或政策调整
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4系统维护升级导致的临时性服务中断
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
5黑客攻击计算机病毒等网络安全事件
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
6法律法规或政府管制导致的服务调整
</text>
</view>
<view class="mb-[40rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
7其他非运营者过错导致的情形
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
8.3 第三方服务本服务可能涉及第三方提供的语言模型语音识别语音合成视觉模型意图识别等服务您在使用时应同时遵守第三方的服务条款因第三方服务引发的问题请直接与第三方联系
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
8.4 开源声明本服务为开源项目"现状"提供在法律允许的最大范围内运营者不就本服务的适销性特定用途适用性或不侵权性作任何明示或暗示的保证
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
8.5 开源软件特别声明本项目为开源软件本服务与对接的任何第三方API服务商包括但不限于语音识别大模型语音合成等平台均不存在商业合作关系不为其服务质量及资金安全提供任何形式的担保本软件不托管任何账户密钥不参与资金流转不承担充值资金损失风险
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
8.6 安全警告本项目功能未完善且未通过网络安全测评请勿在生产环境中使用如果您在公网环境中部署学习本项目请务必做好必要的防护措施包括但不限于设置强密码限制访问权限启用HTTPS加密传输等
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
未成年人保护
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
9.1 若您是未满18周岁的未成年人应在监护人的指导和同意下使用本服务
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
9.2 监护人应加强对未成年人使用本服务的监督引导未成年人合理使用避免过度依赖
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
9.3 未成年人在使用本服务时应注意个人信息保护避免上传或透露个人敏感信息
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
协议终止
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
10.1 用户终止您有权随时停止使用本服务并注销账号
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
10.2 运营者终止出现以下情形时运营者有权终止向您提供服务
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
1您违反本协议的约定
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
2您利用本服务从事违法违规活动
</text>
</view>
<view class="mb-[16rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
3根据法律法规或政策要求
</text>
</view>
<view class="mb-[40rpx] ml-[40rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
4运营者决定停止运营本服务
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
10.3 终止后处理协议终止后运营者无义务保留您的账号信息及相关数据运营者仍有权依据本协议追究您在协议有效期内的违约责任
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
十一法律适用与争议解决
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
11.1 本协议的订立生效解释修订终止及争议解决均适用中华人民共和国法律
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
11.2 因本协议或本服务引发的争议双方应友好协商解决协商不成的任何一方有权向运营者所在地有管辖权的人民法院提起诉讼
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
11.3 本协议的任何条款被认定为无效或不可执行不影响其余条款的效力
</text>
</view>
<view class="mb-[32rpx]">
<text class="text-[28rpx] text-[#333333] font-semibold">
十二其他
</text>
</view>
<view class="mb-[24rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
12.1 本协议构成您与运营者之间关于使用本服务的完整协议
</text>
</view>
<view class="mb-[40rpx] ml-[20rpx]">
<text class="text-[28rpx] text-[#333333] leading-[1.8]">
12.2 运营者未行使或延迟行使本协议项下的任何权利不构成对该权利的放弃
</text>
</view>
</view>
</scroll-view>
</view>
</template>
<style lang="scss" scoped>
</style>
@@ -1,18 +1,21 @@
<route lang="jsonc" type="page">{
<route lang="jsonc" type="page">
{
"layout": "default",
"style": {
"navigationBarTitleText": "设置",
"navigationStyle": "custom"
}
}</route>
}
</route>
<script lang="ts" setup>
import { changeLanguage, getCurrentLanguage, getSupportedLanguages, t } from '@/i18n'
import type { Language } from '@/store/lang'
import { clearServerBaseUrlOverride, getEnvBaseUrl, getServerBaseUrlOverride, setServerBaseUrlOverride } from '@/utils'
import { isMp } from '@/utils/platform'
import { computed, onMounted, reactive, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { changeLanguage, getCurrentLanguage, getSupportedLanguages, t } from '@/i18n'
import { useConfigStore } from '@/store'
import { clearServerBaseUrlOverride, getEnvBaseUrl, getServerBaseUrlOverride, setServerBaseUrlOverride } from '@/utils'
import { isMp } from '@/utils/platform'
defineOptions({
name: 'SettingsPage',
@@ -27,6 +30,8 @@ const cacheInfo = reactive({
dataCache: '0MB',
})
const configStore = useConfigStore()
// 服务端地址设置
const baseUrlInput = ref('')
const urlError = ref('')
@@ -81,18 +86,26 @@ async function testServerBaseUrl() {
const response = await uni.request({
url: `${baseUrlInput.value}/api/ping`,
method: 'GET',
timeout: 3000
timeout: 3000,
})
if (response.statusCode === 200) {
return true
} else {
toast.error(t('message.invalidAddress'))
}
else {
toast.error({
msg: t('message.invalidAddress'),
duration: 3000,
})
return false
}
} catch (error) {
}
catch (error) {
console.error('测试服务端地址失败:', error)
toast.error(t('message.invalidAddress'))
toast.error({
msg: t('message.invalidAddress'),
duration: 3000,
})
return false
}
}
@@ -110,6 +123,20 @@ async function saveServerBaseUrl() {
return
}
setServerBaseUrlOverride(baseUrlInput.value)
// 处理config缓存无法更新的问题
uni.request({
url: `${getEnvBaseUrl()}/user/pub-config`,
method: 'GET',
success: (res: any) => {
if (res.statusCode === 200) {
configStore.setConfig(res.data.data)
uni.setStorageSync('config', res.data.data)
}
},
fail: (err) => {
console.error('获取SM2公钥失败:', err)
},
})
// 切换请求地址后清空所有缓存
clearAllCacheAfterUrlChange()
@@ -138,6 +165,7 @@ const showLanguageSheet = ref(false)
function handleLanguageChange(lang: Language) {
changeLanguage(lang)
showLanguageSheet.value = false
currentLanguage.value = lang
toast.success(t('settings.languageChanged'))
}
@@ -198,7 +226,8 @@ function clearAllCacheAfterUrlChange() {
// 重新获取缓存信息
getCacheInfo()
} catch (error) {
}
catch (error) {
console.error('清除缓存失败:', error)
}
}
@@ -223,7 +252,8 @@ async function clearCache() {
}
},
})
} catch (error) {
}
catch (error) {
console.error('清除缓存失败:', error)
toast.error(t('settings.clearCacheFailed'))
}
@@ -235,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'),
@@ -251,7 +281,7 @@ onMounted(async () => {
// 动态设置导航栏标题为国际化文本
uni.setNavigationBarTitle({
title: t('settings.title')
title: t('settings.title'),
})
})
</script>
@@ -269,8 +299,10 @@ onMounted(async () => {
</text>
</view>
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx] overflow-hidden"
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);">
<view
class="overflow-hidden border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);"
>
<view class="mb-[24rpx]">
<text class="text-[28rpx] text-[#232338] font-semibold">
{{ t('settings.serverApiUrl') }}
@@ -281,11 +313,13 @@ onMounted(async () => {
</view>
<view class="mb-[24rpx]">
<view class="w-full rounded-[16rpx] border border-[#eeeeee] bg-[#f5f7fb] overflow-hidden">
<wd-input v-model="baseUrlInput" type="text" clearable :maxlength="200"
<view class="w-full overflow-hidden border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb]">
<wd-input
v-model="baseUrlInput" type="text" clearable :maxlength="200"
:placeholder="t('settings.enterServerUrl')"
custom-class="!border-none !bg-transparent h-[64rpx] px-[24rpx] items-center"
input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl" />
input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl"
/>
</view>
<text v-if="urlError" class="mt-[8rpx] block text-[24rpx] text-[#ff4d4f]">
{{ urlError }}
@@ -293,14 +327,18 @@ onMounted(async () => {
</view>
<view class="flex gap-[16rpx]">
<wd-button type="primary"
<wd-button
type="primary"
custom-class="flex-1 h-[88rpx] rounded-[20rpx] text-[28rpx] font-semibold bg-[#336cff] border-none shadow-[0_4rpx_16rpx_rgba(51,108,255,0.3)] active:shadow-[0_2rpx_8rpx_rgba(51,108,255,0.4)] active:scale-98"
@click="saveServerBaseUrl">
@click="saveServerBaseUrl"
>
{{ t('settings.saveSettings') }}
</wd-button>
<wd-button type="default"
<wd-button
type="default"
custom-class="flex-1 h-[88rpx] rounded-[20rpx] text-[28rpx] font-semibold bg-white border-[#eeeeee] text-[#65686f] active:bg-[#f5f7fb]"
@click="resetServerBaseUrl">
@click="resetServerBaseUrl"
>
{{ t('settings.resetDefault') }}
</wd-button>
</view>
@@ -315,12 +353,15 @@ onMounted(async () => {
</text>
</view>
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);">
<view
class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);"
>
<view class="space-y-[16rpx]">
<!-- 缓存信息展示参考插件样式 -->
<view
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]">
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]"
>
<view>
<text class="text-[28rpx] text-[#232338] font-medium">
{{ t('settings.totalCacheSize') }}
@@ -336,7 +377,8 @@ onMounted(async () => {
<!-- 清除缓存按钮参考插件编辑按钮样式 -->
<view
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx]">
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx]"
>
<view>
<text class="text-[28rpx] text-[#232338] font-medium">
{{ t('settings.cacheClear') }}
@@ -347,7 +389,8 @@ onMounted(async () => {
</view>
<view
class="cursor-pointer rounded-[24rpx] bg-[rgba(255,107,107,0.1)] px-[28rpx] py-[16rpx] text-[24rpx] text-[#ff6b6b] font-semibold transition-all duration-300 active:scale-95 active:bg-[#ff6b6b] active:text-white"
@click="clearCache">
@click="clearCache"
>
{{ t('settings.clearCache') }}
</view>
</view>
@@ -363,11 +406,14 @@ onMounted(async () => {
</text>
</view>
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);">
<view
class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);"
>
<view
class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]"
@click="showAbout">
@click="showAbout"
>
<view>
<text class="text-[28rpx] text-[#232338] font-medium">
{{ t('settings.aboutUs') }}
@@ -389,11 +435,14 @@ onMounted(async () => {
</text>
</view>
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);">
<view
class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);"
>
<view
class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]"
@click="showLanguageSheet = true">
@click="showLanguageSheet = true"
>
<view>
<text class="text-[32rpx] text-[#232338] font-medium">
{{ t('settings.language') }}
@@ -403,8 +452,8 @@ onMounted(async () => {
</text>
</view>
<view class="flex items-center">
<text class="text-[32rpx] text-[#9d9ea3] font-semibold mr-[16rpx]">
{{supportedLanguages.find(lang => lang.code === currentLanguage)?.name}}
<text class="mr-[16rpx] text-[32rpx] text-[#9d9ea3] font-semibold">
{{ supportedLanguages.find(lang => lang.code === currentLanguage)?.name }}
</text>
<wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" />
</view>
@@ -416,8 +465,10 @@ onMounted(async () => {
<wd-action-sheet v-model="showLanguageSheet" :title="t('settings.selectLanguage')" :close-on-click-modal="true">
<view class="language-sheet">
<scroll-view scroll-y class="language-list">
<view v-for="lang in supportedLanguages" :key="lang.code" class="language-item"
@click="handleLanguageChange(lang.code)">
<view
v-for="lang in supportedLanguages" :key="lang.code" class="language-item"
@click="handleLanguageChange(lang.code)"
>
<text class="language-name">
{{ lang.name }}
</text>
@@ -3,22 +3,25 @@ import type { ChatHistory, CreateSpeakerData, VoicePrint } from '@/api/voiceprin
import { computed, onMounted, ref } from 'vue'
import { useMessage } from 'wot-design-uni'
import { useToast } from 'wot-design-uni/components/wd-toast'
import { createVoicePrint, deleteVoicePrint, getChatHistory, getVoicePrintList, updateVoicePrint } from '@/api/voiceprint'
import { createVoicePrint, deleteVoicePrint, getAudioDownloadId, getChatHistory, getVoicePrintList, updateVoicePrint } from '@/api/voiceprint'
import { t } from '@/i18n'
import { getEnvBaseUrl } from '@/utils'
defineOptions({
name: 'VoicePrintManage',
})
const props = withDefaults(defineProps<Props>(), {
agentId: 'default',
})
const emits = defineEmits(['update-refresher-enabled'])
// 接收props
interface Props {
agentId?: string
}
const props = withDefaults(defineProps<Props>(), {
agentId: 'default'
})
// 获取屏幕边界到安全区域距离
let safeAreaInsets: any
let systemInfo: any
@@ -50,6 +53,10 @@ const chatHistoryActions = ref<any[]>([])
const swipeStates = ref<Record<string, 'left' | 'close' | 'right'>>({})
const loading = ref(false)
// 音频播放相关
const audioRef = ref<UniApp.InnerAudioContext | null>(null)
const playingAudioId = ref<string>('')
// 使用传入的智能体ID
const currentAgentId = computed(() => {
return props.agentId
@@ -130,7 +137,6 @@ async function loadChatHistory() {
audioId: item.audioId,
index,
}))
showChatHistoryDialog.value = true
}
catch (error) {
console.error('获取对话记录失败:', error)
@@ -157,11 +163,13 @@ function openAddDialog() {
introduce: '',
}
showAddDialog.value = true
} catch (error: any) {
}
catch (error: any) {
// 捕捉声纹接口未配置错误
if (error.message && error.message.includes('请求错误[10054]')) {
toast.error(t('voiceprint.voiceprintInterfaceNotConfigured'))
} else {
}
else {
// 其他错误,继续打开弹窗
addForm.value = {
agentId: currentAgentId.value,
@@ -202,6 +210,11 @@ function selectAudioId({ item }: { item: any }) {
showChatHistoryDialog.value = false
}
// 点击选择
function handleItemClick(item: any) {
selectAudioId({ item })
}
// 提交添加说话人
async function submitAdd() {
if (!addForm.value.sourceName.trim()) {
@@ -274,14 +287,93 @@ async function handleDelete(id: string) {
})
}
// 播放音频
async function playAudio(audioId: string, event: Event) {
event.stopPropagation() // 阻止事件冒泡,防止关闭下拉框
if (!audioId) {
toast.warning(t('voiceprint.audioNotExist'))
return
}
// 如果正在播放同一个音频,则停止
if (playingAudioId.value === audioId) {
stopAudio()
return
}
// 停止之前的音频
stopAudio()
try {
// 先获取音频下载ID
playingAudioId.value = audioId
const downloadId = await getAudioDownloadId(audioId)
if (!downloadId) {
toast.error(t('voiceprint.getAudioFailed'))
playingAudioId.value = ''
return
}
// 获取baseURL
const baseURL = getEnvBaseUrl()
const audioUrl = `${baseURL}/agent/play/${downloadId}`
// 创建新的音频实例
audioRef.value = uni.createInnerAudioContext()
audioRef.value.src = audioUrl
audioRef.value.autoplay = true
// 监听播放结束
audioRef.value.onEnded(() => {
playingAudioId.value = ''
})
// 监听播放错误
audioRef.value.onError((error) => {
console.error('音频播放错误:', error)
toast.error(t('voiceprint.audioPlayFailed'))
playingAudioId.value = ''
})
}
catch (error) {
console.error('播放音频失败:', error)
toast.error(t('voiceprint.audioPlayFailed'))
playingAudioId.value = ''
}
}
// 停止音频
function stopAudio() {
if (audioRef.value) {
audioRef.value.stop()
audioRef.value.destroy()
audioRef.value = null
}
playingAudioId.value = ''
}
watch(() => [showAddDialog.value, showEditDialog.value], (newValues) => {
if (newValues.some((value: boolean) => value)) {
emits('update-refresher-enabled', false)
}
else {
emits('update-refresher-enabled', true)
}
})
onMounted(async () => {
// 智能体已简化为默认
loadVoicePrintList()
loadChatHistory()
})
// 暴露方法给父组件
defineExpose({
showAddDialog,
showEditDialog,
refresh,
})
</script>
@@ -349,7 +441,7 @@ defineExpose({
</view>
<!-- 浮动操作按钮 -->
<wd-fab type="primary" size="small" :draggable="true" :expandable="false" @click="openAddDialog">
<wd-fab custom-style="z-index:10" type="primary" size="small" :draggable="true" :expandable="false" @click="openAddDialog">
<wd-icon name="add" />
</wd-fab>
@@ -359,25 +451,24 @@ defineExpose({
<!-- 添加说话人弹窗 -->
<wd-popup
v-model="showAddDialog" position="center" custom-style="width: 90%; max-width: 400px; border-radius: 16px;"
v-model="showAddDialog"
position="center"
custom-style="width: 90%; max-width: 400px; border-radius: 16px;"
safe-area-inset-bottom
>
<view>
<view class="w-full flex items-center justify-between border-b-[2rpx] border-[#eeeeee] p-[32rpx_32rpx_24rpx]">
<text class="w-full text-center text-[32rpx] text-[#232338] font-semibold">
{{ t('voiceprint.addSpeaker') }}
</text>
</view>
<view class="p-[32rpx]">
<!-- 声纹向量选择 -->
<view class="mb-[32rpx]">
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* {{ t('voiceprint.voiceVector') }}
<text class="text-red">
*
</text>
{{ t('voiceprint.voiceVector') }}
</text>
<view
class="flex cursor-pointer items-center justify-between border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]"
@click="loadChatHistory"
@click="showChatHistoryDialog = true"
>
<text
class="m-r-[16rpx] flex-1 text-left text-[26rpx] text-[#232338]"
@@ -392,7 +483,10 @@ defineExpose({
<!-- 姓名 -->
<view class="mb-[32rpx]">
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* {{ t('voiceprint.name') }}
<text class="text-red">
*
</text>
{{ t('voiceprint.name') }}
</text>
<input
v-model="addForm.sourceName"
@@ -404,7 +498,10 @@ defineExpose({
<!-- 描述 -->
<view>
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* {{ t('voiceprint.description') }}
<text class="text-red">
*
</text>
{{ t('voiceprint.description') }}
</text>
<textarea
v-model="addForm.introduce" :maxlength="100" :placeholder="t('voiceprint.pleaseInputDescription')"
@@ -418,11 +515,11 @@ defineExpose({
<view class="flex gap-[16rpx] border-t-[2rpx] border-[#eeeeee] p-[24rpx_32rpx_32rpx]">
<wd-button type="info" custom-class="flex-1" @click="showAddDialog = false">
{{ t('voiceprint.cancel') }}
</wd-button>
<wd-button type="primary" custom-class="flex-1" @click="submitAdd">
{{ t('voiceprint.save') }}
</wd-button>
{{ t('voiceprint.cancel') }}
</wd-button>
<wd-button type="primary" custom-class="flex-1" @click="submitAdd">
{{ t('voiceprint.save') }}
</wd-button>
</view>
</view>
</wd-popup>
@@ -433,7 +530,7 @@ defineExpose({
safe-area-inset-bottom
>
<view>
<view class="w-full flex items-center justify-between border-b-[2rpx] border-[#eeeeee] p-[32rpx_32rpx_24rpx]">
<view class="box-border w-full flex items-center justify-between border-b-[2rpx] border-[#eeeeee] p-[32rpx_32rpx_24rpx]">
<text class="w-full text-center text-[32rpx] text-[#232338] font-semibold">
{{ t('voiceprint.editSpeaker') }}
</text>
@@ -443,11 +540,14 @@ defineExpose({
<!-- 声纹向量选择 -->
<view class="mb-[32rpx]">
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* {{ t('voiceprint.voiceVector') }}
<text class="text-red">
*
</text>
{{ t('voiceprint.voiceVector') }}
</text>
<view
class="flex cursor-pointer items-center justify-between border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]"
@click="loadChatHistory"
@click="showChatHistoryDialog = true"
>
<text
class="m-r-[16rpx] flex-1 text-left text-[26rpx] text-[#232338]"
@@ -462,7 +562,10 @@ defineExpose({
<!-- 姓名 -->
<view class="mb-[32rpx]">
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* {{ t('voiceprint.name') }}
<text class="text-red">
*
</text>
{{ t('voiceprint.name') }}
</text>
<input
v-model="editForm.sourceName"
@@ -474,7 +577,10 @@ defineExpose({
<!-- 描述 -->
<view>
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* {{ t('voiceprint.description') }}
<text class="text-red">
*
</text>
{{ t('voiceprint.description') }}
</text>
<textarea
v-model="editForm.introduce" :maxlength="100" :placeholder="t('voiceprint.pleaseInputDescription')"
@@ -488,23 +594,42 @@ defineExpose({
<view class="flex gap-[16rpx] border-t-[2rpx] border-[#eeeeee] p-[24rpx_32rpx_32rpx]">
<wd-button type="info" custom-class="flex-1" @click="showEditDialog = false">
{{ t('voiceprint.cancel') }}
</wd-button>
<wd-button type="primary" custom-class="flex-1" @click="submitEdit">
{{ t('voiceprint.save') }}
</wd-button>
{{ t('voiceprint.cancel') }}
</wd-button>
<wd-button type="primary" custom-class="flex-1" @click="submitEdit">
{{ t('voiceprint.save') }}
</wd-button>
</view>
</view>
</wd-popup>
<!-- 语音对话记录选择动作面板 -->
<wd-action-sheet
v-model="showChatHistoryDialog" :actions="chatHistoryActions" :title="t('voiceprint.selectVector')"
@select="selectAudioId"
/>
<!-- 自定义语音对话记录选择弹出层 -->
<wd-popup v-model="showChatHistoryDialog" class="custom-popup" position="bottom" @close="stopAudio">
<view class="rounded-[20rpx] bg-white pb-[20rpx] pt-[20rpx]">
<view class="max-h-[600rpx] overflow-y-auto rounded-[20rpx]">
<view
v-for="item in chatHistoryActions"
:key="item.audioId"
class="flex items-center justify-between border-b border-[#f5f5f5] p-[32rpx] transition-all active:bg-[#f5f7fb]"
@click="handleItemClick(item)"
>
<text class="flex-1 text-[28rpx] text-[#232338]">
{{ item.name }}
</text>
<view class="ml-[20rpx]" @click.stop="playAudio(item.audioId, $event)">
<wd-icon
:name="playingAudioId === item.audioId ? 'pause-circle' : 'play-circle'"
size="24px"
:custom-class="playingAudioId === item.audioId ? 'text-[#336cff]' : 'text-[#9d9ea3]'"
/>
</view>
</view>
</view>
</view>
</wd-popup>
</template>
<style scoped>
<style lang="scss" scoped>
.voiceprint-container {
position: relative;
}
@@ -523,14 +648,10 @@ defineExpose({
color: #666666;
}
:deep(.wd-swipe-action) {
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
border: 1rpx solid #eeeeee;
}
:deep(.flex-1) {
flex: 1;
::v-deep .custom-popup {
.wd-popup {
padding: 20rpx !important;
background: transparent;
}
}
</style>
+2
View File
@@ -15,5 +15,7 @@ export default store
export * from './config'
export * from './plugin'
export * from './provider'
export * from './speedPitch'
// 模块统一导出
export * from './user'
+23
View File
@@ -0,0 +1,23 @@
import type { Providers } from '@/api/agent/types'
import { defineStore } from 'pinia'
export const useProvider = defineStore('provider', () => {
const providers = ref<Providers[]>([])
const updateProviders = (val: Providers[]) => {
providers.value = val
}
return {
providers,
updateProviders,
}
}, {
persist: {
key: 'providers',
serializer: {
serialize: state => JSON.stringify(state.providers),
deserialize: value => ({ providers: JSON.parse(value) }),
},
},
})
@@ -0,0 +1,26 @@
import { defineStore } from 'pinia'
export const useSpeedPitch = defineStore('speedPitch', () => {
const speedPitch = ref({
ttsVolume: 0,
ttsRate: 0,
ttsPitch: 0,
})
const updateSpeedPitch = (val: typeof speedPitch.value) => {
speedPitch.value = val
}
return {
speedPitch,
updateSpeedPitch,
}
}, {
persist: {
key: 'speedPitch',
serializer: {
serialize: state => JSON.stringify(state.speedPitch),
deserialize: value => ({ speedPitch: JSON.parse(value) }),
},
},
})
+10 -11
View File
@@ -19,7 +19,7 @@ const userInfoState: UserInfo & { avatar?: string, token?: string } = {
}
export const useUserStore = defineStore(
'user',
'userInfo',
() => {
// 定义用户信息
const userInfo = ref<UserInfo & { avatar?: string, token?: string }>({ ...userInfoState })
@@ -51,15 +51,8 @@ export const useUserStore = defineStore(
*/
const getUserInfo = async () => {
const userData = await _getUserInfo()
const userInfoWithExtras = {
...userData,
avatar: userInfoState.avatar,
token: uni.getStorageSync('token') || '',
}
setUserInfo(userInfoWithExtras)
uni.setStorageSync('userInfo', userInfoWithExtras)
// TODO 这里可以增加获取用户路由的方法 根据用户的角色动态生成路由
return userInfoWithExtras
setUserInfo(userData)
return userData
}
/**
* 退出登录 并 删除用户信息
@@ -78,6 +71,12 @@ export const useUserStore = defineStore(
}
},
{
persist: true,
persist: {
key: 'userInfo',
serializer: {
serialize: state => JSON.stringify(state.userInfo),
deserialize: value => ({ userInfo: JSON.parse(value) }),
},
},
},
)
+79 -32
View File
@@ -1,4 +1,6 @@
import smCrypto from 'sm-crypto'
import { pages, subPackages } from '@/pages.json'
import { isMpWeixin } from './platform'
/**
@@ -197,23 +199,21 @@ export function getEnvBaseUploadUrl() {
return baseUploadUrl
}
import smCrypto from 'sm-crypto'
/**
* 生成SM2密钥对(十六进制格式)
* @returns {Object} 包含公钥和私钥的对象
*/
export function generateSm2KeyPairHex() {
// 使用sm-crypto库生成SM2密钥对
const sm2 = smCrypto.sm2;
const keypair = sm2.generateKeyPairHex();
return {
publicKey: keypair.publicKey,
privateKey: keypair.privateKey,
clientPublicKey: keypair.publicKey, // 客户端公钥
clientPrivateKey: keypair.privateKey // 客户端私钥
};
// 使用sm-crypto库生成SM2密钥对
const sm2 = smCrypto.sm2
const keypair = sm2.generateKeyPairHex()
return {
publicKey: keypair.publicKey,
privateKey: keypair.privateKey,
clientPublicKey: keypair.publicKey, // 客户端公钥
clientPrivateKey: keypair.privateKey, // 客户端私钥
}
}
/**
@@ -223,21 +223,21 @@ export function generateSm2KeyPairHex() {
* @returns {string} 加密后的密文(十六进制格式)
*/
export function sm2Encrypt(publicKey: string, plainText: string): string {
if (!publicKey) {
throw new Error('公钥不能为null或undefined');
}
if (!plainText) {
throw new Error('明文不能为空');
}
const sm2 = smCrypto.sm2;
// SM2加密,添加04前缀表示未压缩公钥
const encrypted = sm2.doEncrypt(plainText, publicKey, 1);
// 转换为十六进制格式(与后端保持一致,添加04前缀)
const result = "04" + encrypted;
return result;
if (!publicKey) {
throw new Error('公钥不能为null或undefined')
}
if (!plainText) {
throw new Error('明文不能为空')
}
const sm2 = smCrypto.sm2
// SM2加密,添加04前缀表示未压缩公钥
const encrypted = sm2.doEncrypt(plainText, publicKey, 1)
// 转换为十六进制格式(与后端保持一致,添加04前缀)
const result = `04${encrypted}`
return result
}
/**
@@ -247,9 +247,56 @@ export function sm2Encrypt(publicKey: string, plainText: string): string {
* @returns {string} 解密后的明文
*/
export function sm2Decrypt(privateKey: string, cipherText: string): string {
const sm2 = smCrypto.sm2;
// 移除04前缀(与后端保持一致)
const dataWithoutPrefix = cipherText.startsWith("04") ? cipherText.substring(2) : cipherText;
// SM2解密
return sm2.doDecrypt(dataWithoutPrefix, privateKey, 1);
const sm2 = smCrypto.sm2
// 移除04前缀(与后端保持一致)
const dataWithoutPrefix = cipherText.startsWith('04') ? cipherText.substring(2) : cipherText
// SM2解密
return sm2.doDecrypt(dataWithoutPrefix, privateKey, 1)
}
type AnyFunction = (...args: any[]) => any
interface DebouncedFunction extends AnyFunction {
cancel: () => void
}
/**
* 防抖函数
* @param fn 要防抖的函数
* @param delay 延迟时间(毫秒),默认500ms
* @param immediate 是否立即执行,默认false
* @returns 防抖处理后的函数
*/
export function debounce<T extends AnyFunction>(
fn: T,
delay = 500,
immediate = false,
): DebouncedFunction {
let timer: ReturnType<typeof setTimeout> | null = null
const debounced = function (this: any, ...args: Parameters<T>) {
if (timer) {
clearTimeout(timer)
}
if (immediate && !timer) {
fn.apply(this, args)
}
timer = setTimeout(() => {
if (!immediate) {
fn.apply(this, args)
}
timer = null
}, delay)
} as DebouncedFunction
debounced.cancel = () => {
if (timer) {
clearTimeout(timer)
timer = null
}
}
return debounced
}
+3 -1
View File
@@ -1 +1,3 @@
VUE_APP_TITLE=智控台
VUE_APP_TITLE=智控台
VUE_APP_DESCRIPTION=小智后端服务(xiaozhi-server)是由华南理工大学刘思源教授团队主导研发的智能终端软硬件体系后端服务系统,专为xiaozhi-esp32开源硬件打造,具备多协议兼容、声纹识别、知识库管理等核心能力。
VUE_APP_KEYWORDS=xiaozhi-server,小智服务端,智控台,AI硬件,智能硬件,AI玩具,情感陪伴,聊天机器人,智能家居,车载机器人
+2 -4
View File
@@ -5,6 +5,8 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="description" content="<%= process.env.VUE_APP_DESCRIPTION %>">
<meta name="keywords" content="<%= process.env.VUE_APP_KEYWORDS %>">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>
<%= 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) { %>
+10 -6
View File
@@ -28,17 +28,16 @@ nav {
}
.copyright {
text-align: center;
padding: 0 !important;
color: rgb(0, 0, 0);
font-size: 12px;
font-weight: 400;
margin-top: auto;
padding: 30px 0 20px;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.el-message {
@@ -60,6 +59,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) {
@@ -170,6 +170,9 @@ export default {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
callback(err);
})
.networkFail(() => {
RequestService.reAjaxFun(() => {
this.getAgentMcpAccessAddress(agentId, callback);
@@ -21,11 +21,35 @@
<div v-if="message.type === 'time'" class="time-divider">
{{ message.content }}
</div>
<div v-else class="message-item" :class="{ 'user-message': message.chatType === 1 }">
<div v-else class="message-item" :class="{ 'user-message': message.chatType === 1, 'tool-message': message.chatType === 3 }">
<img :src="message.chatType === 1 ? getUserAvatar(currentSessionId) : require('@/assets/xiaozhi-logo.png')"
class="avatar" />
<div class="message-content">
{{ extractContentFromString(message.content) }}
<template v-if="Array.isArray(extractContentFromString(message.content))">
<div class="content-wrapper">
<div v-for="(item, idx) in extractContentFromString(message.content)" :key="idx">
<div v-if="item.type === 'text'" class="text-content">{{ item.text }}</div>
<div v-else-if="item.type === 'tool'" class="tool-call-text">{{ item.text }}</div>
<div v-else-if="item.type === 'tool_result'" class="tool-call-text">
<div v-if="item.text && item.text.length > 80" class="tool-result-wrapper">
<div v-if="isToolResultCollapsed(index, idx)" class="tool-result-collapsed">
{{ getFirstLineText(item.text) }}
</div>
<div v-else class="tool-result-expanded">
{{ item.text }}
</div>
<span class="tool-toggle-btn" @click="toggleToolResult(index, idx)">
<i :class="isToolResultCollapsed(index, idx) ? 'el-icon-arrow-down' : 'el-icon-arrow-up'"></i>
</span>
</div>
<div v-else>{{ item.text }}</div>
</div>
</div>
</div>
</template>
<template v-else>
{{ extractContentFromString(message.content) }}
</template>
<i v-if="message.audioId" :class="getAudioIconClass(message)"
@click="playAudio(message)" class="audio-icon"></i>
</div>
@@ -49,6 +73,7 @@
</template>
<script>
import { debounce } from '@/utils'
import Api from '@/apis/api';
export default {
@@ -81,7 +106,8 @@ export default {
scrollTimer: null,
isFirstLoad: true,
playingAudioId: null,
audioElement: null
audioElement: null,
expandedToolResults: {} // 跟踪工具结果的展开状态
};
},
watch: {
@@ -90,6 +116,10 @@ export default {
if (val) {
this.resetData();
this.loadSessions();
} else {
this.audioElement?.pause();
this.audioElement = null;
this.playingAudioId = null;
}
},
dialogVisible(val) {
@@ -154,6 +184,13 @@ export default {
// 尝试解析为 JSON
try {
const jsonObj = JSON.parse(content);
// 如果是数组格式(包含 text 和 tool)
if (Array.isArray(jsonObj)) {
return jsonObj;
}
// 如果是对象且有 content 字段
if (jsonObj && typeof jsonObj === 'object' && jsonObj.content) {
return jsonObj.content;
}
@@ -164,6 +201,23 @@ export default {
// 如果不是 JSON 格式或没有 content 字段,直接返回原内容
return content;
},
// 切换工具结果的展开/折叠状态
toggleToolResult(messageIndex, itemIndex) {
const key = `${messageIndex}-${itemIndex}`;
this.$set(this.expandedToolResults, key, !this.expandedToolResults[key]);
},
// 判断工具结果是否处于折叠状态
isToolResultCollapsed(messageIndex, itemIndex) {
const key = `${messageIndex}-${itemIndex}`;
// 默认折叠(true表示折叠)
return !this.expandedToolResults[key];
},
// 获取截断的文本(只显示第一行)
getFirstLineText(text) {
if (!text) return '';
const firstLine = text.split('\n')[0];
return firstLine.length < text.length ? firstLine + '...' : text;
},
resetData() {
this.sessions = [];
this.messages = [];
@@ -173,6 +227,7 @@ export default {
this.loading = false;
this.hasMore = true;
this.isFirstLoad = true;
this.expandedToolResults = {};
},
handleClose() {
this.dialogVisible = false;
@@ -255,7 +310,7 @@ export default {
}
return 'el-icon-video-play';
},
playAudio(message) {
playAudio: debounce(function(message) {
if (this.playingAudioId === message.audioId) {
// 如果正在播放当前音频,则停止播放
if (this.audioElement) {
@@ -276,9 +331,12 @@ export default {
this.playingAudioId = message.audioId;
Api.agent.getAudioId(message.audioId, (res) => {
if (res.data && res.data.data) {
if (!this.audioElement) {
this.audioElement = new Audio();
}
// 使用获取到的下载ID播放音频
this.audioElement = new Audio(Api.getServiceUrl() + `/agent/play/${res.data.data}`);
this.audioElement.src = Api.getServiceUrl() + `/agent/play/${res.data.data}`;
this.audioElement.onended = () => {
this.playingAudioId = null;
this.audioElement = null;
@@ -287,7 +345,7 @@ export default {
this.audioElement.play();
}
});
},
}, 300),
getUserAvatar(sessionId) {
// 从 sessionId 中提取所有数字
const numbers = sessionId.match(/\d+/g);
@@ -442,6 +500,56 @@ export default {
color: white;
}
.content-wrapper {
width: 100%;
}
.text-content {
display: block;
margin-bottom: 4px;
}
.tool-call-text {
color: #1890ff;
font-family: 'Courier New', monospace;
font-weight: 500;
font-size: 12px;
display: block;
margin-top: 4px;
}
.user-message .tool-call-text {
color: #e6f7ff;
}
.tool-message .message-content {
background-color: #f0f0f0;
}
.tool-result-wrapper {
position: relative;
padding-right: 20px;
}
.tool-result-collapsed {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tool-toggle-btn {
position: absolute;
right: 0;
top: 0;
cursor: pointer;
color: #1890ff;
font-size: 12px;
}
.tool-toggle-btn:hover {
color: #40a9ff;
}
.loading,
.no-more {
text-align: center;
@@ -314,7 +314,7 @@ export default {
if (res.data.code === 0) {
this.mcpUrl = res.data.data || "";
} else {
this.mcpUrl = "";
this.mcpUrl = res.data.msg;
console.error('获取MCP地址失败:', res.data.msg);
}
});
+13 -34
View File
@@ -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;
+76 -11
View File
@@ -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;
}
@@ -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>
@@ -25,6 +25,11 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('voiceClone.languages')" prop="languages">
<el-input v-model="form.languages" :placeholder="$t('voiceClone.languagesPlaceholder')" style="width: 100%">
</el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
@@ -70,6 +75,9 @@ export default {
],
userId: [
{ required: true, message: this.$t('voiceClone.userIdRequired'), trigger: 'change' }
],
languages: [
{ required: true, message: this.$t('voiceClone.languagesRequired'), trigger: 'blur' }
]
}
}
+3
View File
@@ -1132,6 +1132,9 @@ export default {
'voiceClone.platformNamePlaceholder': 'Bitte Plattformname auswählen',
'voiceClone.voiceIdPlaceholder': 'Bitte Stimmen-ID eingeben und Enter drücken',
'voiceClone.userIdPlaceholder': 'Bitte Schlüsselwort eingeben, um Kontoinhaber auszuwählen',
'voiceClone.languages': 'Sprachen',
'voiceClone.languagesPlaceholder': 'Bitte Sprachen eingeben, z.B.: Deutsch, English',
'voiceClone.languagesRequired': 'Bitte Sprachen eingeben',
'voiceClone.waitingUpload': 'Wartet auf Upload',
'voiceClone.waitingTraining': 'Wartet auf Klon',
'voiceClone.training': 'Training',
+3
View File
@@ -1132,6 +1132,9 @@ export default {
'voiceClone.platformNamePlaceholder': 'Please select platform name',
'voiceClone.voiceIdPlaceholder': 'Please enter voice ID and press Enter',
'voiceClone.userIdPlaceholder': 'Please enter keyword to select account owner',
'voiceClone.languages': 'Languages',
'voiceClone.languagesPlaceholder': 'Please enter languages, e.g.: Chinese, English',
'voiceClone.languagesRequired': 'Please enter languages',
'voiceClone.waitingUpload': 'Waiting for upload',
'voiceClone.waitingTraining': 'Waiting for clone',
'voiceClone.training': 'Training',
+6 -3
View File
@@ -670,10 +670,10 @@ export default {
'home.wish': 'Vamos ter um dia maravilhoso!',
'home.languageModel': 'LLM',
'home.voiceModel': 'TTS',
'home.configureRole': 'Configurar Papel',
'home.voiceprintRecognition': 'Impressão Vocal',
'home.configureRole': 'ConfRoles',
'home.voiceprintRecognition': 'RecVoz',
'home.deviceManagement': 'Dispositivos',
'home.chatHistory': 'Histórico de Chat',
'home.chatHistory': 'Conversa',
'home.lastConversation': 'Última Conversa',
'home.noConversation': 'Nenhuma conversa',
'home.justNow': 'Agora mesmo',
@@ -1132,6 +1132,9 @@ export default {
'voiceClone.platformNamePlaceholder': 'Por favor, selecione o nome da plataforma',
'voiceClone.voiceIdPlaceholder': 'Por favor, insira o ID da voz e pressione Enter',
'voiceClone.userIdPlaceholder': 'Por favor, insira palavra-chave para selecionar o proprietário da conta',
'voiceClone.languages': 'Idiomas',
'voiceClone.languagesPlaceholder': 'Por favor, insira idiomas, ex.: Português, English',
'voiceClone.languagesRequired': 'Por favor, insira idiomas',
'voiceClone.waitingUpload': 'Aguardando envio',
'voiceClone.waitingTraining': 'Aguardando clonagem',
'voiceClone.training': 'Treinando',
+3
View File
@@ -1132,6 +1132,9 @@ export default {
'voiceClone.platformNamePlaceholder': 'Vui lòng chọn tên nền tảng',
'voiceClone.voiceIdPlaceholder': 'Vui lòng nhập ID giọng nói và nhấn Enter',
'voiceClone.userIdPlaceholder': 'Vui lòng nhập từ khóa để chọn chủ sở hữu tài khoản',
'voiceClone.languages': 'Ngôn ngữ',
'voiceClone.languagesPlaceholder': 'Vui lòng nhập ngôn ngữ, ví dụ: Tiếng Việt, English',
'voiceClone.languagesRequired': 'Vui lòng nhập ngôn ngữ',
'voiceClone.waitingUpload': 'Đang chờ tải lên',
'voiceClone.waitingTraining': 'Đang chờ nhân bản',
'voiceClone.training': 'Đang huấn luyện',
+4 -1
View File
@@ -189,7 +189,7 @@ export default {
'editVoiceDialog.remarkPlaceholder': '请输入备注内容',
'editVoiceDialog.generatePreview': '生成试听',
'editVoiceDialog.defaultVoiceName': '湾湾小何',
'editVoiceDialog.defaultLanguageType': '中文',
'editVoiceDialog.defaultLanguageType': '普通话',
'editVoiceDialog.requiredVoiceCode': '请输入音色编码',
'editVoiceDialog.requiredVoiceName': '请输入音色名称',
@@ -1132,6 +1132,9 @@ export default {
'voiceClone.platformNamePlaceholder': '请选择平台名称',
'voiceClone.voiceIdPlaceholder': '请输入音色ID并按回车',
'voiceClone.userIdPlaceholder': '请输入关键词选择归属账号',
'voiceClone.languages': '语言',
'voiceClone.languagesPlaceholder': '请输入语言,如:中文、English',
'voiceClone.languagesRequired': '请输入语言',
'voiceClone.waitingUpload': '待上传',
'voiceClone.waitingTraining': '待复刻',
'voiceClone.training': '训练中',
+4 -1
View File
@@ -189,7 +189,7 @@ export default {
'editVoiceDialog.remarkPlaceholder': '請輸入備註內容',
'editVoiceDialog.generatePreview': '生成試聽',
'editVoiceDialog.defaultVoiceName': '灣灣小何',
'editVoiceDialog.defaultLanguageType': '中文',
'editVoiceDialog.defaultLanguageType': '普通話',
'editVoiceDialog.requiredVoiceCode': '請輸入音色編碼',
'editVoiceDialog.requiredVoiceName': '請輸入音色名稱',
@@ -1132,6 +1132,9 @@ export default {
'voiceClone.platformNamePlaceholder': '請選擇平台名稱',
'voiceClone.voiceIdPlaceholder': '請輸入音色ID並按回车',
'voiceClone.userIdPlaceholder': '請輸入关键词選擇歸屬帳號',
'voiceClone.languages': '語言',
'voiceClone.languagesPlaceholder': '請輸入語言,如:中文、English',
'voiceClone.languagesRequired': '請輸入語言',
'voiceClone.waitingUpload': '待上傳',
'voiceClone.waitingTraining': '待複刻',
'voiceClone.training': '訓練中',
+3 -13
View File
@@ -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: {
@@ -67,7 +58,6 @@ export default new Vuex.Store({
return new Promise((resolve) => {
commit('clearAuth')
goToPage(Constant.PAGE.LOGIN, true);
window.location.reload(); // 彻底重置状态
})
},
// 添加获取公共配置的 action
+14 -1
View File
@@ -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()
});
// 触发配置变更事件
+30
View File
@@ -257,3 +257,33 @@ export function sm2Decrypt(privateKey, cipherText) {
return sm2.doDecrypt(dataWithoutPrefix, privateKey, 1);
}
/**
* 防抖函数
* @param {Function} fn 要防抖的函数
* @param {number} delay 延迟时间毫秒默认500ms
* @param {boolean} immediate 是否立即执行默认false
* @returns {Function} 防抖处理后的函数
*/
export function debounce(fn, delay = 500, immediate = false) {
let timer = null;
return function (...args) {
const context = this;
if (timer) {
clearTimeout(timer);
}
if (immediate && !timer) {
fn.apply(context, args);
}
timer = setTimeout(() => {
if (!immediate) {
fn.apply(context, args);
}
timer = null;
}, delay);
};
}
@@ -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() {
@@ -478,11 +483,10 @@ export default {
/* 主容器样式 */
.main-wrapper {
margin: 5px 22px;
// 63px 35px 72px
height: calc(100vh - 63px - 35px - 72px);
margin: 0 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
@@ -587,7 +591,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;
}
@@ -119,7 +119,9 @@
@refresh="fetchBindDevices(currentAgentId)" />
<ManualAddDeviceDialog :visible.sync="manualAddDeviceDialogVisible" :agent-id="currentAgentId"
@refresh="fetchBindDevices(currentAgentId)" />
<el-footer>
<version-footer />
</el-footer>
</div>
</template>
@@ -128,12 +130,14 @@ import Api from '@/apis/api';
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
import HeaderBar from "@/components/HeaderBar.vue";
import ManualAddDeviceDialog from "@/components/ManualAddDeviceDialog.vue";
import VersionFooter from "@/components/VersionFooter.vue";
export default {
components: {
HeaderBar,
AddDeviceDialog,
ManualAddDeviceDialog
ManualAddDeviceDialog,
VersionFooter
},
data() {
return {
@@ -504,11 +508,9 @@ export default {
}
.main-wrapper {
margin: 5px 22px;
height: calc(100vh - 63px - 35px - 72px);
margin: 0 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
@@ -645,7 +647,7 @@ export default {
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding-bottom: 10px;
/* padding-bottom: 10px; */
}
@@ -807,7 +809,7 @@ export default {
flex: 1;
display: flex;
flex-direction: column;
max-height: calc(100vh - 40vh);
/* max-height: calc(100vh - 40vh); */
}
:deep(.el-table__body-wrapper) {
+43 -24
View File
@@ -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">
@@ -461,11 +461,10 @@ export default {
}
.main-wrapper {
margin: 5px 22px;
// 63px 35px 72px
height: calc(100vh - 63px - 35px - 72px);
margin: 0 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
@@ -555,13 +554,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 +574,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 +627,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 +908,7 @@ export default {
}
:deep(.el-card__body) {
padding: 15px;
padding: 0;
display: flex;
flex-direction: column;
flex: 1;
@@ -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)
@@ -433,11 +422,9 @@ export default {
}
.main-wrapper {
margin: 0 22px 5px 22px;
height: calc(100vh - 63px - 35px - 58px);
margin: 0 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
@@ -472,11 +472,10 @@ export default {
}
.main-wrapper {
margin: 5px 22px;
// 63px 35px 72px
height: calc(100vh - 63px - 35px - 72px);
margin: 0 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
@@ -555,7 +554,6 @@ export default {
justify-content: space-between;
align-items: center;
margin-top: auto;
padding-bottom: 10px;
width: 100%;
}
@@ -1127,11 +1127,10 @@ export default {
}
.main-wrapper {
margin: 5px 22px;
// 63px 35px 58px
height: calc(100vh - 63px - 35px - 58px);
margin: 0 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
+63 -40
View File
@@ -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;
}
@@ -660,11 +660,10 @@ export default {
}
.main-wrapper {
margin: 5px 22px;
// 63px 35px 72px
height: calc(100vh - 63px - 35px - 72px);
margin: 0 22px;
border-radius: 15px;
min-height: calc(100vh - 26vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
@@ -689,7 +688,6 @@ export default {
height: 100%;
border-radius: 15px;
background: transparent;
border: 1px solid #fff;
}
.nav-panel {
@@ -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;
+4 -6
View File
@@ -422,11 +422,10 @@ export default {
}
.main-wrapper {
margin: 5px 22px;
// 63px 35px 72px
height: calc(100vh - 63px - 35px - 72px);
margin: 0 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
@@ -505,7 +504,6 @@ export default {
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding-bottom: 10px;
}
.ctrl_btn {
@@ -735,7 +733,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 {
@@ -381,11 +381,10 @@ export default {
}
.main-wrapper {
margin: 5px 22px;
// 63px 35px 72px
height: calc(100vh - 63px - 35px - 72px);
margin: 0 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
@@ -464,7 +463,7 @@ export default {
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding-bottom: 10px;
// padding-bottom: 10px;
}
.ctrl_btn {
@@ -712,7 +711,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 {
@@ -460,11 +460,10 @@ export default {
}
.main-wrapper {
margin: 5px 22px;
// 63px 35px 72px
height: calc(100vh - 63px - 35px - 72px);
margin: 0 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
@@ -546,7 +545,6 @@ export default {
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding-bottom: 10px;
}
.ctrl_btn {
@@ -794,7 +792,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 {
@@ -189,11 +189,10 @@ export default {
}
.main-wrapper {
margin: 5px 22px;
// 63px 35px 58px
height: calc(100vh - 63px - 35px - 58px);
margin: 0 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
@@ -53,12 +53,16 @@
</div>
</div>
</div>
<el-footer>
<version-footer />
</el-footer>
</div>
</template>
<script>
import HeaderBar from "@/components/HeaderBar.vue";
import agentApi from '@/apis/module/agent';
import VersionFooter from "@/components/VersionFooter.vue";
//
const DEFAULT_MODEL_CONFIG = {
@@ -73,7 +77,7 @@ const DEFAULT_MODEL_CONFIG = {
export default {
name: 'TemplateQuickConfig',
components: { HeaderBar },
components: { HeaderBar, VersionFooter },
data() {
return {
form: {
@@ -276,7 +280,7 @@ export default {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5vh 24px;
padding: 16px 24px;
}
.page-title {
@@ -286,9 +290,9 @@ export default {
}
.main-wrapper {
margin: 1vh 22px;
height: calc(100vh - 63px - 35px - 60px);
margin: 0 22px;
border-radius: 15px;
height: calc(100vh - 24vh);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
@@ -381,11 +381,10 @@ export default {
}
.main-wrapper {
margin: 5px 22px;
// 63px 35px 72px
height: calc(100vh - 63px - 35px - 72px);
margin: 0 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
@@ -721,7 +720,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 {
@@ -34,6 +34,8 @@
</span>
</template>
</el-table-column>
<el-table-column :label="$t('voiceClone.languages')" prop="languages"
align="center"></el-table-column>
<el-table-column :label="$t('voiceClone.trainStatus')" prop="trainStatus" align="center">
<template slot-scope="scope">
<div class="status-button" :class="getStatusButtonClass(scope.row)">
@@ -509,11 +511,10 @@ export default {
}
.main-wrapper {
margin: 5px 22px;
// 63px 35px 72px
height: calc(100vh - 63px - 35px - 72px);
margin: 0 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
@@ -592,7 +593,6 @@ export default {
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding-bottom: 10px;
}
.ctrl_btn {
@@ -850,7 +850,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);
@@ -862,7 +862,6 @@ export default {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 40px;
}
:deep(.transparent-table) {
+4 -6
View File
@@ -202,11 +202,9 @@ export default {
}
.main-wrapper {
margin: 5px 22px;
height: calc(100vh - 63px - 35px - 60px);
margin: 0 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
@@ -285,7 +283,7 @@ export default {
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding-bottom: 10px;
// padding-bottom: 10px;
}
.ctrl_btn {
@@ -533,7 +531,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 {
@@ -32,6 +32,8 @@
align="center"></el-table-column>
<el-table-column :label="$t('voiceClone.platformName')" prop="modelName"
align="center"></el-table-column>
<el-table-column :label="$t('voiceClone.languages')" prop="languages"
align="center"></el-table-column>
<el-table-column :label="$t('voiceClone.trainStatus')" prop="trainStatus" align="center">
<template slot-scope="scope">
{{ getTrainStatusText(scope.row) }}
@@ -122,7 +124,8 @@ export default {
voiceCloneForm: {
modelId: "",
voiceIds: [],
userId: null
userId: null,
languages: ""
}
};
},
@@ -198,7 +201,8 @@ export default {
this.voiceCloneForm = {
modelId: "",
voiceIds: [],
userId: null
userId: null,
languages: ""
};
this.$nextTick(() => {
if (this.$refs.voiceCloneForm) {
@@ -359,11 +363,10 @@ export default {
}
.main-wrapper {
margin: 5px 22px;
// 63px 35px 72px
height: calc(100vh - 63px - 35px - 72px);
margin: 0 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
@@ -429,7 +432,7 @@ export default {
overflow: hidden;
::v-deep .el-card__body {
padding: 15px;
padding: 15px 15px 0;
display: flex;
flex-direction: column;
flex: 1;
@@ -672,7 +675,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 {
-1
View File
@@ -76,7 +76,6 @@
margin-top: 20px;
align-items: center;
border-radius: 10px;
background: #f6f8fb;
border: 1px solid #e4e6ef;
height: 40px;
padding: 0 15px;
+1 -1
View File
@@ -276,7 +276,7 @@ export default {
cursor: pointer;
.left-add {
width: 105px;
padding: 0 14px;
height: 34px;
border-radius: 17px;
background: #5778ff;
+15 -16
View File
@@ -238,13 +238,6 @@ export default {
this.$store.dispatch("fetchPubConfig").then(() => {
//
this.isMobileLogin = this.enableMobileRegister;
// pub-configfeatureManager使
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,20 +367,13 @@ export default {
({ data }) => {
showSuccess(this.$t('login.loginSuccess'));
this.$store.commit("setToken", JSON.stringify(data.data));
goToPage("/home");
this.getUserInfo();
},
(err) => {
// 使
let errorMessage = err.data.msg || "登录失败";
showDanger(errorMessage);
if (
err.data != null &&
err.data.msg != null &&
err.data.msg.indexOf("图形验证码") > -1 || err.data.msg.indexOf("Captcha") > -1
) {
this.fetchCaptcha();
}
}
);
+12 -5
View File
@@ -355,6 +355,9 @@
:settings="ttsSettings"
@save="handleTtsSettingsSave"
/>
<el-footer>
<version-footer />
</el-footer>
</div>
</template>
@@ -368,10 +371,11 @@ import TtsAdvancedSettings from "@/components/TtsAdvancedSettings.vue";
import HeaderBar from "@/components/HeaderBar.vue";
import i18n from "@/i18n";
import featureManager from "@/utils/featureManager";
import VersionFooter from "@/components/VersionFooter.vue";
export default {
name: "RoleConfigPage",
components: { HeaderBar, FunctionDialog, ContextProviderDialog, TtsAdvancedSettings },
components: { HeaderBar, FunctionDialog, ContextProviderDialog, TtsAdvancedSettings, VersionFooter },
data() {
return {
showContextProviderDialog: false,
@@ -753,7 +757,10 @@ export default {
//
const filteredVoices = allVoices.filter(voice => {
if (!voice.languages) return false;
if (!voice.languages) {
//
return Boolean(voice.isClone);
}
const languagesArray = voice.languages.split(/[、;;,]/).map(lang => lang.trim()).filter(lang => lang);
return languagesArray.includes(this.selectedLanguage);
});
@@ -1288,7 +1295,7 @@ export default {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5vh 24px;
padding: 16px 24px;
}
.page-title {
@@ -1298,9 +1305,9 @@ export default {
}
.main-wrapper {
margin: 1vh 22px;
height: calc(100vh - 63px - 35px - 60px);
margin: 0 22px;
border-radius: 15px;
height: calc(100vh - 24vh);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
+43 -49
View File
@@ -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>
【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】
+1 -1
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More