Merge branch 'main' into py-display

This commit is contained in:
CGD
2026-03-31 14:24:27 +08:00
committed by GitHub
74 changed files with 1034 additions and 451 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";
/**
* 火山引擎双声道语音克隆
*/
@@ -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工具列表
}
@@ -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);
@@ -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);
@@ -571,6 +571,13 @@ 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
@@ -578,3 +585,4 @@ databaseChangeLog:
- 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
@@ -205,4 +205,7 @@
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
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
@@ -206,3 +206,6 @@
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
@@ -131,6 +131,7 @@ export function getMcpAddress(agentId: string) {
meta: {
ignoreAuth: false,
toast: false,
isExposeError: true,
},
})
}
@@ -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,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',
}
/**
* 创建请求实例
*/
@@ -51,6 +62,7 @@ const alovaInstance = createAlova({
// 检查混合内容错误(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)
@@ -60,6 +72,7 @@ const alovaInstance = createAlova({
method.config.headers = {
'Content-Type': ContentTypeEnum.JSON,
'Accept': 'application/json, text/plain, */*',
'Accept-language': currentLang,
...method.config.headers,
}
+10 -5
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',
@@ -69,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',
@@ -85,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',
@@ -116,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...',
@@ -371,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',
@@ -492,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',
}
+8 -3
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',
@@ -69,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',
@@ -371,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',
@@ -492,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',
}
+8 -3
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',
@@ -69,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',
@@ -371,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',
@@ -492,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',
}
+8 -3
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',
@@ -69,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',
@@ -371,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',
@@ -492,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',
}
+7 -2
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': '请输入用户名',
@@ -69,7 +69,7 @@ export default {
'home.createFirstAgent': '点击右下角 + 号创建您的第一个智能体',
'home.dialogTitle': '创建智能体',
'home.inputPlaceholder': '例如:客服助手、语音助理、知识问答',
'home.createError': '请输入智能体名称',
'home.createError': '名称长度必须在 1 到 64 个字符之间',
'home.createNow': '立即创建',
'home.justNow': '刚刚',
'home.minutesAgo': '分钟前',
@@ -492,4 +492,9 @@ export default {
'deviceConfig.afterConfigSuccessDeviceWillRestart': '配网成功后设备将自动重启',
'deviceConfig.audioPlaybackError': '音频播放错误',
'deviceConfig.playbackFailed': '播放失败',
// Voiceprint page
'voiceprint.audioNotExist': '该音频不存在',
'voiceprint.getAudioFailed': '获取音频失败',
'voiceprint.audioPlayFailed': '音频播放失败',
}
+7 -2
View File
@@ -11,7 +11,7 @@ export default {
'login.navigationTitle': '登錄',
'login.fetchConfigError': '獲取配置失敗:',
'login.selectLanguage': '選擇語言',
'login.selectLanguageTip': '简体',
'login.selectLanguageTip': '繁體',
'login.welcomeBack': '歡迎回來',
'login.pleaseLogin': '請登錄您的賬戶',
'login.enterUsername': '請輸入用戶名',
@@ -90,7 +90,7 @@ export default {
'home.createFirstAgent': '點擊右下角 + 號創建您的第一個智能體',
'home.dialogTitle': '創建智能體',
'home.inputPlaceholder': '例如:客服助手、語音助理、知識問答',
'home.createError': '請輸入智能體暱稱',
'home.createError': '暱稱長度必須在 1 到 64 個字元之間。',
'home.createNow': '立即創建',
'home.justNow': '剛剛',
'home.minutesAgo': '分鐘前',
@@ -492,4 +492,9 @@ export default {
'deviceConfig.afterConfigSuccessDeviceWillRestart': '配網成功後設備將自動重啟',
'deviceConfig.audioPlaybackError': '音頻播放錯誤',
'deviceConfig.playbackFailed': '播放失敗',
// Voiceprint page
'voiceprint.audioNotExist': '該音頻不存在',
'voiceprint.getAudioFailed': '獲取音頻失敗',
'voiceprint.audioPlayFailed': '音頻播放失敗',
}
+119 -17
View File
@@ -110,6 +110,10 @@ const inputVisible = ref(false)
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()
@@ -158,6 +162,9 @@ function handleInputConfirm() {
inputVisible.value = false
}
// 是否禁用历史记忆输入框
const isMemoryDisabled = computed(() => formData.value.memModelId !== 'Memory_mem_local_short')
// 打开上下文源编辑弹窗
function openContextProviderDialog() {
uni.navigateTo({
@@ -422,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()
}
}
@@ -493,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 = ''
}
// 获取模型显示名称
@@ -878,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>
@@ -942,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"
@@ -971,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>
+10 -3
View File
@@ -70,6 +70,7 @@ async function mergeFunctions() {
uni.setStorageSync(`cachedMcpAddress_${agentId.value}`, address)
}
catch (error) {
mcpAddress.value = error
console.error('获取MCP地址失败:', error)
}
@@ -310,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
@@ -346,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
@@ -444,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"
>
@@ -585,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) {
@@ -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()
}
})
@@ -15,7 +15,7 @@ import { computed, onMounted, ref } from 'vue'
import { login } from '@/api/auth'
// 导入国际化相关功能
import { changeLanguage, getCurrentLanguage, getSupportedLanguages, initI18n, t } from '@/i18n'
import { useConfigStore } from '@/store'
import { useConfigStore, useUserStore } from '@/store'
// 导入SM2加密工具
import { getEnvBaseUrl, sm2Encrypt } from '@/utils'
import { toast } from '@/utils/toast'
@@ -61,6 +61,7 @@ const loginType = ref<'username' | 'mobile'>('username')
// 获取配置store
const configStore = useConfigStore()
const userStore = useUserStore()
// 区号选择相关
const showAreaCodeSheet = ref(false)
@@ -227,6 +228,7 @@ async function handleLogin() {
const response = await login(loginData)
// 存储token
uni.setStorageSync('token', JSON.stringify(response))
await userStore.getUserInfo()
toast.success(t('message.loginSuccess'))
@@ -581,7 +583,6 @@ onMounted(async () => {
.input-wrapper {
position: relative;
background: #f8f9fa;
border-radius: 16rpx;
padding: 20rpx 16rpx;
border: 2rpx solid #e9ecef;
@@ -127,10 +127,10 @@ async function saveServerBaseUrl() {
uni.request({
url: `${getEnvBaseUrl()}/user/pub-config`,
method: 'GET',
success: (res) => {
success: (res: any) => {
if (res.statusCode === 200) {
configStore.setConfig(res.data.data)
uni.setStorageSync('config', res.data.data.sm2PubKey)
uni.setStorageSync('config', res.data.data)
}
},
fail: (err) => {
@@ -165,6 +165,7 @@ const showLanguageSheet = ref(false)
function handleLanguageChange(lang: Language) {
changeLanguage(lang)
showLanguageSheet.value = false
currentLanguage.value = lang
toast.success(t('settings.languageChanged'))
}
@@ -225,7 +226,8 @@ function clearAllCacheAfterUrlChange() {
// 重新获取缓存信息
getCacheInfo()
} catch (error) {
}
catch (error) {
console.error('清除缓存失败:', error)
}
}
@@ -250,7 +252,8 @@ async function clearCache() {
}
},
})
} catch (error) {
}
catch (error) {
console.error('清除缓存失败:', error)
toast.error(t('settings.clearCacheFailed'))
}
@@ -278,7 +281,7 @@ onMounted(async () => {
// 动态设置导航栏标题为国际化文本
uni.setNavigationBarTitle({
title: t('settings.title')
title: t('settings.title'),
})
})
</script>
@@ -296,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') }}
@@ -308,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 }}
@@ -320,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>
@@ -342,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') }}
@@ -363,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') }}
@@ -374,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>
@@ -390,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') }}
@@ -416,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') }}
@@ -430,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>
@@ -443,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>
+10 -12
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,16 +51,8 @@ export const useUserStore = defineStore(
*/
const getUserInfo = async () => {
const userData = await _getUserInfo()
const authInfo = JSON.parse(uni.getStorageSync('token') || '{}')
const userInfoWithExtras = {
...userData,
avatar: userInfoState.avatar,
token: authInfo.token || '',
}
setUserInfo(userInfoWithExtras)
uni.setStorageSync('userInfo', userInfoWithExtras)
// TODO 这里可以增加获取用户路由的方法 根据用户的角色动态生成路由
return userInfoWithExtras
setUserInfo(userData)
return userData
}
/**
* 退出登录 并 删除用户信息
@@ -79,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);
@@ -73,6 +73,7 @@
</template>
<script>
import { debounce } from '@/utils'
import Api from '@/apis/api';
export default {
@@ -115,6 +116,10 @@ export default {
if (val) {
this.resetData();
this.loadSessions();
} else {
this.audioElement?.pause();
this.audioElement = null;
this.playingAudioId = null;
}
},
dialogVisible(val) {
@@ -305,7 +310,7 @@ export default {
}
return 'el-icon-video-play';
},
playAudio(message) {
playAudio: debounce(function(message) {
if (this.playingAudioId === message.audioId) {
// 如果正在播放当前音频,则停止播放
if (this.audioElement) {
@@ -326,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;
@@ -337,7 +345,7 @@ export default {
this.audioElement.play();
}
});
},
}, 300),
getUserAvatar(sessionId) {
// 从 sessionId 中提取所有数字
const numbers = sessionId.match(/\d+/g);
@@ -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>
+3 -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',
+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 {
@@ -511,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);
@@ -594,7 +593,6 @@ export default {
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding-bottom: 10px;
}
.ctrl_btn {
@@ -852,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);
@@ -864,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 {
@@ -363,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);
@@ -433,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;
@@ -676,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();
}
}
);
+8 -4
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,
@@ -1291,7 +1295,7 @@ export default {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5vh 24px;
padding: 16px 24px;
}
.page-title {
@@ -1301,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);
+2 -2
View File
@@ -751,8 +751,8 @@ class ConnectionHandler:
memory_type = self.config["Memory"][self.config["selected_module"]["Memory"]][
"type"
]
# 如果使用 nomen,直接返回
if memory_type == "nomem":
# 如果使用 nomen 或 mem_report_only,直接返回
if memory_type == "nomem" or memory_type == "mem_report_only":
return
# 使用 mem_local_short 模式
elif memory_type == "mem_local_short":
@@ -280,6 +280,8 @@ async def send_tts_message(conn: "ConnectionHandler", state, text=None):
await sendAudio(conn, audios)
# 等待所有音频包发送完成
await _wait_for_audio_completion(conn)
# 停止音频发送循环
conn.audio_rate_controller.stop_sending()
# 清除服务端讲话状态
conn.clearSpeakStatus()
@@ -0,0 +1,20 @@
"""
仅上报聊天记录不进行记忆总结
"""
from ..base import MemoryProviderBase, logger
TAG = __name__
class MemoryProvider(MemoryProviderBase):
def __init__(self, config, summary_memory=None):
super().__init__(config)
async def save_memory(self, msgs, session_id=None):
logger.bind(tag=TAG).debug("mem_report_only mode: No memory saving or summarization is performed.")
return None
async def query_memory(self, query: str) -> str:
logger.bind(tag=TAG).debug("mem_report_only mode: No memory query is performed.")
return ""
@@ -27,6 +27,7 @@ class AudioRateController:
self.queue_empty_event = asyncio.Event() # 队列清空事件
self.queue_empty_event.set() # 初始为空状态
self.queue_has_data_event = asyncio.Event() # 队列数据事件
self._last_queue_empty_time = 0 # 上次队列清空的时间(秒)
def reset(self):
"""重置控制器状态"""
@@ -37,12 +38,25 @@ class AudioRateController:
self.queue.clear()
self.play_position = 0
self.start_timestamp = None # 由首个音频包设置
self._last_queue_empty_time = 0 # 重置时间
# 相关事件处理
self.queue_empty_event.set()
self.queue_has_data_event.clear()
def add_audio(self, opus_packet):
"""添加音频包到队列"""
# 如果队列之前为空,需要调整时间戳以保持播放时间连续
# 这样工具调用等待期间,新加入的音频不会提前播放
# 如果间隔很短(<1帧),说明是正常的流式传输,不需要重置
if len(self.queue) == 0 and self.play_position > 0:
elapsed_since_empty = (time.monotonic() - self._last_queue_empty_time) * 1000
# 只有间隔超过1帧时长,才认为是真正的"暂停恢复"
if elapsed_since_empty >= self.frame_duration:
self.start_timestamp = time.monotonic() - (self.play_position / 1000)
self.logger.bind(tag=TAG).debug(
f"队列从空恢复,重置时间戳,当前播放位置: {self.play_position}ms,间隔: {elapsed_since_empty:.0f}ms"
)
self.queue.append(("audio", opus_packet))
# 相关事件处理
self.queue_empty_event.clear()
@@ -55,6 +69,14 @@ class AudioRateController:
Args:
message_callback: 消息发送回调函数 async def()
"""
if len(self.queue) == 0 and self.play_position > 0:
elapsed_since_empty = (time.monotonic() - self._last_queue_empty_time) * 1000
if elapsed_since_empty >= self.frame_duration:
self.start_timestamp = time.monotonic() - (self.play_position / 1000)
self.logger.bind(tag=TAG).debug(
f"队列从空恢复,重置时间戳,当前播放位置: {self.play_position}ms,间隔: {elapsed_since_empty:.0f}ms"
)
self.queue.append(("message", message_callback))
# 相关事件处理
self.queue_empty_event.clear()
@@ -126,6 +148,7 @@ class AudioRateController:
# 队列处理完后清除事件
self.queue_empty_event.set()
self.queue_has_data_event.clear()
self._last_queue_empty_time = time.monotonic() # 记录队列清空时间
def start_sending(self, send_audio_callback):
"""
+1 -1
View File
@@ -26,7 +26,7 @@ mem0ai==1.0.0
powermem>=0.3.1
bs4==0.0.2
modelscope==1.32.0
sherpa_onnx==1.12.17
sherpa_onnx==1.12.29
mcp==1.22.0
cnlunar==0.2.0
PySocks==1.7.1
+2 -2
View File
@@ -192,7 +192,7 @@ class App {
}
log('正在请求摄像头权限...', 'info');
this.cameraStream = await navigator.mediaDevices.getUserMedia({
video: { width: 320, height: 240, facingMode: this.currentFacingMode },
video: { width: 180, height: 240, facingMode: this.currentFacingMode },
audio: false
});
cameraVideo.srcObject = this.cameraStream;
@@ -274,7 +274,7 @@ class App {
return;
}
canvas.width = video.videoWidth || 320;
canvas.width = video.videoWidth || 180;
canvas.height = video.videoHeight || 240;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);