From 860cbc36fda94de327ddf9db505c57a9734eec1f Mon Sep 17 00:00:00 2001 From: zhuoqinglian <1035449612@qq.com> Date: Mon, 16 Mar 2026 09:46:43 +0800 Subject: [PATCH 01/27] =?UTF-8?q?fix:=20=E5=A4=84=E7=90=86=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E4=BD=93=E7=AE=A1=E7=90=86=E5=85=A8=E5=B1=80=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E4=B8=8B=E6=8B=89=E5=88=B7=E6=96=B0=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-mobile/src/pages/agent/index.vue | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) 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" /> From 286079c93afcd8828aa5d790aa3aa5f563ef08ad Mon Sep 17 00:00:00 2001 From: zhuoqinglian <1035449612@qq.com> Date: Mon, 16 Mar 2026 09:50:24 +0800 Subject: [PATCH 02/27] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=A3=B0?= =?UTF-8?q?=E7=BA=B9=E7=AE=A1=E7=90=86-=E5=A3=B0=E7=BA=B9=E5=90=91?= =?UTF-8?q?=E9=87=8F=E9=9F=B3=E9=A2=91=E6=92=AD=E6=94=BE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/api/voiceprint/voiceprint.ts | 10 ++ main/manager-mobile/src/i18n/de.ts | 5 + main/manager-mobile/src/i18n/en.ts | 5 + main/manager-mobile/src/i18n/pt_BR.ts | 5 + main/manager-mobile/src/i18n/vi.ts | 5 + main/manager-mobile/src/i18n/zh_CN.ts | 5 + main/manager-mobile/src/i18n/zh_TW.ts | 5 + main/manager-mobile/src/pages/agent/edit.vue | 87 +++++++++- .../src/pages/voiceprint/index.vue | 163 +++++++++++++++--- 9 files changed, 261 insertions(+), 29 deletions(-) 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/i18n/de.ts b/main/manager-mobile/src/i18n/de.ts index 9d0711ea..8cff59cd 100644 --- a/main/manager-mobile/src/i18n/de.ts +++ b/main/manager-mobile/src/i18n/de.ts @@ -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..c65ffefa 100644 --- a/main/manager-mobile/src/i18n/en.ts +++ b/main/manager-mobile/src/i18n/en.ts @@ -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..b2ea99f1 100644 --- a/main/manager-mobile/src/i18n/pt_BR.ts +++ b/main/manager-mobile/src/i18n/pt_BR.ts @@ -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..ba94de40 100644 --- a/main/manager-mobile/src/i18n/vi.ts +++ b/main/manager-mobile/src/i18n/vi.ts @@ -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..4a1876d3 100644 --- a/main/manager-mobile/src/i18n/zh_CN.ts +++ b/main/manager-mobile/src/i18n/zh_CN.ts @@ -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..e15fd0ea 100644 --- a/main/manager-mobile/src/i18n/zh_TW.ts +++ b/main/manager-mobile/src/i18n/zh_TW.ts @@ -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..1a01e051 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() @@ -493,6 +497,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 +1001,32 @@ onMounted(async () => { @select="({ item }) => onPickerConfirm('tts', item.value, item.name)" /> - + + + + + {{ t('agent.voiceprint') }} + + + + + {{ voice.name }} + + + + + + + (), { + 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({ - + @@ -418,11 +510,11 @@ defineExpose({ - {{ t('voiceprint.cancel') }} - - - {{ t('voiceprint.save') }} - + {{ t('voiceprint.cancel') }} + + + {{ t('voiceprint.save') }} + @@ -488,20 +580,45 @@ defineExpose({ - {{ t('voiceprint.cancel') }} - - - {{ t('voiceprint.save') }} - + {{ t('voiceprint.cancel') }} + + + {{ t('voiceprint.save') }} + - - + + + + + + {{ t('voiceprint.selectVector') }} + + + + + + + {{ item.name }} + + + + + + + + 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/voiceprint/index.vue b/main/manager-mobile/src/pages/voiceprint/index.vue index 43ef5230..e0917c98 100644 --- a/main/manager-mobile/src/pages/voiceprint/index.vue +++ b/main/manager-mobile/src/pages/voiceprint/index.vue @@ -451,20 +451,16 @@ defineExpose({ - - - {{ t('voiceprint.addSpeaker') }} - - - - + * {{ t('voiceprint.voiceVector') }} - + * {{ t('voiceprint.name') }} - + * {{ t('voiceprint.description') }}