diff --git a/main/manager-mobile/src/api/agent/agent.ts b/main/manager-mobile/src/api/agent/agent.ts index 1b17f28a..32b72ad7 100644 --- a/main/manager-mobile/src/api/agent/agent.ts +++ b/main/manager-mobile/src/api/agent/agent.ts @@ -131,6 +131,7 @@ export function getMcpAddress(agentId: string) { meta: { ignoreAuth: false, toast: false, + isExposeError: true, }, }) } diff --git a/main/manager-mobile/src/api/voiceprint/voiceprint.ts b/main/manager-mobile/src/api/voiceprint/voiceprint.ts index 600696dd..0726c535 100644 --- a/main/manager-mobile/src/api/voiceprint/voiceprint.ts +++ b/main/manager-mobile/src/api/voiceprint/voiceprint.ts @@ -60,3 +60,13 @@ export function updateVoicePrint(data: VoicePrint) { }, }) } + +// 获取音频下载ID +export function getAudioDownloadId(audioId: string) { + return http.Post(`/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..aa048248 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', @@ -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..f0fe4c14 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', @@ -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..2901583b 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', @@ -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..e9406afe 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', @@ -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..a9684485 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': '请输入用户名', @@ -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..a8b5a18e 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': '請輸入用戶名', @@ -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..93e715ca 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() @@ -422,15 +426,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 +506,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 = '' } // 获取模型显示名称 @@ -946,12 +1010,30 @@ onMounted(async () => { @select="({ item }) => onPickerConfirm('tts', item.value, item.name)" /> - + + + + + + + {{ voice.name }} + + + + + + + + { ::v-deep .wd-tag__close { color: #336cff !important; } +::v-deep .custom-popup { + .wd-popup { + padding: 20rpx !important; + background: transparent !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..ab0b595b 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) } 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..e0917c98 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 @@ -157,11 +164,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 +211,11 @@ function selectAudioId({ item }: { item: any }) { showChatHistoryDialog.value = false } +// 点击选择 +function handleItemClick(item: any) { + selectAudioId({ item }) +} + // 提交添加说话人 async function submitAdd() { if (!addForm.value.sourceName.trim()) { @@ -274,6 +288,82 @@ 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 () => { // 智能体已简化为默认 @@ -282,6 +372,8 @@ onMounted(async () => { // 暴露方法给父组件 defineExpose({ + showAddDialog, + showEditDialog, refresh, }) @@ -349,7 +441,7 @@ defineExpose({ - + @@ -359,20 +451,16 @@ defineExpose({ - - - {{ t('voiceprint.addSpeaker') }} - - - - + * {{ t('voiceprint.voiceVector') }} - + * {{ t('voiceprint.name') }} - + * {{ t('voiceprint.description') }}