From c05ddc79adc5e782b8cc78f1a547263cde76ce07 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Mon, 30 Jun 2025 11:13:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(agent):=20=E6=B7=BB=E5=8A=A0=E8=93=9D?= =?UTF-8?q?=E7=89=99=E8=80=B3=E6=9C=BA=E5=94=A4=E9=86=92=E5=92=8C=E9=80=9A?= =?UTF-8?q?=E8=AF=9D=E6=A8=A1=E5=BC=8F=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增蓝牙服务订阅和唤醒处理逻辑 - 实现输入模式状态保存和恢复机制 - 增加耳机唤醒通话专用入口 - 添加语音输入状态验证和安全管理 --- .../agent/controllers/agent_controller.dart | 223 +++++++++++++++++- 1 file changed, 211 insertions(+), 12 deletions(-) diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index b71f502d1..1ef3cd73a 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -9,6 +9,7 @@ import 'package:deep_voice/data/services/location_manager.dart'; import 'package:deep_voice/data/services/music_manager.dart'; import 'package:deep_voice/data/services/navigation_manager.dart'; import 'package:deep_voice/data/services/network/api.dart'; +import 'package:ble_service/ble_service.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:agent_service/agent_service.dart'; @@ -22,6 +23,13 @@ import 'package:photo_view/photo_view.dart'; import '../../../core/utils/permission_util.dart'; import '../../../data/models/message_model.dart'; +// 语音输入模式枚举 +enum VoiceInputMode { + text, // 文字输入模式 + pushToTalk, // 按住说话模式 + phoneCall, // 打电话模式 +} + class AgentController extends GetxController { static AgentController get to => Get.find(); //单例访问模式 @@ -87,6 +95,9 @@ class AgentController extends GetxController { // 按住说话模式状态 - 用于区分是否处于按住说话模式(即使当前没有按住) final isPushToTalkMode = false.obs; // 是否处于按住说话模式 + // 蓝牙唤醒状态 + final isBleWakeupActive = false.obs; // 是否正在进行蓝牙唤醒对话 + // 键盘状态 final isKeyboardVisible = false.obs; // 键盘是否可见 @@ -96,6 +107,9 @@ class AgentController extends GetxController { // 导航流事件订阅 StreamSubscription? _amapeventSubscription; + // BLE事件订阅 + StreamSubscription? _bleEventSubscription; + // 播放进度更新定时器 Timer? _progressTimer; @@ -127,6 +141,7 @@ class AgentController extends GetxController { _loadTtsPreference(); //加载TTS偏好设置 _loadChatHistory(); //加载聊天历史记录 _subscribeToEvents(); //订阅代理服务事件 + _subscribeToBleEvents(); //订阅BLE事件 _initMusicStatus(); //初始化音乐状态 _setupMediaSessionCallbacks(); //设置媒体会话回调 _setupTextInputListener(); //设置文本输入监听器 @@ -167,6 +182,7 @@ class AgentController extends GetxController { scrollController.dispose(); _eventSubscription?.cancel(); _amapeventSubscription?.cancel(); + _bleEventSubscription?.cancel(); _progressTimer?.cancel(); super.onClose(); } @@ -346,12 +362,21 @@ class AgentController extends GetxController { break; case AgentServiceEventType.recognitionStopped: isListening.value = false; - // 只有在非按住说话模式且非打电话模式下才自动切换到文字模式 - // 按住说话模式下应该保持在语音模式,等待下次按住 - if (!isPushToTalkMode.value && !isPhoneCallMode.value) { - isTextInputMode.value = true; + + // 处理蓝牙唤醒结束 + if (isBleWakeupActive.value) { + isBleWakeupActive.value = false; + // 蓝牙唤醒结束后,恢复之前的输入模式 + _restorePreviousInputMode(); + Logger.i(TAG, '蓝牙唤醒对话结束,恢复之前的输入模式'); + } else { + // 只有在非按住说话模式且非打电话模式下才自动切换到文字模式 + // 按住说话模式下应该保持在语音模式,等待下次按住 + if (!isPushToTalkMode.value && !isPhoneCallMode.value) { + isTextInputMode.value = true; + } } - // isTextInputMode.refresh(); + Logger.i(TAG, '识别停止'); break; @@ -651,6 +676,66 @@ class AgentController extends GetxController { }); } + // 订阅BLE事件 + void _subscribeToBleEvents() { + try { + _bleEventSubscription = BleService().onStatusUpdated.listen((event) { + if (event is Map && event['type'] == 'wakeup') { + _handleBleWakeup(); + } + }); + Logger.i(TAG, 'BLE事件监听已设置'); + } catch (e) { + Logger.e(TAG, 'BLE事件监听设置失败: $e'); + } + } + + // 处理BLE唤醒信号 + void _handleBleWakeup() { + Logger.i(TAG, '🎧 收到BLE唤醒信号'); + + // 检查是否可以启动蓝牙唤醒 + if (!_canStartBleWakeup()) { + Logger.w(TAG, '当前状态不允许BLE唤醒'); + return; + } + + // 保存当前输入模式 + _saveCurrentInputMode(); + + // 设置蓝牙唤醒状态 + isBleWakeupActive.value = true; + + // 注意:这里不需要调用startVoiceInput(),因为原生层已经启动了语音识别 + // 我们只需要同步Flutter层的状态 + isTextInputMode.value = false; + + Logger.i(TAG, 'BLE唤醒状态已设置,等待原生层语音识别结果'); + } + + // 检查是否可以启动BLE唤醒 + bool _canStartBleWakeup() { + // 如果已经在BLE唤醒状态,不允许重复启动 + if (isBleWakeupActive.value) { + Logger.w(TAG, '已在BLE唤醒状态,忽略重复唤醒'); + return false; + } + + // 如果正在打电话模式,不允许BLE唤醒 + if (isPhoneCallMode.value) { + Logger.w(TAG, '正在打电话模式,忽略BLE唤醒'); + return false; + } + + // 如果正在处理TTS,暂时不允许启动 + if (isSpeaking.value) { + Logger.w(TAG, '正在播放TTS,延迟BLE唤醒'); + return false; + } + + return true; + } + // 发送消息(包括所有支持的消息类型) //场景1:用户在文本输入框中输入文本,点击发送按钮,调用sendMessage()方法。 //场景2:当用户选择图片后在文本框输入内容并发送时,调用sendMessage()转发给sendImageMessage()。 @@ -1039,9 +1124,46 @@ class AgentController extends GetxController { useImagePrompt('请翻译这张图片中的文字'); } - // 开始打电话模式 - void startPhoneCall() async { - Logger.i(TAG, '🔥 开始打电话模式'); + // 模式历史记录 + VoiceInputMode? _previousInputMode; + + // 保存当前输入模式 + void _saveCurrentInputMode() { + if (isPushToTalkMode.value) { + _previousInputMode = VoiceInputMode.pushToTalk; + } else if (isTextInputMode.value) { + _previousInputMode = VoiceInputMode.text; + } else { + _previousInputMode = null; + } + } + + // 恢复之前的输入模式 + void _restorePreviousInputMode() { + switch (_previousInputMode) { + case VoiceInputMode.pushToTalk: + isTextInputMode.value = false; + isPushToTalkMode.value = true; + break; + case VoiceInputMode.text: + isTextInputMode.value = true; + isPushToTalkMode.value = false; + break; + default: + // 默认恢复到文字模式 + isTextInputMode.value = true; + isPushToTalkMode.value = false; + break; + } + _previousInputMode = null; // 清除历史记录 + } + + // 开始打电话模式(支持耳机唤醒) + Future startPhoneCall({bool isHeadsetTriggered = false}) async { + Logger.i(TAG, '🔥 开始打电话模式 (耳机触发: $isHeadsetTriggered)'); + + // 保存当前输入模式,以便通话结束后恢复 + _saveCurrentInputMode(); // 收缩"+"选项 collapsePlusOptions(); @@ -1062,8 +1184,9 @@ class AgentController extends GetxController { ); if (!hasPermission) { - // 权限未获取,退出打电话模式 + // 权限未获取,退出打电话模式并恢复之前状态 isPhoneCallMode.value = false; + _previousInputMode = null; // 清除历史记录 return; } @@ -1085,9 +1208,85 @@ class AgentController extends GetxController { // 停止语音输入 await stopVoiceInput(); - // 切换回文本模式,并禁用按住说话模式 - isTextInputMode.value = true; - isPushToTalkMode.value = false; + // 恢复之前的输入模式,而不是强制切换到文字模式 + _restorePreviousInputMode(); + } + + // 耳机唤醒通话模式(专用入口) + Future startHeadsetCall() async { + Logger.i(TAG, '🎧 耳机唤醒通话模式'); + if (canStartHeadsetCall()) { + await startPhoneCall(isHeadsetTriggered: true); + } + } + + // 检查当前是否可以启动耳机通话 + bool canStartHeadsetCall() { + // 如果已经在打电话模式,不允许重复启动 + if (isPhoneCallMode.value) { + Logger.w(TAG, '已在打电话模式,忽略耳机唤醒'); + return false; + } + + // 如果正在处理语音或TTS,暂时不允许启动 + if (isListening.value || isSpeaking.value || isProcessing.value) { + Logger.w(TAG, '当前正在处理语音,延迟耳机唤醒'); + return false; + } + + return true; + } + + // 验证语音输入状态一致性 + void _validateVoiceInputStates() { + // 检查状态冲突 + if (isPhoneCallMode.value && isPushToTalkMode.value) { + Logger.w(TAG, '状态冲突:同时启用打电话和按住说话模式,优先保持打电话模式'); + isPushToTalkMode.value = false; + } + + // 检查文字模式与语音模式的一致性 + if (isTextInputMode.value && (isPushToTalkMode.value || isPhoneCallMode.value)) { + Logger.w(TAG, '状态冲突:文字模式与语音模式同时启用,优先保持语音模式'); + isTextInputMode.value = false; + } + + // 检查按住说话状态的合理性 + if (isPushToTalkActive.value && !isPushToTalkMode.value) { + Logger.w(TAG, '状态冲突:按住说话激活但未启用按住说话模式'); + isPushToTalkActive.value = false; + } + } + + // 安全的状态切换方法 + void _safeStateTransition(VoiceInputMode targetMode) { + Logger.i(TAG, '安全状态切换到: $targetMode'); + + switch (targetMode) { + case VoiceInputMode.text: + isTextInputMode.value = true; + isPushToTalkMode.value = false; + isPhoneCallMode.value = false; + isPushToTalkActive.value = false; + break; + + case VoiceInputMode.pushToTalk: + isTextInputMode.value = false; + isPushToTalkMode.value = true; + isPhoneCallMode.value = false; + isPushToTalkActive.value = false; + break; + + case VoiceInputMode.phoneCall: + isTextInputMode.value = false; + isPushToTalkMode.value = false; + isPhoneCallMode.value = true; + isPushToTalkActive.value = false; + break; + } + + // 验证状态一致性 + _validateVoiceInputStates(); } // 开始按住说话