From 050538a312ed38ae3416b48ac74038f65f220925 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 20 Mar 2026 22:00:00 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=A7=BB=E5=8A=A8=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E7=95=8C=E9=9D=A2=20=E7=9A=84=E7=BB=86=E8=8A=82?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/mobile_elf_controller.dart | 35 +++++++++++++++---- .../agent_service/AgentService.kt | 15 ++++++-- .../yunqiinnovation/agent_service/StsAgent.kt | 11 +++++- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart b/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart index e11b599fa..b59fc43aa 100644 --- a/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart +++ b/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart @@ -57,6 +57,8 @@ class MobileElfController extends GetxController { // BLE/AI 唤醒触发的会话(非通话、非PTT模式下的唤醒) bool _bleWakeupActive = false; final _storage = GetStorage(); + // 滚动:layout 变化前是否已在底部 + bool _atBottom = true; // ═══════════════════════════════════════════════════════════════════════════ // 生命周期 @@ -172,8 +174,8 @@ class MobileElfController extends GetxController { case AgentServiceEventType.ttsCompleted: if (!sessionActive) break; - _pttResultPending = false; // PTT 一轮结束 - _bleWakeupActive = false; // BLE 唤醒一轮结束 + _pttResultPending = false; + _endBleWakeup(); // BLE 唤醒一轮结束,清理 native PTT 状态 if (_inCallMode) { status.value = MobileElfStatus.recording; micAlive.value = true; @@ -186,16 +188,16 @@ class MobileElfController extends GetxController { case AgentServiceEventType.ttsCanceled: case AgentServiceEventType.ttsStopped: if (!sessionActive) break; - _pttResultPending = false; // TTS 被打断也视为结束 - _bleWakeupActive = false; + _pttResultPending = false; + _endBleWakeup(); status.value = _inCallMode ? MobileElfStatus.recording : MobileElfStatus.idle; micAlive.value = _inCallMode; break; case AgentServiceEventType.error: if (!sessionActive) break; - _pttResultPending = false; // 出错时结束等待 - _bleWakeupActive = false; + _pttResultPending = false; + _endBleWakeup(); final msg = event.data['message']?.toString() ?? '未知错误'; status.value = MobileElfStatus.error; errorMsg.value = msg; @@ -305,6 +307,13 @@ class MobileElfController extends GetxController { if (pttRecording.value || _pttStarting) return; _pttStarting = true; _checkHeartbeat(); + // BLE 唤醒/PTT 等待结果期间用户按下 PTT,先中断当前会话(同 startCall 逻辑) + if (_bleWakeupActive || _pttResultPending) { + _pttResultPending = false; + _bleWakeupActive = false; + await AgentService.interruptCurrentResponse(); + await AgentService.stopConversation(); + } final granted = await PermissionUtil.instance.requestPermission( permissionType: Permission.microphone, permissionName: '麦克风', @@ -386,9 +395,21 @@ class MobileElfController extends GetxController { _scrollToBottom(); } + /// BLE 唤醒会话标志清除 + /// Native 状态(pttRecording 等)由 StsAgent.startPtt() 的重入逻辑懒清理, + /// 不在此调用 stopConversation(),避免与新会话产生竞态。 + void _endBleWakeup() { + _bleWakeupActive = false; + } + void _scrollToBottom() { + // layout 变化前捕获是否在底部,避免新内容撑开后差值变大误判为"已滚离底部" + if (scrollController.hasClients) { + final pos = scrollController.position; + _atBottom = pos.maxScrollExtent - pos.pixels <= 80; + } WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollController.hasClients) { + if (scrollController.hasClients && _atBottom) { scrollController.animateTo( scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index bbad03eaa..eacbecf4b 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -225,6 +225,7 @@ object AgentService : CoroutineScope { // STS 模式:跳过 Azure/OpenAI 初始化,直接启动 StsAgent if (agentMode == "sts") { this.context = appContext.applicationContext + audioPlayer = AudioPlayer(context) // STS 模式也需要音频播放器(提示音等) val stsConfig = StsConfig( productId = config["stsProductId"]?.toString() ?: "", productKey = config["stsProductKey"]?.toString() ?: "", @@ -236,7 +237,13 @@ object AgentService : CoroutineScope { stsAgent = StsAgent( context = context, config = stsConfig, - onEvent = { name, data -> sendEvent(name, data) }, + onEvent = { name, data -> + // BLE 唤醒:AI 开始回复时关闭 BLE 编解码器(说明用户已说完) + if (currentRecognitionMode == "ble_wakeup" && name == "assistant_response") { + BleService.closeCodec() + } + sendEvent(name, data) + }, onCredentialsFetched = { deviceId, deviceSecret, deviceNo -> sendEvent("sts_credentials", mapOf( "deviceId" to deviceId, @@ -685,10 +692,14 @@ object AgentService : CoroutineScope { */ fun startRecognition(isExternalActive: Boolean, mode: String = "normal"): Boolean { if (agentMode == "sts") { + currentRecognitionMode = mode // 记录模式供 onEvent 回调使用 when (mode) { "push_to_talk" -> stsAgent?.startPtt() "phone_call" -> stsAgent?.startCall() - "ble_wakeup" -> stsAgent?.startCall() // BLE 唤醒:进入通话模式(VAD 自动检测) + "ble_wakeup" -> { + audioPlayer?.playAudio(R.raw.start) // 立即播提示音,不等 WS 连接完成 + stsAgent?.startPtt() + } else -> stsAgent?.startPtt() // 普通模式默认 PTT } return true diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/StsAgent.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/StsAgent.kt index b0bd08d0c..5c28b2c5e 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/StsAgent.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/StsAgent.kt @@ -289,7 +289,16 @@ class StsAgent( /** PTT 模式:立即连 WS 并开始录音 */ fun startPtt() { - if (pttRecording) return + if (pttRecording) { + if (forwardingAudio) return // 真正在录音中,忽略重复调用 + // forwardingAudio=false:WS 已关闭,正处于 TTS 播报等待阶段(如 BLE 唤醒等待 AI 回复期间) + // 调用方(BleAgent)已通过 interruptCurrentResponse() 中断了 TTS, + // 此处只做静默状态重置,不再重复发出 tts_canceled 事件 + Log.i(TAG, "[YIDONG] startPtt: 静默重置旧会话,重新启动") + stopMicHard() + sessionEnded = false + pttRecording = false + } if (ttsPlaying) interruptTts() if (!credentialReady) { emitError("移动精灵凭证未就绪"); return } Log.i(TAG, "[YIDONG] startPtt: 启动PTT模式 deviceNo=${config.deviceNo} agentId=${config.agentId}")