diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 2aef7f7ac..617cd5258 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -8,6 +8,9 @@ import 'package:agent_service/agent_service.dart'; import 'package:logger/logger.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:app_settings/app_settings.dart'; class Message { final bool isUser; @@ -51,6 +54,8 @@ class AgentController extends GetxController { final isListening = false.obs; final isSpeaking = false.obs; final isProcessing = false.obs; + // TTS启用状态 + final isTtsEnabled = true.obs; // 图片处理状态 final isImageProcessing = false.obs; @@ -81,6 +86,7 @@ class AgentController extends GetxController { @override void onInit() { super.onInit(); + _loadTtsPreference(); _loadChatHistory(); _subscribeToEvents(); } @@ -95,6 +101,36 @@ class AgentController extends GetxController { super.onClose(); } + // 加载TTS偏好设置 + Future _loadTtsPreference() async { + try { + final storage = GetStorage(); + isTtsEnabled.value = storage.read('agent_tts_enabled') ?? true; + } catch (e) { + // 如果读取失败,使用默认值 + logger.e('加载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('保存TTS偏好设置失败: $e'); + } + } + // 加载聊天历史记录 Future _loadChatHistory() async { try { @@ -134,12 +170,8 @@ class AgentController extends GetxController { _addWelcomeMessage(); } finally { isHistoryLoaded.value = true; - messages.refresh(); - // 在加载完成后直接定位到底部,无需动画 - WidgetsBinding.instance.addPostFrameCallback((_) { - scrollController.jumpTo(scrollController.position.maxScrollExtent); - }); + // 使用reverse:true时不需要手动滚动到底部 } } @@ -199,7 +231,7 @@ class AgentController extends GetxController { )); } messages.refresh(); - _scrollToBottom(); + // 使用reverse:true时通常不需要手动滚动 } break; @@ -229,8 +261,7 @@ class AgentController extends GetxController { isProcessing.value = true; // 标记为新的AI回复 _isNewAssistantResponse = true; - // 滚动到底部 - _scrollToBottom(); + // 使用reverse:true时通常不需要手动滚动 } else if (index >= 0) { // 如果识别结果为空,移除临时的识别消息 messages.removeAt(index); @@ -282,8 +313,7 @@ class AgentController extends GetxController { ); } } - // 滚动到底部 - _scrollToBottom(); + // 使用reverse:true时通常不需要手动滚动 } break; @@ -334,8 +364,7 @@ class AgentController extends GetxController { // 发送此消息到AI处理 isProcessing.value = true; _isNewAssistantResponse = true; - AgentService.processTextInput(messages[index].text, - speakResponse: true); + _handleAutoStop(index); } } break; @@ -351,33 +380,35 @@ class AgentController extends GetxController { final result = event.data['result'] ?? ''; final functionCall = event.data['function_call'] ?? ''; logger.i('Flutter 结束调用 mcp: $result, $functionCall'); - if (result.isNotEmpty) { final parsedResult = jsonDecode(result); - final meta = parsedResult["meta"] ?? ""; + final meta = parsedResult["meta"] as String; final metaResult = jsonDecode(meta); - - logger.i('Flutter 结束调用 mcp metaResult: $metaResult'); - // 判断是否为新的回复或响应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: meta, - timestamp: messages[lastIndex].timestamp, - ); + if (meta.isNotEmpty) { + logger.i('Flutter 结束调用 mcp metaResult: $metaResult'); + // 判断是否为新的回复或响应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, + ); + } } + messages.refresh(); + // 滚动到底部 + _scrollToBottom(); } - // 滚动到底部 - _scrollToBottom(); } break; default: @@ -388,11 +419,13 @@ class AgentController extends GetxController { // 滚动到底部 void _scrollToBottom() { + // 使用reverse:true时,此方法可大部分场景下不再需要 + // 保留此方法但简化实现,仅用于特殊情况下的手动滚动 WidgetsBinding.instance.addPostFrameCallback((_) { if (scrollController.hasClients) { try { scrollController.animateTo( - scrollController.position.maxScrollExtent, + 0, // 使用reverse:true时,0表示列表底部 duration: Duration(milliseconds: 300), curve: Curves.easeOut, ); @@ -423,14 +456,15 @@ class AgentController extends GetxController { messages.add(message); textController.clear(); - // 滚动到底部 - _scrollToBottom(); + // 使用reverse:true时通常不需要手动滚动 try { isProcessing.value = true; // 标记为新的AI回复 _isNewAssistantResponse = true; - await AgentService.processTextInput(text, speakResponse: true); + // 根据TTS状态决定是否朗读 + await AgentService.processTextInput(text, + speakResponse: isTtsEnabled.value); } catch (e) { logger.e('发送文本消息失败: $e'); isProcessing.value = false; @@ -459,8 +493,7 @@ class AgentController extends GetxController { // 清空文本输入 textController.clear(); - // 滚动到底部 - _scrollToBottom(); + // 使用reverse:true时通常不需要手动滚动 try { isProcessing.value = true; @@ -468,11 +501,11 @@ class AgentController extends GetxController { // 标记为新的AI回复 _isNewAssistantResponse = true; - // 调用Agent Service处理图片 + // 调用Agent Service处理图片,考虑TTS状态 await AgentService.processImageInput( imagePath, text: text ?? '', - speakResponse: true, + speakResponse: isTtsEnabled.value, ); } catch (e) { logger.e('处理图片失败: $e'); @@ -481,10 +514,108 @@ class AgentController extends GetxController { } } + // 切换输入模式 + void toggleInputMode() async { + // 如果当前在图片输入模式,先清除 + if (isImageInputActive.value) { + clearImageInput(); + return; + } + + // 如果要切换到语音模式,需要先检查麦克风权限 + if (isTextInputMode.value) { + final hasPermission = await _checkMicrophonePermission(); + if (!hasPermission) { + return; // 权限未获取,保持文本模式 + } + } + + isTextInputMode.toggle(); + + // 切换到语音模式时,直接开始语音输入 + if (!isTextInputMode.value) { + startVoiceInput(); + } + // 切换到文本模式时,直接停止语音输入 + else { + stopVoiceInput(); + } + } + + // 检查麦克风权限 + Future _checkMicrophonePermission() async { + try { + PermissionStatus status = await Permission.microphone.status; + + // 如果已授权,直接返回 true + if (status.isGranted) { + return true; + } + + // 如果被永久拒绝,提示用户前往设置页面 + if (status.isPermanentlyDenied) { + _showPermissionDialog(true); + return false; + } + + // 如果之前拒绝但不是永久拒绝,或首次请求,尝试请求权限 + if (status.isDenied) { + status = await Permission.microphone.request(); + if (status.isGranted) { + return true; + } else { + _showPermissionDialog(false); + return false; + } + } + + return false; + } catch (e) { + logger.e('检查麦克风权限失败: $e'); + return false; + } + } + + // 显示权限提示对话框 + void _showPermissionDialog(bool isPermanent) { + Get.dialog( + AlertDialog( + title: const Text('需要麦克风权限'), + content: Text(isPermanent + ? '语音功能需要麦克风权限。请前往设置页面手动开启权限。' + : '语音功能需要麦克风权限。请允许使用麦克风权限以继续使用语音功能。'), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('取消'), + ), + TextButton( + onPressed: () { + Get.back(); + if (isPermanent) { + AppSettings.openAppSettings(); + } else { + Permission.microphone.request(); + } + }, + child: Text(isPermanent ? '前往设置' : '授权'), + ), + ], + ), + ); + } + // 开始语音输入 Future startVoiceInput() async { if (isListening.value) return; + // 检查麦克风权限 + final hasPermission = await _checkMicrophonePermission(); + if (!hasPermission) { + isTextInputMode.value = true; // 如果没有权限,切回文本模式 + return; + } + try { await AgentService.startConversation(); } catch (e) { @@ -521,26 +652,6 @@ class AgentController extends GetxController { } } - // 切换输入模式 - void toggleInputMode() { - // 如果当前在图片输入模式,先清除 - if (isImageInputActive.value) { - clearImageInput(); - return; - } - - isTextInputMode.toggle(); - - // 切换到语音模式时,直接开始语音输入 - if (!isTextInputMode.value) { - startVoiceInput(); - } - // 切换到文本模式时,直接停止语音输入 - else { - stopVoiceInput(); - } - } - // 从相册选择图片 Future pickImage() async { try { @@ -645,4 +756,13 @@ class AgentController extends GetxController { _musiceManager.resume(); isPlaying.value = true; } + + // 修改自动停止处理,遵循TTS设置 + void _handleAutoStop(int index) { + // 发送此消息到AI处理 + isProcessing.value = true; + _isNewAssistantResponse = true; + AgentService.processTextInput(messages[index].text, + speakResponse: isTtsEnabled.value); + } } diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index be56a48f9..592df110e 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -10,42 +10,64 @@ class AgentView extends StatelessWidget { @override Widget build(BuildContext context) { final controller = Get.find(); - + final theme = Theme.of(context); + final primaryColor = theme.primaryColor; + final backgroundColor = theme.scaffoldBackgroundColor; + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + return Scaffold( appBar: AppBar( + elevation: 1, + backgroundColor: backgroundColor, + leading: IconButton( + icon: Icon(Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black87, + size: 20), + onPressed: () => Get.back(), + ), + titleSpacing: 0, title: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( - color: Theme.of(context).primaryColor, + color: primaryColor, borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], ), child: const Icon( Icons.smart_toy_outlined, color: Colors.white, + size: 22, ), ), - const SizedBox(width: 10), + const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( 'DeepVoice AI', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.white : Colors.black87, ), ), Row( children: [ Container( - width: 8, - height: 8, - decoration: BoxDecoration( + width: 6, + height: 6, + decoration: const BoxDecoration( color: Colors.green, - borderRadius: BorderRadius.circular(4), + shape: BoxShape.circle, ), ), const SizedBox(width: 4), @@ -54,6 +76,7 @@ class AgentView extends StatelessWidget { style: TextStyle( fontSize: 12, color: Colors.green, + fontWeight: FontWeight.w500, ), ), ], @@ -63,14 +86,62 @@ class AgentView extends StatelessWidget { ], ), actions: [ - PopupMenuButton( - icon: const Icon(Icons.more_vert), - itemBuilder: (context) => [ - PopupMenuItem( - child: const Text('清除聊天记录'), - onTap: () => controller.clearChat(), + // TTS开关 + Obx(() => IconButton( + icon: Icon( + controller.isTtsEnabled.value + ? Icons.volume_up + : Icons.volume_off, + color: controller.isTtsEnabled.value + ? primaryColor + : isDarkMode + ? Colors.grey[400] + : Colors.grey, + ), + onPressed: () => controller.toggleTts(), + )), + + // 更多选项 + Container( + width: 34, + height: 34, + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], + shape: BoxShape.circle, + boxShadow: [ + if (!isDarkMode) + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 2, + spreadRadius: 0.5, + ), + ], + ), + child: PopupMenuButton( + icon: Icon(Icons.more_vert, + color: isDarkMode ? Colors.white : Colors.black87, + size: 20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), - ], + color: isDarkMode ? Colors.grey[800] : Colors.white, + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + Icon(Icons.delete_outline, color: primaryColor, size: 20), + const SizedBox(width: 8), + const Text('清除聊天记录', style: TextStyle(fontSize: 14)), + ], + ), + onTap: () => Future.delayed( + const Duration(milliseconds: 100), + () => controller.clearChat(), + ), + ), + ], + ), ), ], ), @@ -81,19 +152,22 @@ class AgentView extends StatelessWidget { child: Obx( () => ListView.builder( controller: controller.scrollController, - reverse: false, - padding: const EdgeInsets.all(16), + reverse: true, + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), itemCount: controller.messages.length, itemBuilder: (context, index) { - final message = controller.messages[index]; - return MessageBubble( - isUser: message.isUser, - message: message.text, - timestamp: message.timestamp, - isRecognizing: message.isRecognizing, - hasImage: message.hasImage, - imagePath: message.imagePath, - mate: message.meta, + final message = controller.messages[controller.messages.length - 1 - index]; + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: MessageBubble( + isUser: message.isUser, + message: message.text, + timestamp: message.timestamp, + isRecognizing: message.isRecognizing, + hasImage: message.hasImage, + imagePath: message.imagePath, + mate: message.meta, + ), ); }, ), @@ -102,16 +176,22 @@ class AgentView extends StatelessWidget { // 输入区域 Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, + color: backgroundColor, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 5, + color: Colors.black.withOpacity(0.06), + blurRadius: 8, offset: const Offset(0, -1), ), ], + border: Border( + top: BorderSide( + color: Colors.grey.withOpacity(0.15), + width: 1, + ), + ), ), child: Column( mainAxisSize: MainAxisSize.min, @@ -141,12 +221,12 @@ class AgentView extends StatelessWidget { child: GestureDetector( onTap: () => controller.clearImageInput(), child: Container( - padding: EdgeInsets.all(4), + padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.black.withOpacity(0.6), shape: BoxShape.circle, ), - child: Icon( + child: const Icon( Icons.close, color: Colors.white, size: 16, @@ -158,7 +238,7 @@ class AgentView extends StatelessWidget { ), // 预设提示选项 - SizedBox(height: 8), + const SizedBox(height: 12), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( @@ -168,13 +248,13 @@ class AgentView extends StatelessWidget { label: '总结图片内容', onTap: () => controller.summarizeImage(), ), - SizedBox(width: 8), + const SizedBox(width: 8), _buildPromptChip( context: context, label: '抽取图像文字', onTap: () => controller.extractTextFromImage(), ), - SizedBox(width: 8), + const SizedBox(width: 8), _buildPromptChip( context: context, label: '翻译图像文字', @@ -183,11 +263,11 @@ class AgentView extends StatelessWidget { ], ), ), - SizedBox(height: 12), + const SizedBox(height: 12), ], ); } else { - return SizedBox.shrink(); + return const SizedBox.shrink(); } }), @@ -202,15 +282,16 @@ class AgentView extends StatelessWidget { controller.toggleInputMode(); }, child: Container( - width: 48, - height: 48, + width: 44, + height: 44, decoration: BoxDecoration( color: Colors.grey[200], - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(22), ), child: Icon( isTextMode ? Icons.mic_none : Icons.keyboard, - color: Colors.grey[600], + color: Colors.grey[700], + size: 22, ), ), ); @@ -229,7 +310,11 @@ class AgentView extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: Colors.grey[200], - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: Colors.grey.withOpacity(0.1), + width: 1.0, + ), ), child: Row( children: [ @@ -243,10 +328,18 @@ class AgentView extends StatelessWidget { controller: controller.textController, maxLines: isImageActive ? 2 : 1, minLines: isImageActive ? 2 : 1, + style: TextStyle( + fontSize: 15, + color: isDarkMode ? Colors.white : Colors.black87, + ), decoration: InputDecoration( hintText: isImageActive ? '添加对图片的问题或描述...' : '输入消息...', + hintStyle: TextStyle( + color: Colors.grey[500], + fontSize: 15, + ), border: InputBorder.none, contentPadding: EdgeInsets.symmetric( vertical: isImageActive ? 12 : 0), @@ -258,9 +351,13 @@ class AgentView extends StatelessWidget { ), GestureDetector( onTap: () => controller.sendTextMessage(), - child: Icon( - Icons.send, - color: Theme.of(context).primaryColor, + child: Container( + padding: const EdgeInsets.all(6), + child: Icon( + Icons.send, + color: primaryColor, + size: 22, + ), ), ), ], @@ -277,12 +374,16 @@ class AgentView extends StatelessWidget { } }, child: Container( - height: 48, + height: 44, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: Colors.grey[200], - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: Colors.grey.withOpacity(0.1), + width: 1.0, + ), ), child: Row( children: [ @@ -306,19 +407,18 @@ class AgentView extends StatelessWidget { Icons.mic, size: 14, color: isListening - ? Theme.of(context) - .primaryColor - : Colors.grey, + ? primaryColor + : Colors.grey[500], ), const SizedBox(width: 4), Text( '正在聆听', style: TextStyle( - fontSize: 12, + fontSize: 13, + fontWeight: FontWeight.w500, color: isListening - ? Theme.of(context) - .primaryColor - : Colors.grey, + ? primaryColor + : Colors.grey[500], ), ), ], @@ -339,18 +439,22 @@ class AgentView extends StatelessWidget { // 图片选择按钮 Container( - width: 48, - height: 48, + width: 44, + height: 44, decoration: BoxDecoration( color: Colors.grey[200], - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(22), ), child: PopupMenuButton( icon: Icon( Icons.add_photo_alternate_outlined, - color: Colors.grey[600], + color: Colors.grey[700], + size: 22, ), padding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), onSelected: (value) async { if (value == 'camera') { await controller.takePhoto(); @@ -364,9 +468,9 @@ class AgentView extends StatelessWidget { child: Row( children: [ Icon(Icons.camera_alt, - color: Theme.of(context).primaryColor), - SizedBox(width: 8), - Text('拍照'), + color: primaryColor, size: 20), + const SizedBox(width: 8), + const Text('拍照', style: TextStyle(fontSize: 14)), ], ), ), @@ -375,9 +479,9 @@ class AgentView extends StatelessWidget { child: Row( children: [ Icon(Icons.photo_library, - color: Theme.of(context).primaryColor), - SizedBox(width: 8), - Text('从相册选择'), + color: primaryColor, size: 20), + const SizedBox(width: 8), + const Text('从相册选择', style: TextStyle(fontSize: 14)), ], ), ), @@ -400,22 +504,26 @@ class AgentView extends StatelessWidget { required String label, required VoidCallback onTap, }) { + final theme = Theme.of(context); + final primaryColor = theme.primaryColor; + return GestureDetector( onTap: onTap, child: Container( - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: primaryColor.withOpacity(0.08), borderRadius: BorderRadius.circular(16), border: Border.all( - color: Theme.of(context).primaryColor.withOpacity(0.3), + color: primaryColor.withOpacity(0.2), + width: 1, ), ), child: Text( label, style: TextStyle( fontSize: 13, - color: Theme.of(context).primaryColor, + color: primaryColor, fontWeight: FontWeight.w500, ), ), @@ -463,7 +571,7 @@ class _SoundBarState extends State<_SoundBar> duration: const Duration(milliseconds: 500), ); - _animation = Tween(begin: 3, end: 15).animate( + _animation = Tween(begin: 3, end: 18).animate( CurvedAnimation( parent: _controller, curve: Curves.easeInOut, @@ -493,7 +601,7 @@ class _SoundBarState extends State<_SoundBar> } else { _controller.reset(); // 设置为小高度的静态状态 - _controller.animateTo(0.1, duration: Duration(milliseconds: 300)); + _controller.animateTo(0.1, duration: const Duration(milliseconds: 300)); } } @@ -505,13 +613,17 @@ class _SoundBarState extends State<_SoundBar> @override Widget build(BuildContext context) { + final primaryColor = Theme.of(context).primaryColor; + return Container( - width: 2, + width: 2.5, height: widget.isActive ? _animation.value : 3, + margin: const EdgeInsets.symmetric(horizontal: 1), decoration: BoxDecoration( - color: - widget.isActive ? Theme.of(context).primaryColor : Colors.grey[400], - borderRadius: BorderRadius.circular(1), + color: widget.isActive + ? primaryColor.withOpacity(0.8) + : Colors.grey.withOpacity(0.4), + borderRadius: BorderRadius.circular(1.5), ), ); } diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index 952551e18..eb0f9f201 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -197,7 +197,7 @@ class MessageBubble extends GetView { // 判断消息内容,显示特殊卡片 if (!isUser && mate != null) { if (mate!.keys.contains('card_music')) { - final musicId = mate!['card_music']['id'] as String; + final musicId = mate!['card_music']['id'].toString(); final musicName = mate!['card_music']['name'] as String; final musicUrl = mate!['card_music']['url'] as String; final musicSinger = mate!['card_music']['sgener'] as String; @@ -326,7 +326,7 @@ class MessageBubble extends GetView { ...playlist.asMap().entries.map((entry) { final index = entry.key; final item = entry.value; - final id = item["id"] ?? ""; + final id = item["id"]?.toString() ?? ""; final name = item["name"] ?? "未知歌曲"; final url = item["url"] ?? ""; final singer = item["sgener"] ?? "未知歌手"; diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 7ecf4e076..98d7df449 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -186,7 +186,7 @@ class PairingController extends GetxController { if (Platform.isAndroid) { permissionsToRequest.add(Permission.locationWhenInUse); - permissionsToRequest.add(Permission.microphone); + // permissionsToRequest.add(Permission.microphone); if (await _getAndroidSdkVersion() >= 31) { permissionsToRequest.add(Permission.bluetoothScan); permissionsToRequest.add(Permission.bluetoothConnect); diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 7ac469322..56fbded96 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -666,12 +666,11 @@ object AgentService : CoroutineScope { override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) { audioPlayer?.stopAudio() - FileLogger.d(TAG, "mcp调用结果: $functionCallResult") sendEvent("function_call_result", mapOf( "function_call" to functionCall.toString(), "result" to functionCallResult.toString(), )) - FileLogger.d(TAG, "mcp调用结果: $functionCallResult") + FileLogger.d(TAG, "mcp调用结果: $functionCallResult") metadata = autoPlayMusic(functionCallResult); } } diff --git a/local_plugins/music_service/android/src/main/AndroidManifest.xml b/local_plugins/music_service/android/src/main/AndroidManifest.xml index 3154f0c1d..0b1f1fde9 100644 --- a/local_plugins/music_service/android/src/main/AndroidManifest.xml +++ b/local_plugins/music_service/android/src/main/AndroidManifest.xml @@ -8,20 +8,18 @@ + android:foregroundServiceType="mediaPlayback" + > - - - - + + android:exported="true"> diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index e0091e1ff..1274555d4 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -55,6 +55,7 @@ class MusicService : MediaBrowserServiceCompat() { } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + FileLogger.i(TAG, "onStartCommand: $intent") intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let { // 分发给 MediaSession 处理媒体按钮 MediaButtonReceiver.handleIntent(mediaSession, intent) @@ -62,7 +63,6 @@ class MusicService : MediaBrowserServiceCompat() { } val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY - FileLogger.i(TAG, "liwei------------------- onStartCommand: $command") when (command) { "play" -> intent.getSerializableExtra("song")?.let { url -> val songMap = intent.getSerializableExtra("song") as? Map diff --git a/local_plugins/open_ai_service/android/build.gradle.kts b/local_plugins/open_ai_service/android/build.gradle.kts index d1247752b..f5bbc9337 100644 --- a/local_plugins/open_ai_service/android/build.gradle.kts +++ b/local_plugins/open_ai_service/android/build.gradle.kts @@ -60,4 +60,5 @@ dependencies { implementation("io.ktor:ktor-client-logging:3.1.2") implementation("com.google.code.gson:gson:2.10.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") + implementation("androidx.media:media:1.6.0") } \ No newline at end of file diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt index 0349b06be..6dd0bd660 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt @@ -528,7 +528,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { val call = client.newCall(request) // 保存当前Call对象以便后续取消 currentStreamCall = call - Log.d(TAG, "发送消息流式输出: request=$request") + // Log.d(TAG, "发送消息流式输出: request=$request") call.enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { Log.d(TAG, "发送消息流式输出失败: e=$e") @@ -638,7 +638,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { // 处理普通文本内容 if (delta.has("content")) { val content = delta.getString("content") - Log.d(TAG, "处理流式JSON数据: content=$content") + // Log.d(TAG, "处理流式JSON数据: content=$content") if (!isCanceled) { callback.onToken(content) } @@ -698,7 +698,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { put("arguments", firstToolCall.arguments) put("id", firstToolCall.id) } - Log.d(TAG, "工具调用: id=${firstToolCall.id}, name=${firstToolCall.name}") + Log.d(TAG, "工具调用: id=${firstToolCall.id}, name=${firstToolCall.name} arguments=${firstToolCall.arguments}") // 通知上层工具调用事件 callback.onFunctionCall(functionCall) diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt index bf5b98ddc..e197f4892 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt @@ -17,7 +17,7 @@ import android.util.Log import org.json.JSONObject import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume - +import android.view.KeyEvent /** * 系统功能处理器 - 负责调用系统API执行各种功能以及注册系统功能 */ @@ -430,27 +430,22 @@ class SystemFunctionHandler(private val context: Context) { ) flags = Intent.FLAG_RECEIVER_FOREGROUND } - context.sendOrderedBroadcast(keyIntent, null) + keyIntent.setPackage("com.yunqiinnovation.deep_voice") + context.sendBroadcast(keyIntent) return Result( success = true, message = "已开始播放媒体" ) } "pause" -> { - // 尝试发送暂停广播 - intent.action = "android.intent.action.MEDIA_PAUSE" - context.sendBroadcast(intent) + // 发送完整的按键事件序列 + val downEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE) + val keyIntent = Intent(Intent.ACTION_MEDIA_BUTTON) + keyIntent.putExtra(Intent.EXTRA_KEY_EVENT, downEvent) + keyIntent.setPackage("com.yunqiinnovation.deep_voice") + context.sendBroadcast(keyIntent) - // 也尝试使用媒体按键广播 - val keyIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply { - putExtra( - Intent.EXTRA_KEY_EVENT, - android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_PAUSE) - ) - flags = Intent.FLAG_RECEIVER_FOREGROUND - } - context.sendOrderedBroadcast(keyIntent, null) - return Result( + return Result( success = true, message = "已暂停媒体播放" ) @@ -468,7 +463,8 @@ class SystemFunctionHandler(private val context: Context) { ) flags = Intent.FLAG_RECEIVER_FOREGROUND } - context.sendOrderedBroadcast(keyIntent, null) + keyIntent.setPackage("com.yunqiinnovation.deep_voice") + context.sendBroadcast(keyIntent) return Result( success = true, message = "已切换到上一首" @@ -487,7 +483,8 @@ class SystemFunctionHandler(private val context: Context) { ) flags = Intent.FLAG_RECEIVER_FOREGROUND } - context.sendOrderedBroadcast(keyIntent, null) + keyIntent.setPackage("com.yunqiinnovation.deep_voice") + context.sendBroadcast(keyIntent) return Result( success = true, message = "已切换到下一首"