From 625dcc928a5c44edda622c474e8a42955ba1e542 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Tue, 17 Jun 2025 11:45:02 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E7=90=86=E8=A7=A3=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/chat_history_service.dart | 43 ++--- .../agent/controllers/agent_controller.dart | 7 +- .../agent_service/AgentService.kt | 94 +++++----- .../agent_service/AgentServicePlugin.kt | 7 +- .../agent_service/lib/agent_service.dart | 3 +- .../yunqiinnovation/chat_api/ChatApiPlugin.kt | 64 +++---- .../chat_api/ChatApiService.kt | 162 ++++++++++++------ .../Sources/chat_api/ChatApiService.swift | 6 +- .../location_service/LocationService.kt | 2 +- 9 files changed, 229 insertions(+), 159 deletions(-) diff --git a/lib/data/services/chat_history_service.dart b/lib/data/services/chat_history_service.dart index 9a1688314..e1c017902 100644 --- a/lib/data/services/chat_history_service.dart +++ b/lib/data/services/chat_history_service.dart @@ -8,50 +8,50 @@ import 'package:chat_storage/chat_storage.dart'; class ChatHistoryService extends GetxService { // 最大历史记录数量 static const int _maxHistoryCount = 200; // 每个对话最多保存的消息数 - + @override void onInit() { super.onInit(); Logger.info('ChatHistoryService 已初始化'); } - + /// 保存聊天记录 - /// + /// /// [agentId] 代理ID /// [messages] 消息列表 Future saveHistory(String agentId, List messages) async { try { if (messages.isEmpty) return; - + // 获取最新的消息 final latestMessage = messages.last; - + // 如果是加载中的消息则跳过 if (latestMessage.isIntermediate) return; - + // 直接插入最新的消息到数据库 await ChatStorage.saveMessage( sessionId: agentId, message: latestMessage.content, sender: latestMessage.role, ); - + // 检查并清理过多的历史记录 await _cleanupOldMessages(agentId); } catch (e) { Logger.error('保存聊天记录失败', e.toString()); } } - + /// 清理旧消息,保持每个会话的消息数量不超过最大限制 Future _cleanupOldMessages(String agentId) async { try { final pagedResult = await ChatStorage.getMessages( sessionId: agentId, page: 1, - pageSize: 1, // 只获取总数信息 + pageSize: 1, // 只获取总数信息 ); - + // 如果消息总数超过最大限制,删除最早的消息 if (pagedResult.totalCount > _maxHistoryCount) { final messagesToDelete = pagedResult.totalCount - _maxHistoryCount; @@ -61,13 +61,13 @@ class ChatHistoryService extends GetxService { page: 1, pageSize: messagesToDelete, ); - + // 构建要删除的消息ID列表 final List messageIds = oldestMessages.messages .where((m) => m.id != null) .map((m) => m.id!) .toList(); - + // 删除这些消息 if (messageIds.isNotEmpty) { await ChatStorage.deleteMessages(messageIds: messageIds); @@ -77,9 +77,9 @@ class ChatHistoryService extends GetxService { Logger.error('清理旧消息失败', e); } } - + /// 加载聊天记录 - /// + /// /// [agentId] 代理ID /// 返回消息列表,如果没有找到或解析错误则返回空列表 Future> loadHistory(String agentId) async { @@ -89,29 +89,30 @@ class ChatHistoryService extends GetxService { page: 1, pageSize: _maxHistoryCount, ); - + if (pagedResult.messages.isEmpty) { return []; } - + // 将ChatStorage.ChatMessage转换为Message final messages = pagedResult.messages.map((chatMessage) { return Message( role: chatMessage.sender, content: chatMessage.message, - timestamp: DateTime.parse(chatMessage.timestamp), // 直接使用chatMessage的timestamp + timestamp: DateTime.parse( + chatMessage.timestamp), // 直接使用chatMessage的timestamp // isLoading 默认false // isLoading: metadata['isLoading'] ?? false, ); }).toList(); - + return messages; } catch (e) { Logger.error('加载聊天记录失败', e); return []; } } - + /// 清除特定代理的聊天记录 Future clearHistory(String agentId) async { try { @@ -121,7 +122,7 @@ class ChatHistoryService extends GetxService { Logger.error('清除聊天记录失败', e); } } - + /// 清除所有聊天记录 Future clearAllHistory() async { try { @@ -131,4 +132,4 @@ class ChatHistoryService extends GetxService { Logger.error('清除所有聊天记录失败', e); } } -} \ No newline at end of file +} diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index c665ff063..698a44d99 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:amap_service/amap_service.dart'; +import 'package:deep_voice/core/utils/upload_oss.dart'; +import 'package:deep_voice/data/models/user_Info.dart'; import 'package:deep_voice/data/services/location_manager.dart'; import 'package:deep_voice/data/services/music_manager.dart'; import 'package:deep_voice/data/services/navigation_manager.dart'; @@ -548,7 +550,9 @@ class AgentController extends GetxController { // 复制图片到应用永久存储目录 final permanentImagePath = await _copyImageToPermanentStorage(imagePath); - + var ImageUrl = await UploadOss.upload( + filepath: imagePath, rootDir: 'User/${User.instance.uid}/ChatImages'); + Logger.i(TAG, '图片上传: $imagePath URL: $ImageUrl'); // 显示的文本,如果没有提供则使用默认值 final displayText = text?.isNotEmpty == true ? text! : '[图片]'; @@ -578,6 +582,7 @@ class AgentController extends GetxController { // 调用Agent Service处理图片,考虑TTS状态 await AgentService.processImageInput( permanentImagePath, + ImageUrl, text: text ?? '', speakResponse: isTtsEnabled.value, ); 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 ac3f5d9eb..24a3d1e74 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 @@ -576,6 +576,7 @@ object AgentService : CoroutineScope { ) { // 创建带图片的用户消息并处理 val userMessage = createUserMessageWithImage(text, imageBase64) + Log.d(TAG, "liwei--------- 图像Mesage: ${userMessage}") // 图片描述用于存储 val displayText = text.ifEmpty { "[图片]" } processWithChatApiInternal(userMessage, displayText, speakResponse, true) @@ -599,7 +600,7 @@ object AgentService : CoroutineScope { ) { // 如果有正在进行的AI流式输出,先停止它 stopAiStream() - + stopTts() // 记录新的AI任务 currentAiJob = launch { try { @@ -621,16 +622,16 @@ object AgentService : CoroutineScope { val messagesWithSystemPrompt = JSONArray() // 先添加系统提示词 - if (systemPrompt.isNotEmpty()) { - val location = LocationService.getCachedLocation()?: mapOf( - "user_location" to "定位未开启", - "user_lat" to "定位未开启", - "user_lng" to "定位未开启", - ) - val _systemPrompt = Utils.fillTemplate(systemPrompt,location) - messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt)) - Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}") - } + if (systemPrompt.isNotEmpty()) { + val location = LocationService.getCachedLocation()?: mapOf( + "user_location" to "定位未开启", + "user_lat" to "定位未开启", + "user_lng" to "定位未开启", + ) + val _systemPrompt = Utils.fillTemplate(systemPrompt,location) + messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt)) + Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}") + } // 再添加历史消息 for (i in 0 until historyMessages.length()) { @@ -665,7 +666,7 @@ object AgentService : CoroutineScope { // 兜底处理,直接添加原消息 addToHistoryMessages(userMessage) } - +// Log.e("ChatApiService", "liwei---------sendMessageStream messages:${messagesWithSystemPrompt}") chatApiService.sendMessageStream( messagesWithSystemPrompt, object : StreamCallback { @@ -684,25 +685,21 @@ object AgentService : CoroutineScope { 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()) { - // 发送完整回复,包含是否有图片的标记 - val responseData = mutableMapOf( - "text" to response, - "userInput" to displayText - ) - if (hasImage) { - responseData["hasImage"] = true - } - sendEvent("assistant_response", responseData) - // 添加AI回复到历史记录 addToHistoryMessages(createAssistantMessage(response)) - - // 保存聊天记录 - saveChatMessage(displayText, response,aiMetadata,userMetadata.toString()) } - + // 保存聊天记录 + saveChatMessage(displayText, response,aiMetadata,userMetadata.toString()) // 标记AI流式输出已完成 isAiStreaming = false currentAiJob = null @@ -861,14 +858,15 @@ object AgentService : CoroutineScope { ) if (userMessageId != -1L) { - // 保存AI回复 - val assistantMessageId = chatStorageHelper.saveMessage( - sessionId = sessionId, - message = assistantMessage, - sender = "assistant", - metadata = aiMetadata - ) - + if (assistantMessage.isNotEmpty() ) { + // 保存AI回复 + val assistantMessageId = chatStorageHelper.saveMessage( + sessionId = sessionId, + message = assistantMessage, + sender = "assistant", + metadata = aiMetadata + ) + } // if (assistantMessageId != -1L) { // Log.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId") // } else { @@ -970,7 +968,7 @@ object AgentService : CoroutineScope { /** * 创建带图片的用户消息 */ - private fun createUserMessageWithImage(text: String, imageBase64: String): JSONObject { + private fun createUserMessageWithImage(text: String, imageUrl: String): JSONObject { val content = JSONArray().apply { if (text.isNotEmpty()) { put(JSONObject().apply { @@ -981,8 +979,8 @@ object AgentService : CoroutineScope { put(JSONObject().apply { put("type", "image_url") put("image_url", JSONObject().apply { - put("url", "data:image/jpeg;base64,$imageBase64") - put("detail", "auto") +// put("url", "data:image/jpeg;base64,$imageBase64") + put("url", imageUrl) }) }) } @@ -996,7 +994,7 @@ object AgentService : CoroutineScope { /** * 处理图片输入 */ - fun processImageInput(imagePath: String, text: String = "", speakResponse: Boolean = false): Boolean { + fun processImageInput(imagePath: String, imageUrl: String,text: String = "", speakResponse: Boolean = false): Boolean { if (!isInitialized) { Log.e(TAG, "服务未初始化") sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) @@ -1019,16 +1017,16 @@ object AgentService : CoroutineScope { launch { try { // 将图片转换为Base64格式 - val imageBase64 = chatApiService.fileToBase64(imagePath) ?: run { - sendEvent("error", mapOf( - "code" to "IMAGE_CONVERSION_FAILED", - "message" to "图片转换失败" - )) - return@launch - } + // val imageBase64 = chatApiService.fileToBase64(imagePath) ?: run { + // sendEvent("error", mapOf( + // "code" to "IMAGE_CONVERSION_FAILED", + // "message" to "图片转换失败" + // )) + // return@launch + // } // 创建带图片的用户消息并处理 - val userMessage = createUserMessageWithImage(text, imageBase64) + val userMessage = createUserMessageWithImage(text, imageUrl) // 图片描述用于存储 val displayText = text.ifEmpty { "[图片]" } @@ -1037,7 +1035,7 @@ object AgentService : CoroutineScope { "status" to "ready", "imagePath" to imagePath )) - + Log.d(TAG, "liwei--------- 图像Mesage: ${userMessage}") // 处理消息并传入存储路径 processWithChatApiInternal(userMessage, displayText, speakResponse, true, imagePath) } catch (e: Exception) { diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index cf00c7f3c..06b8a29b5 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -130,13 +130,18 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.error("INVALID_ARGUMENTS", "图片路径不能为空", null) return } - + val imageUrl = call.argument("imageUrl") + if (imageUrl.isNullOrEmpty()) { + result.error("INVALID_ARGUMENTS", "图片Url不能为空", null) + return + } val text = call.argument("text") ?: "" val speakResponse = call.argument("speakResponse") ?: false try { val isProcessing = AgentService.processImageInput( imagePath = imagePath, + imageUrl = imageUrl, text = text, speakResponse = speakResponse ) diff --git a/local_plugins/agent_service/lib/agent_service.dart b/local_plugins/agent_service/lib/agent_service.dart index 2c53859c1..4ee6907cb 100644 --- a/local_plugins/agent_service/lib/agent_service.dart +++ b/local_plugins/agent_service/lib/agent_service.dart @@ -326,11 +326,12 @@ class AgentService { /// [text] 可选的文本描述或问题 /// [speakResponse] 是否朗读响应 /// 返回是否成功处理图片 - static Future processImageInput(String imagePath, + static Future processImageInput(String imagePath, String imageUrl, {String text = "", bool speakResponse = false}) async { try { final bool result = await _channel.invokeMethod('processImageInput', { 'imagePath': imagePath, + 'imageUrl': imageUrl, 'text': text, 'speakResponse': speakResponse, }); diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt index c369deac9..080390e55 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt @@ -37,30 +37,30 @@ class ChatApiPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandl chatApiService = ChatApiService(flutterPluginBinding.applicationContext) // 设置流式回调 - chatApiService?.setStreamCallback(object : StreamCallback { - override fun onToken(token: String) { - channel.invokeMethod("onToken", token) - } - - override fun onComplete() { - channel.invokeMethod("onComplete", null) - } - - override fun onError(error: Exception) { - channel.invokeMethod("onError", error.message) - } - - override fun onFunctionCall(functionCall: org.json.JSONObject) { - channel.invokeMethod("onFunctionCall", functionCall.toString()) - } - - override fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject) { - channel.invokeMethod("onFunctionCallResult", mapOf( - "functionCall" to functionCall.toString(), - "functionCallResult" to functionCallResult.toString() - )) - } - }) +// chatApiService?.setStreamCallback(object : StreamCallback { +// override fun onToken(token: String) { +// channel.invokeMethod("onToken", token) +// } +// +// override fun onComplete() { +// channel.invokeMethod("onComplete", null) +// } +// +// override fun onError(error: Exception) { +// channel.invokeMethod("onError", error.message) +// } +// +// override fun onFunctionCall(functionCall: org.json.JSONObject) { +// channel.invokeMethod("onFunctionCall", functionCall.toString()) +// } +// +// override fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject) { +// channel.invokeMethod("onFunctionCallResult", mapOf( +// "functionCall" to functionCall.toString(), +// "functionCallResult" to functionCallResult.toString() +// )) +// } +// }) } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { @@ -84,14 +84,14 @@ class ChatApiPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandl val success = chatApiService?.initialize(apiKey, baseUrl, model, mcpServer) ?: false result.success(success) } - "chatCompletionStream" -> { - val messages = call.argument>>("messages") ?: emptyList() - val tool = call.argument("tool") ?: false - pluginScope.launch { - chatApiService?.chatCompletionStream(messages, tool) - } - result.success(true) - } + // "chatCompletionStream" -> { + // val messages = call.argument>>("messages") ?: emptyList() + // val tool = call.argument("tool") ?: false + // pluginScope.launch { + // chatApiService?.chatCompletionStream(messages, tool) + // } + // result.success(true) + // } "cancelChatStream" -> { chatApiService?.cancelChatStream() result.success(true) 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 bad3aa1a2..9e505f8ec 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 @@ -23,6 +23,7 @@ import android.util.Log import org.json.JSONObject import android.os.Handler import android.os.Looper +import java.util.UUID /** * ChatAPI服务异常 @@ -109,10 +110,12 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor // 流式请求相关 private var currentStreamJob: Job? = null - private var streamCallback: StreamCallback? = null +// private var streamCallback: StreamCallback? = null private var currentMessages: List> = emptyList() private var toolCalls: MutableMap = mutableMapOf() - private var isCanceled = false + private var sessions: MutableMap = mutableMapOf() + private var currSessionId = "" // +// private var isCanceled = false @@ -124,9 +127,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor /** * 设置流式回调 */ - fun setStreamCallback(callback: StreamCallback) { - streamCallback = callback - } +// fun setStreamCallback(callback: StreamCallback) { +// streamCallback = callback +// } /** * 初始化ChatAPI服务 @@ -258,32 +261,33 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor */ fun sendMessageStream(messages: org.json.JSONArray, callback: StreamCallback) { // 设置回调 - setStreamCallback(callback) +// setStreamCallback(callback) // 转换JSONArray为List> val messageList = convertJsonArrayToList(messages) - + currSessionId = UUID.randomUUID().toString() + sessions[currSessionId] = callback // 调用原始方法 - sendMessageStream(messageList) + sendMessageStream(currSessionId,messageList) } /** * 发送消息(流式输出) */ - fun sendMessageStream(messages: List>) { + fun sendMessageStream(sessionid:String,messages: List>) { if (!isInitialized || apiKey.isEmpty() || openAI == null) { Log.e("ChatApiService", "ChatAPI服务未初始化,无法发送消息") - streamCallback?.onError(ChatApiException("ChatAPI服务未初始化")) + sessions[sessionid]?.onError(ChatApiException("ChatAPI服务未初始化")) return } // 重置状态 - isCanceled = false +// isCanceled = false currentMessages = messages toolCalls.clear() - + val currentModel = model // 检查是否包含图片,决定使用哪个模型 - val currentModel = if (containsImage(messages)) visionModel else model +// val currentModel = if (containsImage(messages)) visionModel else model currentStreamJob = launch { try { @@ -323,7 +327,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor Log.e("ChatApiService", "openAI对象为null") throw IllegalStateException("OpenAI客户端未初始化") } - +// Log.e("ChatApiService", "liwei---------,发送AI请求 model:${currentModel} messages:${chatMessages}") val flow = openAI!!.chatCompletions(chatCompletionRequest) flow } catch (e: Exception) { @@ -332,7 +336,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor } chatsFlow.collect { result -> - if (isCanceled) return@collect + if (currSessionId != sessionid) { + return@collect + } val choice = result.choices.firstOrNull() if (choice == null) { @@ -348,7 +354,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor // 处理普通文本内容 delta.content?.let { content -> - streamCallback?.onToken(content) + sessions[sessionid]?.onToken(content) } // 收集工具调用信息 @@ -389,18 +395,20 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor } } - if (!isCanceled) { + if (currSessionId == sessionid) { // 检查是否有工具调用需要处理 - val hasToolCalls = processToolCalls() + val hasToolCalls = processToolCalls(sessionid) if (!hasToolCalls) { - streamCallback?.onComplete() + sessions[sessionid]?.onComplete() } + }else{ + sessions[sessionid]?.onComplete() } } catch (e: Exception) { - if (!isCanceled) { - streamCallback?.onError(ChatApiException("流式请求失败: ${e.message}")) - } +// if (currSessionId == sessionid) { + sessions[sessionid]?.onError(ChatApiException("流式请求失败: ${e.message}")) +// } } } } @@ -408,7 +416,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor /** * 处理工具调用 */ - private suspend fun processToolCalls(): Boolean { + private suspend fun processToolCalls(sessionid: String): Boolean { val firstToolCall = try { toolCalls.values.firstOrNull { it.isValid() } } catch (e: Exception) { @@ -430,11 +438,11 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor return false } // 通知上层工具调用事件 - streamCallback?.onFunctionCall(convertMapToJsonObject(functionCall)) + sessions[sessionid]?.onFunctionCall(convertMapToJsonObject(functionCall)) // 在后台队列处理工具调用 launch { try { - if (!isCanceled) { + if (currSessionId == sessionid) { // 通过MCP客户端处理工具调用 val functionName = firstToolCall.name val argumentsJson = firstToolCall.arguments @@ -471,29 +479,35 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor // 工具不存在 mapOf("context" to "Tool not found: $functionName") } - if (!isCanceled) { + if (currSessionId == sessionid) { // 处理结果 - streamCallback?.onFunctionCallResult( + sessions[sessionid]?.onFunctionCallResult( convertMapToJsonObject(functionCall), convertMapToJsonObject(result) ) // 将结果发送回OpenAI继续对话 sendFunctionCallResultInternal( + sessionid= sessionid, messages = currentMessages, functionCall = functionCall, functionResult = gson.toJson(result) ) + }else{ + sessions[sessionid]?.onComplete() } } } catch (e: Exception) { Log.e("ChatApiService", "工具调用处理过程中出错: ${e.message}", e) - if (!isCanceled) { + if (currSessionId == sessionid) { val errorMessage = "工具调用处理失败: ${e.message}" sendFunctionCallResultInternal( + sessionid= sessionid, messages = currentMessages, functionCall = functionCall, functionResult = errorMessage ) + }else{ + sessions[sessionid]?.onComplete() } } } @@ -505,11 +519,12 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor * 发送函数调用结果(内部方法) */ private suspend fun sendFunctionCallResultInternal( + sessionid: String, messages: List>, functionCall: Map, functionResult: String ) { - if (isCanceled) return + if (currSessionId != sessionid) return val fullMessages = messages.toMutableList() @@ -542,14 +557,16 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor ) // 重新发起对话,让AI基于工具调用结果生成回复 - sendMessageStream(fullMessages) + sendMessageStream(sessionid,fullMessages) } /** * 取消当前流式请求 */ fun cancelCurrentStream(): Boolean { - isCanceled = true + sessions[currSessionId]?.onComplete() + sessions.remove(currSessionId) + currSessionId = "" currentStreamJob?.cancel() currentStreamJob = null return true @@ -778,9 +795,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor * 聊天完成流式接口 * 与iOS版本保持一致的接口 */ - fun chatCompletionStream(messages: List>, tool: Boolean = false) { + fun chatCompletionStream(sessionid: String,messages: List>, tool: Boolean = false) { // 直接调用sendMessageStream,因为该方法已经处理了工具调用 - sendMessageStream(messages) + sendMessageStream(sessionid,messages) } // MARK: - 工具方法 @@ -893,27 +910,70 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor } is List<*> -> { // 对于多模态内容,提取文本部分(简化处理) - var textContent = "" + val contentParts = mutableListOf() content.forEach { item -> if (item is Map<*, *>) { val type = item["type"] as? String - if (type == "text") { - val text = item["text"] as? String ?: "" - textContent += text + when (type) { + "text" -> { + val text = item["text"] as? String ?: "" + contentParts.add(TextPart(text)) + } + "image_url" -> { + val imageUrlMap = item["image_url"] as? Map<*, *> + val url = imageUrlMap?.get("url") as? String ?: "" + val detail = imageUrlMap?.get("detail") as? String ?: "auto" + if (url.isNotEmpty()) { + contentParts.add(ImagePart(url, detail)) + } + } // 处理其他可能的内容类型 + else -> { + // 记录未知类型,便于调试 + Log.d("MessageParser", "Unknown content type: $type") + } } + } else { + // 非Map类型的内容,记录日志 + Log.d("MessageParser", "Item is not a Map: $item") } } - - val finalContent = if (textContent.isEmpty()) "多媒体内容" else textContent - - val chatMessage = when (chatRole) { - Role.System -> ChatMessage.System(finalContent) - Role.User -> ChatMessage.User(finalContent) - Role.Assistant -> ChatMessage.Assistant(finalContent) - else -> ChatMessage.User(finalContent) + + // 创建消息对象 + if (contentParts.isNotEmpty()) { + val chatMessage = when (chatRole) { + Role.System -> { + // 系统消息简化显示,将图片替换为[图片] + val displayText = contentParts.joinToString { part -> + when (part) { + is TextPart -> part.text // 使用TextPart + is ImagePart -> "[图片]" // 使用ImagePart + } + } + ChatMessage.System(displayText) + } + Role.User -> ChatMessage.User(contentParts) + Role.Assistant -> { + // 将多模态内容转换为ListContent + val listContent = ListContent(contentParts) + // 提取文本内容作为Assistant消息的content + val textContent = contentParts + .filterIsInstance() + .joinToString(separator = "") { it.text } + + // 调用Assistant函数,传递文本内容 + ChatMessage.Assistant( + content = textContent.ifEmpty { null }, + toolCalls = null // 如果有工具调用,需要在这里添加 + ) + } + else -> ChatMessage.User(contentParts) + } + chatMessages.add(chatMessage) + } else { + // 内容为空时的处理 + Log.d("MessageParser", "No valid content parts found") } - chatMessages.add(chatMessage) } } } @@ -1035,13 +1095,13 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor */ private fun convertJsonArrayToList(jsonArray: org.json.JSONArray): List> { val result = mutableListOf>() - +// Log.e("ChatApiService", "convertJsonArrayToList 1:${jsonArray}") for (i in 0 until jsonArray.length()) { val jsonObject = jsonArray.optJSONObject(i) ?: continue val map = convertJsonObjectToMap(jsonObject) result.add(map) } - +// Log.e("ChatApiService", "convertJsonArrayToList 2:${result}") return result } @@ -1123,9 +1183,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor } // 通知Flutter端处理函数调用 - mainHandler.post { - streamCallback?.onFunctionCall(functionCall) - } +// mainHandler.post { +// streamCallback?.onFunctionCall(functionCall) +// } // 返回一个标记,表示函数已被调用 return "Function '$functionName' called with arguments: $arguments" diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift index c5638966d..cf1c6a3a0 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift @@ -184,9 +184,9 @@ public class ChatApiService: NSObject { // 检查是否包含图片,决定使用哪个模型 var currentModel = model - if containsImage(in: messages) { - currentModel = visionModel - } + // if containsImage(in: messages) { + // currentModel = visionModel + // } // 转换消息格式 let chatMessages = try convertToChatMessages(messages) diff --git a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt index e36eaa569..937828580 100644 --- a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt +++ b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt @@ -33,7 +33,7 @@ object LocationService { locationClient?.setLocationListener { location -> if (location != null && location.errorCode == 0) { currentLocation = location - Log.d(TAG, "liwei---------定位更新: ${location.latitude}, ${location.longitude}") + // Log.d(TAG, "liwei---------定位更新: ${location.latitude}, ${location.longitude}") } else { Log.w(TAG, "liwei---------定位失败: ${location?.errorCode} - ${location?.errorInfo}") } From fd3e363f11894033cfb89ad6b62696088c66b2a6 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Tue, 17 Jun 2025 12:15:52 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=A4=9A=E5=AA=92?= =?UTF-8?q?=E4=BD=93=E5=A4=A7=E6=A8=A1=E5=9E=8B=E9=85=8D=E7=BD=AE=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/ble_manager.dart | 3 +++ .../com/yunqiinnovation/agent_service/AgentService.kt | 1 + .../yunqiinnovation/agent_service/AgentServicePlugin.kt | 1 + .../com/yunqiinnovation/agent_service/BleMediaService.kt | 3 +++ .../kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt | 3 ++- .../com/yunqiinnovation/chat_api/ChatApiService.kt | 9 +++++---- 6 files changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 4805bb03f..aaffa8ffc 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -501,6 +501,8 @@ class BleManager extends GetxService { final openaiBaseUrl = AppConfig.env('OPENAI_BASE_URL') ?? 'https://api.openai.com/v1'; final openaiModel = AppConfig.env('OPENAI_MODEL') ?? 'gpt-3.5-turbo'; + final openaiModelMedia = AppConfig.env('OPENAI_API_VISION_MODEL') ?? + 'doubao-1-5-vision-pro-32k-250115'; final openaiagentid = AppConfig.env('OPENAI_AGENT_ID') ?? 'personal_assistant'; final systemPrompt = AppConfig.agentsystemPrompt(openaiagentid); @@ -515,6 +517,7 @@ class BleManager extends GetxService { 'openaiApiKey': openaiApiKey, 'openaiBaseUrl': openaiBaseUrl, 'openaiModel': openaiModel, + 'openaivisionModel': openaiModelMedia, 'systemPrompt': systemPrompt, 'mcpServer': jsonEncode(jsonData), 'volcanoAppId': '2316081933', 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 24a3d1e74..c5a3b18a3 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 @@ -153,6 +153,7 @@ object AgentService : CoroutineScope { config["openaiApiKey"]?.toString() ?: "", config["openaiBaseUrl"]?.toString() ?: "", config["openaiModel"]?.toString() ?: "", + config["openaivisionModel"]?.toString() ?: "", config["mcpServer"]?.toString() ?: "" ) diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index 06b8a29b5..64353c834 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -57,6 +57,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream config["openaiApiKey"]?.toString()?.let { intent.putExtra("openaiApiKey", it) } config["openaiBaseUrl"]?.toString()?.let { intent.putExtra("openaiBaseUrl", it) } config["openaiModel"]?.toString()?.let { intent.putExtra("openaiModel", it) } + config["openaivisionModel"]?.toString()?.let { intent.putExtra("openaivisionModel", it) } config["systemPrompt"]?.toString()?.let { intent.putExtra("systemPrompt", it) } config["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", it) } config["volcanoAppId"]?.toString()?.let { intent.putExtra("volcanoAppId", it) } diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt index e7452eb45..ccaba0407 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt @@ -43,6 +43,7 @@ class BleMediaService : Service(), CoroutineScope { private const val KEY_OPENAI_API_KEY = "openaiApiKey" private const val KEY_OPENAI_BASE_URL = "openaiBaseUrl" private const val KEY_OPENAI_MODEL = "openaiModel" + private const val KEY_OPENAI_VISION_MODEL = "openaivisionModel" private const val KEY_SYSTEM_PROMPT = "systemPrompt" private const val KEY_MCP_SERVER = "mcpServer" private const val KEY_VOLCANO_APP_ID = "volcanoAppId" @@ -165,6 +166,7 @@ class BleMediaService : Service(), CoroutineScope { it.getStringExtra(KEY_OPENAI_API_KEY)?.let { value -> config[KEY_OPENAI_API_KEY] = value } it.getStringExtra(KEY_OPENAI_BASE_URL)?.let { value -> config[KEY_OPENAI_BASE_URL] = value } it.getStringExtra(KEY_OPENAI_MODEL)?.let { value -> config[KEY_OPENAI_MODEL] = value } + it.getStringExtra(KEY_OPENAI_VISION_MODEL)?.let { value -> config[KEY_OPENAI_VISION_MODEL] = value } it.getStringExtra(KEY_SYSTEM_PROMPT)?.let { value -> config[KEY_SYSTEM_PROMPT] = value } it.getStringExtra(KEY_MCP_SERVER)?.let { value -> config[KEY_MCP_SERVER] = value } it.getStringExtra(KEY_VOLCANO_APP_ID)?.let { value -> config[KEY_VOLCANO_APP_ID] = value } @@ -198,6 +200,7 @@ class BleMediaService : Service(), CoroutineScope { KEY_OPENAI_API_KEY to getConfigString(prefs, KEY_OPENAI_API_KEY, ""), KEY_OPENAI_BASE_URL to getConfigString(prefs, KEY_OPENAI_BASE_URL, "https://api.openai.com/v1"), KEY_OPENAI_MODEL to getConfigString(prefs, KEY_OPENAI_MODEL, "gpt-3.5-turbo"), + KEY_OPENAI_VISION_MODEL to getConfigString(prefs, KEY_OPENAI_VISION_MODEL, "gpt-4-vision-preview"), KEY_SYSTEM_PROMPT to getConfigString(prefs, KEY_SYSTEM_PROMPT, "gpt-3.5-turbo"), KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, ""), KEY_VOLCANO_APP_ID to getConfigString(prefs, KEY_VOLCANO_APP_ID, ""), diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt index 080390e55..055d37757 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt @@ -80,8 +80,9 @@ class ChatApiPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandl val apiKey = call.argument("apiKey") ?: "" val baseUrl = call.argument("baseUrl") ?: "" val model = call.argument("model") ?: "" + val modelmedia = call.argument("modelmedia") ?: "" val mcpServer = call.argument("mcpServer") ?: "" - val success = chatApiService?.initialize(apiKey, baseUrl, model, mcpServer) ?: false + val success = chatApiService?.initialize(apiKey, baseUrl, model,modelmedia, mcpServer) ?: false result.success(success) } // "chatCompletionStream" -> { 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 9e505f8ec..036ad2782 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 @@ -134,7 +134,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor /** * 初始化ChatAPI服务 */ - fun initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String): Boolean { + fun initialize(apiKey: String, baseUrl: String, model: String, visionModel: String, mcpServer: String): Boolean { this.apiKey = apiKey if (baseUrl.isNotEmpty()) { this.baseUrl = baseUrl @@ -142,7 +142,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor if (model.isNotEmpty()) { this.model = model } - + if (visionModel.isNotEmpty()) { + this.visionModel = visionModel + } // 处理 baseUrl:移除末尾的 /chat/completions(如果存在) // 因为 openai-kotlin 会自动拼接 /chat/completions var processedBaseUrl = if (this.baseUrl.endsWith("/chat/completions")) { @@ -285,9 +287,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor // isCanceled = false currentMessages = messages toolCalls.clear() - val currentModel = model // 检查是否包含图片,决定使用哪个模型 -// val currentModel = if (containsImage(messages)) visionModel else model + val currentModel = if (containsImage(messages)) visionModel else model currentStreamJob = launch { try { From d37d893d46405aae9ebf50b114855f70f172b7e2 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Tue, 17 Jun 2025 15:20:32 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E8=AF=AD=E9=9F=B3?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E5=AF=BC=E8=88=AA=E5=92=8C=E9=9F=B3=E4=B9=90?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../amap_service/AmapServicePlugin.kt | 8 +++ .../amap_service/NavigationActivity.kt | 37 ++++++++++++- .../amap_service/NavigationService.kt | 40 +++++++++++++- .../chat_api/SystemFunctionHandler.kt | 53 ++++++++++++++++++- .../music_service/MusicService.kt | 5 ++ 5 files changed, 137 insertions(+), 6 deletions(-) diff --git a/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/AmapServicePlugin.kt b/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/AmapServicePlugin.kt index e28e7a484..e9f9d08d6 100644 --- a/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/AmapServicePlugin.kt +++ b/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/AmapServicePlugin.kt @@ -1,7 +1,13 @@ package com.yunqiinnovation.amap_service import android.app.Activity +import android.content.BroadcastReceiver import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.util.Log +import androidx.core.content.ContextCompat.registerReceiver import com.amap.api.navi.AMapNaviView import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.EventChannel @@ -98,4 +104,6 @@ class AmapServicePlugin: FlutterPlugin, MethodCallHandler,EventChannel.StreamHan } + + } diff --git a/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationActivity.kt b/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationActivity.kt index 9009ec725..c900c32d5 100644 --- a/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationActivity.kt +++ b/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationActivity.kt @@ -1,6 +1,10 @@ package com.yunqiinnovation.amap_service +import android.content.BroadcastReceiver import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity @@ -23,7 +27,7 @@ import com.amap.api.navi.model.AimLessModeCongestionInfo import com.amap.api.navi.model.AimLessModeStat import com.amap.api.navi.model.NaviInfo import com.amap.api.navi.model.NaviLatLng -import com.yunqiinnovation.amap_service.NavigationService.Companion.sharedNavi + class NavigationActivity : AppCompatActivity(), AMapNaviListener, AMapNaviViewListener { private lateinit var naviView: AMapNaviView @@ -32,6 +36,7 @@ class NavigationActivity : AppCompatActivity(), AMapNaviListener, AMapNaviViewLi private var startpos:String = ""; private var endpos:String = ""; private var ntype:String = ""; + private var isReceiverRegistered = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_navigation) @@ -60,7 +65,31 @@ class NavigationActivity : AppCompatActivity(), AMapNaviListener, AMapNaviViewLi } navi?.addAMapNaviListener(this); - // 绑定当前 activity 到导航状态 + + // 注册广播监听器 + val filter = IntentFilter().apply { + addAction("com.yunqiinnovation.amap_service.EXIT_NAVIGATION") + } + + // Android 14+ 需要指定 flag + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(controlReceiver, filter, Context.RECEIVER_NOT_EXPORTED) + } else { + registerReceiver(controlReceiver, filter) + } + isReceiverRegistered = true + } + + private val controlReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + Log.i("NavigationActivity", "接受到通知事件 ${intent?.action}") + when (intent?.action) { + "com.yunqiinnovation.amap_service.EXIT_NAVIGATION" -> { + Log.d("NavigationService", "收到退出导航广播,准备停止服务") + finish() + } + } + } } override fun onResume() { @@ -80,6 +109,10 @@ class NavigationActivity : AppCompatActivity(), AMapNaviListener, AMapNaviViewLi NavigationServiceHelper.startNavigation(context,"stop") naviView.onDestroy() super.onDestroy() + if (isReceiverRegistered) { + unregisterReceiver(controlReceiver) + isReceiverRegistered = false + } Log.i("NavigationActivity", "liwei------------------- onDestroy!") } diff --git a/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt b/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt index dceab4f76..c926010b0 100644 --- a/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt +++ b/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt @@ -3,8 +3,10 @@ package com.yunqiinnovation.amap_service import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.graphics.Bitmap import android.os.Build import android.os.IBinder @@ -52,11 +54,24 @@ class NavigationService: Service(),AMapNaviListener { fun isServiceRunning() = isRunning.get() } - + private var isReceiverRegistered = false override fun onCreate() { super.onCreate() instance = this startForegroundNotification() + + // 注册广播监听器 + val filter = IntentFilter().apply { + addAction("com.yunqiinnovation.amap_service.EXIT_NAVIGATION") + } + + // Android 14+ 需要指定 flag + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(controlReceiver, filter, Context.RECEIVER_NOT_EXPORTED) + } else { + registerReceiver(controlReceiver, filter) + } + isReceiverRegistered = false } override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { @@ -84,7 +99,17 @@ class NavigationService: Service(),AMapNaviListener { } return START_STICKY } - + private val controlReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + Log.i(TAG, "接受到通知事件 ${intent?.action}") + when (intent?.action) { + "com.yunqiinnovation.amap_service.EXIT_NAVIGATION" -> { + Log.d("NavigationService", "收到退出导航广播,准备停止服务") + stopNavigation(); + } + } + } + } private fun initializeAMapNavi() { try { sharedNavi = AMapNavi.getInstance(applicationContext).apply { @@ -399,4 +424,15 @@ class NavigationService: Service(),AMapNaviListener { Log.e(TAG, "onGpsSignalWeak! $p0") // TODO("Not yet implemented") } + + + override fun onDestroy() { + if (isReceiverRegistered) { + unregisterReceiver(controlReceiver) + isReceiverRegistered = false + } + stopForeground(true) + isRunning.set(false) + super.onDestroy() + } } \ No newline at end of file diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt index 794b66547..672c29634 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt @@ -163,7 +163,17 @@ class SystemFunctionHandler(private val context: Context? = null) { ), handler = MediaPauseHandler(context) ) - + // 注册媒体暂停功能 + client.registerLocalFunction( + name = "media_close", + description = "关闭媒体播放", + parameters = mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + handler = MediaCloseHandler(context) + ) // 注册媒体上一首功能 client.registerLocalFunction( name = "media_previous", @@ -199,6 +209,32 @@ class SystemFunctionHandler(private val context: Context? = null) { ), handler = OpenRecorderHandler(context) ) + + // 注册退出交互函数 + client.registerLocalFunction( + name = "exit_navigation", + description = "结束当前导航服务", + parameters = mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + handler = ExitNavigationHandler(context) + ) + } +} + +/** + * 退出导航服务 + */ +private class ExitNavigationHandler(private val context: Context?) : FunctionHandler { + override suspend fun handle(arguments: Map): String { + // 发送广播通知退出交互 + val intent = Intent("com.yunqiinnovation.amap_service.EXIT_NAVIGATION").apply { + setPackage(context?.packageName) // 设置包名使其成为显式 Intent + } + context?.sendBroadcast(intent) + return "{\"result\": \"已结束当前导航服务\"}" } } @@ -417,7 +453,20 @@ private class MediaPauseHandler(private val context: Context?) : FunctionHandler return "{\"result\": \"已暂停媒体播放\"}" } } - +/** + * 媒体暂停处理器 + */ +private class MediaCloseHandler(private val context: Context?) : FunctionHandler { + override suspend fun handle(arguments: Map): String { + Log.d("MediaPauseHandler", "📤 正在发送广播 context = $context") + // 发送媒体暂停广播 + val intent = Intent("com.yunqiinnovation.music_service.MEDIA_CLOSE").apply { + setPackage(context?.packageName) // 设置包名使其成为显式 Intent + } + context?.sendBroadcast(intent) + return "{\"result\": \"已关闭媒体播放\"}" + } +} /** * 媒体上一首处理器 */ 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 d346baed0..dfe71e4c2 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 @@ -60,6 +60,7 @@ class MusicService : MediaBrowserServiceCompat() { val filter = IntentFilter().apply { addAction("com.yunqiinnovation.music_service.MEDIA_PLAY") addAction("com.yunqiinnovation.music_service.MEDIA_PAUSE") + addAction("com.yunqiinnovation.music_service.MEDIA_CLOSE") addAction("com.yunqiinnovation.music_service.MEDIA_PREVIOUS") addAction("com.yunqiinnovation.music_service.MEDIA_NEXT") } @@ -132,6 +133,10 @@ class MusicService : MediaBrowserServiceCompat() { Log.i(TAG, "收到 MEDIA_PLAY 广播") resume() } + "com.yunqiinnovation.music_service.MEDIA_CLOSE" -> { + Log.i(TAG, "收到 MEDIA_CLOSE 广播") + stopSelf() + } "com.yunqiinnovation.music_service.MEDIA_PREVIOUS" -> { Log.i(TAG, "收到 MEDIA_PREVIOUS 广播") playPrevious() From a1f1bd8019843c3eb0dbfd3998748c9fa457b63d Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Tue, 17 Jun 2025 16:01:24 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=92=AD=E6=8A=A5?= =?UTF-8?q?=E7=89=B9=E6=AE=8A=E7=AC=A6=E5=8F=B7=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt | 2 ++ .../com/yunqiinnovation/music_service/MusicServiceStarter.kt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt index d750fae41..62ac83d49 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt @@ -395,6 +395,8 @@ class AzureTtsHelper(private val context: Context) : ITtsService { .replace("&", "&") .replace("<", "<") .replace(">", ">") + .replace("#", ">") + .replace("*", ">") .replace(Regex("[😀-🟿]+"), "") // 简化表情符号移除 .trim() diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt index b46fa3230..4ea229c4e 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt @@ -34,7 +34,7 @@ object MusicServiceStarter { Log.e("MusicServiceStarter", "liwei------------------- Failed to start service", e) false } - Log.d(" MusicServiceStarter", "liwei------------------- Service start success: $success") + Log.d(" MusicServiceStarter", "Service start success: $success") } //启动播放列表 From 4603486a4df415dd781129ddd9bccced1694079d Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Tue, 17 Jun 2025 16:03:11 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AE=9A=E4=BD=8D?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E9=A2=91=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../location_service/LocationService.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt index 937828580..5a2319917 100644 --- a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt +++ b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt @@ -15,21 +15,21 @@ object LocationService { fun initialize(context: Context) { if (initialized) return - Log.e(TAG, "liwei--------- 初始化高德定位服务 1") +// Log.e(TAG, "liwei--------- 初始化高德定位服务 1") try { AMapLocationClient.updatePrivacyShow(context, true, true) AMapLocationClient.updatePrivacyAgree(context, true) - Log.e(TAG, "liwei--------- 初始化高德定位服务 2") +// Log.e(TAG, "liwei--------- 初始化高德定位服务 2") locationClient = AMapLocationClient(context.applicationContext) val option = AMapLocationClientOption().apply { locationMode = AMapLocationClientOption.AMapLocationMode.Hight_Accuracy isOnceLocation = false - interval = 5000L + interval = 30000L } - Log.e(TAG, "liwei--------- 初始化高德定位服务 3") +// Log.e(TAG, "liwei--------- 初始化高德定位服务 3") locationClient?.setLocationOption(option) - Log.e(TAG, "liwei--------- 初始化高德定位服务 4") +// Log.e(TAG, "liwei--------- 初始化高德定位服务 4") locationClient?.setLocationListener { location -> if (location != null && location.errorCode == 0) { currentLocation = location @@ -38,9 +38,9 @@ object LocationService { Log.w(TAG, "liwei---------定位失败: ${location?.errorCode} - ${location?.errorInfo}") } } - Log.e(TAG, "liwei--------- 初始化高德定位服务 5") +// Log.e(TAG, "liwei--------- 初始化高德定位服务 5") locationClient?.startLocation() - Log.e(TAG, "liwei--------- 初始化高德定位服务 6") +// Log.e(TAG, "liwei--------- 初始化高德定位服务 6") initialized = true } catch (e: Exception) { Log.e(TAG, "高德定位初始化失败: ${e.message}", e)