diff --git a/docs/FAQ.md b/docs/FAQ.md
index b6bec471..a5dc0b08 100644
--- a/docs/FAQ.md
+++ b/docs/FAQ.md
@@ -83,6 +83,7 @@ VAD:
9、[知识库ragflow集成指南](./ragflow-integration.md)
10、[如何部署上下文源](./context-provider-integration.md)
11、[如何集成PowerMem智能记忆](./powermem-integration.md)
+12、[如何配置天气插件查询天气](./weather-integration.md)
### 11、语音克隆、本地语音部署相关教程
1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)
diff --git a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java
index 8a9e4fba..b438fec1 100644
--- a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java
+++ b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java
@@ -151,6 +151,11 @@ public interface Constant {
*/
String MEMORY_NO_MEM = "Memory_nomem";
+ /**
+ * 仅上报聊天记录(不总结记忆)
+ */
+ String MEMORY_MEM_REPORT_ONLY = "Memory_mem_report_only";
+
/**
* 火山引擎双声道语音克隆
*/
diff --git a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java
index 5681ec9e..820f4089 100644
--- a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java
+++ b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java
@@ -251,4 +251,9 @@ public interface ErrorCode {
int AGENT_TAG_NOT_EXIST = 10198; // 标签不存在
int RAG_DOCUMENT_PARSING_DELETE_ERROR = 10199; // 文档解析中,禁止删除
+
+ // 智能体MCP相关错误码
+ int MCP_ACCESS_POINT_ADDRESS_NO_PERMISSION = 10200; // 没有权限查看该智能体的MCP接入点地址
+ int MCP_ACCESS_POINT_ADDRESS_NOT_CONFIGURED = 10201; // 请联系管理员进入参数管理配置mcp接入点地址
+ int MCP_ACCESS_POINT_TOOLS_LIST_NO_PERMISSION = 10202; // 没有权限查看该智能体的MCP工具列表
}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentMcpAccessPointController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentMcpAccessPointController.java
index bee728f5..c805a4e7 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentMcpAccessPointController.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentMcpAccessPointController.java
@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
+import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
@@ -40,11 +41,11 @@ public class AgentMcpAccessPointController {
// 检查权限
if (!agentService.checkAgentPermission(agentId, user.getId())) {
- return new Result().error("没有权限查看该智能体的MCP接入点地址");
+ return new Result().error(ErrorCode.MCP_ACCESS_POINT_ADDRESS_NO_PERMISSION);
}
String agentMcpAccessAddress = agentMcpAccessPointService.getAgentMcpAccessAddress(agentId);
if (agentMcpAccessAddress == null) {
- return new Result().ok("请联系管理员进入参数管理配置mcp接入点地址");
+ return new Result().error(ErrorCode.MCP_ACCESS_POINT_ADDRESS_NOT_CONFIGURED);
}
return new Result().ok(agentMcpAccessAddress);
}
@@ -58,7 +59,7 @@ public class AgentMcpAccessPointController {
// 检查权限
if (!agentService.checkAgentPermission(agentId, user.getId())) {
- return new Result>().error("没有权限查看该智能体的MCP工具列表");
+ return new Result>().error(ErrorCode.MCP_ACCESS_POINT_TOOLS_LIST_NO_PERMISSION);
}
List agentMcpToolsList = agentMcpAccessPointService.getAgentMcpToolsList(agentId);
return new Result>().ok(agentMcpToolsList);
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatSummaryServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatSummaryServiceImpl.java
index 38fef94b..26e601ec 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatSummaryServiceImpl.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatSummaryServiceImpl.java
@@ -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());
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java
index 24bdb751..a49ce69c 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java
@@ -57,7 +57,8 @@ public class ModelConfigServiceImpl extends BaseServiceImpl queryWrapper = new QueryWrapper<>();
queryWrapper.eq("model_type", StringUtils.isBlank(modelType) ? "" : modelType);
+ queryWrapper.orderByAsc("sort");
List providerEntities = modelProviderDao.selectList(queryWrapper);
return ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class);
}
@@ -147,7 +148,8 @@ public class ModelProviderServiceImpl extends BaseServiceImpl(`/agent/audio/${audioId}`, {}, {
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ })
+}
diff --git a/main/manager-mobile/src/http/request/alova.ts b/main/manager-mobile/src/http/request/alova.ts
index e7887120..42243bc1 100644
--- a/main/manager-mobile/src/http/request/alova.ts
+++ b/main/manager-mobile/src/http/request/alova.ts
@@ -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 = {
+ 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,
}
diff --git a/main/manager-mobile/src/i18n/de.ts b/main/manager-mobile/src/i18n/de.ts
index 9d0711ea..d76a4093 100644
--- a/main/manager-mobile/src/i18n/de.ts
+++ b/main/manager-mobile/src/i18n/de.ts
@@ -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',
}
diff --git a/main/manager-mobile/src/i18n/en.ts b/main/manager-mobile/src/i18n/en.ts
index ea232160..0066b0fd 100644
--- a/main/manager-mobile/src/i18n/en.ts
+++ b/main/manager-mobile/src/i18n/en.ts
@@ -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',
}
diff --git a/main/manager-mobile/src/i18n/pt_BR.ts b/main/manager-mobile/src/i18n/pt_BR.ts
index 7d91096a..416b1f8f 100644
--- a/main/manager-mobile/src/i18n/pt_BR.ts
+++ b/main/manager-mobile/src/i18n/pt_BR.ts
@@ -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',
}
diff --git a/main/manager-mobile/src/i18n/vi.ts b/main/manager-mobile/src/i18n/vi.ts
index 2deab9d9..8ee1a35e 100644
--- a/main/manager-mobile/src/i18n/vi.ts
+++ b/main/manager-mobile/src/i18n/vi.ts
@@ -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',
}
\ No newline at end of file
diff --git a/main/manager-mobile/src/i18n/zh_CN.ts b/main/manager-mobile/src/i18n/zh_CN.ts
index dbe3980c..a5dec898 100644
--- a/main/manager-mobile/src/i18n/zh_CN.ts
+++ b/main/manager-mobile/src/i18n/zh_CN.ts
@@ -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': '音频播放失败',
}
diff --git a/main/manager-mobile/src/i18n/zh_TW.ts b/main/manager-mobile/src/i18n/zh_TW.ts
index d290a118..9125e296 100644
--- a/main/manager-mobile/src/i18n/zh_TW.ts
+++ b/main/manager-mobile/src/i18n/zh_TW.ts
@@ -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': '音頻播放失敗',
}
diff --git a/main/manager-mobile/src/pages/agent/edit.vue b/main/manager-mobile/src/pages/agent/edit.vue
index 734c2722..0f56db88 100644
--- a/main/manager-mobile/src/pages/agent/edit.vue
+++ b/main/manager-mobile/src/pages/agent/edit.vue
@@ -110,6 +110,10 @@ const inputVisible = ref(false)
const languageOptions = ref([])
const isVisibleReport = ref(false)
+// 音频播放相关
+const audioRef = ref(null)
+const playingVoiceId = ref('')
+
// 使用插件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 () => {
@@ -942,16 +1010,35 @@ onMounted(async () => {
onPickerConfirm('tts', item.value, item.name)"
/>
- onPickerConfirm('voiceprint', item.value, item.name)"
- />
+
+
{
::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;
+ }
+}
diff --git a/main/manager-mobile/src/pages/agent/index.vue b/main/manager-mobile/src/pages/agent/index.vue
index 49f11e57..c6befce0 100644
--- a/main/manager-mobile/src/pages/agent/index.vue
+++ b/main/manager-mobile/src/pages/agent/index.vue
@@ -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"
/>
diff --git a/main/manager-mobile/src/pages/agent/tools.vue b/main/manager-mobile/src/pages/agent/tools.vue
index 45a845bb..fb47deae 100644
--- a/main/manager-mobile/src/pages/agent/tools.vue
+++ b/main/manager-mobile/src/pages/agent/tools.vue
@@ -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"
>
-
+
{
v-if="selectedList.length === 0"
class="h-[400rpx] flex items-center justify-center"
>
-
+
{
@@ -585,3 +586,9 @@ onMounted(async () => {
+
+
diff --git a/main/manager-mobile/src/pages/chat-history/detail.vue b/main/manager-mobile/src/pages/chat-history/detail.vue
index 4d8f91e7..c3d399d5 100644
--- a/main/manager-mobile/src/pages/chat-history/detail.vue
+++ b/main/manager-mobile/src/pages/chat-history/detail.vue
@@ -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) {
diff --git a/main/manager-mobile/src/pages/index/index.vue b/main/manager-mobile/src/pages/index/index.vue
index 4a19e279..21aba9ce 100644
--- a/main/manager-mobile/src/pages/index/index.vue
+++ b/main/manager-mobile/src/pages/index/index.vue
@@ -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()
}
})
diff --git a/main/manager-mobile/src/pages/login/index.vue b/main/manager-mobile/src/pages/login/index.vue
index 4a83d710..adaf5fb3 100644
--- a/main/manager-mobile/src/pages/login/index.vue
+++ b/main/manager-mobile/src/pages/login/index.vue
@@ -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;
diff --git a/main/manager-mobile/src/pages/settings/index.vue b/main/manager-mobile/src/pages/settings/index.vue
index 806eea20..9aa52927 100644
--- a/main/manager-mobile/src/pages/settings/index.vue
+++ b/main/manager-mobile/src/pages/settings/index.vue
@@ -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'),
})
})
@@ -296,8 +299,10 @@ onMounted(async () => {
-
+
{{ t('settings.serverApiUrl') }}
@@ -308,11 +313,13 @@ onMounted(async () => {
-
-
+
+ input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl"
+ />
{{ urlError }}
@@ -320,14 +327,18 @@ onMounted(async () => {
-
+ @click="saveServerBaseUrl"
+ >
{{ t('settings.saveSettings') }}
-
+ @click="resetServerBaseUrl"
+ >
{{ t('settings.resetDefault') }}
@@ -342,12 +353,15 @@ onMounted(async () => {
-
+
+ class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]"
+ >
{{ t('settings.totalCacheSize') }}
@@ -363,7 +377,8 @@ onMounted(async () => {
+ class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx]"
+ >
{{ t('settings.cacheClear') }}
@@ -374,7 +389,8 @@ onMounted(async () => {
+ @click="clearCache"
+ >
{{ t('settings.clearCache') }}
@@ -390,11 +406,14 @@ onMounted(async () => {
-
+
+ @click="showAbout"
+ >
{{ t('settings.aboutUs') }}
@@ -416,11 +435,14 @@ onMounted(async () => {
-
+
+ @click="showLanguageSheet = true"
+ >
{{ t('settings.language') }}
@@ -430,8 +452,8 @@ onMounted(async () => {
-
- {{supportedLanguages.find(lang => lang.code === currentLanguage)?.name}}
+
+ {{ supportedLanguages.find(lang => lang.code === currentLanguage)?.name }}
@@ -443,8 +465,10 @@ onMounted(async () => {
-
+
{{ lang.name }}
diff --git a/main/manager-mobile/src/pages/voiceprint/index.vue b/main/manager-mobile/src/pages/voiceprint/index.vue
index 3cca4442..e6ba2ccc 100644
--- a/main/manager-mobile/src/pages/voiceprint/index.vue
+++ b/main/manager-mobile/src/pages/voiceprint/index.vue
@@ -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(), {
+ agentId: 'default',
+})
+
+const emits = defineEmits(['update-refresher-enabled'])
+
// 接收props
interface Props {
agentId?: string
}
-const props = withDefaults(defineProps(), {
- agentId: 'default'
-})
-
// 获取屏幕边界到安全区域距离
let safeAreaInsets: any
let systemInfo: any
@@ -50,6 +53,10 @@ const chatHistoryActions = ref([])
const swipeStates = ref>({})
const loading = ref(false)
+// 音频播放相关
+const audioRef = ref(null)
+const playingAudioId = ref('')
+
// 使用传入的智能体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,
})
@@ -349,7 +441,7 @@ defineExpose({
-
+
@@ -359,25 +451,24 @@ defineExpose({
-
-
- {{ t('voiceprint.addSpeaker') }}
-
-
-
- * {{ t('voiceprint.voiceVector') }}
+
+ *
+
+ {{ t('voiceprint.voiceVector') }}
- * {{ t('voiceprint.name') }}
+
+ *
+
+ {{ t('voiceprint.name') }}
- * {{ t('voiceprint.description') }}
+
+ *
+
+ {{ t('voiceprint.description') }}
@@ -433,7 +530,7 @@ defineExpose({
safe-area-inset-bottom
>
-
+
{{ t('voiceprint.editSpeaker') }}
@@ -443,11 +540,14 @@ defineExpose({
- * {{ t('voiceprint.voiceVector') }}
+
+ *
+
+ {{ t('voiceprint.voiceVector') }}
- * {{ t('voiceprint.name') }}
+
+ *
+
+ {{ t('voiceprint.name') }}
- * {{ t('voiceprint.description') }}
+
+ *
+
+ {{ t('voiceprint.description') }}
-
-
+
+
-
diff --git a/main/manager-mobile/src/store/user.ts b/main/manager-mobile/src/store/user.ts
index 1e19f34b..a062fcc5 100644
--- a/main/manager-mobile/src/store/user.ts
+++ b/main/manager-mobile/src/store/user.ts
@@ -19,7 +19,7 @@ const userInfoState: UserInfo & { avatar?: string, token?: string } = {
}
export const useUserStore = defineStore(
- 'user',
+ 'userInfo',
() => {
// 定义用户信息
const userInfo = ref({ ...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) }),
+ },
+ },
},
)
diff --git a/main/manager-mobile/src/utils/index.ts b/main/manager-mobile/src/utils/index.ts
index 0d9070b4..8ac842c8 100644
--- a/main/manager-mobile/src/utils/index.ts
+++ b/main/manager-mobile/src/utils/index.ts
@@ -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(
+ fn: T,
+ delay = 500,
+ immediate = false,
+): DebouncedFunction {
+ let timer: ReturnType | null = null
+
+ const debounced = function (this: any, ...args: Parameters) {
+ 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
}
diff --git a/main/manager-web/.env b/main/manager-web/.env
index 1b5f8138..d234c067 100644
--- a/main/manager-web/.env
+++ b/main/manager-web/.env
@@ -1 +1,3 @@
-VUE_APP_TITLE=智控台
\ No newline at end of file
+VUE_APP_TITLE=智控台
+VUE_APP_DESCRIPTION=小智后端服务(xiaozhi-server)是由华南理工大学刘思源教授团队主导研发的智能终端软硬件体系后端服务系统,专为xiaozhi-esp32开源硬件打造,具备多协议兼容、声纹识别、知识库管理等核心能力。
+VUE_APP_KEYWORDS=xiaozhi-server,小智服务端,智控台,AI硬件,智能硬件,AI玩具,情感陪伴,聊天机器人,智能家居,车载机器人
diff --git a/main/manager-web/public/index.html b/main/manager-web/public/index.html
index e3ae5a53..3cd3bb73 100644
--- a/main/manager-web/public/index.html
+++ b/main/manager-web/public/index.html
@@ -5,6 +5,8 @@
+
+
<%= process.env.VUE_APP_TITLE %>
@@ -17,10 +19,6 @@
-
<% if (htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
diff --git a/main/manager-web/src/App.vue b/main/manager-web/src/App.vue
index 180f8f0d..f101e155 100644
--- a/main/manager-web/src/App.vue
+++ b/main/manager-web/src/App.vue
@@ -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) {
diff --git a/main/manager-web/src/apis/module/agent.js b/main/manager-web/src/apis/module/agent.js
index 5cbd60cb..d79f8da4 100644
--- a/main/manager-web/src/apis/module/agent.js
+++ b/main/manager-web/src/apis/module/agent.js
@@ -170,6 +170,9 @@ export default {
RequestService.clearRequestTime();
callback(res);
})
+ .fail((err) => {
+ callback(err);
+ })
.networkFail(() => {
RequestService.reAjaxFun(() => {
this.getAgentMcpAccessAddress(agentId, callback);
diff --git a/main/manager-web/src/components/ChatHistoryDialog.vue b/main/manager-web/src/components/ChatHistoryDialog.vue
index 9e8b742f..7821ba2d 100644
--- a/main/manager-web/src/components/ChatHistoryDialog.vue
+++ b/main/manager-web/src/components/ChatHistoryDialog.vue
@@ -73,6 +73,7 @@
-