diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 2852e4f69..d6563ce10 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -795,12 +795,7 @@ class AgentController extends GetxController with WidgetsBindingObserver { orElse: () => "", ); if (musicKey.isNotEmpty) { - // final qqmusic_search_songs = - // metaResult['qqmusic_searchandplay_songs'] - // as Map; - // final data = qqmusic_search_songs['data']; - // if (data != null && data is List) { - Future.delayed(Duration(seconds: 2), () { + Future.delayed(Duration(seconds: 1), () { _qqmusicManager.syncCurrentPlayInfo(isOpenPlay: true); }); // } diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index 63615d5e0..8922a0cb4 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -236,9 +236,9 @@ class MessageBubble extends GetView { //音乐播放列表 return buildMusicContent(context, playlist); } - } else if (meta!.keys.contains('qqmusic_searchandplay_songs')) { + } else if (meta!.keys.contains('qqmusic_search_songs')) { final qqmusic_search_songs = - meta!['qqmusic_searchandplay_songs'] as Map; + meta!['qqmusic_search_songs'] as Map; final data = qqmusic_search_songs['data']; if (data != null && data is List) { //qq音乐 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 702ee4098..8ea45fded 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 @@ -36,6 +36,11 @@ import java.io.IOException import java.text.SimpleDateFormat import java.util.Locale import java.util.Date +// 添加后台启动相关导入 +import android.app.PendingIntent +import android.app.NotificationManager +import android.app.NotificationChannel +import androidx.core.app.NotificationCompat /** @@ -1015,7 +1020,7 @@ object AgentService : CoroutineScope { if (reply) { sendEvent("assistant_response", responseData) } - + Log.d(TAG, "liwei--------------- AI Call onComplete:$response") if (response.isNotEmpty()) { // 添加AI回复到历史记录 addToHistoryMessages(createAssistantMessage(response)) @@ -1081,19 +1086,23 @@ object AgentService : CoroutineScope { audioPlayer?.stopAudio() val name = functionCall.get("name") as String; val resultmap = autoHandleFcunCallResult(name, functionCallResult); - aiMetadata = resultmap["meta"] as String; broadcast = resultmap["broadcast"] as Boolean; reply = !(resultmap["notreply"] as Boolean); iscard = resultmap["iscard"] as Boolean; if (iscard){ - sendEvent( - "function_call_result", mapOf( - "function_call" to functionCall.toString(), - "result" to functionCallResult.toString(), - "meta" to aiMetadata, - ) - ) + aiMetadata = resultmap["meta"] as String; } + sendEvent( + "function_call_result", mapOf( + "function_call" to functionCall.toString(), + "result" to functionCallResult.toString(), + "meta" to aiMetadata, + ) + ) + Log.e( + TAG, + "liwei--------------- AI Call onFunctionCallResult broadcast:$broadcast reply:$reply iscard:$iscard" + ) } catch (e: Exception) { Log.e( TAG, @@ -1859,13 +1868,13 @@ object AgentService : CoroutineScope { toolname: String, functionCallResult: JSONObject ): Map { - println("协议工具返回数据 检查是否存在卡片! functionCallResult: ${functionCallResult}") + // println("协议工具返回数据 检查是否存在卡片! functionCallResult: ${functionCallResult}") try { val contentStr = functionCallResult.optString("context") - println("协议工具返回数据 检查是否存在卡片! context: ${contentStr}") + // println("协议工具返回数据 检查是否存在卡片! context: ${contentStr}") val content = JSONObject(contentStr) val textStr = content.optString("text") - println("协议工具返回数据 检查是否存在卡片! textStr: ${textStr}") + // println("协议工具返回数据 检查是否存在卡片! textStr: ${textStr}") val metadata: MutableMap = mutableMapOf() var broadcast = true; var notreply = false; @@ -2170,12 +2179,25 @@ object AgentService : CoroutineScope { "src=$src") val intent = Intent(Intent.ACTION_VIEW, uri).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK + flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_SINGLE_TOP + // 添加包名确保启动正确的应用 + setPackage("com.baidu.BaiduMap") } if (isAppInstalled("com.baidu.BaiduMap")) { - context.startActivity(intent) - Log.i(TAG, "已启动百度地图导航") + try { + context.startActivity(intent) + Log.i(TAG, "已启动百度地图导航") + } catch (e: SecurityException) { + // Android 10+ 后台启动限制,尝试使用 PendingIntent + Log.w(TAG, "后台启动受限,尝试使用通知启动百度地图: ${e.message}") + startActivityWithPendingIntent(intent, "百度地图导航") + } catch (e: Exception) { + Log.e(TAG, "启动百度地图失败: ${e.message}", e) + throw e + } } else { Log.w(TAG, "百度地图未安装") throw Exception("百度地图未安装") @@ -2216,12 +2238,25 @@ object AgentService : CoroutineScope { val uri = android.net.Uri.parse(uriBuilder.toString()) val intent = Intent(Intent.ACTION_VIEW, uri).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK + flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_SINGLE_TOP + // 添加包名确保启动正确的应用 + setPackage("com.autonavi.minimap") } if (isAppInstalled("com.autonavi.minimap")) { - context.startActivity(intent) - Log.i(TAG, "已启动高德地图导航") + try { + context.startActivity(intent) + Log.i(TAG, "已启动高德地图导航") + } catch (e: SecurityException) { + // Android 10+ 后台启动限制,尝试使用 PendingIntent + Log.w(TAG, "后台启动受限,尝试使用通知启动高德地图: ${e.message}") + startActivityWithPendingIntent(intent, "高德地图导航") + } catch (e: Exception) { + Log.e(TAG, "启动高德地图失败: ${e.message}", e) + throw e + } } else { Log.w(TAG, "高德地图未安装") throw Exception("高德地图未安装") @@ -2249,20 +2284,44 @@ object AgentService : CoroutineScope { // 尝试使用Google Maps应用 val appUri = android.net.Uri.parse("comgooglemaps://?saddr=Current+Location&daddr=$destination&directionsmode=$googleMode") val appIntent = Intent(Intent.ACTION_VIEW, appUri).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK + flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_SINGLE_TOP + // 添加包名确保启动正确的应用 + setPackage("com.google.android.apps.maps") } if (isAppInstalled("com.google.android.apps.maps")) { - context.startActivity(appIntent) - Log.i(TAG, "已启动谷歌地图导航") + try { + context.startActivity(appIntent) + Log.i(TAG, "已启动谷歌地图导航") + } catch (e: SecurityException) { + // Android 10+ 后台启动限制,尝试使用 PendingIntent + Log.w(TAG, "后台启动受限,尝试使用通知启动谷歌地图: ${e.message}") + startActivityWithPendingIntent(appIntent, "谷歌地图导航") + } catch (e: Exception) { + Log.e(TAG, "启动谷歌地图失败: ${e.message}", e) + throw e + } } else { // 回退到网页版 val webUri = android.net.Uri.parse("https://www.google.com/maps/dir/?api=1&origin=Current+Location&destination=$destination&travelmode=$googleMode") val webIntent = Intent(Intent.ACTION_VIEW, webUri).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK + flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_SINGLE_TOP + } + try { + context.startActivity(webIntent) + Log.i(TAG, "已启动谷歌地图网页版导航") + } catch (e: SecurityException) { + // Android 10+ 后台启动限制,尝试使用 PendingIntent + Log.w(TAG, "后台启动受限,尝试使用通知启动谷歌地图网页版: ${e.message}") + startActivityWithPendingIntent(webIntent, "谷歌地图网页版导航") + } catch (e: Exception) { + Log.e(TAG, "启动谷歌地图网页版失败: ${e.message}", e) + throw e } - context.startActivity(webIntent) - Log.i(TAG, "已启动谷歌地图网页版导航") } } catch (e: Exception) { Log.e(TAG, "启动谷歌地图失败: ${e.message}", e) @@ -2305,6 +2364,54 @@ object AgentService : CoroutineScope { return true } + /** + * 使用 PendingIntent 启动 Activity(适用于后台启动场景) + */ + private fun startActivityWithPendingIntent(intent: android.content.Intent, description: String) { + try { + val pendingIntent = PendingIntent.getActivity( + context, + System.currentTimeMillis().toInt(), // 使用时间戳作为唯一ID + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + // 创建通知渠道(Android 8.0+) + val channelId = "navigation_channel" + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + val channel = NotificationChannel( + channelId, + "导航服务", + NotificationManager.IMPORTANCE_HIGH + ).apply { + this.description = "用于启动导航应用" + enableVibration(true) + enableLights(true) + } + notificationManager.createNotificationChannel(channel) + } + + // 创建通知 + val notification = NotificationCompat.Builder(context, channelId) + .setSmallIcon(android.R.drawable.ic_dialog_map) + .setContentTitle("启动导航") + .setContentText("点击启动$description") + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .build() + + notificationManager.notify(1001, notification) + Log.i(TAG, "已通过通知启动$description") + + } catch (e: Exception) { + Log.e(TAG, "PendingIntent 启动失败: ${e.message}", e) + } + } + /** * 检查应用是否已安装 */ diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt index a820c23d3..79ac37e95 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt @@ -27,16 +27,16 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) { */ fun registerAllFunctions(client: MCPClient) { try { - // Only register QQ Music search function + // 注册QQ音乐搜索功能(仅搜索,不播放) client.registerLocalFunction( - "qqmusic_searchandplay_songs", - "在QQ音乐中搜索并播放歌曲,支持歌曲、专辑、歌手等搜索类型", + "qqmusic_search_songs", + "在QQ音乐中搜索歌曲,支持歌曲、专辑、歌手等搜索类型", mapOf( "type" to "object", "properties" to mapOf( "keyword" to mapOf( "type" to "string", - "description" to "搜索关键词,歌曲名、专辑名或歌手名" + "description" to "搜索关键词,歌曲名、专辑名或歌手名或者类型" ), "searchType" to mapOf( "type" to "integer", @@ -46,7 +46,30 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) { ), "required" to listOf("keyword") ), - QQMusicSearchHandler(context) + QQMusicSearchOnlyHandler(context) + ) + + // 注册QQ音乐播放歌曲列表功能 + client.registerLocalFunction( + "qqmusic_play_songs", + "播放指定的QQ音乐歌曲列表", + mapOf( + "type" to "object", + "properties" to mapOf( + "songMids" to mapOf( + "type" to "array", + "items" to mapOf("type" to "string"), + "description" to "歌曲MID列表" + ), + "index" to mapOf( + "type" to "integer", + "description" to "开始播放的歌曲索引,默认为0", + "default" to 0 + ) + ), + "required" to listOf("songMids") + ), + QQMusicPlaySongsHandler(context) ) client.registerLocalFunction( @@ -111,8 +134,8 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) { } } - -private class QQMusicSearchHandler(private val context: Context?) : FunctionHandler { +// 新的仅搜索处理器(不包含播放功能) +private class QQMusicSearchOnlyHandler(private val context: Context?) : FunctionHandler { override suspend fun handle(arguments: Map): String { return try { val keyword = arguments["keyword"] as? String ?: return "{\"success\": false, \"message\": \"缺少搜索关键词\"}" @@ -124,25 +147,6 @@ private class QQMusicSearchHandler(private val context: Context?) : FunctionHand qqMusicSingleton.searchSongs(keyword) { searchResult -> val result = if (searchResult.isSuccess) { val songs = searchResult.getOrNull() ?: emptyList() - var playError: Throwable? = null - if (songs.isNotEmpty()) { - // 从搜索结果中提取歌曲MID列表 - val songMids = songs.mapNotNull { song -> - val songMap = song as Map - songMap["mid"] as? String - }.filter { it.isNotEmpty() } - - if (songMids.isNotEmpty()) { - // 播放搜索到的歌曲列表,从第一首开始播放 - qqMusicSingleton.playSongs(songMids, 0) { playResult -> - Log.d("QQMusicSearchHandler", "播放结果: ${playResult.isSuccess}") - if (!playResult.isSuccess) { - playError = playResult.exceptionOrNull() -// Log.e("QQMusicSearchHandler", "播放失败: ${playError?.message}") - } - } - } - } // 根据搜索类型处理结果 val processedSongs = when (searchType) { @@ -157,7 +161,7 @@ private class QQMusicSearchHandler(private val context: Context?) : FunctionHand val songsJson = processedSongs.joinToString(",") { song -> val songMap = song as Map // 添加调试日志 - Log.d("QQMusicSearchHandler", "原始数据: $songMap") + Log.d("QQMusicSearchOnlyHandler", "原始数据: $songMap") // 修正字段名映射 val mid = songMap["mid"] ?: "unknown" val name = songMap["name"] ?: "unknown" @@ -169,13 +173,7 @@ private class QQMusicSearchHandler(private val context: Context?) : FunctionHand } if (songs.isNotEmpty()) { - if (playError == null){ - "{\"success\": true, \"iscard\": true, \"broadcast\": false, \"message\": \"搜索成功,正在播放\", \"data\": [$songsJson]}" - }else{ - Log.e("QQMusicSearchHandler", "播放失败: ${playError?.message}") - "{\"success\": false, \"iscard\": true, \"broadcast\": true, \"message\": \"${playError?.message}\", \"data\": [$songsJson]}" - } - + "{\"success\": true, \"message\": \"搜索成功\",\"iscard\": true,\"broadcast\": false, \"data\": [$songsJson]}" } else { "{\"success\": false, \"message\": \"未找到相关歌曲\"}" } @@ -188,7 +186,7 @@ private class QQMusicSearchHandler(private val context: Context?) : FunctionHand } } } catch (e: Exception) { - Log.e("QQMusicSearchHandler", "QQ音乐搜索失败", e) + Log.e("QQMusicSearchOnlyHandler", "QQ音乐搜索失败", e) "{\"success\": false, \"message\": \"搜索异常: ${e.message}\"}" } } @@ -205,16 +203,16 @@ private class QQMusicPlaySongsHandler(private val context: Context?) : FunctionH qqMusicSingleton.playSongs(songMids, index) { playResult -> result = if (playResult.isSuccess) { - "{\"nocard\": true, \"success\": true, \"message\": \"开始播放歌曲列表\"}" + "{\"broadcast\": false, \"success\": true, \"message\": \"开始播放歌曲列表\"}" } else { val error = playResult.exceptionOrNull() - "{\"nocard\": true, \"success\": false, \"message\": \"播放失败: ${error?.message}\"}" + "{\"success\": false, \"message\": \"播放失败: ${error?.message}\"}" } } - // 等待回调完成 - delay(3000) - result.ifEmpty { "{\"nocard\": true, \"success\": false, \"message\": \"播放超时\"}" } + // 等待回调完成 十秒 + delay(10000) + result.ifEmpty { "{\"nocard\": false,\"broadcast\": true, \"success\": false, \"message\": \"播放超时\"}" } } catch (e: Exception) { Log.e("QQMusicPlaySongsHandler", "QQ音乐播放歌曲失败", e) "{\"nocard\": true, \"success\": false, \"message\": \"播放异常: ${e.message}\"}" @@ -230,14 +228,14 @@ private class QQMusicPlayHandler(private val context: Context?) : FunctionHandle qqMusicSingleton.playMusic { playResult -> result = if (playResult.isSuccess) { - "{\"success\": true, \"message\": \"开始播放\"}" + "{\"broadcast\": false, \"success\": true, \"message\": \"开始播放\"}" } else { val error = playResult.exceptionOrNull() "{\"success\": false, \"message\": \"播放失败: ${error?.message}\"}" } } - - delay(2000) + + delay(10000) result.ifEmpty { "{\"success\": false, \"message\": \"播放超时\"}" } } catch (e: Exception) { Log.e("QQMusicPlayHandler", "QQ音乐播放失败", e) diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt index c69c781cd..f0fee9144 100644 --- a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt @@ -567,18 +567,9 @@ class QQMusicSingleton private constructor() { if (allagain > 0 && curragain < allagain){ //重试机制 executePlaybackAction(actionName,curragain+1,allagain,action)(callback) }else{ - handler.post { - Log.e(TAG, "${actionName}失败: $errorCode $errorMessage") - if (errorCode == -1){ - musicManager?.bindQQMusicApiService() - callback(Result.failure(Exception("播放失败: $errorMessage"))) - } else if (errorCode == 1030) { - callback(Result.failure(Exception("播放失败: $errorMessage"))) -// callback(Result.success(Unit)) - }else{ - callback(Result.failure(Exception("播放失败: $errorMessage"))) - } - } + //播放失败时,检查当前播放状态 + Log.d(TAG, "${actionName}失败,检查当前播放状态: $errorCode $errorMessage") + checkPlaybackStateOnError(actionName, errorCode, errorMessage, callback) } } }) @@ -592,7 +583,55 @@ class QQMusicSingleton private constructor() { } } }) + } + + /** + * 播放失败时检查播放状态 + */ + private fun checkPlaybackStateOnError( + actionName: String, + errorCode: Int, + errorMessage: String, + callback: (Result) -> Unit + ) { + musicManager?.getPlaybackState(object : QQMusicManager.InfoCallback { + override fun onSuccess(data: Map) { + val playState = data["state"] as? Int ?: 0 + val isPlaying = when (playState) { + PlayState.STARTED -> true + PlayState.PREPARED -> true + PlayState.BUFFERING -> true + PlayState.PAUSED -> false + PlayState.STOPPED -> false + PlayState.ERROR -> false + else -> false + } + + handler.post { + if (isPlaying) { + Log.d(TAG, "${actionName}虽然返回失败,但检查到正在播放,返回成功") + callback(Result.success(Unit)) + } else { + Log.e(TAG, "${actionName}失败且未在播放: $errorCode $errorMessage") + if (errorCode == -1){ + musicManager?.bindQQMusicApiService() + } + callback(Result.failure(Exception("播放失败: $errorMessage"))) + } + } + } + override fun onError(errorCode: Int, errorMessage: String) { + // 获取播放状态失败,直接返回原始错误 + handler.post { + Log.e(TAG, "${actionName}失败且未在播放: $errorCode $errorMessage") + if (errorCode == -1){ + musicManager?.bindQQMusicApiService() + } + callback(Result.failure(Exception("播放失败: $errorMessage"))) + } + } + }) }