import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:amap_service/amap_service.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:uuid/uuid.dart'; import '/data/models/appconfig.dart'; import '/data/models/appconfig_model.dart'; import '/data/models/song_model.dart'; import '/data/services/ble_manager.dart'; import '/data/services/bluetooth_manager.dart'; import '/data/services/qq_music.dart'; import 'package:flutter_intro/flutter_intro.dart'; import '../../../data/services/novice_guide_service.dart'; import '/core/utils/upload_oss.dart'; import '/data/models/user_Info.dart'; import '/data/services/music_manager.dart'; import '/data/services/navigation_manager.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:agent_service/agent_service.dart'; import 'package:chat_storage/chat_storage.dart'; import 'package:device_jieli/device_jieli.dart'; import 'package:image_picker/image_picker.dart'; import 'package:get_storage/get_storage.dart'; import 'package:permission_handler/permission_handler.dart'; import '../../../core/utils/logger.dart'; import 'package:path_provider/path_provider.dart'; import 'package:photo_view/photo_view.dart'; import '../../../core/utils/permission_util.dart'; import '../../../data/models/message_model.dart'; import '../../common/widgets/recharge_dialog.dart'; // 语音输入模式枚举 enum VoiceInputMode { text, // 文字输入模式 pushToTalk, // 按住说话模式 phoneCall, // 打电话模式 } class AgentController extends GetxController with WidgetsBindingObserver { static AgentController get to => Get.find(); //单例访问模式 final GetStorage _storage = GetStorage(); // 聊天消息列表 final messages = [].obs; final currsessionid = ''.obs; final vipExpiryTime = 0.obs; // UI控制器 final TextEditingController textController = TextEditingController(); final ScrollController scrollController = ScrollController(); final noviceGuideService = Get.find(); // 确保服务已初始化 // ========== 依赖服务 ========== final BluetoothManager _bluetoothManager = Get.find(); final BleManager _bleManager = Get.find(); //音乐播放管理 final _musiceManager = Get.find(); final _qqmusicManager = Get.find(); // 导航管理 final _navigationManager = Get.find(); RxBool isPlaying = false.obs; RxInt currentId = 0.obs; // 播放进度相关状态 RxInt currentPosition = 0.obs; // 当前播放位置(毫秒) RxInt duration = 0.obs; // 总时长(毫秒) RxDouble progress = 0.0.obs; // 播放进度(0.0-1.0) // 当前播放列表管理 final currentPlaylist = >[].obs; // 当前播放列表 final currentPlaylistIndex = 0.obs; // 当前播放歌曲在列表中的索引 // 各种状态标志 final isInitialized = false.obs; final isListening = false.obs; //是否正在语音识别 final isSpeaking = false.obs; //是否正在播放tts final isProcessing = false.obs; // TTS启用状态 final isTtsEnabled = true.obs; final ttsRatePercent = 25.obs; // TTS播报打断启用状态 final isTtsInterruptEnabled = false.obs; // 是否显示工具调用 final isShowToolInterruptEnabled = false.obs; // 图片处理状态 final isImageProcessing = false.obs; // 输入模式控制 final isTextInputMode = true.obs; //文本输入模式vs语音输入模式 // 当前输入的文本内容 final currentText = ''.obs; // 图片输入相关 final isImageInputActive = false.obs; // 是否处于图片输入模式 final selectedImagePath = Rx(null); // 当前选择的图片路径 // 输入状态控制 - 用于动态显示发送按钮 final hasTextInput = false.obs; // 是否有文本输入 // 全屏编辑相关状态 final isFullScreenEditMode = false.obs; // 是否处于全屏编辑模式 final shouldShowExpandButton = false.obs; // 是否显示展开按钮 // "+"号选项展开状态 final isPlusOptionsExpanded = false.obs; // 是否展开"+"号选项 // 打电话模式状态 final isPhoneCallMode = false.obs; // 是否处于打电话模式 // ─── 设备通话模式(device_jieli 耳机录音 → 外部注入 ASR)───── // 独立于普通打电话模式,不复用 isPhoneCallMode final isDeviceCallMode = false.obs; // 是否处于设备通话模式 final isDeviceCallMicMuted = false.obs; // 设备通话模式下是否静音(丢帧) StreamSubscription? _deviceCallJieliSub; bool _deviceCallPushing = false; // 是否当前允许把设备 PCM 推给 ASR // 调试:每秒打印一次 PCM 推送统计,确认音频是否真正流到 ASR int _deviceCallPushedFrames = 0; int _deviceCallPushedBytes = 0; int _deviceCallLastReportMs = 0; // 按住说话状态 final isPushToTalkActive = false.obs; // 是否正在按住说话 // 按住说话模式状态 - 用于区分是否处于按住说话模式(即使当前没有按住) //final isPushToTalkMode = false.obs; // 是否处于按住说话模式 // 键盘状态 final isKeyboardVisible = false.obs; // 键盘是否可见 // 流事件订阅 StreamSubscription? _eventSubscription; // 导航流事件订阅 StreamSubscription? _amapeventSubscription; // 进度更新定时器 Timer? _progressTimer; // 用于跟踪AI回复状态 bool _isNewAssistantResponse = true; RxBool newAIReplyCompleted = true.obs; String _currentResponseId = ''; final TAG = 'AgentController'; var AppDir = "".obs; // 会话ID final String agentId = 'default_agent'; var musicServiceType = 'WY'; // 分页加载相关状态 final isLoadingMore = false.obs; // 是否正在加载更多消息 final hasMoreMessages = true.obs; // 是否还有更多消息 final currentPage = 1.obs; // 当前页码 final pageSize = 20; // 每页消息数量 final isLoadingMoreError = false.obs; // 加载更多时是否出错 // 是否已加载聊天记录 final isHistoryLoaded = false.obs; // 添加加载状态跟踪 final RxBool isLoadingMusic = false.obs; final RxInt loadingMusicId = 0.obs; // 设置加载状态的方法 void setLoadingMusic(int id, bool loading) { loadingMusicId.value = loading ? id : 0; isLoadingMusic.value = loading; } @override void onInit() { super.onInit(); musicServiceType = AppConfig.env('APP_ANDROID_MUSICSERVICE_TYPE') ?? 'AUTO'; if (Platform.isIOS) { musicServiceType = AppConfig.env('APP_IOS_MUSICSERVICE_TYPE') ?? 'AUTO'; } vipExpiryTime.value = User.isLoggedIn() ? User.instance.vipexptime : 0; ever(User.refreshTick, (_) { vipExpiryTime.value = User.instance.vipexptime; }); _initAppDir(); _loadTtsPreference(); //加载TTS偏好设置 _loadTtsRatePreference(); _loadTtsInterrupe(); //加载TTS播报打断偏好设置 // 监听存储中的“agent_tts_interrupe”键变化,实现跨页面状态同步 // 中文注释:当 Home 页或 Agent 页任意一处切换时,另一处可以通过监听存储自动刷新 UI _storage.listenKey('agent_tts_interrupt', (value) { try { final bool newValue = value is bool ? value : (value == 'true'); if (isTtsInterruptEnabled.value != newValue) { isTtsInterruptEnabled.value = newValue; } } catch (e) { // 中文注释:忽略异常,避免因类型不一致导致崩溃 } }); _setupScrollListener(); //设置滚动监听器 _loadChatHistory(); //加载聊天历史记录 _subscribeToEvents(); //订阅代理服务事件 _initMusicStatus(); //初始化音乐状态 _setupMediaSessionCallbacks(); //设置媒体会话回调 _setupTextInputListener(); //设置文本输入监听器 startVoiceInput(); //开启语音输入 // 监听应用生命周期:当应用切到后台/失焦时兜底结束按住说话,防止 onPointerUp 未触发 WidgetsBinding.instance.addObserver(this); // _qqmusicManager.syncCurrentPlayInfo(); } Future _initAppDir() async { AppDir.value = (await getApplicationDocumentsDirectory()).path; } // 初始化音乐播放状态 Future _initMusicStatus() async { try { await musicStatus(); Logger.d('AgentController', '音乐状态初始化完成'); } catch (e) { Logger.e('AgentController', '音乐状态初始化失败: $e'); } } // 设置媒体会话回调 void _setupMediaSessionCallbacks() async { try { await _musiceManager.setMediaSessionCallbacks( onPlay: onMediaSessionPlay, onPause: onMediaSessionPause, onNext: onMediaSessionNext, onPrevious: onMediaSessionPrevious, onSeekTo: onMediaSessionSeekTo, onStop: onMediaSessionStop, onProgress: onMediaSessionProgress, ); Logger.d('AgentController', '媒体会话回调设置完成'); } catch (e) { Logger.e('AgentController', '媒体会话回调设置失败: $e'); } } @override void onClose() { // 若处于设备通话模式,fire-and-forget 结束 if (isDeviceCallMode.value) { unawaited(_stopDeviceCallInternal()); } AgentService.stopAiSteam(); //停止AI流 AgentService.stopConversation(); // 停止当前会话 AgentService.stopTts(); // 停止当前语音 textController.dispose(); scrollController.dispose(); _eventSubscription?.cancel(); _amapeventSubscription?.cancel(); _deviceCallJieliSub?.cancel(); _deviceCallJieliSub = null; Intro.of(Get.context!).dispose(); // 取消应用生命周期监听 WidgetsBinding.instance.removeObserver(this); super.onClose(); } // 加载TTS偏好设置 Future _loadTtsPreference() async { try { final storage = GetStorage(); isTtsEnabled.value = storage.read('agent_tts_enabled') ?? true; } catch (e) { // 如果读取失败,使用默认值 Logger.e(TAG, '加载TTS偏好设置失败: $e'); isTtsEnabled.value = true; } } Future _loadTtsRatePreference() async { try { final value = _storage.read('agent_tts_rate_percent'); if (value is int) { ttsRatePercent.value = value; } else if (value is String) { final parsed = int.tryParse(value); if (parsed != null) { ttsRatePercent.value = parsed; } } else { ttsRatePercent.value = 25; } } catch (e) { ttsRatePercent.value = 25; } try { await AgentService.setTtsRate(ttsRatePercent.value); } catch (e) { Logger.e(TAG, '同步TTS语速失败: $e'); } } // 切换TTS状态 Future toggleTts() async { isTtsEnabled.toggle(); // 如果关闭TTS,停止当前语音 if (!isTtsEnabled.value && isSpeaking.value) { await AgentService.stopTts(); } // 保存偏好设置 try { final storage = GetStorage(); await storage.write('agent_tts_enabled', isTtsEnabled.value); } catch (e) { Logger.e(TAG, '保存TTS偏好设置失败: $e'); } } Future updateTtsRate(int rate) async { final newRate = rate.clamp(0, 100); ttsRatePercent.value = newRate; if (isSpeaking.value) { try { await AgentService.stopTts(); } catch (_) {} } try { await AgentService.setTtsRate(newRate); } catch (e) { Logger.e(TAG, '设置TTS语速失败: $e'); } try { await _storage.write('agent_tts_rate_percent', newRate); } catch (e) { Logger.e(TAG, '保存TTS语速偏好设置失败: $e'); } } // 加载TTS偏好设置 Future _loadTtsInterrupe() async { try { if (!_storage.hasData("agent_tts_interrupt")) { // 获取设备类型以确定检查策略 String deviceName = getDeviceName(); DBProduct? product = AppConfig.getproduct(deviceName); if (deviceName != "" && product != null) { // Logger.d('AgentController', // 'liwe----------product $deviceName ${product.toJson()}'); isTtsInterruptEnabled.value = !product.iscontinuouschat!; } else { isTtsInterruptEnabled.value = false; } } else { isTtsInterruptEnabled.value = _storage.read('agent_tts_interrupt') ?? false; } await AgentService.isInterrupt(isTtsInterruptEnabled.value); } catch (e) { // 如果读取失败,使用默认值 Logger.e(TAG, '加载TTS偏好设置失败: $e'); isTtsInterruptEnabled.value = false; } } /// 切换TTS播报打断状态 /// 当启用播报打断时,用户说话会自动停止当前的TTS播放 Future toggleTtsInterrupt() async { isTtsInterruptEnabled.toggle(); await _storage.write('agent_tts_interrupt', isTtsInterruptEnabled.value); // 保存偏好设置 try { await AgentService.isInterrupt(isTtsInterruptEnabled.value); } catch (e) { Logger.e(TAG, '保存TTS播报打断偏好设置失败: $e'); } } /// 当启用显示工具调用效果 Future toggleShowAIToolInterrupt() async { isShowToolInterruptEnabled.toggle(); } // 设置文本输入监听器 void _setupTextInputListener() { textController.addListener(() { final text = textController.text.trim(); hasTextInput.value = text.isNotEmpty; // 检查是否需要显示展开按钮 _checkShouldShowExpandButton(); }); } // 检查是否需要显示展开按钮 void _checkShouldShowExpandButton() { final text = textController.text; if (text.isEmpty) { shouldShowExpandButton.value = false; return; } // 计算文本行数 final lines = text.split('\n'); final maxLines = isImageInputActive.value ? 3 : 5; // 简单估算:如果换行符数量超过maxLines-1,或者单行文本过长,显示展开按钮 final hasMultipleLines = lines.length > maxLines; final hasLongLine = lines.any((line) => line.length > 50); // 估算单行字符数 shouldShowExpandButton.value = hasMultipleLines || hasLongLine; } // 打开全屏编辑模式 void openFullScreenEdit() { isFullScreenEditMode.value = true; } // 关闭全屏编辑模式 void closeFullScreenEdit() { isFullScreenEditMode.value = false; } // 从全屏编辑模式发送消息 Future sendMessageFromFullScreen() async { await sendMessage(); closeFullScreenEdit(); } // 加载聊天历史记录 Future _loadChatHistory() async { try { Logger.i(TAG, '正在加载聊天历史...'); //通过本地插件ChatStorage获取聊天记录 final result = await ChatStorage.getMessages( agentId: agentId, //会话ID page: 1, //从第一页开始 pageSize: pageSize, //每页20条记录 ); Logger.i(TAG, '加载聊天历史记录: $result'); if (result.messages.isNotEmpty) { // 转换并添加消息到列表 final historyMessages = _convertChatMessagesToMessages(result.messages); // 消息按时间排序,最新的消息在底部 historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); messages.assignAll(historyMessages); // 更新分页状态 currentPage.value = 1; hasMoreMessages.value = result.totalPages > 1; // print( // 'liwei-------------读取历史记录 ${AppDir.value}${historyMessages[0].imagePath}'); Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录,总页数: ${result.totalPages}'); _logMessageDetails(historyMessages); } else { // 没有历史记录时添加欢迎消息 _addWelcomeMessage(); hasMoreMessages.value = false; Logger.i(TAG, '没有历史记录,添加欢迎消息'); } } catch (e) { Logger.e(TAG, '加载聊天历史失败: $e'); _addWelcomeMessage(); hasMoreMessages.value = false; } finally { isHistoryLoaded.value = true; messages.refresh(); // 使用reverse:true时不需要手动滚动到底部 } } // 加载更多历史消息(分页加载) Future loadMoreMessages() async { // 防止重复加载 if (isLoadingMore.value || !hasMoreMessages.value) { Logger.d(TAG, '跳过加载更多:isLoadingMore=${isLoadingMore.value}, hasMoreMessages=${hasMoreMessages.value}'); return; } try { isLoadingMore.value = true; isLoadingMoreError.value = false; final nextPage = currentPage.value + 1; Logger.i(TAG, '正在加载第 $nextPage 页历史消息...'); final result = await ChatStorage.getMessages( agentId: agentId, page: nextPage, pageSize: pageSize, ); if (result.messages.isNotEmpty) { // 转换新消息 final newMessages = _convertChatMessagesToMessages(result.messages); // 按时间排序新消息 newMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); // 添加短暂延迟,让用户感知到加载过程 await Future.delayed(const Duration(milliseconds: 300)); // 将新消息插入到现有消息列表的开头(因为是历史消息) final updatedMessages = [...newMessages, ...messages]; messages.assignAll(updatedMessages); // 更新分页状态 currentPage.value = nextPage; hasMoreMessages.value = nextPage < result.totalPages; Logger.i(TAG, '成功加载 ${newMessages.length} 条历史消息,当前页: $nextPage/${result.totalPages}'); } else { // 没有更多消息 hasMoreMessages.value = false; Logger.i(TAG, '没有更多历史消息'); } } catch (e) { Logger.e(TAG, '加载更多消息失败: $e'); isLoadingMoreError.value = true; } finally { isLoadingMore.value = false; } } // 转换ChatMessage到Message的通用方法 List _convertChatMessagesToMessages(List chatMessages) { return chatMessages.map((msg) { final meta = msg.metadata != null && msg.metadata != "" ? jsonDecode(msg.metadata!) : null; // 如果为空,使用空对象 // 从元数据中提取图片信息 final hasImage = meta != null && meta.containsKey('hasImage') ? meta['hasImage'] as bool : false; final imagePath = meta != null && meta.containsKey('imagePath') ? meta['imagePath'] as String? : null; return Message( sessionid: msg.sessionId, // 会话ID isUser: msg.sender == 'user', text: msg.message, meta: meta, timestamp: DateTime.parse(msg.timestamp).toLocal(), hasImage: hasImage, imagePath: imagePath, ); }).toList(); } // 打印消息详情的通用方法 void _logMessageDetails(List messageList) { Logger.d(TAG, '===== 历史消息详情开始 ====='); for (int i = 0; i < messageList.length; i++) { final msg = messageList[i]; Logger.d( TAG, '消息[$i]: {' 'isUser: ${msg.isUser}, ' 'text: "${msg.text.length > 50 ? msg.text.substring(0, 50) + "..." : msg.text}", ' 'time: ${msg.timestamp}, ' 'hasImage: ${msg.hasImage}, ' 'imagePath: ${msg.imagePath ?? "无"}, ' 'meta: ${msg.meta != null ? "有" : "无"}' '}'); } Logger.d(TAG, '===== 历史消息详情结束 ==lishi==='); } // 设置滚动监听器 void _setupScrollListener() { scrollController.addListener(() { // 检查是否滚动到顶部(因为使用了reverse: true,顶部实际是最大滚动位置) if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 100) { // 当距离顶部100像素时触发加载更多 if (hasMoreMessages.value && !isLoadingMore.value) { Logger.d(TAG, '触发加载更多历史消息'); loadMoreMessages(); } } }); } // 手动触发加载更多(用于测试或手动调用) void triggerLoadMore() { if (hasMoreMessages.value && !isLoadingMore.value) { Logger.i(TAG, '手动触发加载更多历史消息'); loadMoreMessages(); } else { Logger.w(TAG, '无法加载更多:hasMoreMessages=${hasMoreMessages.value}, isLoadingMore=${isLoadingMore.value}'); } } // 重置分页状态(用于刷新聊天记录) void resetPagination() { currentPage.value = 1; hasMoreMessages.value = true; isLoadingMore.value = false; isLoadingMoreError.value = false; Logger.i(TAG, '分页状态已重置'); } // 添加初始欢迎消息 void _addWelcomeMessage() { messages.add(Message( sessionid: "", // 会话ID isUser: false, text: 'welcomeMessage'.tr, // 你好!我是**Lily**,我能帮你: )); } // 订阅代理服务事件 void _subscribeToEvents() { _eventSubscription = AgentService.events.listen((event) { final String asrProvider = (event.data['provider'] ?? '').toString(); if (asrProvider.isNotEmpty) Logger.i(TAG, 'ASR provider: $asrProvider'); switch (event.type) { case AgentServiceEventType.recognitionStarted: isListening.value = true; //isTextInputMode.value = false; Logger.i(TAG, '订阅代理服务事件识别开始'); // isTextInputMode.refresh(); // 只标记识别开始,不添加消息 Logger.i(TAG, '识别开始'); break; case AgentServiceEventType.recognitionStopped: isListening.value = false; Logger.i(TAG, '识别停止'); break; case AgentServiceEventType.recognizing: final sessionid = event.data['sessionid'] ?? ''; final text = event.data['text'] ?? ''; currentText.value = text; // 保留当前文本,以便其他地方使用 Logger.i(TAG, '[$sessionid]识别中间结果: $text'); if (text.isNotEmpty) { // 查找是否有正在识别中的消息 final index = messages.lastIndexWhere( (msg) => msg.sessionid == sessionid && msg.isUser); if (index >= 0) { // 更新已有的识别中消息 messages[index] = Message( sessionid: sessionid, // 会话ID isUser: true, text: text, isRecognizing: true, timestamp: messages[index].timestamp, ); } else { currsessionid.value = event.data['sessionid'] ?? ''; // 没有识别中的消息,创建新消息 messages.add(Message( sessionid: sessionid, // 会话ID isUser: true, text: text, isRecognizing: true, )); } // messages.refresh(); } break; case AgentServiceEventType.recognitionResult: final sessionid = event.data['sessionid'] ?? ''; final text = event.data['text'] ?? ''; Logger.i(TAG, '[$sessionid]识别最终结果: $text'); // if (currsessionid.value != "" && currsessionid.value != sessionid) { // final index = messages.lastIndexWhere( // (msg) => msg.sessionid == currsessionid.value && msg.isUser); // //出现了为识别结束的消息 // if (index >= 0) { // //异除去当前会话的识别中消息 // messages.removeAt(index); // } // } currsessionid.value = sessionid; // 查找是否有正在识别中的消息 final index = messages.lastIndexWhere( (msg) => msg.sessionid == sessionid && msg.isUser); if (text.isNotEmpty) { if (index >= 0) { // 将识别中的消息转换为最终消息 messages[index] = Message( sessionid: sessionid, isUser: true, text: text, isRecognizing: false, timestamp: messages[index].timestamp, functionCalls: messages[index].functionCalls, isToolCallsExpanded: messages[index].isToolCallsExpanded, ); // messages.refresh(); } else { // 如果没有找到识别中的消息,添加新消息 messages.add(Message( sessionid: sessionid, isUser: true, text: text, isRecognizing: false)); } isProcessing.value = true; // 标记为新的AI回复 _isNewAssistantResponse = true; // 使用reverse:true时通常不需要手动滚动 } else if (index >= 0) { // 如果识别结果为空,移除临时的识别消息 messages.removeAt(index); // messages.refresh(); } break; //识别取消(网络波动会触发) case AgentServiceEventType.recognitionCanceled: final sessionid = event.data['sessionid'] ?? ''; // 查找是否有正在识别中的消息 final index = messages.lastIndexWhere( (msg) => msg.sessionid == sessionid && msg.isUser); currsessionid.value = ''; if (currentText.value.isNotEmpty) { if (index >= 0) { // 将识别中的消息转换为最终消息 messages[index] = Message( sessionid: sessionid, // 会话ID isUser: true, text: currentText.value, isRecognizing: false, timestamp: messages[index].timestamp, functionCalls: messages[index].functionCalls, isToolCallsExpanded: messages[index].isToolCallsExpanded, ); } else { // 如果没有找到识别中的消息,添加新消息 messages.add(Message( sessionid: sessionid, isUser: true, text: currentText.value, isRecognizing: false, )); } isProcessing.value = true; // 标记为新的AI回复 _isNewAssistantResponse = true; // 使用reverse:true时通常不需要手动滚动 } else if (index >= 0) { // 如果识别结果为空,移除临时的识别消息 messages.removeAt(index); } messages.refresh(); break; case AgentServiceEventType.ttsStarted: isSpeaking.value = true; print('TTS开始播放'); break; case AgentServiceEventType.ttsCompleted: case AgentServiceEventType.ttsStopped: case AgentServiceEventType.ttsCanceled: isSpeaking.value = false; Logger.i(TAG, 'TTS停止播放, $event.type'); break; case AgentServiceEventType.playbackStarted: isSpeaking.value = true; Logger.i(TAG, 'TTS播放开始'); break; case AgentServiceEventType.playbackCompleted: isSpeaking.value = false; Logger.i(TAG, 'TTS播放完成'); break; case AgentServiceEventType.imageProcessing: isImageProcessing.value = true; break; case AgentServiceEventType.imageReady: // 图片准备就绪,但AI还没有开始处理,保持processing状态 break; case AgentServiceEventType.assistantToken: if (!isProcessing.value) isProcessing.value = true; final sessionid = event.data['sessionid'] ?? ''; final token = event.data['token'] ?? ''; // Logger.i(TAG, 'AI回复Token: $token, responseId: $responseId'); if (token.isNotEmpty) { // 如果是新的回复或者响应ID改变,创建新消息 final index = messages.lastIndexWhere( (msg) => msg.sessionid == sessionid && !msg.isUser); if (index == -1) { messages.add( Message(sessionid: sessionid, isUser: false, text: token)); _isNewAssistantResponse = false; } else { messages[index] = Message( sessionid: sessionid, isUser: false, text: messages[index].text + token, meta: messages[index].meta, timestamp: messages[index].timestamp, functionCalls: messages[index].functionCalls, isToolCallsExpanded: messages[index].isToolCallsExpanded, ); } // 使用reverse:true时通常不需要手动滚动 } break; case AgentServiceEventType.assistantResponse: isProcessing.value = false; isImageProcessing.value = false; // 标记当前回复完成 _isNewAssistantResponse = true; newAIReplyCompleted.value = true; break; case AgentServiceEventType.error: final sessionid = event.data['sessionid'] ?? ''; final message = event.data['message'] ?? ''; final code = event.data['code'] ?? 0; // //验证错误码 if (code == 1000) { isListening.value = false; isSpeaking.value = false; isProcessing.value = false; isImageProcessing.value = false; if (sessionid != "") { final index = messages.lastIndexWhere( (msg) => msg.sessionid == sessionid && !msg.isUser); if (index >= 0) { messages.removeAt(index); messages.refresh(); } } Get.snackbar('error'.tr, '${event.data['message']}'); } else if (code == 2001) { // 余额不足或需要充值,给出弹窗确认是否跳转充值界面 showRechargeDialog('agent'); } Logger.e(TAG, '代理服务错误: ${event.data['message']}'); break; case AgentServiceEventType.autoStop: isListening.value = false; final sessionid = event.data['sessionid'] ?? ''; // 移除临时的识别消息 final index = messages.lastIndexWhere( (msg) => msg.sessionid == sessionid && msg.isUser); if (index >= 0) { // 只有当消息为空或文本太短时才移除 if (messages[index].text.trim().length < 2) { messages.removeAt(index); // messages.refresh(); } else { // 否则转换为正式消息 messages[index] = Message( sessionid: sessionid, isUser: true, text: messages[index].text, timestamp: messages[index].timestamp, functionCalls: messages[index].functionCalls, isToolCallsExpanded: messages[index].isToolCallsExpanded, ); // messages.refresh(); // 发送此消息到AI处理 isProcessing.value = true; _isNewAssistantResponse = true; _handleAutoStop(sessionid, index); } } break; case AgentServiceEventType.functionCall: final sessionid = event.data['sessionid'] ?? ''; final functionName = event.data['name'] ?? ''; final arguments = event.data['arguments'] ?? ''; // debugPrint('Fultter 开始调用 mcp: $functionName, $arguments'); if (functionName == 'exit_interaction') { if (isPhoneCallMode.value) { endPhoneCall(); //退出语音交互,同时退出打电话模式 } } final index = messages.lastIndexWhere( (msg) => msg.sessionid == sessionid && !msg.isUser); if (index == -1) { messages.add(Message( sessionid: sessionid, isUser: false, text: '', functionCalls: { functionName: {'arguments': arguments} })); _isNewAssistantResponse = false; } else { // 否则更新最后一条消息 final lastIndex = messages.length - 1; if (lastIndex >= 0 && !messages[lastIndex].isUser) { messages[lastIndex] = Message( sessionid: sessionid, isUser: false, text: messages[lastIndex].text, meta: messages[lastIndex].meta, timestamp: messages[lastIndex].timestamp, functionCalls: messages[lastIndex].functionCalls ?? {} ..addAll({ functionName: {'arguments': arguments} }), ); } } break; case AgentServiceEventType.functionCallResult: final sessionid = event.data['sessionid'] ?? ''; final function_name = event.data['function_name'] ?? ''; final function_result = event.data['result'] ?? ''; final metaStr = event.data['meta'] ?? ''; final index = messages.lastIndexWhere( (msg) => msg.sessionid == sessionid && !msg.isUser); if (index != -1) { // 获取当前的 functionCalls,如果为空则创建新的 Map final functionCalls = Map>.from( messages[index].functionCalls?.map((key, value) => MapEntry( key, Map.from(value as Map))) ?? {}); // 更新对应工具的调用结果 if (function_name.isNotEmpty) { // 如果该工具已存在,更新其结果;如果不存在,创建新的条目 if (functionCalls.containsKey(function_name)) { functionCalls[function_name]!['result'] = function_result; } else { functionCalls[function_name] = { 'result': function_result, }; } } messages[index] = Message( sessionid: sessionid, isUser: false, text: messages[index].text, meta: messages[index].meta, timestamp: messages[index].timestamp, functionCalls: functionCalls, isToolCallsExpanded: messages[index].isToolCallsExpanded, ); } Logger.i(TAG, 'Flutter 结束调用 $sessionid mcp: $metaStr'); if (metaStr.isNotEmpty) { Map metaResult = {}; metaResult = jsonDecode(metaStr) as Map; if (metaResult.isNotEmpty) { var notreply = false; if (metaResult.containsKey("notreply")) { // 如果有导航卡片,直接添加 notreply = metaResult["notreply"] as bool; } if (notreply) { Logger.i(TAG, 'Flutter 结束调用 不回复'); //不回复直接返回 break; } Logger.i(TAG, 'Flutter 结束调用 卡片数据 $index $metaResult'); // 判断是否为新的回复或响应ID是否改变 if (index == -1) { // 添加新消息 messages.add(Message( sessionid: sessionid, isUser: false, text: "", meta: metaResult)); _isNewAssistantResponse = false; } else { // 更新最后一条消息 messages[index] = Message( sessionid: sessionid, isUser: false, text: messages[index].text, meta: metaResult, timestamp: messages[index].timestamp, functionCalls: messages[index].functionCalls, isToolCallsExpanded: messages[index].isToolCallsExpanded, ); } messages.refresh(); if (metaResult.containsKey("maps_route_navigation")) { final maps_route_navigation = metaResult["maps_route_navigation"]; if (maps_route_navigation is Map) { final card_navigation = maps_route_navigation["card_navigation"]; final isinternal = metaResult["isinternal"] ?? false; if (card_navigation is Map) { //导航 final start = card_navigation["start"] as String; final destination = card_navigation["destination"] as String; final end_longitude = card_navigation["end_longitude"] as String; final end_latitude = card_navigation["end_latitude"] as String; // End_longitude string `json:"end_longitude"` //目标点经度 // End_latitude string `json:"end_latitude"` //目标点纬度 final items = card_navigation['items']; if (items != null) { Future.delayed(Duration(seconds: 2), () { // Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end '); // 2秒后执行的代码 final navigationMode = AppConfig.env('APP_NAVIGATION_MODE') ?? 'external_auto'; if (navigationMode == 'internal_gaode' || isinternal) { _navigationManager.open( start, "$end_longitude,$end_latitude", "driving"); } // openNavigation(false, start, destination, end_latitude, // end_longitude, ""); }); } } } } else if (metaResult .containsKey("maps_google_route_navigation")) { //导航 final maps_google_route_navigation_raw = metaResult["maps_google_route_navigation"]; // 安全的类型检查 if (maps_google_route_navigation_raw is Map) { final maps_google_route_navigation = maps_google_route_navigation_raw; final data = maps_google_route_navigation["data"]; if (data != null) { final end = data["destination"] as String; final mode = data["items"][0]["ntype"] as String; Future.delayed(Duration(seconds: 1), () { // Logger.i(TAG, 'liwei-------- 打开google地图: $end '); openGoogleMap(end, mode); }); } } } } else { // 其他函数调用 Logger.i(TAG, 'Flutter 结束调用 解析失败 $metaStr'); } } if (function_name.contains("music")) { Future.delayed(Duration(seconds: 1), () { Logger.i(TAG, '音乐服务 $musicServiceType'); if (musicServiceType == "QQ") { _qqmusicManager.syncCurrentPlayInfo(isOpenPlay: false); } else if (musicServiceType == "WY") { musicStatus(); } }); // } } break; case AgentServiceEventType.assistantUsages: final sessionid = event.data['sessionid'] ?? ''; final integral = event.data['integral'] ?? 0; User.instance.aichatintegral = integral; break; default: break; } }); //后台导航事件同步 _amapeventSubscription = _navigationManager.events().listen((event) { switch (event.type) { case AmapServiceEventType.starte: Logger.i(TAG, '导航通知 started!'); break; case AmapServiceEventType.stope: Logger.i(TAG, '导航通知 stoped!'); break; case AmapServiceEventType.update: Logger.i(TAG, '导航通知 update! $event'); Message? result; try { result = messages.lastWhere( (msg) => !msg.isUser && msg.meta?.containsKey("card_navigation") == true, ); } catch (e) { result = null; } if (result != null) { int index = messages.indexOf(result); if (index != -1) { // ✅ 创建 meta 的深拷贝 final newMeta = Map.from(result.meta ?? {}); newMeta["card_navigation"]["navigation"] = event.data; // ✅ 创建新的 Message 替换原来的 messages[index] = Message( sessionid: result.sessionid, isUser: result.isUser, text: result.text, meta: newMeta, timestamp: result.timestamp, ); messages.refresh(); Logger.i( TAG, '更新导航卡片! ${newMeta["card_navigation"]["navigation"]}'); } } break; case AmapServiceEventType.error: Logger.i(TAG, '导航通知 error!'); break; case AmapServiceEventType.unknown: Logger.i(TAG, '导航通知 unknown!'); break; } }); } // 发送消息(包括所有支持的消息类型) //场景1:用户在文本输入框中输入文本,点击发送按钮,调用sendMessage()方法。 //场景2:当用户选择图片后在文本框输入内容并发送时,调用sendMessage()转发给sendImageMessage()。 Future sendMessage() async { // 收回键盘 FocusManager.instance.primaryFocus?.unfocus(); // 1. 处理图片+文本的混合输入 if (isImageInputActive.value && selectedImagePath.value != null) { final text = textController.text.trim(); final imagePath = selectedImagePath.value!; // 使用!强制断言非空 await sendImageMessage(imagePath, text: text); // 清除图片输入状态 clearImageInput(); return; } // 2. 处理纯文本消息 final text = textController.text.trim(); if (text.isEmpty) return; // 使用reverse:true时通常不需要手动滚动 // 3. 调用AI服务处理 try { isProcessing.value = true; // 标记为新的AI回复 _isNewAssistantResponse = true; newAIReplyCompleted.value = false; currsessionid.value = const Uuid().v4(); final sessionid = currsessionid.value; // 根据TTS状态决定是否朗读 AgentService.processTextInput(sessionid, text, speakResponse: isTtsEnabled.value); final message = Message(sessionid: sessionid, isUser: true, text: text); Logger.i(TAG, '发送AI消息:$sessionid $text'); messages.add(message); textController.clear(); } catch (e) { Logger.e(TAG, '发送文本消息失败: $e'); isProcessing.value = false; } } // 发送图片消息 Future sendImageMessage(String imagePath, {String? text}) async { if (imagePath.isEmpty || !File(imagePath).existsSync()) { Logger.e(TAG, '图片不存在: $imagePath'); return; } // 复制图片到应用永久存储目录 // 复制图片到应用永久存储目录 final imagePathResult = await _copyImageToPermanentStorage(imagePath); final permanentImagePath = imagePathResult['fullPath']!; final relativeImagePath = imagePathResult['relativePath']!; var ImageUrl = await UploadOss.upload( filepath: permanentImagePath, rootDir: 'User/${User.instance.uid}/ChatImages'); // 显示的文本,如果没有提供则使用默认值 final displayText = text?.isNotEmpty == true ? text! : '[图片]'; currsessionid.value = const Uuid().v4(); final sessionid = currsessionid.value; Logger.i(TAG, '发送AI图片消息:$sessionid $displayText $permanentImagePath URL: $ImageUrl'); // 添加用户消息 final message = Message( sessionid: sessionid, isUser: true, text: displayText, hasImage: true, imagePath: relativeImagePath, ); messages.add(message); // 清空文本输入 textController.clear(); // 使用reverse:true时通常不需要手动滚动 try { isProcessing.value = true; isImageProcessing.value = true; // 标记为新的AI回复 _isNewAssistantResponse = true; // 保存消息到持久化存储 //await _saveMessageToStorage(message); currsessionid.value = const Uuid().v4(); final sessionid = currsessionid.value; // 调用Agent Service处理图片,考虑TTS状态 await AgentService.processImageInput( sessionid, relativeImagePath, ImageUrl, text: text ?? '', speakResponse: isTtsEnabled.value, ); } catch (e) { Logger.e(TAG, '处理图片失败: $e'); isProcessing.value = false; isImageProcessing.value = false; } } // 将图片复制到应用的永久存储目录 Future> _copyImageToPermanentStorage( String sourcePath) async { try { final File sourceFile = File(sourcePath); if (!await sourceFile.exists()) { Logger.e(TAG, '源图片文件不存在: $sourcePath'); return { 'fullPath': sourcePath, 'relativePath': sourcePath }; // 如果源文件不存在,返回原路径 } // 创建应用文档目录下的chatImages文件夹 final appDir = await getApplicationDocumentsDirectory(); final imagesDir = Directory('${appDir.path}/chatImages'); if (!await imagesDir.exists()) { await imagesDir.create(recursive: true); } // 生成唯一文件名 final fileName = '${DateTime.now().millisecondsSinceEpoch}_${sourcePath.split('/').last}'; final targetPath = '${imagesDir.path}/$fileName'; final relativePath = '/chatImages/$fileName'; // 复制文件 await sourceFile.copy(targetPath); Logger.i(TAG, '图片已复制到永久存储: $targetPath'); return {'fullPath': targetPath, 'relativePath': relativePath}; } catch (e) { Logger.e(TAG, '复制图片到永久存储失败: $e'); return {'fullPath': sourcePath, 'relativePath': sourcePath}; // 出错时返回原路径 } } // 切换输入模式(改造为按住说话模式) void toggleInputMode() async { // 如果当前在图片输入模式,先清除 if (isImageInputActive.value) { clearImageInput(); return; } // 如果要切换到语音模式,需要先检查麦克风权限 if (isTextInputMode.value) { // 使用 PermissionUtil 检查麦克风权限 final hasPermission = await PermissionUtil.instance.requestPermission( // context: Get.context!, permissionType: Permission.microphone, permissionName: 'microphonePermission'.tr, // 麦克风 explanationText: 'voiceRecognitionPermissionText'.tr, // 语音识别功能需要访问麦克风。 permanentDenialText: 'permissionDeniedText'.tr, // 您已永久拒绝麦克风权限,请前往系统设置开启权限。 settingsButtonText: 'goToSettings'.tr, // 去设置 cancelButtonText: 'cancel'.tr, // 取消 ); if (!hasPermission) { return; // 权限未获取,保持文本模式 } } isTextInputMode.toggle(); // 切换到语音模式时,启用按住说话模式 // 切换到文本模式时,禁用按住说话模式并停止任何正在进行的语音输入 if (isTextInputMode.value) { // 切换到文本模式 isTextInputMode.value = true; // stopVoiceInput(); } else { // 切换到语音模式,启用按住说话模式 isTextInputMode.value = false; } } // 开始语音输入 Future startVoiceInput() async { if (isListening.value) return; Logger.i(TAG, '开始语音输入'); // 开始语音输入 // final hasPermission = await PermissionUtil.instance.requestPermission( // // context: Get.context!, // permissionType: Permission.microphone, // permissionName: 'microphonePermission'.tr, // 麦克风 // explanationText: 'voiceTranslationPermissionText'.tr, // 语音识别与翻译功能需要访问麦克风。 // permanentDenialText: // 'permissionDeniedText'.tr, // 您已永久拒绝麦克风权限,请前往系统设置开启权限。 // settingsButtonText: 'goToSettings'.tr, // 去设置 // cancelButtonText: 'cancel'.tr, // 取消 // ); // if (!hasPermission) { // isTextInputMode.value = true; // 如果没有权限,切回文本模式 // return; // } try { await AgentService.stopConversation(); await AgentService.recognizeCallback(); if (_bluetoothManager.currentDeviceRx.value != null) { // await AgentService.restoreOriginalAudioState(); } else { // await AgentService.disableBluetoothAudio(); } } catch (e) { Logger.e(TAG, '启动识别器失败: $e'); } } // 停止语音输入 Future stopVoiceInput() async { if (!isListening.value) return; try { await AgentService.stopConversation(); } catch (e) { Logger.e(TAG, '停止语音输入失败: $e'); } } // 清除聊天历史 Future clearChat() async { try { await AgentService.clearChatHistory(); // 清空本地消息列表 messages.clear(); // 添加欢迎消息 _addWelcomeMessage(); // 重置回复状态 _isNewAssistantResponse = true; } catch (e) { Logger.e(TAG, '清除聊天历史失败: $e'); } } // 从相册选择图片 Future pickImage() async { try { final ImagePicker picker = ImagePicker(); final XFile? image = await picker.pickImage( source: ImageSource.gallery, imageQuality: 80, // 设置图片质量 maxWidth: 1024, // 限制最大宽度 maxHeight: 1024, // 限制最大高度 ); if (image != null) { // 设置已选择的图片 selectedImagePath.value = image.path; // 激活图片输入模式 isImageInputActive.value = true; } } catch (e) { Logger.e(TAG, '选择图片失败: $e'); } } // 拍照获取图片 Future takePhoto() async { try { final ImagePicker picker = ImagePicker(); final XFile? photo = await picker.pickImage( source: ImageSource.camera, imageQuality: 80, // 设置图片质量 maxWidth: 1024, // 限制最大宽度 maxHeight: 1024, // 限制最大高度 ); if (photo != null) { // 设置已选择的图片 selectedImagePath.value = photo.path; // 激活图片输入模式 isImageInputActive.value = true; } } catch (e) { Logger.e(TAG, '拍照失败: $e'); } } // 清除图片输入状态 void clearImageInput() { selectedImagePath.value = null; isImageInputActive.value = false; } // 切换'+'选项展开状态 void togglePlusOptions() { if (isPlusOptionsExpanded.value) { // 如果当前已展开,则收缩 collapsePlusOptions(); _pendingPlusOptionsExpansion = false; // 取消待展开状态 } else { // 如果当前未展开,则展开并关闭键盘 if (isKeyboardVisible.value) { // 设置待展开标记 _pendingPlusOptionsExpansion = true; hideKeyboard(); // 使用双重机制:轮询检测 + 键盘状态监听 _waitForKeyboardToClose(); } else { // 键盘未显示,直接展开 isPlusOptionsExpanded.value = true; } } } // 等待键盘关闭的轮询机制 void _waitForKeyboardToClose() { int attempts = 0; const maxAttempts = 30; // 最多尝试30次,总计3秒 const checkInterval = Duration(milliseconds: 100); // 每100ms检查一次 Timer.periodic(checkInterval, (timer) { attempts++; // 检查是否还需要展开(用户可能已经取消了) if (!_pendingPlusOptionsExpansion) { timer.cancel(); return; } if (!isKeyboardVisible.value) { // 键盘已关闭,展开选项 timer.cancel(); _pendingPlusOptionsExpansion = false; // 添加小延迟确保UI稳定 Future.delayed(const Duration(milliseconds: 50), () { if (!isKeyboardVisible.value) { // 再次确认键盘状态 isPlusOptionsExpanded.value = true; } }); } else if (attempts >= maxAttempts) { // 超时,强制展开(可能键盘检测有问题) timer.cancel(); _pendingPlusOptionsExpansion = false; Logger.d('AgentController', '键盘关闭检测超时,强制展开选项'); isPlusOptionsExpanded.value = true; } }); } // 收缩'+'选项 void collapsePlusOptions() { isPlusOptionsExpanded.value = false; } // 更新键盘可见状态 void updateKeyboardVisibility(bool isVisible) { final wasVisible = isKeyboardVisible.value; isKeyboardVisible.value = isVisible; // 只有当键盘从隐藏变为显示时,才收缩'+'选项 // 避免键盘关闭时误触发收缩逻辑 if (!wasVisible && isVisible && isPlusOptionsExpanded.value) { collapsePlusOptions(); } // 如果键盘关闭了,并且有待展开的选项,则展开 if (wasVisible && !isVisible && _pendingPlusOptionsExpansion) { _pendingPlusOptionsExpansion = false; Future.delayed(const Duration(milliseconds: 100), () { if (!isKeyboardVisible.value) { isPlusOptionsExpanded.value = true; } }); } } // 标记是否有待展开的'+'选项 bool _pendingPlusOptionsExpansion = false; // 隐藏键盘 void hideKeyboard() { FocusManager.instance.primaryFocus?.unfocus(); } // 使用预设提示词处理图片 void useImagePrompt(String prompt) { // 确保selectedImagePath.value不为null if (selectedImagePath.value == null) return; // 填充文本框同时直接发送消息 textController.text = prompt; // 立即发送图片和提示词 final imagePath = selectedImagePath.value!; sendImageMessage(imagePath, text: prompt); // 发送后清除图片输入状态 clearImageInput(); } // 预设提示词:总结图片内容 void summarizeImage() { useImagePrompt('请总结这张图片的内容'); } // 预设提示词:抽取图像文字 void extractTextFromImage() { useImagePrompt('请提取这张图片中的所有文字'); } // 预设提示词:翻译图像文字 void translateImageText() { useImagePrompt('请翻译这张图片中的文字'); } // 预设提示词处理(文本输入模式) void preSetPromptAction(String presetType) { // 确保在文本输入模式下 if (!isTextInputMode.value) { // 如果不在文本模式,先切换到文本模式 isTextInputMode.value = true; } switch (presetType) { case 'Music': textController.text = 'playSomeMusic'.tr; break; case 'Navigation': textController.text = 'navigationTo'.tr; break; case 'Weather': textController.text = 'todayWeather'.tr; break; case 'News': textController.text = 'todayNews'.tr; break; case 'Food': textController.text = 'nearbyFood'.tr; break; default: textController.text = presetType; break; } // 自动发送消息 if (presetType != 'Navigation') { sendMessage(); } } // 模式历史记录 VoiceInputMode? _previousInputMode; // 保存当前输入模式 void _saveCurrentInputMode() { if (!isTextInputMode.value) { _previousInputMode = VoiceInputMode.pushToTalk; } else { _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(); // 进入打电话模式 isPhoneCallMode.value = true; // 如果当前是文本模式,切换到语音模式???? // if (isListening.value) { // 检查麦克风权限 final hasPermission = await PermissionUtil.instance.requestPermission( permissionType: Permission.microphone, permissionName: 'microphonePermission'.tr, // 麦克风 explanationText: 'phoneCallPermissionText'.tr, // 打电话功能需要访问麦克风进行语音通话。 permanentDenialText: 'permissionDeniedText'.tr, // 您已永久拒绝麦克风权限,请前往系统设置开启权限。 settingsButtonText: 'goToSettings'.tr, // 去设置 cancelButtonText: 'cancel'.tr, // 取消 ); if (!hasPermission) { // 权限未获取,退出打电话模式并恢复之前状态 isPhoneCallMode.value = false; _previousInputMode = null; // 清除历史记录 return; } // 打电话模式 isTextInputMode.value = false; Logger.i(TAG, '打电话模式已启动,开始语音输入'); await AgentService.startConversation(mode: 'phone_call'); } // 结束打电话模式 void endPhoneCall() async { Logger.i(TAG, '🔥 结束打电话模式'); try { // 如果正在录音,先停止录音 if (isListening.value) { final result = await AgentService.stopConversation(); if (result) { isListening.value = false; } } // 退出打电话模式 isPhoneCallMode.value = false; _restorePreviousInputMode(); } catch (e) { Logger.e(TAG, '结束打电话模式失败: $e'); // 即使出现异常,也要确保退出打电话模式 isPhoneCallMode.value = false; isListening.value = false; _restorePreviousInputMode(); } } // 打电话模式专用:切换录音状态 Future togglePhoneCallRecording() async { Logger.i(TAG, '🎤 打电话模式切换录音状态'); // 只有在打电话模式下才能使用此方法 if (!isPhoneCallMode.value) { Logger.w(TAG, '不在打电话模式,无法切换录音状态'); return; } try { if (isListening.value) { // 当前正在录音,停止录音 Logger.i(TAG, '停止打电话模式录音'); final result = await AgentService.stopConversation(); // 手动更新状态,确保UI同步 if (result) { isListening.value = false; } } else { // 当前未录音,开始录音 Logger.i(TAG, '开始打电话模式录音'); final result = await AgentService.startConversation(mode: 'phone_call'); // 手动更新状态,确保UI同步 if (result) { isListening.value = true; } } } catch (e) { Logger.e(TAG, '打电话模式录音状态切换失败: $e'); } } // 耳机唤醒通话模式(专用入口) Future startHeadsetCall() async { Logger.i(TAG, '🎧 耳机唤醒通话模式'); if (canStartHeadsetCall()) { await startPhoneCall(isHeadsetTriggered: true); } } // ─── 设备通话模式(device_jieli AI 助手通路) ──────────────────── // 走 device_jieli 的 AI 助手通路(speechStart/speechStop): // 上行帧由 SpeechAudioEvent 派发,PCM_S16LE/16k/mono/20ms,直接灌给 ASR。 // 与翻译 / 设备录音互斥;不影响已有 phone_call / push_to_talk 任何路径。 /// 启动设备通话模式 Future startDeviceCall() async { Logger.i(TAG, '🎧 开始设备通话模式(AI 助手通路)'); if (isDeviceCallMode.value) { Logger.w(TAG, '已处于设备通话模式,忽略'); return; } // 必须已连接杰理耳机;iOS / Android 都走 device_jieli 的 AI 助理通路 final BleManager bleManager = Get.find(); if (!bleManager.isConnected) { Get.snackbar('tips'.tr, '请先连接耳机'); return; } // 收缩 + 号 collapsePlusOptions(); isDeviceCallMode.value = true; isDeviceCallMicMuted.value = false; _deviceCallPushing = true; try { // 先停之前可能存在的会话(不同模式之间切换保底) await AgentService.stopConversation(); // 订阅 device_jieli 事件流,按 uplink 推给 ASR await _deviceCallJieliSub?.cancel(); _deviceCallJieliSub = Jielihome.instance.events.listen(_onDeviceCallEvent); // ignore: avoid_print print( '[DEVCALL] _deviceCallJieliSub 已订阅 sub=$_deviceCallJieliSub mode=${isDeviceCallMode.value} pushing=$_deviceCallPushing', ); // 启动 AgentService 外部音频模式 + 电话通话模式(避免 push_to_talk 静默超时) final started = await AgentService.startConversation( mode: 'phone_call', isExternalActive: true, ); if (!started) { Logger.w(TAG, '设备通话:AgentService 启动失败'); } // 启动 AI 助理通路(JieliAssistantPort):MODE_RECORD + DEVICE_ALWAYS_RECORDING, // 耳机自主持续推 OPUS,由 Port 解码成 16k/16bit/mono/20ms PCM 帧 final ok = await Jielihome.instance.assistantStart(); if (!ok) { Logger.w(TAG, '设备通话:assistantStart 启动失败'); await _stopDeviceCallInternal(); Get.snackbar('error'.tr, '设备通话启动失败'); return; } Logger.i(TAG, '设备通话模式已启动(JieliAssistantPort)'); } catch (e) { Logger.e(TAG, '启动设备通话失败: $e'); await _stopDeviceCallInternal(); Get.snackbar('error'.tr, '设备通话启动失败'); } } /// 结束设备通话模式 Future endDeviceCall() async { Logger.i(TAG, '🎧 结束设备通话模式'); await _stopDeviceCallInternal(); } /// 设备通话模式下切换静音(丢帧) void toggleDeviceCallMic() { if (!isDeviceCallMode.value) return; isDeviceCallMicMuted.value = !isDeviceCallMicMuted.value; _deviceCallPushing = !isDeviceCallMicMuted.value; Logger.i(TAG, '设备通话静音切换: muted=${isDeviceCallMicMuted.value}'); } Future _stopDeviceCallInternal() async { _deviceCallPushing = false; try { await _deviceCallJieliSub?.cancel(); } catch (_) {} _deviceCallJieliSub = null; try { await Jielihome.instance.assistantStop(); } catch (e) { Logger.w(TAG, 'assistantStop 失败(忽略): $e'); } try { await AgentService.stopConversation(); } catch (e) { Logger.w(TAG, 'stopConversation 失败(忽略): $e'); } isDeviceCallMicMuted.value = false; isDeviceCallMode.value = false; isListening.value = false; } void _onDeviceCallEvent(JieliEvent event) { // 入口诊断:每秒打一次"是否进入此函数",无条件,用 print 走 I/flutter 通道 final now0 = DateTime.now().millisecondsSinceEpoch; if (event is AssistantAudioEvent) { _deviceCallPushedFrames += 1; _deviceCallPushedBytes += event.pcm.length; if (now0 - _deviceCallLastReportMs >= 1000) { // ignore: avoid_print print( '[DEVCALL] _onDeviceCallEvent 入口 (last 1s): frames=$_deviceCallPushedFrames bytes=$_deviceCallPushedBytes mode=${isDeviceCallMode.value} pushing=$_deviceCallPushing seq=${event.sequence}', ); _deviceCallPushedFrames = 0; _deviceCallPushedBytes = 0; _deviceCallLastReportMs = now0; } } if (!isDeviceCallMode.value) return; if (event is AssistantAudioEvent) { if (!_deviceCallPushing) return; if (event.encoding != 'pcm16') return; if (event.pcm.isEmpty) return; // fire-and-forget 推送给 ASR AgentService.pushAudioData(event.pcm).catchError((e) { Logger.w(TAG, 'pushAudioData 失败: $e'); return false; }); } else if (event is AssistantStartEvent) { Logger.i(TAG, 'AI 助理通路已启动(端侧通知)sr=${event.sampleRate}'); } else if (event is AssistantErrorEvent) { Logger.e(TAG, 'AI 助理错误 code=${event.code} msg=${event.message ?? ''}'); } else if (event is AssistantEndEvent) { Logger.i(TAG, 'AI 助理通路已停止(端侧通知)'); } } // 检查当前是否可以启动耳机通话 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(); } // 开始按住说话 void startPushToTalk() async { print('liwei--------- 🎤 开始按住说话'); // 只有在语音模式且按住说话模式下才能按住说话 if (isTextInputMode.value) return; // 如果已经在按住说话,不重复开始 if (isPushToTalkActive.value) return; // 设置按住说话状态 isPushToTalkActive.value = true; //await AgentService.recognizeCallback(); // 开始语音识别 AgentService.startConversation(mode: 'push_to_talk'); // await startVoiceInput(); } // 结束按住说话 void endPushToTalk() async { print('liwei----------🎤 结束按住说话'); // 如果不在按住说话状态,直接返回 if (!isPushToTalkActive.value) return; // 清除按住说话状态(但保持按住说话模式) isPushToTalkActive.value = false; AgentService.stopConversation(); // 停止语音识别 // await stopVoiceInput(); } // 获取音乐播放状态 Future musicStatus() async { try { var status = await _musiceManager.getStatus(); isPlaying.value = status['isPlaying'] ?? false; currentId.value = status['id'] as int; Logger.d( TAG, '音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}'); Logger.d( TAG, '音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}'); } catch (e) { Logger.e(TAG, '获取音乐状态失败: $e'); } } // 播放音乐 Future musicPlay(Map song) async { try { Logger.d(TAG, '开始播放音乐: ${song['title'] ?? song['name']}'); // 立即更新UI状态,提供即时反馈 isPlaying.value = true; // 修复:确保currentId接收int类型 final songId = song['id']; currentId.value = songId ?? 0; // 重置进度状态 currentPosition.value = 0; duration.value = 0; progress.value = 0.0; // 转换为MusicManager期望的格式 final musicData = { 'id': song['id'], 'title': song['title']?.toString() ?? song['name']?.toString() ?? '', 'artist': song['artist']?.toString() ?? song['sgener']?.toString() ?? '', 'coverUrl': song['coverUrl']?.toString() ?? song['image']?.toString() ?? '', }; // 调用音乐管理器播放 await _musiceManager.play(musicData); // 更新媒体会话元数据和播放状态 await _updateMediaSessionMetadata(musicData); await _updateMediaSessionPlaybackState(true); Logger.d(TAG, '音乐播放启动成功'); } catch (e) { Logger.e(TAG, '播放音乐失败: $e'); // 发生错误时重置状态 isPlaying.value = false; currentId.value = 0; } } // 暂停音乐 Future musicPause() async { try { Logger.d('AgentController', '暂停音乐播放'); await _musiceManager.pause(); isPlaying.value = false; _stopProgressTimer(); // 更新媒体会话播放状态 await _updateMediaSessionPlaybackState(false); } catch (e) { Logger.e('AgentController', '暂停音乐失败: $e'); } } // 恢复音乐播放 Future musicResume() async { try { Logger.d('AgentController', '恢复播放音乐'); await _musiceManager.resume(); isPlaying.value = true; // 更新媒体会话播放状态 await _updateMediaSessionPlaybackState(true); Logger.d('AgentController', '音乐恢复播放成功'); } catch (e) { Logger.e('AgentController', '恢复播放音乐失败: $e'); isPlaying.value = false; } } // 停止进度更新定时器 void _stopProgressTimer() { if (_progressTimer != null) { Logger.d('AgentController', '停止播放进度定时器'); _progressTimer?.cancel(); _progressTimer = null; } } // 获取播放进度 Future updateMusicProgress(int currentIdInternal, int newPosition, int newDuration, double newProgress) async { try { // 如果正在加载音乐,避免进度回调干扰状态更新 if (isLoadingMusic.value) { return; } if (currentIdInternal != 0) { isPlaying.value = true; // return; // 如果没有在播放,直接返回 } bool hasUpdate = false; if (currentId.value != currentIdInternal) { currentId.value = currentIdInternal; hasUpdate = true; } // 只有当值发生变化时才更新,避免不必要的UI刷新 if (currentPosition.value != newPosition) { currentPosition.value = newPosition; hasUpdate = true; } if (duration.value != newDuration) { duration.value = newDuration; hasUpdate = true; } if ((progress.value - newProgress).abs() > 0.001) { // 避免浮点数精度问题 progress.value = newProgress; hasUpdate = true; } // // 如果有进度更新,通知系统媒体会话更新进度信息 // if (hasUpdate) { // await _updateMediaSessionProgress(); // } } catch (e) { Logger.e('AgentController', '获取播放进度失败: $e'); } } /// 更新系统媒体会话的进度信息 Future _updateMediaSessionProgress() async { try { // 这里将调用music_service插件的方法来更新系统媒体会话进度 // 具体实现将在music_service插件中添加 await _musiceManager.updateMediaSessionProgress( currentPosition.value, duration.value, progress.value, ); } catch (e) { // 忽略错误,避免影响正常播放 Logger.d('AgentController', '更新媒体会话进度失败: $e'); } } /// 更新系统媒体会话的元数据 Future _updateMediaSessionMetadata(Map song) async { try { await _musiceManager.updateMediaSessionMetadata(song); } catch (e) { Logger.d('AgentController', '更新媒体会话元数据失败: $e'); } } /// 更新系统媒体会话的播放状态 Future _updateMediaSessionPlaybackState(bool isPlaying) async { try { await _musiceManager.updateMediaSessionPlaybackState(isPlaying); } catch (e) { Logger.d('AgentController', '更新媒体会话播放状态失败: $e'); } } // 跳转到指定位置 Future seekToPosition(int positionMs) async { try { await _musiceManager.seekTo(positionMs); } catch (e) { Logger.e('AgentController', '跳转播放位置失败: $e'); } } // 跳转到指定位置 Future seekToProgress(int currentIdInternal, int newPosition, int newDuration, double newProgress) async { try { // 立即更新进度状态 await updateMusicProgress( currentIdInternal, newPosition, newDuration, newProgress); } catch (e) { Logger.e('AgentController', '跳转播放位置失败: $e'); } } // 修改自动停止处理,遵循TTS设置 void _handleAutoStop(String sessionid, int index) { // 发送此消息到AI处理 isProcessing.value = true; // 标记为新的AI回复 _isNewAssistantResponse = true; currsessionid.value = sessionid; AgentService.processTextInput(sessionid, messages[index].text, speakResponse: isTtsEnabled.value); } //去导航 void openNavigation(bool isshow, String start, String destination, String endlatitude, String endlongitude, String mode) { print('liwei 导航:$destination $endlatitude $endlongitude $mode'); if (isshow) { // 弹出导航选择面板 _showNavigationSelectionDialog( start, destination, endlatitude, endlongitude, mode); } else { // 使用默认配置直接导航 _navigateWithDefaultMode( start, destination, endlatitude, endlongitude, mode); } } // 显示导航选择底部面板 void _showNavigationSelectionDialog(String start, String destination, String endlatitude, String endlongitude, String mode) { Get.bottomSheet( Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 顶部指示条 Container( margin: const EdgeInsets.only(top: 12), width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(2), ), ), // 标题 Padding( padding: const EdgeInsets.all(20), child: Text( 'selectNavigationApp'.tr, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ), // 导航选项列表 ListTile( leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.map, color: Colors.blue), ), title: Text('amapMap'.tr), subtitle: Text('useAmapNavigation'.tr), onTap: () { Get.back(); openGaodeMap(destination, endlatitude, endlongitude, mode); }, ), ListTile( leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.green.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.map, color: Colors.green), ), title: Text('baiduMap'.tr), subtitle: Text('useBaiduNavigation'.tr), onTap: () { Get.back(); openBaiduMap(destination, endlatitude, endlongitude, mode); }, ), ListTile( leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.red.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.map, color: Colors.red), ), title: Text('googleMap'.tr), subtitle: Text('useGoogleNavigation'.tr), onTap: () { Get.back(); openGoogleMap(destination, mode); }, ), // 苹果导航 - 仅在 iOS 平台显示 if (Platform.isIOS) ListTile( leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.grey.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.map, color: Colors.orange), ), title: Text('appleMap'.tr), subtitle: Text('useAppleNavigation'.tr), onTap: () { Get.back(); openAppleMap(destination, mode); }, ), ListTile( leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.grey.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.navigation, color: Colors.grey), ), title: Text('builtInNavigation'.tr), subtitle: Text('useBuiltInNavigation'.tr), onTap: () { Get.back(); openInternalGaodeMap( start, destination, endlatitude, endlongitude, mode); }, ), // 底部安全区域 SizedBox(height: MediaQuery.of(Get.context!).padding.bottom + 20), ], ), ), isScrollControlled: true, isDismissible: true, // 点击外部区域可关闭 enableDrag: true, // 支持拖拽关闭 ); } // 使用默认配置导航 void _navigateWithDefaultMode(String start, String destination, String endlatitude, String endlongitude, String mode) { final navigationMode = AppConfig.env('APP_NAVIGATION_MODE') ?? 'external_auto'; if (navigationMode == 'internal_gaode') { _navigationManager.open(start, "$endlongitude,$endlatitude", mode); } else if (navigationMode == 'external_baidu') { openBaiduMap(destination, endlatitude, endlongitude, mode); } else if (navigationMode == 'external_gaode') { openGaodeMap(destination, endlatitude, endlongitude, mode); } else if (navigationMode == 'external_google') { openGoogleMap(destination, mode); } else { // 自动选择导航应用,优先级:百度 > 高德 > 谷歌 _autoSelectNavigation( start, destination, endlatitude, endlongitude, mode); } } // 自动选择导航应用 Future _autoSelectNavigation(String start, String destination, String endlatitude, String endlongitude, String mode) async { try { // 1. 优先尝试百度地图 final baiduUri = Uri( scheme: 'baidumap', host: 'map', path: 'direction', queryParameters: { 'destination': destination, 'coord_type': 'gcj02', 'mode': mode == 'driving' ? 'driving' : 'transit', 'src': 'deepvoice', }, ); if (await canLaunchUrl(baiduUri)) { print('自动选择:使用百度地图'); openBaiduMap(destination, endlatitude, endlongitude, mode); return; } // 2. 其次尝试高德地图 final gaodeUri = Uri( scheme: 'amapuri', host: 'navi', queryParameters: { 'to': destination, 'mode': mode.isNotEmpty ? mode : 'driving', 'callnative': '1', }, ); if (await canLaunchUrl(gaodeUri)) { print('自动选择:使用高德地图'); openGaodeMap(destination, endlatitude, endlongitude, mode); return; } // 3. 然后尝试谷歌地图 final googleUri = Uri( scheme: 'comgooglemaps', queryParameters: { 'daddr': destination, 'directionsmode': mode == 'driving' ? 'driving' : 'transit', }, ); if (await canLaunchUrl(googleUri)) { print('自动选择:使用谷歌地图'); openGoogleMap(destination, mode); return; } // 4. 在 iOS 平台尝试苹果地图 if (Platform.isIOS) { final appleUri = Uri( scheme: 'maps', queryParameters: { 'daddr': destination, 'dirflg': mode == 'driving' ? 'd' : 'r', }, ); if (await canLaunchUrl(appleUri)) { print('自动选择:使用苹果地图'); openAppleMap(destination, mode); return; } } // 5. 最后尝试内置高德导航 _navigationManager.open(start, "$endlongitude,$endlatitude", mode); // 5. 如果都没有安装,提示用户 // print('未找到可用的导航应用'); // final availableApps = // Platform.isIOS ? '百度地图、高德地图、谷歌地图或苹果地图' : '百度地图、高德地图或谷歌地图'; // Get.snackbar('提示', '未找到可用的导航应用,请安装$availableApps'); } catch (e) { print('自动选择导航失败: $e'); Get.snackbar('error'.tr, 'navigationStartFailed'.tr); } } // 打开图片查看器 void openImageViewer(BuildContext context, String imagePath) { // 构建完整的图片路径 String fullImagePath; if (imagePath.startsWith('/')) { // 如果是相对路径(以/开头),需要拼接AppDir.value fullImagePath = '${AppDir.value}$imagePath'; } else { // 如果已经是完整路径,直接使用 fullImagePath = imagePath; } Navigator.push( context, MaterialPageRoute( builder: (context) => Scaffold( // appBar: AppBar( // backgroundColor: Colors.black, // leading: IconButton( // icon: Icon(Icons.arrow_back_ios, color: Colors.white), // onPressed: () => Navigator.of(context).pop(), // ), // iconTheme: IconThemeData(color: Colors.white), // //title: Text('图片预览', style: TextStyle(color: Colors.white)), // ), body: Container( color: Colors.black, child: GestureDetector( onTap: () => Navigator.of(context).pop(), // 点击退出 child: PhotoView( imageProvider: FileImage(File(fullImagePath)), minScale: PhotoViewComputedScale.contained, maxScale: PhotoViewComputedScale.covered * 2, backgroundDecoration: BoxDecoration(color: Colors.black), // errorBuilder: (context, error, stackTrace) { // return const Center( // child: Column( // mainAxisAlignment: MainAxisAlignment.center, // children: [ // Icon(Icons.error, color: Colors.white, size: 64), // SizedBox(height: 16), // Text('图片加载失败', style: TextStyle(color: Colors.white)), // ], // ), // ); // }, ), ), ), ), ), ); } /// 播放音乐列表 Future playPlaylist(List> playlist) async { try { if (playlist.isEmpty) { Logger.e(TAG, '播放列表为空,无法播放'); return; } Logger.d(TAG, '开始播放音乐列表,共${playlist.length}首歌曲'); // 保存当前播放列表 currentPlaylist.assignAll(playlist); currentPlaylistIndex.value = 0; // 设置加载状态 isLoadingMusic.value = true; loadingMusicId.value = playlist[0]['id']; // 重置播放状态 isPlaying.value = true; currentPosition.value = 0; duration.value = 0; progress.value = 0.0; // 格式化歌曲列表数据 final formattedPlaylist = playlist.map((song) { return { 'id': song['id'], 'title': song['title']?.toString() ?? song['name']?.toString() ?? '', 'artist': song['artist']?.toString() ?? song['sgener']?.toString() ?? '', 'coverUrl': song['coverUrl']?.toString() ?? song['image']?.toString() ?? '', }; }).toList(); // 调用音乐管理器播放列表 await _musiceManager.playPlaylist(formattedPlaylist); // 更新当前播放歌曲ID if (formattedPlaylist.isNotEmpty) { currentId.value = formattedPlaylist[0]['id'] ?? 0; } Logger.d(TAG, '音乐列表播放启动成功'); } catch (e) { Logger.e(TAG, '播放音乐列表失败: $e'); isPlaying.value = false; currentId.value = 0; // 显示错误提示 Get.snackbar( 'playFailed'.tr, 'cannotPlayMusicListRetryLater'.tr, snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red[400], colorText: Colors.white, duration: const Duration(seconds: 2), ); } finally { // 清除加载状态 isLoadingMusic.value = false; loadingMusicId.value = 0; } } /// 播放上一曲 Future playPrevious() async { if (currentPlaylist.isEmpty) { Logger.w(TAG, '当前播放列表为空,无法播放上一曲'); return; } // 计算上一曲的索引 int previousIndex = currentPlaylistIndex.value - 1; if (previousIndex < 0) { previousIndex = currentPlaylist.length - 1; // 循环到最后一首 } currentPlaylistIndex.value = previousIndex; final previousSong = currentPlaylist[previousIndex]; Logger.d(TAG, '播放上一曲: ${previousSong['name'] ?? previousSong['title']}'); await musicPlay(previousSong); } /// 播放下一曲 Future playNext() async { if (currentPlaylist.isEmpty) { Logger.w(TAG, '当前播放列表为空,无法播放下一曲'); return; } // 计算下一曲的索引 int nextIndex = currentPlaylistIndex.value + 1; if (nextIndex >= currentPlaylist.length) { nextIndex = 0; // 循环到第一首 } currentPlaylistIndex.value = nextIndex; final nextSong = currentPlaylist[nextIndex]; Logger.d(TAG, '播放下一曲: ${nextSong['name'] ?? nextSong['title']}'); await musicPlay(nextSong); } /// 更新当前播放列表和索引(当单独播放某首歌曲时调用) void updateCurrentPlaylistContext( List> playlist, int songId) { currentPlaylist.assignAll(playlist); // 查找当前歌曲在列表中的索引 int index = -1; for (int i = 0; i < playlist.length; i++) { if (playlist[i]['id'] == songId) { index = i; break; } } if (index != -1) { currentPlaylistIndex.value = index; Logger.d(TAG, '更新播放列表上下文,当前歌曲索引: $index'); } } /// 应用生命周期变化回调 /// 目的: /// - 当应用进入后台、失去焦点或被系统分离(inactive/paused/detached)时, /// 兜底结束按住说话(调用 endPushToTalk),避免因 onPointerUp 未触发而卡在“按下”状态。 /// 参数: /// - state: 当前应用生命周期状态 @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.inactive || state == AppLifecycleState.paused || state == AppLifecycleState.detached) { if (isPushToTalkActive.value) { // 使用 microtask 避免阻塞当前生命周期回调 Future.microtask(() { endPushToTalk(); }); } } } // ========== 媒体会话回调处理方法 ========== /// 处理来自系统通知栏的播放操作 Future onMediaSessionPlay() async { Logger.d(TAG, '收到媒体会话播放回调'); if (!isPlaying.value) { await musicResume(); } } /// 处理来自系统通知栏的暂停操作 Future onMediaSessionPause() async { Logger.d(TAG, '收到媒体会话暂停回调'); if (isPlaying.value) { await musicPause(); } } /// 处理来自系统通知栏的下一曲操作 Future onMediaSessionNext() async { Logger.d(TAG, '收到媒体会话下一曲回调'); await playNext(); } /// 处理来自系统通知栏的上一曲操作 Future onMediaSessionPrevious() async { Logger.d(TAG, '收到媒体会话上一曲回调'); await playPrevious(); } /// 处理来自系统通知栏的跳转操作 Future onMediaSessionSeekTo(int positionMs) async { Logger.d(TAG, '收到媒体会话跳转回调: ${positionMs}ms'); await seekToPosition(positionMs); } /// 处理来自系统通知栏的停止操作 Future onMediaSessionStop() async { Logger.d(TAG, '收到媒体会话停止回调'); await musicPause(); // 重置播放状态 currentPosition.value = 0; progress.value = 0.0; _stopProgressTimer(); } /// 处理来自系统进度条 Future onMediaSessionProgress(int currentIdInternal, int newPosition, int newDuration, double newProgress) async { // print( // "onMediaSessionProgress currentIdInternal :$currentIdInternal ,newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress"); await seekToProgress( currentIdInternal, newPosition, newDuration, newProgress); } /// 处理来自系统通知栏的停止操作 Future playQQMusic( List songs, SongModel song, int index) async { try { if (_qqmusicManager.isPlaying.value) { if (_qqmusicManager.currentSong.value?.mid == song.mid) { // 当前歌曲正在播放,暂停它 await _qqmusicManager.pauseMusic(); } else { final songMids = songs.map((s) => s.mid).toList(); // 其他歌曲正在播放,播放这首歌 final result = await _qqmusicManager.playSongs( songMids: songMids, index: index, songList: [song], currentSongModel: song, ); if (result['success'] == true) { // 播放成功后,如果歌曲不在当前列表中,保持列表显示状态不变 // Get.snackbar('开始播放', song.name); _qqmusicManager.syncCurrentPlayInfo(); } else { Get.snackbar('playFailed'.tr, result['error'] ?? 'cannotPlaySong'.tr); } return; } } else { if (_qqmusicManager.currentSong.value?.mid == song.mid) { // 当前歌曲已暂停,恢复播放 await _qqmusicManager.resumeMusic(); } else { final songMids = songs.map((s) => s.mid).toList(); // 播放新歌曲 final result = await _qqmusicManager.playSongs( songMids: songMids, index: index, songList: [song], currentSongModel: song, ); if (result['success'] == true) { // 播放成功后,如果歌曲不在当前列表中,保持列表显示状态不变 // Get.snackbar('开始播放', song.name); _qqmusicManager.syncCurrentPlayInfo(); } else { Get.snackbar('playFailed'.tr, result['error'] ?? 'cannotPlaySong'.tr); } } } } catch (e) { // 静默处理错误,避免影响用户体验 print('播放控制失败: $e'); } } /// 打开内置高德地图进行导航 Future openInternalGaodeMap( String start, String destination, String endlatitude, String endlongitude, String mode // 'driving', 'transit', 'walking', 'cycling' ) async { try { _navigationManager.open(start, "$endlongitude,$endlatitude", mode); return true; } catch (e) { print('打开内置高德地图失败: $e'); Get.snackbar('error'.tr, 'openMapError'.tr); return false; } } /// 打开Google地图进行导航 Future openGoogleMap(String daddr, String mode) async { try { // 方案1: 尝试使用Google Maps应用的URL scheme // final Uri googleMapsUri = Uri.parse(url); // 方案1: Google Maps应用 URL scheme final Uri appUri = Uri.parse( 'comgooglemaps://?saddr=CurrentLocation&daddr=$daddr&directionsmode=$mode'); if (await canLaunchUrl(appUri)) { await launchUrl(appUri, mode: LaunchMode.externalApplication); return true; } else { final Uri webUrl = Uri.parse( "https://www.google.com/maps/dir/?api=1&origin=Current+Location&destination=$daddr&travelmode=$mode"); if (await canLaunchUrl(webUrl)) { await launchUrl(webUrl); return true; } return false; } } catch (e) { print('打开地图失败: $e'); return false; } } /// 打开高德地图进行导航 Future openGaodeMap( String destination, String lat, String lon, String mode, // 'driving', 'transit', 'walking', 'cycling' ) async { try { // 获取应用包名 final packageInfo = await PackageInfo.fromPlatform(); // 构建src参数(带平台前缀) final String src = Platform.isAndroid ? packageInfo.packageName : "ios.${packageInfo.packageName}"; // 导航模式映射 String gaodeMode; switch (mode) { case 'transit': gaodeMode = '1'; // 公交 break; case 'walking': gaodeMode = '2'; // 步行 break; case 'cycling': gaodeMode = '3'; // 骑行 break; case 'driving': default: gaodeMode = '0'; // 驾车 break; } // 根据平台选择不同的 scheme 和参数 final Uri uri; if (Platform.isAndroid) { uri = Uri( scheme: 'androidamap', host: 'route', path: 'plan', queryParameters: { 'dlat': lat, 'dlon': lon, 'dname': destination, 'dev': '0', // 0: gcj02 't': gaodeMode, 'sourceApplication': src, }, ); } else { // iOS uri = Uri( scheme: 'amapuri', host: 'route', path: 'plan', queryParameters: { 'dlat': lat, 'dlon': lon, 'dname': destination, 'dev': '0', // 0: gcj02 't': gaodeMode, 'sourceApplication': src, }, ); } print('高德导航链接: ${uri.toString()}'); if (await canLaunchUrl(uri)) { final bool launched = await launchUrl( uri, mode: LaunchMode.externalApplication, ); return launched; } else { print('高德地图未安装'); Get.snackbar('tip'.tr, 'pleaseInstallAmapApp'.tr); return false; } } catch (e) { print('打开高德地图失败: $e'); Get.snackbar('error'.tr, 'openMapError'.tr); return false; } } /// 打开Baidu地图进行导航 Future openBaiduMap( String destination, String lat, String lon, String mode, // 'driving', 'transit', 'walking', 'riding' ) async { try { // 获取应用包名 final packageInfo = await PackageInfo.fromPlatform(); final String src = '${Platform.isAndroid ? '' : 'ios.'}${packageInfo.packageName}'; // 百度地图导航模式映射 String baiduMode; switch (mode) { case 'transit': baiduMode = 'transit'; // 公交 break; case 'walking': baiduMode = 'walking'; // 步行 break; case 'cycling': baiduMode = 'riding'; // 骑行 break; case 'driving': default: baiduMode = 'driving'; // 驾车 break; } final Uri uri = Uri( scheme: 'baidumap', host: 'map', path: 'direction', queryParameters: { 'destination': 'name:$destination|latlng:$lat,$lon', 'coord_type': 'gcj02', 'mode': baiduMode, 'src': src, }, ); print('百度导航链接: ${uri.toString()}'); if (await canLaunchUrl(uri)) { final bool launched = await launchUrl( uri, mode: LaunchMode.externalApplication, ); return launched; } else { print('百度地图未安装'); Get.snackbar('tip'.tr, 'pleaseInstallBaiduMapApp'.tr); return false; } } catch (e) { print('打开百度地图失败: $e'); Get.snackbar('error'.tr, 'openMapError'.tr); return false; } } /// 打开苹果地图进行导航 Future openAppleMap(String destination, String mode) async { try { // 构建苹果地图导航链接 final Uri uri = Uri( scheme: 'maps', queryParameters: { 'daddr': destination, // 目的地地址 'dirflg': mode == 'driving' ? 'd' : 'r', // d=驾车, r=公交 }, ); print('苹果地图导航链接: ${uri.toString()}'); // 尝试打开苹果地图应用 if (await canLaunchUrl(uri)) { final bool launched = await launchUrl( uri, mode: LaunchMode.externalApplication, ); if (!launched) { print('苹果地图启动失败'); Get.snackbar('tip'.tr, 'appleMapStartFailed'.tr); } } else { print('苹果地图不可用'); Get.snackbar('tip'.tr, 'appleMapUnavailable'.tr); } } catch (e) { print('打开苹果地图失败: $e'); Get.snackbar('error'.tr, 'openAppleMapError'.tr); } } Future updateToolCallsExpanded( String sessionid, bool isExpanded, ) async { final message = messages.firstWhere( (msg) => msg.sessionid == sessionid && msg.isUser == false, ); message.isToolCallsExpanded = isExpanded; messages.refresh(); } String getDeviceName() { if (_bluetoothManager.currentDevice != null) { return _bluetoothManager.currentDevice!.name; } return ""; } }