From f92d2e5ce766e01f44e51d2a038b5df0ac131edf Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Sat, 28 Jun 2025 12:06:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent_service/AgentService.kt | 215 ++++++++++++------ .../chat_api/ChatApiService.kt | 120 ++++++---- .../yunqiinnovation/chat_api/MCPSubClient.kt | 44 +++- .../music_service/MusicService.kt | 73 ++++-- 4 files changed, 316 insertions(+), 136 deletions(-) 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 adda12336..fdd51c709 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 @@ -1,9 +1,11 @@ package com.yunqiinnovation.agent_service import android.content.Context +import android.content.Intent import org.json.JSONArray import org.json.JSONObject import com.yunqiinnovation.azure_speech.AzureAsrHelper +import com.yunqiinnovation.azure_speech.AzureTtsHelper import com.yunqiinnovation.chat_api.ChatApiService import com.yunqiinnovation.chat_api.StreamCallback import com.yunqiinnovation.chat_storage.ChatStorageHelper @@ -23,6 +25,8 @@ import com.deep_voice.speech.tts.TtsEvent import com.deep_voice.speech.tts.TtsEventListener import com.deep_voice.speech.tts.TtsEventType import com.deep_voice.bytedance_speech.BytedanceTTS +import java.util.UUID + /** * 代理服务事件监听接口 */ @@ -56,7 +60,7 @@ object AgentService : CoroutineScope { private var azureAsrHelper: AzureAsrHelper? = null // 使用BytedanceTTS作为唯一的TTS实现 - private var ttsService: BytedanceTTS? = null + private var ttsService: AzureTtsHelper? = null // ChatAPI服务 - 使用新的ChatApiService private lateinit var chatApiService: ChatApiService @@ -97,7 +101,7 @@ object AgentService : CoroutineScope { // AI流生成相关 private var currentAiJob: Job? = null - + private var currsessionId:String = "" // 空闲检测相关 private var idleCheckJob: Job? = null private val maxIdleSeconds = 10 // 最大空闲秒数 @@ -244,14 +248,14 @@ object AgentService : CoroutineScope { val ttsLanguage = config["ttsLanguage"]?.toString() ?: "zh-CN" // 创建并初始化BytedanceTTS - val bytedanceTts = BytedanceTTS(context) - ttsService = bytedanceTts + val azureTtsHelper = AzureTtsHelper(context) + ttsService = azureTtsHelper // 初始化Bytedance TTS - val success = bytedanceTts.initialize( - ttsAppId = ttsAppId, - ttsAppToken = ttsAppToken, - ttsResource = "", // Bytedance TTS不需要resource参数 + val success = azureTtsHelper.initialize( + ttsAppId = "", // Azure TTS不需要appId + ttsAppToken = config["azureSpeechKey"]?.toString() ?: "", // Azure需要subscription key + ttsResource = config["azureSpeechRegion"]?.toString() ?: "", // Azure需要region信息 language = ttsLanguage ) @@ -278,13 +282,23 @@ object AgentService : CoroutineScope { restartIdleCheck() sendEvent("tts_canceled", mapOf("status" to "canceled")) } - TtsEventType.PLAYBACK_STARTED -> { + TtsEventType.PLAYBACK_STARTED -> { //开始AI播报内容 restartIdleCheck() sendEvent("playback_started", mapOf("status" to "playback_started")) + // 发送AI说话开始广播 + val intent = Intent("com.yunqiinnovation.music_service.AI_SAY_START").apply { + setPackage(context.packageName) + } + context.sendBroadcast(intent) } - TtsEventType.PLAYBACK_COMPLETED -> { + TtsEventType.PLAYBACK_COMPLETED -> { //结束AI播报内容 restartIdleCheck() sendEvent("playback_completed", mapOf("status" to "playback_completed")) + // 发送AI说话开始广播 + val intent = Intent("com.yunqiinnovation.music_service.AI_SAY_END").apply { + setPackage(context.packageName) + } + context.sendBroadcast(intent) } TtsEventType.ERROR -> { _isTtsSpeaking.set(false) @@ -666,6 +680,7 @@ object AgentService : CoroutineScope { // 记录新的AI任务 currentAiJob = launch { try { + currsessionId = UUID.randomUUID().toString() // 设置状态为正在流式输出 _isAiStreaming.set(true) audioPlayer?.playAudio(R.raw.await, true,0.3f) @@ -692,8 +707,8 @@ object AgentService : CoroutineScope { ) val _systemPrompt = Utils.fillTemplate(systemPrompt,location) messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt)) - // Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}") - } + // Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}") + } // 再添加历史消息 for (i in 0 until historyMessages.length()) { @@ -730,61 +745,84 @@ object AgentService : CoroutineScope { } // Log.e("ChatApiService", "liwei---------sendMessageStream messages:${messagesWithSystemPrompt}") chatApiService.sendMessageStream( + currsessionId, messagesWithSystemPrompt, object : StreamCallback { - override fun onToken(token: String) { - responseBuilder.append(token) - if (speakResponse && broadcast) { - ttsService?.speakStream(token) - if (token.length > 0){ - audioPlayer?.stopAudio() + override fun onToken(sessionid:String,token: String) { + try { + if (sessionid != currsessionId){ + return } - } - if (broadcast){ - // 发送流式回复token - sendEvent("assistant_token", mapOf("token" to token)) + responseBuilder.append(token) + if (speakResponse && broadcast) { + ttsService?.speakStream(token) + if (token.length > 0) { + audioPlayer?.stopAudio() + } + } + if (broadcast) { + // 发送流式回复token + sendEvent("assistant_token", mapOf("token" to token)) + } + } catch (e: Exception) { + Log.e(TAG, "liwei--------------- AI Call onToken 异常", e) } } - override fun onComplete() { - // 视情况决定是否朗读回复 - if (speakResponse && broadcast) { - ttsService?.flushStream() - } - val response = responseBuilder.toString() - // 发送完整回复,包含是否有图片的标记 - val responseData = mutableMapOf( - "text" to response, - "userInput" to displayText - ) - if (hasImage) { - responseData["hasImage"] = true - } - sendEvent("assistant_response", responseData) - if (response.isNotEmpty()) { - // 添加AI回复到历史记录 - addToHistoryMessages(createAssistantMessage(response)) + override fun onComplete(sessionid:String) { + try { + // 视情况决定是否朗读回复 + if (speakResponse && broadcast && sessionid == currsessionId){ + ttsService?.flushStream() + } + val response = responseBuilder.toString() + // 发送完整回复,包含是否有图片的标记 + val responseData = mutableMapOf( + "text" to response, + "userInput" to displayText + ) + if (hasImage) { + responseData["hasImage"] = true + } + sendEvent("assistant_response", responseData) + if (response.isNotEmpty()) { + // 添加AI回复到历史记录 + addToHistoryMessages(createAssistantMessage(response)) + } + // 保存聊天记录 + saveChatMessage( + displayText, + response, + aiMetadata, + userMetadata.toString() + ) + // 标记AI流式输出已完成 + _isAiStreaming.set(false) + currentAiJob = null + } catch (e: Exception) { + Log.e(TAG, "liwei--------------- AI Call onComplete 异常", e) } - // 保存聊天记录 - saveChatMessage(displayText, response,aiMetadata,userMetadata.toString()) - // 标记AI流式输出已完成 - _isAiStreaming.set(false) - currentAiJob = null } - override fun onError(e: Exception) { - Log.e(TAG, "AI处理出错", e) - sendEvent("error", mapOf( - "code" to "AI_ERROR", - "message" to e.message.toString() - )) - - // 标记AI流式输出已完成 - _isAiStreaming.set(false) - currentAiJob = null + override fun onError(sessionid:String,e: Exception) { + try { + Log.e(TAG, "AI处理出错", e) + sendEvent( + "error", mapOf( + "code" to "AI_ERROR", + "message" to e.message.toString() + ) + ) + + // 标记AI流式输出已完成 + _isAiStreaming.set(false) + currentAiJob = null + }catch (e: Exception) { + Log.e(TAG, "liwei--------------- AI Call onError 异常", e) + } } - override fun onFunctionCall(call: JSONObject) { + override fun onFunctionCall(sessionid:String,call: JSONObject) { try { // audioPlayer?.playAudio(R.raw.calling, true) val name = call.getString("name") @@ -796,28 +834,34 @@ object AgentService : CoroutineScope { stopRecognition() } } catch (e: Exception) { - Log.e(TAG, "处理函数调用事件失败", e) + Log.e(TAG, "liwei--------------- AI Call onFunctionCall 异常", e) } } - override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) { - audioPlayer?.stopAudio() - val name = functionCall.get("name") as String; - val resultmap = autoHandleFcunCallResult(name,functionCallResult); - aiMetadata = resultmap["meta"] as String; - broadcast = resultmap["broadcast"] as Boolean; - sendEvent("function_call_result", mapOf( - "function_call" to functionCall.toString(), - "result" to functionCallResult.toString(), - "meta" to aiMetadata, - )) + override fun onFunctionCallResult(sessionid:String,functionCall: JSONObject, functionCallResult: JSONObject) { + try { + audioPlayer?.stopAudio() + val name = functionCall.get("name") as String; + val resultmap = autoHandleFcunCallResult(name, functionCallResult); + aiMetadata = resultmap["meta"] as String; + broadcast = resultmap["broadcast"] as Boolean; + sendEvent( + "function_call_result", mapOf( + "function_call" to functionCall.toString(), + "result" to functionCallResult.toString(), + "meta" to aiMetadata, + ) + ) + } catch (e: Exception) { + Log.e(TAG, "liwei--------------- AI Call onFunctionCallResult 异常", e) + } } } ) } catch (e: Exception) { val errorType = if (hasImage) "AI_IMAGE_PROCESS_ERROR" else "AI_PROCESS_ERROR" - Log.e(TAG, "AI处理出错", e) + Log.e(TAG, "liwei--------------- AI处理出错", e) sendEvent("error", mapOf( "code" to errorType, "message" to e.message.toString() @@ -1119,6 +1163,7 @@ object AgentService : CoroutineScope { */ class AudioPlayer(private val context: Context) { private var mediaPlayer: MediaPlayer? = null + private var isInitialized = false /** * 播放音频资源 @@ -1155,21 +1200,47 @@ object AgentService : CoroutineScope { release() } } + fun stopAudio() { - mediaPlayer?.stop() + try { + mediaPlayer?.apply { + when { + isPlaying -> { + stop() + Log.d(TAG, "音频已停止") + } + else -> { + Log.d(TAG, "音频未在播放状态,无需停止") + } + } + } + } catch (e: IllegalStateException) { + Log.e(TAG, "MediaPlayer 状态异常,无法停止: ${e.message}", e) + // 重置 MediaPlayer + release() + } catch (e: Exception) { + Log.e(TAG, "停止音频播放异常: ${e.message}", e) + } } + /** * 释放资源 */ fun release() { try { mediaPlayer?.apply { - if (isPlaying) stop() + if (isPlaying) { + stop() + } + reset() release() } mediaPlayer = null + isInitialized = false } catch (e: Exception) { - // 忽略异常 + Log.e(TAG, "释放音频资源异常: ${e.message}", e) + mediaPlayer = null + isInitialized = false } } } diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt index ee7fae90a..45d5a0caf 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt @@ -39,27 +39,27 @@ interface StreamCallback { /** * 接收到新的token */ - fun onToken(token: String) + fun onToken(sessionid: String,token: String) /** * 流式响应完成 */ - fun onComplete() + fun onComplete(sessionid: String) /** * 出现错误 */ - fun onError(error: Exception) + fun onError(sessionid: String,error: Exception) /** * 函数调用 - 兼容JSONObject格式 */ - fun onFunctionCall(functionCall: org.json.JSONObject) + fun onFunctionCall(sessionid: String,functionCall: org.json.JSONObject) /** * 函数调用结果 - 兼容JSONObject格式 */ - fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject) + fun onFunctionCallResult(sessionid: String,functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject) } /** @@ -142,7 +142,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor private var currentMessages: List> = emptyList() private var toolCalls: MutableMap = Collections.synchronizedMap(mutableMapOf()) private var currSessionId = "" - private var currSessionCallback: StreamCallback? = null + // 为每个会话维护独立的回调函数 + private val sessionCallbacks: MutableMap = Collections.synchronizedMap(mutableMapOf()) @@ -156,17 +157,18 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor */ private fun abortCurrentSession() { if (currSessionId.isNotEmpty()) { + val oldSessionId = currSessionId + // 1. 取消当前的协程任务 currentStreamJob?.cancel() currentStreamJob = null // 2. 通知旧会话被中止 - // currSessionCallback?.onError(ChatApiException("Session aborted by new request")) - currSessionCallback?.onComplete() // 直接完成当前会话 + getSessionCallback(oldSessionId)?.onComplete(currSessionId) // 直接完成当前会话 // 3. 清理状态 currSessionId = "" - currSessionCallback = null + sessionCallbacks.remove(oldSessionId) toolCalls.clear() } } @@ -175,12 +177,20 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor * 清理当前会话 */ private fun clearCurrentSession() { + val oldSessionId = currSessionId currSessionId = "" - currSessionCallback = null + sessionCallbacks.remove(oldSessionId) currentStreamJob = null toolCalls.clear() } + /** + * 获取指定会话的回调函数 + */ + private fun getSessionCallback(sessionId: String): StreamCallback? { + return if (sessionId == currSessionId) sessionCallbacks[sessionId] else null + } + /** * 初始化ChatAPI服务 */ @@ -312,17 +322,18 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor /** * 发送消息(流式输出)- 兼容JSONArray格式 */ - fun sendMessageStream(messages: org.json.JSONArray, callback: StreamCallback) { + fun sendMessageStream(sessionid: String,messages: org.json.JSONArray, callback: StreamCallback) { // 1. 立即中止旧会话 abortCurrentSession() // 2. 创建新会话 - currSessionId = UUID.randomUUID().toString() - currSessionCallback = callback +// val newSessionId = UUID.randomUUID().toString() + currSessionId = sessionid + sessionCallbacks[sessionid] = callback // 3. 转换消息并开始处理 val messageList = convertJsonArrayToList(messages) - sendMessageStream(currSessionId, messageList) + sendMessageStream(sessionid, messageList) } /** @@ -336,7 +347,11 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor if (!isInitialized || apiKey.isEmpty() || openAI == null) { Log.e("ChatApiService", "ChatAPI服务未初始化,无法发送消息") - currSessionCallback?.onError(ChatApiException("ChatAPI服务未初始化")) + try { + getSessionCallback(sessionid)?.onError(sessionid,ChatApiException("ChatAPI服务未初始化")) + } catch (e: Exception) { + Log.e(TAG, "onError回调异常: ${e.message}", e) + } return } @@ -345,7 +360,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor toolCalls.clear() // 检查是否包含图片,决定使用哪个模型 val currentModel = if (containsImage(messages)) visionModel else model - + // Log.d(TAG, "liwei-------------------------开始AI 对话 0") currentStreamJob = launch { try { // 转换消息格式 @@ -355,22 +370,22 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor Log.e("ChatApiService", "转换消息格式失败: ${e.message}", e) throw e } - + // Log.d(TAG, "liwei-------------------------开始AI 对话 1") // 直接获取工具列表 val tools = getOpenAiTools() - + // Log.d(TAG, "liwei-------------------------开始AI 对话 2") // 构建请求 if (currentModel.isEmpty()) { Log.e("ChatApiService", "模型名称为空") throw IllegalArgumentException("模型名称不能为空") } - + // Log.d(TAG, "liwei-------------------------开始AI 对话 3") if (chatMessages.isEmpty()) { Log.e("ChatApiService", "消息列表为空") throw IllegalArgumentException("消息列表不能为空") } // Log.e("ChatApiService", "AI解析工具列表: ${tools}") - + // Log.d(TAG, "liwei-------------------------开始AI 对话 4") val chatsFlow = try { val chatCompletionRequest = ChatCompletionRequest( model = ModelId(currentModel), @@ -379,42 +394,49 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor temperature = 0.7, tools = if (tools.isNotEmpty()) tools else null ) - + // Log.d(TAG, "liwei-------------------------开始AI 对话 5") if (openAI == null) { Log.e("ChatApiService", "openAI对象为null") throw IllegalStateException("OpenAI客户端未初始化") } // Log.e("ChatApiService", "liwei---------,发送AI请求 model:${currentModel} messages:${chatMessages}") val flow = openAI!!.chatCompletions(chatCompletionRequest) + // Log.d(TAG, "liwei-------------------------开始AI 对话 6") flow } catch (e: Exception) { Log.e("ChatApiService", "创建ChatCompletionRequest或调用chatCompletions失败: ${e.message}", e) throw e } - + // Log.d(TAG, "liwei-------------------------开始AI 对话 7") chatsFlow.collect { result -> // 每次处理前检查是否仍是当前会话 if (sessionid != currSessionId) { - throw CancellationException("Session cancelled") +// throw CancellationException("Session cancelled") + return@collect } - + // Log.d(TAG, "liwei-------------------------开始AI 对话 7-1") val choice = result.choices.firstOrNull() if (choice == null) { Log.w("ChatApiService", "[Session: $sessionid] choice为null") return@collect } - + // Log.d(TAG, "liwei-------------------------开始AI 对话 7-2") val delta = choice.delta if (delta == null) { Log.w("ChatApiService", "[Session: $sessionid] delta为null") return@collect } - + // Log.d(TAG, "liwei-------------------------开始AI 对话 7-3") // 处理普通文本内容 delta.content?.let { content -> - currSessionCallback?.onToken(content) + try { + getSessionCallback(sessionid)?.onToken(sessionid,content) + } catch (e: Exception) { + Log.e(TAG, "onToken回调异常: ${e.message}", e) + // 不重新抛出异常,继续处理后续内容 + } } - + // Log.d(TAG, "liwei-------------------------开始AI 对话 7-4") // 收集工具调用信息 delta.toolCalls?.forEach { toolCall -> try { @@ -461,23 +483,32 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor } } } - + // Log.d(TAG, "liwei-------------------------开始AI 对话 8") // 完成处理 if (sessionid == currSessionId) { + // Log.d(TAG, "liwei-------------------------开始AI 对话 9") // 检查是否有工具调用需要处理 val hasToolCalls = processToolCalls(sessionid) if (!hasToolCalls) { - currSessionCallback?.onComplete() + try { + getSessionCallback(sessionid)?.onComplete(sessionid) + } catch (e: Exception) { + Log.e(TAG, "onComplete回调异常: ${e.message}", e) + } clearCurrentSession() } } - + // Log.d(TAG, "liwei-------------------------开始AI 对话 10") } catch (e: CancellationException) { // 会话被取消,不需要处理 - Log.d("ChatApiService", "Session $sessionid cancelled") + Log.d(TAG, "Session $sessionid cancelled") } catch (e: Exception) { if (sessionid == currSessionId) { - currSessionCallback?.onError(ChatApiException("流式请求失败: ${e.message}")) + try { + getSessionCallback(sessionid)?.onError(sessionid,ChatApiException("流式请求失败: ${e.message}")) + } catch (ex: Exception) { + Log.e(TAG, "onError回调异常: ${ex.message}", ex) + } clearCurrentSession() } } @@ -517,7 +548,11 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor "id" to firstToolCall.id ) // 通知上层工具调用事件 - currSessionCallback?.onFunctionCall(convertMapToJsonObject(functionCall)) + try { + getSessionCallback(sessionid)?.onFunctionCall(sessionid,convertMapToJsonObject(functionCall)) + } catch (e: Exception) { + Log.e(TAG, "onFunctionCall回调异常: ${e.message}", e) + } // 在当前协程作用域内处理工具调用,使用async确保生命周期管理 val toolCallDeferred = async { try { @@ -564,10 +599,15 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor } if (sessionid == currSessionId) { // 处理结果 - currSessionCallback?.onFunctionCallResult( - convertMapToJsonObject(functionCall), - convertMapToJsonObject(result) - ) + try { + getSessionCallback(sessionid)?.onFunctionCallResult( + sessionid, + convertMapToJsonObject(functionCall), + convertMapToJsonObject(result) + ) + } catch (e: Exception) { + Log.e(TAG, "onFunctionCallResult回调异常: ${e.message}", e) + } // 将结果发送回OpenAI继续对话 sendFunctionCallResultInternal( sessionid= sessionid, @@ -843,7 +883,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor coroutineContext[Job]?.cancelChildren() // 清理其他资源 - currSessionCallback = null + sessionCallbacks.clear() currSessionId = "" currentMessages = emptyList() toolCalls.clear() @@ -1305,4 +1345,4 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor return "Function '$functionName' called with arguments: $arguments" } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt index d04ce3bb6..cf3a96703 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt @@ -50,6 +50,7 @@ class MCPSubClient( * 连接到MCP服务器 */ suspend fun connect(): Boolean = connectionMutex.withLock { + // Log.e(TAG, "liwei---------Mcp [$serverId] 连接 1") if (isConnected) return true Log.e(TAG, "[$serverId] 开始连接mcp服务器: $serverUrl") return try { @@ -60,7 +61,7 @@ class MCPSubClient( version = "1.0.0" ) ) - + // Log.e(TAG, "liwei---------Mcp [$serverId] 连接 2") // 根据URL类型选择传输方式 val newTransport = when { serverUrl.startsWith("http://") || serverUrl.startsWith("https://") -> { @@ -83,15 +84,26 @@ class MCPSubClient( return false } } - + // Log.e(TAG, "liwei---------Mcp [$serverId] 连接 3") transport = newTransport - - // 连接到服务器 - client.connect(newTransport) - + try { + // 连接到服务器 + withTimeout(10000) { // 10秒超时 + client.connect(newTransport) + } + // Log.e(TAG, "liwei---------Mcp [$serverId] 连接 4") + } catch (e: TimeoutCancellationException) { + Log.e(TAG, "liwei---------Mcp [$serverId] 连接超时") + return false + } catch (e: Exception) { + Log.e(TAG, "liwei---------Mcp [$serverId] 连接异常: ${e.message}", e) + return false + } + // 获取可用工具列表 try { val toolsResult = client.listTools() + // Log.e(TAG, "liwei---------Mcp [$serverId] 连接 5") if (toolsResult != null) { availableTools.clear() val filtered = toolsResult.tools.filter { tool -> @@ -99,17 +111,19 @@ class MCPSubClient( } Log.w(TAG, "[$serverId] [${filterTools}] 获取工具列表: ${filtered} 原始列表:${toolsResult.tools}") availableTools.addAll(filtered) + // Log.e(TAG, "liwei---------Mcp [$serverId] 连接 6") } } catch (e: Exception) { Log.w(TAG, "[$serverId] 获取工具列表失败: ${e.message}") // 即使获取工具失败,连接也可能是成功的 } - + // Log.e(TAG, "liwei---------Mcp [$serverId] 连接 7") mcpClient = client isConnected = true retryCount = 0 currentReconnectDelay = initialReconnectDelay + // Log.e(TAG, "liwei---------Mcp [$serverId] 连接 8") // 启动心跳检测 // startHeartbeat() Log.e(TAG, "[$serverId] 连接mcp服务器成功: $serverUrl") @@ -162,11 +176,23 @@ class MCPSubClient( // 先检查连接状态 if (!checkConnection()) { Log.e(TAG, "无法连接到服务器,工具调用失败") - return null + return mapOf( + "content" to listOf(mapOf( + "type" to "text", + "text" to "Tool call failed: mcp: service not connected" + )), + "isError" to true + ) } if (!containsTool(name)) { Log.w(TAG, "此客户端不包含工具: $name") - return null + return mapOf( + "content" to listOf(mapOf( + "type" to "text", + "text" to "Tool call failed: no found tool" + )), + "isError" to true + ) } return try { // 创建工具调用请求 - 将Map转换为JsonObject 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 b807dbc03..e1d38f7e4 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 @@ -39,6 +39,9 @@ class MusicService : MediaBrowserServiceCompat() { private var currentStatus = false private var playlist = mutableListOf>() private var currentIndex = 0 + private var currentVolume: Float = 1.0f // 当前音量 + private var originalVolume: Float = 1.0f // 原始音量,用于恢复 + private var isVolumeReduced: Boolean = false // 是否已降低音量 fun isServiceRunning() = isRunning.get() fun isPlaying() = currentStatus @@ -179,6 +182,8 @@ class MusicService : MediaBrowserServiceCompat() { addAction("com.yunqiinnovation.music_service.MEDIA_CLOSE") addAction("com.yunqiinnovation.music_service.MEDIA_PREVIOUS") addAction("com.yunqiinnovation.music_service.MEDIA_NEXT") + addAction("com.yunqiinnovation.music_service.AI_SAY_START") + addAction("com.yunqiinnovation.music_service.AI_SAY_END") } // Android 14+ 需要指定 flag @@ -205,7 +210,7 @@ class MusicService : MediaBrowserServiceCompat() { val songMap = intent.getSerializableExtra("song") as? Map songMap?.let { song -> val songId = song["id"] ?: return@let - + val existingIndex = playlist.indexOfFirst { it["id"] == songId } if (existingIndex == -1) { playlist.add(song) @@ -213,7 +218,7 @@ class MusicService : MediaBrowserServiceCompat() { } else { currentIndex = existingIndex } - + play(song) } } @@ -238,6 +243,10 @@ class MusicService : MediaBrowserServiceCompat() { seekTo(position) } } + "setVolume" -> { + val volume = intent.getFloatExtra("volume", 1.0f) + setVolume(volume) + } } return START_STICKY } @@ -267,6 +276,14 @@ class MusicService : MediaBrowserServiceCompat() { Log.i(TAG, "收到 MEDIA_NEXT 广播") playNext() } + "com.yunqiinnovation.music_service.AI_SAY_START" -> { + Log.i(TAG, "收到 AI_SAY_START 广播 降低播放音量") + reduceVolume() + } + "com.yunqiinnovation.music_service.AI_SAY_END" -> { + Log.i(TAG, "收到 AI_SAY_START 广播 降低播放音量") + restoreVolume() + } // 你也可以扩展支持更多指令 } } @@ -427,19 +444,7 @@ class MusicService : MediaBrowserServiceCompat() { return PendingIntent.getBroadcast(this, action.toInt(), intent, flags) } -// fun mediaButtonPendingIntent(action: Long): PendingIntent { -// val intent = Intent(this, MusicService::class.java).apply { -// this.action = Intent.ACTION_MEDIA_BUTTON -// putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, action.toKeyCode())) -// } -// val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { -// PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE -// } else { -// PendingIntent.FLAG_UPDATE_CURRENT -// } -// return PendingIntent.getService(this, action.toInt(), intent, flags) -// } - // 转换 PlaybackStateCompat.ACTION 到 KeyEvent + fun Long.toKeyCode(): Int = when (this) { PlaybackStateCompat.ACTION_PLAY -> KeyEvent.KEYCODE_MEDIA_PLAY PlaybackStateCompat.ACTION_PAUSE -> KeyEvent.KEYCODE_MEDIA_PAUSE @@ -447,6 +452,43 @@ class MusicService : MediaBrowserServiceCompat() { PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS -> KeyEvent.KEYCODE_MEDIA_PREVIOUS else -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE } + + + /** + * 设置播放音量 + * @param volume 音量值,范围 0.0f - 1.0f + */ + private fun setVolume(volume: Float) { + val clampedVolume = volume.coerceIn(0.0f, 1.0f) + currentVolume = clampedVolume + mediaPlayer?.setVolume(clampedVolume, clampedVolume) + Log.i(TAG, "设置音量: $clampedVolume") + } + + /** + * 降低音量(AI说话时) + */ + private fun reduceVolume() { + if (!isVolumeReduced) { + originalVolume = currentVolume + val reducedVolume = (currentVolume * 0.3f).coerceIn(0.0f, 1.0f) // 降低到30% + setVolume(reducedVolume) + isVolumeReduced = true + Log.i(TAG, "AI说话开始,音量从 $originalVolume 降低到 $reducedVolume") + } + } + + /** + * 恢复原始音量(AI说话结束时) + */ + private fun restoreVolume() { + if (isVolumeReduced) { + setVolume(originalVolume) + isVolumeReduced = false + Log.i(TAG, "AI说话结束,音量恢复到 $originalVolume") + } + } + /** * 设置播放状态 */ @@ -601,3 +643,4 @@ class MusicService : MediaBrowserServiceCompat() { override fun onBind(intent: Intent?): IBinder? = null } +