import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:amap_service/amap_service.dart'; import 'package:deep_voice/data/services/music_manager.dart'; import 'package:deep_voice/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:image_picker/image_picker.dart'; import 'package:get_storage/get_storage.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:deep_voice/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'; class AgentController extends GetxController { static AgentController get to => Get.find(); //单例访问模式 // 聊天消息列表 final messages = [].obs; // UI控制器 final TextEditingController textController = TextEditingController(); final ScrollController scrollController = ScrollController(); //音乐播放管理 final _musiceManager = Get.find(); final _navigationManager = Get.find(); var isPlaying = false.obs; var currentUrl = "".obs; // 各种状态标志 final isInitialized = false.obs; final isListening = false.obs; //是否正在语音识别 final isSpeaking = false.obs; //是否正在播放tts final isProcessing = false.obs; // TTS启用状态 final isTtsEnabled = true.obs; // 图片处理状态 final isImageProcessing = false.obs; // 输入模式控制 final isTextInputMode = true.obs; //文本输入模式vs语音输入模式 // 当前输入的文本内容 final currentText = ''.obs; // 图片输入相关 final isImageInputActive = false.obs; // 是否处于图片输入模式 final selectedImagePath = Rx(null); // 当前选择的图片路径 // 流事件订阅 StreamSubscription? _eventSubscription; // 导航流事件订阅 StreamSubscription? _amapeventSubscription; // 用于跟踪AI回复状态 bool _isNewAssistantResponse = true; String _currentResponseId = ''; final TAG = 'AgentController'; // 会话ID final String sessionId = 'default_agent'; // 是否已加载聊天记录 final isHistoryLoaded = false.obs; @override void onInit() { super.onInit(); _loadTtsPreference(); //加载TTS偏好设置 _loadChatHistory(); //加载聊天历史记录 _subscribeToEvents(); //订阅代理服务事件 } @override void onClose() { AgentService.stopConversation(); // 停止当前会话 AgentService.stopTts(); // 停止当前语音 textController.dispose(); scrollController.dispose(); _eventSubscription?.cancel(); 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; } } // 切换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 _loadChatHistory() async { try { Logger.i(TAG, '正在加载聊天历史...'); //通过本地插件ChatStorage获取聊天记录 final result = await ChatStorage.getMessages( sessionId: sessionId, //会话ID page: 1, //从第一页开始 pageSize: 20, //每页20条记录 ); if (result.messages.isNotEmpty) { // 转换并添加消息到列表 final historyMessages = result.messages.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( isUser: msg.sender == 'user', text: msg.message, meta: meta, timestamp: DateTime.parse(msg.timestamp), hasImage: hasImage, imagePath: imagePath, ); }).toList(); // 消息按时间排序,最新的消息在底部 historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); messages.assignAll(historyMessages); Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录'); // 详细打印每条消息的内容和属性,便于调试 Logger.d(TAG, '===== 历史消息详情开始 ====='); for (int i = 0; i < historyMessages.length; i++) { final msg = historyMessages[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, '===== 历史消息详情结束 ====='); } else { // 没有历史记录时添加欢迎消息 _addWelcomeMessage(); Logger.i(TAG, '没有历史记录,添加欢迎消息'); } } catch (e) { Logger.e(TAG, '加载聊天历史失败: $e'); _addWelcomeMessage(); } finally { isHistoryLoaded.value = true; messages.refresh(); // 使用reverse:true时不需要手动滚动到底部 } } // 添加初始欢迎消息 void _addWelcomeMessage() { messages.add(Message( isUser: false, text: '你好!我是**小言**,我能帮你:\n\n' '• 答问题和提供信息\n\n' '• 实时语音翻译\n\n' '• 推荐音乐和播客\n\n' '• 语音备忘录和提醒\n\n' '• 控制耳机功能\n\n' '有什么我可以帮你的吗?', )); } // 订阅代理服务事件 void _subscribeToEvents() { _eventSubscription = AgentService.events.listen((event) { switch (event.type) { case AgentServiceEventType.recognitionStarted: isListening.value = true; isTextInputMode.value = false; // isTextInputMode.refresh(); // 只标记识别开始,不添加消息 Logger.i(TAG, '识别开始'); break; case AgentServiceEventType.recognitionStopped: isListening.value = false; isTextInputMode.value = true; // isTextInputMode.refresh(); Logger.i(TAG, '识别停止'); break; case AgentServiceEventType.recognizing: final text = event.data['text'] ?? ''; currentText.value = text; // 保留当前文本,以便其他地方使用 if (text.isNotEmpty) { // 查找是否有正在识别中的消息 final index = messages .lastIndexWhere((msg) => msg.isRecognizing && msg.isUser); if (index >= 0) { // 更新已有的识别中消息 messages[index] = Message( isUser: true, text: text, isRecognizing: true, timestamp: messages[index].timestamp, ); } else { // 没有识别中的消息,创建新消息 messages.add(Message( isUser: true, text: text, isRecognizing: true, )); } // messages.refresh(); } break; case AgentServiceEventType.recognitionResult: final text = event.data['text'] ?? ''; Logger.i(TAG, '识别结果: $text'); // 查找是否有正在识别中的消息 final index = messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser); if (text.isNotEmpty) { if (index >= 0) { // 将识别中的消息转换为最终消息 messages[index] = Message( isUser: true, text: text, isRecognizing: false, timestamp: messages[index].timestamp, ); // messages.refresh(); } else { // 如果没有找到识别中的消息,添加新消息 messages .add(Message(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.ttsStarted: isSpeaking.value = true; Logger.i(TAG, 'TTS开始播放'); break; case AgentServiceEventType.ttsCompleted: case AgentServiceEventType.ttsStopped: case AgentServiceEventType.ttsCanceled: 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 token = event.data['token'] ?? ''; final responseId = event.data['responseId'] ?? ''; if (token.isNotEmpty) { // 如果是新的回复或者响应ID改变,创建新消息 if (_isNewAssistantResponse || (_currentResponseId != responseId && responseId.isNotEmpty)) { messages.add(Message(isUser: false, text: token)); _isNewAssistantResponse = false; _currentResponseId = responseId; } else { // 否则更新最后一条消息 final lastIndex = messages.length - 1; if (lastIndex >= 0 && !messages[lastIndex].isUser) { messages[lastIndex] = Message( isUser: false, text: messages[lastIndex].text + token, meta: messages[lastIndex].meta, timestamp: messages[lastIndex].timestamp, ); } } // 使用reverse:true时通常不需要手动滚动 } break; case AgentServiceEventType.assistantResponse: isProcessing.value = false; isImageProcessing.value = false; // 标记当前回复完成 _isNewAssistantResponse = true; break; case AgentServiceEventType.error: isListening.value = false; isSpeaking.value = false; isProcessing.value = false; isImageProcessing.value = false; // 移除临时的识别消息 final index = messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser); if (index >= 0) { messages.removeAt(index); // messages.refresh(); } Logger.e(TAG, '代理服务错误: ${event.data['message']}'); break; case AgentServiceEventType.autoStop: isListening.value = false; // 移除临时的识别消息 final index = messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser); if (index >= 0) { // 只有当消息为空或文本太短时才移除 if (messages[index].text.trim().length < 2) { messages.removeAt(index); // messages.refresh(); } else { // 否则转换为正式消息 messages[index] = Message( isUser: true, text: messages[index].text, timestamp: messages[index].timestamp, ); // messages.refresh(); // 发送此消息到AI处理 isProcessing.value = true; _isNewAssistantResponse = true; _handleAutoStop(index); } } break; case AgentServiceEventType.functionCall: final functionName = event.data['name'] ?? ''; final arguments = event.data['arguments'] ?? ''; // debugPrint('Fultter 开始调用 mcp: $functionName, $arguments'); break; case AgentServiceEventType.functionCallResult: final responseId = event.data['responseId'] ?? ''; // final result = event.data['result']; final metaStr = event.data['meta'] ?? ''; // final functionCall = event.data['function_call'] ?? ''; Logger.i(TAG, 'Flutter 结束调用 mcp: $metaStr'); if (metaStr.isNotEmpty) { Map metaResult = {}; metaResult = jsonDecode(metaStr) as Map; if (metaResult.isNotEmpty) { // 判断是否为新的回复或响应ID是否改变 if (_isNewAssistantResponse || _currentResponseId != responseId) { // 添加新消息 messages .add(Message(isUser: false, text: "", meta: metaResult)); _isNewAssistantResponse = false; _currentResponseId = responseId; } else { // 更新最后一条消息 final lastIndex = messages.length - 1; if (lastIndex >= 0 && !messages[lastIndex].isUser) { messages[lastIndex] = Message( isUser: false, text: messages[lastIndex].text, meta: metaResult, timestamp: messages[lastIndex].timestamp, ); } } if (metaResult.containsKey("maps_route_navigation")) { //导航 final start = metaResult["maps_route_navigation"] ["card_navigation"]["start"] as String; final end = metaResult["maps_route_navigation"] ["card_navigation"]["end"] as String; // Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end '); Future.delayed(Duration(seconds: 2), () { // Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end '); // 2秒后执行的代码 openNavigation(start, end, ""); }); } } } 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( 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; final message = Message(isUser: true, text: text); messages.add(message); textController.clear(); // 使用reverse:true时通常不需要手动滚动 // 3. 调用AI服务处理 try { isProcessing.value = true; // 标记为新的AI回复 _isNewAssistantResponse = true; // 根据TTS状态决定是否朗读 await AgentService.processTextInput(text, speakResponse: isTtsEnabled.value); } 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 permanentImagePath = await _copyImageToPermanentStorage(imagePath); // 显示的文本,如果没有提供则使用默认值 final displayText = text?.isNotEmpty == true ? text! : '[图片]'; // 添加用户消息 final message = Message( isUser: true, text: displayText, hasImage: true, imagePath: permanentImagePath, ); messages.add(message); // 清空文本输入 textController.clear(); // 使用reverse:true时通常不需要手动滚动 try { isProcessing.value = true; isImageProcessing.value = true; // 标记为新的AI回复 _isNewAssistantResponse = true; // 保存消息到持久化存储 //await _saveMessageToStorage(message); // 调用Agent Service处理图片,考虑TTS状态 await AgentService.processImageInput( permanentImagePath, 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 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'; // 复制文件 await sourceFile.copy(targetPath); Logger.i(TAG, '图片已复制到永久存储: $targetPath'); return targetPath; } catch (e) { Logger.e(TAG, '复制图片到永久存储失败: $e'); return 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: '麦克风', explanationText: '语音识别与翻译功能需要访问麦克风。', permanentDenialText: '您已永久拒绝麦克风权限,请前往系统设置开启权限。', settingsButtonText: '去设置', cancelButtonText: '取消', ); if (!hasPermission) { return; // 权限未获取,保持文本模式 } } isTextInputMode.toggle(); // 切换到语音模式时,直接开始语音输入 if (!isTextInputMode.value) { startVoiceInput(); } // 切换到文本模式时,直接停止语音输入 else { stopVoiceInput(); } } // 开始语音输入 Future startVoiceInput() async { if (isListening.value) return; // 检查麦克风权限 final hasPermission = await PermissionUtil.instance.requestPermission( // context: Get.context!, permissionType: Permission.microphone, permissionName: '麦克风', explanationText: '语音识别与翻译功能需要访问麦克风。', permanentDenialText: '您已永久拒绝麦克风权限,请前往系统设置开启权限。', settingsButtonText: '去设置', cancelButtonText: '取消', ); if (!hasPermission) { isTextInputMode.value = true; // 如果没有权限,切回文本模式 return; } try { await AgentService.startConversation(); } 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 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 musicStatus() async { var status = await _musiceManager.getStatus(); // debugPrint('获取当前音乐播放状态 $status'); isPlaying.value = status['isPlaying']; currentUrl.value = status['url']; } // 播放音乐 void musicPlay(Map song) { _musiceManager.play(song); isPlaying.value = true; currentUrl.value = song['url'] ?? ''; } // 播放音乐 void musicPause() { _musiceManager.pause(); isPlaying.value = false; } void musicResume() { _musiceManager.resume(); isPlaying.value = true; } // 修改自动停止处理,遵循TTS设置 void _handleAutoStop(int index) { // 发送此消息到AI处理 isProcessing.value = true; _isNewAssistantResponse = true; AgentService.processTextInput(messages[index].text, speakResponse: isTtsEnabled.value); } //去导航 void openNavigation(String start, String end, String ntype) { _navigationManager.open(start, end, ntype); } // 打开图片查看器 void openImageViewer(BuildContext context, String imagePath) { Navigator.push( context, MaterialPageRoute( builder: (context) => Scaffold( appBar: AppBar( backgroundColor: Colors.black, iconTheme: IconThemeData(color: Colors.white), ), body: Container( color: Colors.black, child: PhotoView( imageProvider: FileImage(File(imagePath)), minScale: PhotoViewComputedScale.contained, maxScale: PhotoViewComputedScale.covered * 2, backgroundDecoration: BoxDecoration(color: Colors.black), ), ), ), ), ); } }