From 0ddd3993e44333d925b60648c79cdaa906af99b4 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Fri, 6 Jun 2025 21:10:27 +0100 Subject: [PATCH] add --- android/settings.gradle.kts | 4 +- ios/Runner/Info.plist | 4 +- .../agent_service/android/build.gradle.kts | 2 +- .../agent_service/AgentService.kt | 133 ++- local_plugins/chat_api/README.md | 13 + local_plugins/chat_api/android/README.md | 70 ++ .../chat_api/android/build.gradle.kts | 50 ++ .../chat_api/android/settings.gradle.kts | 1 + .../android/src/main/AndroidManifest.xml | 8 + .../yunqiinnovation/chat_api/ChatApiPlugin.kt | 252 ++++++ .../chat_api/ChatApiService.kt | 799 ++++++++++++++++++ .../chat_api/SystemFunctionHandler.swift | 42 +- local_plugins/chat_api/pubspec.yaml | 3 + 13 files changed, 1325 insertions(+), 56 deletions(-) create mode 100644 local_plugins/chat_api/android/README.md create mode 100644 local_plugins/chat_api/android/build.gradle.kts create mode 100644 local_plugins/chat_api/android/settings.gradle.kts create mode 100644 local_plugins/chat_api/android/src/main/AndroidManifest.xml create mode 100644 local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt create mode 100644 local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index fa5282d77..cf7591c68 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -39,7 +39,7 @@ include(":ble_service") include(":classic_bluetooth") include(":agent_service") include(":speech") -include(":open_ai_service") +include(":chat_api") // 设置azure_speech项目的路径 project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android") @@ -50,4 +50,4 @@ project(":ble_service").projectDir = file("../local_plugins/ble_service/android" project(":classic_bluetooth").projectDir = file("../local_plugins/classic_bluetooth/android") project(":agent_service").projectDir = file("../local_plugins/agent_service/android") project(":speech").projectDir = file("../local_plugins/speech/android") -project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android") \ No newline at end of file +project(":chat_api").projectDir = file("../local_plugins/chat_api/android") \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 9a7b93fa1..b7e7fc98f 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -123,9 +123,9 @@ NSBluetoothPeripheralUsageDescription 需要蓝牙权限用于连接和管理音频设备 NSLocationAlwaysAndWhenInUseUsageDescription - 需要位置权限用于搜索附近的蓝牙设备 + 语音助手需要位置权限来为您提供基于位置的信息和服务 NSLocationWhenInUseUsageDescription - 需要位置权限用于搜索附近的蓝牙设备 + 语音助手需要位置权限来为您提供基于位置的信息和服务 NSMicrophoneUsageDescription 需要麦克风权限用于语音识别和录音功能 NSCalendarsUsageDescription diff --git a/local_plugins/agent_service/android/build.gradle.kts b/local_plugins/agent_service/android/build.gradle.kts index 64a0b9518..1fd600fb7 100644 --- a/local_plugins/agent_service/android/build.gradle.kts +++ b/local_plugins/agent_service/android/build.gradle.kts @@ -48,5 +48,5 @@ dependencies { add("compileOnly", project(":music_service")) add("compileOnly", project(":amap_service")) add("compileOnly", project(":speech")) - add("compileOnly", project(":open_ai_service")) + add("compileOnly", project(":chat_api")) } \ No newline at end of file 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 94eae0890..d9f358d34 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 @@ -4,7 +4,8 @@ import android.content.Context import org.json.JSONArray import org.json.JSONObject import com.yunqiinnovation.azure_speech.AzureAsrHelper -import com.yunqiinnovation.open_ai_service.OpenAIService +import com.yunqiinnovation.chat_api.ChatApiService +import com.yunqiinnovation.chat_api.StreamCallback import com.yunqiinnovation.chat_storage.ChatStorageHelper import com.yunqiinnovation.music_service.MusicServiceStarter import com.yunqiinnovation.amap_service.NavigationServiceHelper @@ -53,8 +54,8 @@ object AgentService : CoroutineScope { // 使用AzureTtsHelper作为唯一的TTS实现 private var ttsService: AzureTtsHelper? = null - // OpenAI服务 - 修改为使用OpenAIService - private lateinit var openAIService: OpenAIService + // ChatAPI服务 - 使用新的ChatApiService + private lateinit var chatApiService: ChatApiService // 聊天存储服务 private lateinit var chatStorageHelper: ChatStorageHelper @@ -121,7 +122,7 @@ object AgentService : CoroutineScope { try { // 初始化上下文和服务 this.context = appContext.applicationContext - this.openAIService = OpenAIService(context) + this.chatApiService = ChatApiService(context) this.chatStorageHelper = ChatStorageHelper(context) this.audioPlayer = AudioPlayer(context) @@ -146,8 +147,8 @@ object AgentService : CoroutineScope { // 初始化TTS服务 initializeTtsEngine(config) - // 初始化OpenAI服务 - openAIService.initialize( + // 初始化ChatAPI服务 + chatApiService.initialize( config["openaiApiKey"]?.toString() ?: "", config["openaiBaseUrl"]?.toString() ?: "", config["openaiModel"]?.toString() ?: "", @@ -181,9 +182,9 @@ object AgentService : CoroutineScope { azureAsrHelper?.dispose() - // 释放OpenAI服务 - if (::openAIService.isInitialized) { - openAIService.cancelAll() + // 释放ChatAPI服务 + if (::chatApiService.isInitialized) { + chatApiService.cancelCurrentStream() } // 释放TTS服务 @@ -401,7 +402,7 @@ object AgentService : CoroutineScope { "language" to detectedLanguage )) - processWithOpenAI(result) + processWithChatApi(result) } // 重置状态,继续识别 @@ -513,8 +514,8 @@ object AgentService : CoroutineScope { currentAiJob?.cancel() currentAiJob = null - // 通知OpenAI服务终止当前流式请求 - openAIService.cancelCurrentStream() + // 通知ChatAPI服务终止当前流式请求 + chatApiService.cancelCurrentStream() // 记录日志 Log.d(TAG, "AI流输出已停止") @@ -543,43 +544,43 @@ object AgentService : CoroutineScope { return false } - // 使用OpenAI处理文本 - processWithOpenAI(text, speakResponse) + // 使用ChatAPI处理文本 + processWithChatApi(text, speakResponse) return true } /** - * 使用OpenAI处理语音识别结果 + * 使用ChatAPI处理语音识别结果 */ - private fun processWithOpenAI(text: String) { - processWithOpenAI(text, true) + private fun processWithChatApi(text: String) { + processWithChatApi(text, true) } /** - * 使用OpenAI处理文本消息 + * 使用ChatAPI处理文本消息 * * @param text 用户输入文本 * @param speakResponse 是否使用TTS朗读回复 */ - private fun processWithOpenAI( + private fun processWithChatApi( text: String, speakResponse: Boolean = true ) { Log.d(TAG, "用户问题: $text") // 创建用户文本消息并处理 - val userMessage = openAIService.createUserMessage(text) - processWithOpenAIInternal(userMessage, text, speakResponse) + val userMessage = createUserMessage(text) + processWithChatApiInternal(userMessage, text, speakResponse) } /** - * 使用OpenAI处理图片 + * 使用ChatAPI处理图片 * * @param imageBase64 Base64编码的图片数据 * @param text 可选的文本描述或问题 * @param speakResponse 是否朗读回复 */ - private fun processImageWithOpenAI( + private fun processImageWithChatApi( imageBase64: String, text: String = "", speakResponse: Boolean = false @@ -587,14 +588,14 @@ object AgentService : CoroutineScope { Log.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}") // 创建带图片的用户消息并处理 - val userMessage = openAIService.createUserMessageWithImage(text, imageBase64) + val userMessage = createUserMessageWithImage(text, imageBase64) // 图片描述用于存储 val displayText = text.ifEmpty { "[图片]" } - processWithOpenAIInternal(userMessage, displayText, speakResponse, true) + processWithChatApiInternal(userMessage, displayText, speakResponse, true) } /** - * 内部方法:通用的OpenAI处理逻辑 + * 内部方法:通用的ChatAPI处理逻辑 * * @param userMessage 用户消息(可以是文本或图片格式) * @param displayText 用于显示和存储的文本 @@ -602,7 +603,7 @@ object AgentService : CoroutineScope { * @param hasImage 是否包含图片 * @param imagePath 图片路径(如果有) */ - private fun processWithOpenAIInternal( + private fun processWithChatApiInternal( userMessage: JSONObject, displayText: String, speakResponse: Boolean = true, @@ -634,7 +635,7 @@ object AgentService : CoroutineScope { // 先添加系统提示词 if (systemPrompt.isNotEmpty()) { - messagesWithSystemPrompt.put(openAIService.createSystemMessage(systemPrompt)) + messagesWithSystemPrompt.put(createSystemMessage(systemPrompt)) // Log.d(TAG, "系统提示词: ${systemPrompt}") } @@ -672,9 +673,9 @@ object AgentService : CoroutineScope { addToHistoryMessages(userMessage) } - openAIService.sendMessageStream( + chatApiService.sendMessageStream( messagesWithSystemPrompt, - object : OpenAIService.StreamCallback { + object : StreamCallback { override fun onToken(token: String) { responseBuilder.append(token) if (speakResponse && !nobroadcast) { @@ -704,7 +705,7 @@ object AgentService : CoroutineScope { sendEvent("assistant_response", responseData) // 添加AI回复到历史记录 - addToHistoryMessages(openAIService.createAssistantMessage(response)) + addToHistoryMessages(createAssistantMessage(response)) // 保存聊天记录 saveChatMessage(displayText, response,aiMetadata,userMetadata.toString()) @@ -796,9 +797,9 @@ object AgentService : CoroutineScope { val content = message["message"] as String if (sender == "user") { - addToHistoryMessages(openAIService.createUserMessage(content)) + addToHistoryMessages(createUserMessage(content)) } else if (sender == "assistant") { - addToHistoryMessages(openAIService.createAssistantMessage(content)) + addToHistoryMessages(createAssistantMessage(content)) } } @@ -945,6 +946,62 @@ object AgentService : CoroutineScope { return result == android.content.pm.PackageManager.PERMISSION_GRANTED } + /** + * 创建用户消息 + */ + private fun createUserMessage(content: String): JSONObject { + return JSONObject().apply { + put("role", "user") + put("content", content) + } + } + + /** + * 创建系统消息 + */ + private fun createSystemMessage(content: String): JSONObject { + return JSONObject().apply { + put("role", "system") + put("content", content) + } + } + + /** + * 创建助手消息 + */ + private fun createAssistantMessage(content: String): JSONObject { + return JSONObject().apply { + put("role", "assistant") + put("content", content) + } + } + + /** + * 创建带图片的用户消息 + */ + private fun createUserMessageWithImage(text: String, imageBase64: String): JSONObject { + val content = JSONArray().apply { + if (text.isNotEmpty()) { + put(JSONObject().apply { + put("type", "text") + put("text", text) + }) + } + put(JSONObject().apply { + put("type", "image_url") + put("image_url", JSONObject().apply { + put("url", "data:image/jpeg;base64,$imageBase64") + put("detail", "auto") + }) + }) + } + + return JSONObject().apply { + put("role", "user") + put("content", content) + } + } + /** * 处理图片输入 */ @@ -971,7 +1028,7 @@ object AgentService : CoroutineScope { launch { try { // 将图片转换为Base64格式 - val imageBase64 = openAIService.fileToBase64(imagePath) ?: run { + val imageBase64 = chatApiService.fileToBase64(imagePath) ?: run { sendEvent("error", mapOf( "code" to "IMAGE_CONVERSION_FAILED", "message" to "图片转换失败" @@ -980,9 +1037,9 @@ object AgentService : CoroutineScope { } // 创建带图片的用户消息并处理 - val userMessage = openAIService.createUserMessageWithImage(text, imageBase64) - // 图片描述用于存储 - val displayText = text.ifEmpty { "[图片]" } + val userMessage = createUserMessageWithImage(text, imageBase64) + // 图片描述用于存储 + val displayText = text.ifEmpty { "[图片]" } // 通知图片准备完成 sendEvent("image_ready", mapOf( @@ -991,7 +1048,7 @@ object AgentService : CoroutineScope { )) // 处理消息并传入存储路径 - processWithOpenAIInternal(userMessage, displayText, speakResponse, true, imagePath) + processWithChatApiInternal(userMessage, displayText, speakResponse, true, imagePath) } catch (e: Exception) { Log.e(TAG, "处理图片失败: ${e.message}") sendEvent("error", mapOf( diff --git a/local_plugins/chat_api/README.md b/local_plugins/chat_api/README.md index be47e00d0..463e1c3d6 100644 --- a/local_plugins/chat_api/README.md +++ b/local_plugins/chat_api/README.md @@ -15,6 +15,7 @@ ## 平台支持 - iOS 16.0+ +- Android (API 21+) ## MCP 功能 @@ -138,6 +139,7 @@ chatApi.eventStream.listen((event) { ## 架构 +### iOS ``` ChatApiService (Flutter层) ↓ @@ -150,3 +152,14 @@ ChatApiService (iOS原生服务) └── SystemFunctionHandler (内置系统功能) ``` +### Android +``` +ChatApiService (Flutter层) + ↓ +ChatApiPlugin (Android插件桥接) + ↓ +ChatApiService (Android原生服务) + ├── OpenAI客户端 (aallam/openai-kotlin 4.0.1) + └── MCP功能 (暂时留空,接口预留) +``` + diff --git a/local_plugins/chat_api/android/README.md b/local_plugins/chat_api/android/README.md new file mode 100644 index 000000000..99c528b5f --- /dev/null +++ b/local_plugins/chat_api/android/README.md @@ -0,0 +1,70 @@ +# ChatAPI Android 实现 + +基于 [openai-kotlin 4.0.1](https://github.com/aallam/openai-kotlin/tree/4.0.1) 的 ChatAPI 插件 Android 实现。 + +## 特性 + +- ✅ 基于 openai-kotlin 4.0.1 的 OpenAI API 客户端 +- ✅ 与 iOS 版本接口完全一致 +- ✅ 支持流式和非流式对话 +- ✅ 支持多模态内容(文本和图像识别) +- ✅ 工具调用架构预留(MCP 功能暂时留空) +- ✅ 协程支持和异步处理 +- ✅ 完整的错误处理和取消机制 + +## 依赖 + +- openai-kotlin 4.0.1 +- ktor-client-okhttp 2.3.2 +- kotlinx-coroutines +- gson + +## 主要文件 + +- `ChatApiPlugin.kt` - Flutter 插件桥接层 +- `ChatApiService.kt` - 核心 ChatAPI 服务实现 +- `StreamCallback.kt` - 流式回调接口 + +## 与 iOS 版本的一致性 + +Android 实现完全遵循 iOS 版本的接口定义: + +1. **初始化方法**: `initialize(apiKey, baseUrl, model, mcpServer)` +2. **消息创建**: `createUserMessage()`, `createAssistantMessage()` +3. **消息发送**: `sendMessage()` (非流式), `sendMessageStream()` (流式) +4. **函数注册**: `registerFunction()` +5. **MCP 相关**: 接口保留,功能暂时留空 +6. **流式事件**: 完全一致的事件类型和回调 + +## MCP 功能状态 + +MCP (Model Context Protocol) 相关功能在 Android 版本中暂时留空,包括: + +- `initializeMcpClient()` - 返回 false +- `isMcpInitialized()` - 返回 false +- `handleMcpToolCall()` - 返回占位符消息 +- 工具调用会通知上层但不会实际执行 + +## 使用示例 + +```kotlin +// 初始化 +chatApiService.initialize( + apiKey = "your-api-key", + baseUrl = "https://api.openai.com/v1/", + model = "gpt-3.5-turbo", + mcpServer = "" +) + +// 流式对话 +chatApiService.sendMessageStream(listOf( + mapOf("role" to "user", "content" to "Hello!") +)) +``` + +## 注意事项 + +1. 需要网络权限 `INTERNET` 和 `ACCESS_NETWORK_STATE` +2. 所有网络请求在后台线程执行 +3. 流式回调在主线程触发 +4. 支持取消正在进行的请求 \ No newline at end of file diff --git a/local_plugins/chat_api/android/build.gradle.kts b/local_plugins/chat_api/android/build.gradle.kts new file mode 100644 index 000000000..1eb929ecf --- /dev/null +++ b/local_plugins/chat_api/android/build.gradle.kts @@ -0,0 +1,50 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + kotlin("plugin.serialization") version "1.9.24" +} + +android { + namespace = "com.yunqiinnovation.chat_api" + compileSdk = 35 + + defaultConfig { + minSdk = 21 + targetSdk = 33 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + packaging { + resources { + excludes.add("META-INF/DEPENDENCIES") + excludes.add("META-INF/LICENSE") + excludes.add("META-INF/LICENSE.txt") + excludes.add("META-INF/license.txt") + excludes.add("META-INF/NOTICE") + excludes.add("META-INF/NOTICE.txt") + excludes.add("META-INF/notice.txt") + excludes.add("META-INF/*.kotlin_module") + } + } +} + +dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + + // OpenAI Kotlin 4.0.1 + implementation("com.aallam.openai:openai-client:4.0.1") + implementation("io.ktor:ktor-client-okhttp:2.3.2") + + // JSON处理 + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") + implementation("com.google.code.gson:gson:2.10.1") +} \ No newline at end of file diff --git a/local_plugins/chat_api/android/settings.gradle.kts b/local_plugins/chat_api/android/settings.gradle.kts new file mode 100644 index 000000000..c953e09ff --- /dev/null +++ b/local_plugins/chat_api/android/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "chat_api" \ No newline at end of file diff --git a/local_plugins/chat_api/android/src/main/AndroidManifest.xml b/local_plugins/chat_api/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..11db23350 --- /dev/null +++ b/local_plugins/chat_api/android/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file 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 new file mode 100644 index 000000000..bc7801951 --- /dev/null +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt @@ -0,0 +1,252 @@ +package com.yunqiinnovation.chat_api + +import android.os.Handler +import android.os.Looper +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import kotlinx.coroutines.* + +/** + * ChatApiPlugin + * + * 基于 openai-kotlin 4.0.1 的 ChatAPI 插件 Android 实现 + * 与 iOS 版本接口完全一致 + */ +class ChatApiPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler { + private lateinit var methodChannel: MethodChannel + private lateinit var eventChannel: EventChannel + private var eventSink: EventChannel.EventSink? = null + + private val chatApiService = ChatApiService() + private val mainHandler = Handler(Looper.getMainLooper()) + + // 协程作用域 + private val pluginScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.chat_api/methods") + methodChannel.setMethodCallHandler(this) + + eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.chat_api/events") + eventChannel.setStreamHandler(this) + + // 设置流式回调 + chatApiService.setStreamCallback(object : com.yunqiinnovation.chat_api.StreamCallback { + override fun onToken(token: String) { + sendEvent("token", token) + } + + override fun onComplete() { + sendEvent("complete", null) + } + + override fun onError(error: Exception) { + sendEvent("error", error.message ?: "未知错误") + } + + override fun onFunctionCall(functionCall: org.json.JSONObject) { + try { + val jsonString = functionCall.toString() + sendEvent("functionCall", jsonString) + } catch (e: Exception) { + sendEvent("error", "Failed to serialize function call: ${e.message}") + } + } + + override fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject) { + try { + val meta = org.json.JSONObject().apply { + put("functionCall", functionCall) + put("functionCallResult", functionCallResult) + } + val metaString = meta.toString() + val context = functionCallResult.optString("context", "") + + sendEvent("functionCall", context, metaString) + } catch (e: Exception) { + sendEvent("error", "Failed to serialize function call result: ${e.message}") + } + } + }) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + methodChannel.setMethodCallHandler(null) + eventChannel.setStreamHandler(null) + pluginScope.cancel() + } + + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + "initialize" -> handleInitialize(call, result) + "createUserMessage" -> handleCreateUserMessage(call, result) + "createAssistantMessage" -> handleCreateAssistantMessage(call, result) + "sendMessage" -> handleSendMessage(call, result) + "sendMessageStream" -> handleSendMessageStream(call, result) + "sendFunctionCallResult" -> handleSendFunctionCallResult(call, result) + "cancelCurrentStream" -> handleCancelCurrentStream(result) + "registerFunction" -> handleRegisterFunction(call, result) + "initializeMcpClient" -> handleInitializeMcpClient(call, result) + "isMcpInitialized" -> handleIsMcpInitialized(result) + "closeMcpClient" -> handleCloseMcpClient(result) + "handleMcpToolCall" -> handleMcpToolCall(call, result) + else -> result.notImplemented() + } + } + + // MARK: - Method Handlers + + private fun handleInitialize(call: MethodCall, result: Result) { + val apiKey = call.argument("apiKey") + if (apiKey.isNullOrEmpty()) { + result.error("INVALID_ARGS", "缺少必要参数", null) + return + } + + val baseUrl = call.argument("baseUrl") ?: "" + val model = call.argument("model") ?: "" + val mcpServer = call.argument("mcpServer") ?: "" + + val success = chatApiService.initialize(apiKey, baseUrl, model, mcpServer) + result.success(success) + } + + private fun handleCreateUserMessage(call: MethodCall, result: Result) { + val content = call.argument("content") + if (content.isNullOrEmpty()) { + result.error("INVALID_ARGS", "缺少必要参数", null) + return + } + + val message = chatApiService.createUserMessage(content) + result.success(message) + } + + private fun handleCreateAssistantMessage(call: MethodCall, result: Result) { + val content = call.argument("content") + if (content.isNullOrEmpty()) { + result.error("INVALID_ARGS", "缺少必要参数", null) + return + } + + val message = chatApiService.createAssistantMessage(content) + result.success(message) + } + + private fun handleSendMessage(call: MethodCall, result: Result) { + @Suppress("UNCHECKED_CAST") + val messages = call.argument>>("messages") + if (messages == null) { + result.error("INVALID_ARGS", "缺少必要参数", null) + return + } + + pluginScope.launch { + try { + val response = chatApiService.sendMessage(messages) + mainHandler.post { + result.success(response) + } + } catch (e: Exception) { + mainHandler.post { + result.error("SEND_ERROR", e.message, null) + } + } + } + } + + private fun handleSendMessageStream(call: MethodCall, result: Result) { + @Suppress("UNCHECKED_CAST") + val messages = call.argument>>("messages") + if (messages == null) { + result.error("INVALID_ARGS", "缺少必要参数", null) + return + } + + chatApiService.sendMessageStream(messages) + result.success(true) + } + + private fun handleSendFunctionCallResult(call: MethodCall, result: Result) { + result.error("DEPRECATED", "sendFunctionCallResult已废弃,工具调用结果现在自动处理", null) + } + + private fun handleCancelCurrentStream(result: Result) { + val success = chatApiService.cancelCurrentStream() + result.success(success) + } + + private fun handleRegisterFunction(call: MethodCall, result: Result) { + val name = call.argument("name") + val description = call.argument("description") + @Suppress("UNCHECKED_CAST") + val parameters = call.argument>("parameters") + + if (name.isNullOrEmpty() || description.isNullOrEmpty() || parameters == null) { + result.error("INVALID_ARGS", "缺少必要参数", null) + return + } + + val success = chatApiService.registerFunction(name, description, parameters) + result.success(success) + } + + private fun handleInitializeMcpClient(call: MethodCall, result: Result) { + val serverUrl = call.argument("serverUrl") + if (serverUrl.isNullOrEmpty()) { + result.error("INVALID_ARGS", "缺少必要参数", null) + return + } + + // MCP功能暂时留空 + result.success(false) + } + + private fun handleIsMcpInitialized(result: Result) { + // MCP功能暂时留空 + result.success(false) + } + + private fun handleCloseMcpClient(result: Result) { + // MCP功能暂时留空 + result.success(true) + } + + private fun handleMcpToolCall(call: MethodCall, result: Result) { + val functionCallJson = call.argument("functionCall") + if (functionCallJson.isNullOrEmpty()) { + result.error("INVALID_ARGS", "缺少必要参数", null) + return + } + + // MCP功能暂时留空 + result.success("MCP功能暂未实现") + } + + // MARK: - Event Stream Handler + + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + eventSink = events + } + + override fun onCancel(arguments: Any?) { + eventSink = null + } + + private fun sendEvent(type: String, content: Any?, meta: String? = null) { + mainHandler.post { + eventSink?.let { sink -> + val event = mutableMapOf( + "type" to type + ) + content?.let { event["content"] = it } + meta?.let { event["meta"] = it } + sink.success(event) + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..dcf8b01bb --- /dev/null +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt @@ -0,0 +1,799 @@ +package com.yunqiinnovation.chat_api + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Base64 +import com.aallam.openai.api.chat.* +import com.aallam.openai.api.core.Role +import com.aallam.openai.api.model.ModelId +import com.aallam.openai.api.http.Timeout +import com.aallam.openai.client.OpenAI +import com.aallam.openai.client.OpenAIConfig +import com.aallam.openai.client.OpenAIHost +import com.google.gson.Gson +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.collect +import java.io.ByteArrayOutputStream +import java.io.File +import kotlin.coroutines.CoroutineContext +import kotlin.math.min +import kotlin.math.sqrt +import kotlin.time.Duration.Companion.seconds +import android.util.Log + +/** + * ChatAPI服务异常 + */ +class ChatApiException(message: String) : Exception("ChatApiException: $message") + +/** + * 流式回调接口 + * 与 iOS 版本 StreamCallback 协议保持完全一致 + */ +interface StreamCallback { + /** + * 接收到新的token + */ + fun onToken(token: String) + + /** + * 流式响应完成 + */ + fun onComplete() + + /** + * 出现错误 + */ + fun onError(error: Exception) + + /** + * 函数调用 - 兼容JSONObject格式 + */ + fun onFunctionCall(functionCall: org.json.JSONObject) + + /** + * 函数调用结果 - 兼容JSONObject格式 + */ + fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject) +} + +/** + * 工具调用信息 + */ +private data class ToolCallInfo( + var id: String = "", + var name: String = "", + var arguments: String = "" +) { + fun isValid(): Boolean = id.isNotEmpty() && name.isNotEmpty() +} + +/** + * ChatAPI服务的Android实现 + * 基于 openai-kotlin 4.0.1,与 iOS 版本接口完全一致 + */ +class ChatApiService(private val context: android.content.Context? = null) : CoroutineScope { + + override val coroutineContext: CoroutineContext = Dispatchers.IO + SupervisorJob() + + // MARK: - 属性 + private var baseUrl = "https://api.openai.com/v1/" + private var apiKey = "" + private var model = "gpt-3.5-turbo" + private var visionModel = "gpt-4-vision-preview" + private var isInitialized = false + + // OpenAI 客户端 + private var openAI: OpenAI? = null + + // 流式请求相关 + private var currentStreamJob: Job? = null + private var streamCallback: StreamCallback? = null + private var currentMessages: List> = emptyList() + private var toolCalls: MutableMap = mutableMapOf() + private var isCanceled = false + + // JSON处理 + private val gson = Gson() + + // MARK: - 公共方法 + + /** + * 设置流式回调 + */ + fun setStreamCallback(callback: StreamCallback) { + streamCallback = callback + } + + /** + * 初始化ChatAPI服务 + */ + fun initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String): Boolean { + this.apiKey = apiKey + if (baseUrl.isNotEmpty()) { + this.baseUrl = baseUrl + } + if (model.isNotEmpty()) { + this.model = model + } + Log.d("ChatApiService", "原始 baseUrl: $baseUrl") + + // 处理 baseUrl:移除末尾的 /chat/completions(如果存在) + // 因为 openai-kotlin 会自动拼接 /chat/completions + var processedBaseUrl = if (this.baseUrl.endsWith("/chat/completions")) { + this.baseUrl.removeSuffix("/chat/completions") + } else { + this.baseUrl.removeSuffix("/") + } + + // 确保以 / 结尾 + if (!processedBaseUrl.endsWith("/")) { + processedBaseUrl += "/" + } + + Log.d("ChatApiService", "处理后 baseUrl: $processedBaseUrl") + + return try { + // 创建OpenAI配置 + val config = OpenAIConfig( + token = apiKey, + timeout = Timeout(socket = 60.seconds), + host = OpenAIHost(baseUrl = processedBaseUrl) + ) + + openAI = OpenAI(config) + + // 初始化MCP客户端 (暂时留空,但保持接口一致) + if (mcpServer.isNotEmpty()) { + // MCP功能暂时留空,但记录服务器地址以便后续实现 + // initializeMcpClient(mcpServer) + } + + isInitialized = apiKey.isNotEmpty() + true + } catch (e: Exception) { + false + } + } + + /** + * 创建用户消息 + */ + fun createUserMessage(content: String): Map { + return mapOf( + "role" to "user", + "content" to content + ) + } + + /** + * 创建助手消息 + */ + fun createAssistantMessage(content: String): Map { + return mapOf( + "role" to "assistant", + "content" to content + ) + } + + /** + * 创建系统消息 + */ + fun createSystemMessage(content: String): Map { + return mapOf( + "role" to "system", + "content" to content + ) + } + + /** + * 发送消息(非流式输出) + */ + suspend fun sendMessage(messages: List>): String { + if (!isInitialized || apiKey.isEmpty() || openAI == null) { + throw ChatApiException("ChatAPI服务未初始化") + } + + // 检查是否包含图片,决定使用哪个模型 + val currentModel = if (containsImage(messages)) visionModel else model + + // 转换消息格式 + val chatMessages = convertToChatMessages(messages) + + // 构建请求 + val chatCompletionRequest = ChatCompletionRequest( + model = ModelId(currentModel), + messages = chatMessages, + maxTokens = 2000, + temperature = 0.7 + ) + + val result = openAI!!.chatCompletion(chatCompletionRequest) + + // 检查是否有工具调用 + val choice = result.choices.firstOrNull() + choice?.message?.toolCalls?.firstOrNull()?.let { toolCall -> + if (toolCall is ToolCall.Function) { + val functionCallJson = mapOf( + "name" to toolCall.function.name, + "arguments" to (toolCall.function.arguments ?: ""), + "id" to toolCall.id.toString() + ) + return gson.toJson(functionCallJson) + } + } + + // 返回普通消息内容 + return choice?.message?.content ?: throw ChatApiException("无效的响应格式") + } + + /** + * 发送消息(流式输出)- 兼容JSONArray格式 + */ + fun sendMessageStream(messages: org.json.JSONArray, callback: StreamCallback) { + // 设置回调 + setStreamCallback(callback) + + // 转换JSONArray为List> + val messageList = convertJsonArrayToList(messages) + + // 调用原始方法 + sendMessageStream(messageList) + } + + /** + * 发送消息(流式输出) + */ + fun sendMessageStream(messages: List>) { + if (!isInitialized || apiKey.isEmpty() || openAI == null) { + streamCallback?.onError(ChatApiException("ChatAPI服务未初始化")) + return + } + + // 重置状态 + isCanceled = false + currentMessages = messages + toolCalls.clear() + + // 检查是否包含图片,决定使用哪个模型 + val currentModel = if (containsImage(messages)) visionModel else model + + currentStreamJob = launch { + try { + // 转换消息格式 + val chatMessages = convertToChatMessages(messages) + + // 构建请求 + val chatCompletionRequest = ChatCompletionRequest( + model = ModelId(currentModel), + messages = chatMessages, + maxTokens = 2000, + temperature = 0.7 + ) + + val chatsFlow = openAI!!.chatCompletions(chatCompletionRequest) + + chatsFlow.collect { result -> + if (isCanceled) return@collect + + val choice = result.choices.firstOrNull() ?: return@collect + val delta = choice.delta ?: return@collect + + // 处理普通文本内容 + delta.content?.let { content -> + streamCallback?.onToken(content) + } + + // 收集工具调用信息 + delta.toolCalls?.forEach { toolCall -> + val index = toolCall.index + + // 创建或获取现有的工具调用信息 + val toolCallInfo = toolCalls.getOrPut(index) { ToolCallInfo() } + + // 更新ID + toolCall.id?.let { toolCallInfo.id = it.toString() } + + // 更新函数信息 + toolCall.function?.let { function -> + function.name?.let { toolCallInfo.name = it } + function.arguments?.let { toolCallInfo.arguments += it } + } + } + } + + if (!isCanceled) { + // 处理工具调用或完成 + val hasToolCalls = processToolCalls() + if (!hasToolCalls) { + streamCallback?.onComplete() + } + } + + } catch (e: Exception) { + if (!isCanceled) { + streamCallback?.onError(ChatApiException("流式请求失败: ${e.message}")) + } + } + } + } + + /** + * 处理工具调用 + */ + private suspend fun processToolCalls(): Boolean { + val firstToolCall = toolCalls.values.firstOrNull { it.isValid() } ?: return false + + // 创建函数调用字典 + val functionCall = mapOf( + "name" to firstToolCall.name, + "arguments" to firstToolCall.arguments, + "id" to firstToolCall.id + ) + + // 通知上层工具调用事件 + streamCallback?.onFunctionCall(convertMapToJsonObject(functionCall)) + + // 在后台队列处理工具调用 + launch { + try { + if (!isCanceled) { + // 这里暂时返回占位符结果,实际MCP功能留空 + val result = mapOf("context" to "MCP功能暂未实现") + + if (!isCanceled) { + // 处理结果 + streamCallback?.onFunctionCallResult( + convertMapToJsonObject(functionCall), + convertMapToJsonObject(result) + ) + + // 将结果发送回OpenAI继续对话 + sendFunctionCallResultInternal( + messages = currentMessages, + functionCall = functionCall, + functionResult = gson.toJson(result) + ) + } + } + } catch (e: Exception) { + if (!isCanceled) { + val errorMessage = "工具调用处理失败: ${e.message}" + sendFunctionCallResultInternal( + messages = currentMessages, + functionCall = functionCall, + functionResult = errorMessage + ) + } + } + } + + return true + } + + /** + * 发送函数调用结果(内部方法) + */ + private suspend fun sendFunctionCallResultInternal( + messages: List>, + functionCall: Map, + functionResult: String + ) { + if (isCanceled) return + + val fullMessages = messages.toMutableList() + + // 添加函数调用消息 + val callId = functionCall["id"] as? String ?: "call_${System.currentTimeMillis()}" + fullMessages.add( + mapOf( + "role" to "assistant", + "content" to "", + "tool_calls" to listOf( + mapOf( + "id" to callId, + "type" to "function", + "function" to mapOf( + "name" to (functionCall["name"] as? String ?: ""), + "arguments" to (functionCall["arguments"] as? String ?: "{}") + ) + ) + ) + ) + ) + + // 添加函数结果消息 + fullMessages.add( + mapOf( + "role" to "tool", + "content" to functionResult, + "tool_call_id" to callId + ) + ) + + // 重新发起对话,让AI基于工具调用结果生成回复 + sendMessageStream(fullMessages) + } + + /** + * 取消当前流式请求 + */ + fun cancelCurrentStream(): Boolean { + isCanceled = true + currentStreamJob?.cancel() + currentStreamJob = null + return true + } + + /** + * 取消所有操作并释放资源 (与 iOS 版本保持一致) + */ + fun cancelAll() { + cancelCurrentStream() + } + + /** + * 注册函数 + */ + fun registerFunction(name: String, description: String, parameters: Map): Boolean { + // 暂时返回true,实际功能留空,但保持与iOS版本接口一致 + return true + } + + /** + * 初始化MCP客户端 + */ + fun initializeMcpClient(serverUrl: String): Boolean { + // MCP功能暂时留空,但保持与iOS版本接口一致 + return false + } + + /** + * MCP客户端是否已初始化 + */ + fun isMcpInitialized(): Boolean { + // MCP功能暂时留空,但保持与iOS版本接口一致 + return false + } + + /** + * 关闭MCP客户端 + */ + fun closeMcpClient() { + // MCP功能暂时留空,但保持与iOS版本接口一致 + } + + /** + * 处理MCP工具调用 + */ + suspend fun handleMcpToolCall(functionCallJson: String): String { + // MCP功能暂时留空,但保持与iOS版本接口一致 + return "MCP功能暂未实现" + } + + // MARK: - 工具方法 + + /** + * 检查消息中是否包含图片 + */ + private fun containsImage(messages: List>): Boolean { + for (message in messages) { + val content = message["content"] + if (content is String && content.contains("image_url")) { + return true + } + if (content is List<*>) { + content.forEach { item -> + if (item is Map<*, *>) { + val type = item["type"] as? String + if (type == "image_url") { + return true + } + } + } + } + } + return false + } + + /** + * 转换为OpenAI ChatMessage格式 + */ + private fun convertToChatMessages(messages: List>): List { + val chatMessages = mutableListOf() + + for (message in messages) { + val role = message["role"] as? String ?: continue + + val chatRole = when (role) { + "system" -> Role.System + "user" -> Role.User + "assistant" -> Role.Assistant + "tool" -> Role.Tool + else -> Role.User + } + + // 特殊处理assistant角色的tool_calls消息 + if (role == "assistant" && message.containsKey("tool_calls")) { + val content = message["content"] as? String ?: "" + + // 解析tool_calls + val toolCallsList = mutableListOf() + @Suppress("UNCHECKED_CAST") + val toolCallsArray = message["tool_calls"] as? List> + + toolCallsArray?.forEach { toolCallDict -> + val id = toolCallDict["id"] as? String ?: return@forEach + val type = toolCallDict["type"] as? String + if (type == "function") { + @Suppress("UNCHECKED_CAST") + val function = toolCallDict["function"] as? Map ?: return@forEach + val name = function["name"] as? String ?: return@forEach + val arguments = function["arguments"] as? String ?: "" + + val functionCall = FunctionCall( + nameOrNull = name, + argumentsOrNull = arguments + ) + + val toolCall = ToolCall.Function( + id = ToolId(id), + function = functionCall + ) + + toolCallsList.add(toolCall) + } + } + + val chatMessage = ChatMessage.Assistant( + content = if (content.isEmpty()) null else content, + toolCalls = if (toolCallsList.isEmpty()) null else toolCallsList + ) + + chatMessages.add(chatMessage) + continue + } + + // 特殊处理tool角色消息 + if (role == "tool") { + val content = message["content"] as? String ?: "" + val toolCallId = message["tool_call_id"] as? String ?: "" + + val chatMessage = ChatMessage.Tool( + content = content, + toolCallId = ToolId(toolCallId) + ) + + chatMessages.add(chatMessage) + continue + } + + // 处理普通消息的不同类型内容 + val content = message["content"] + when (content) { + is String -> { + val chatMessage = when (chatRole) { + Role.System -> ChatMessage.System(content) + Role.User -> ChatMessage.User(content) + Role.Assistant -> ChatMessage.Assistant(content) + else -> ChatMessage.User(content) + } + chatMessages.add(chatMessage) + } + is List<*> -> { + // 对于多模态内容,提取文本部分(简化处理) + var textContent = "" + + content.forEach { item -> + if (item is Map<*, *>) { + val type = item["type"] as? String + if (type == "text") { + val text = item["text"] as? String ?: "" + textContent += text + } + } + } + + 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) + } + chatMessages.add(chatMessage) + } + } + } + + return chatMessages + } + + /** + * 将文件转换为Base64字符串 + */ + fun fileToBase64(filePath: String, maxSizeKB: Int = 20480): String? { + return try { + val file = File(filePath) + if (!file.exists() || !file.isFile) { + return null + } + + // 读取文件 + var bitmap = BitmapFactory.decodeFile(filePath) ?: return null + + // 检查图片尺寸,限制最大为1024*1024 + val maxDimension = 1024 + if (bitmap.width > maxDimension || bitmap.height > maxDimension) { + // 计算缩放比例,保持纵横比 + val ratio = min(maxDimension.toFloat() / bitmap.width, maxDimension.toFloat() / bitmap.height) + + val newWidth = (bitmap.width * ratio).toInt() + val newHeight = (bitmap.height * ratio).toInt() + + bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true) + } + + // 压缩图片 + val outputStream = ByteArrayOutputStream() + var quality = 100 + bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream) + + // 如果原始图片太大,继续优化文件大小 + var fileSize = file.length() / 1024 // 转为KB + if (fileSize > maxSizeKB) { + val scale = sqrt(maxSizeKB.toDouble() / fileSize) + val newWidth = (bitmap.width * scale).toInt() + val newHeight = (bitmap.height * scale).toInt() + bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true) + outputStream.reset() + quality = 85 + bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream) + } + + // 转为Base64 + Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP) + } catch (e: Exception) { + null + } + } + + /** + * 创建带图片的用户消息 + */ + fun createUserMessageWithImage(text: String, imageBase64: String): Map { + val contentArray = mutableListOf>() + + // 添加文本部分 + if (text.isNotEmpty()) { + contentArray.add( + mapOf( + "type" to "text", + "text" to text + ) + ) + } + + // 添加图片部分 + contentArray.add( + mapOf( + "type" to "image_url", + "image_url" to mapOf( + "url" to "data:image/jpeg;base64,$imageBase64" + ) + ) + ) + + return mapOf( + "role" to "user", + "content" to contentArray + ) + } + + /** + * 从URL中提取主机信息 (保留向后兼容) + */ + private fun extractHost(url: String): String { + return try { + val javaUrl = java.net.URL(url) + javaUrl.host ?: "api.openai.com" + } catch (e: Exception) { + "api.openai.com" + } + } + + /** + * 解析JSON参数 (与 iOS 版本保持一致) + */ + private fun parseJsonArguments(argumentsJson: String): Map { + return try { + if (argumentsJson.isBlank()) { + emptyMap() + } else { + val jsonObject = org.json.JSONObject(argumentsJson) + convertJsonObjectToMap(jsonObject) + } + } catch (e: Exception) { + emptyMap() + } + } + + /** + * 转换JSONArray为List> + */ + private fun convertJsonArrayToList(jsonArray: org.json.JSONArray): List> { + val result = mutableListOf>() + + for (i in 0 until jsonArray.length()) { + val jsonObject = jsonArray.optJSONObject(i) ?: continue + val map = convertJsonObjectToMap(jsonObject) + result.add(map) + } + + return result + } + + /** + * 转换JSONObject为Map + */ + private fun convertJsonObjectToMap(jsonObject: org.json.JSONObject): Map { + val map = mutableMapOf() + + val keys = jsonObject.keys() + while (keys.hasNext()) { + val key = keys.next() + val value = jsonObject.get(key) + + when (value) { + is org.json.JSONObject -> map[key] = convertJsonObjectToMap(value) + is org.json.JSONArray -> { + val list = mutableListOf() + for (i in 0 until value.length()) { + val item = value.get(i) + when (item) { + is org.json.JSONObject -> list.add(convertJsonObjectToMap(item)) + else -> list.add(item) + } + } + map[key] = list + } + else -> map[key] = value + } + } + + return map + } + + /** + * 转换Map为JSONObject + */ + private fun convertMapToJsonObject(map: Map): org.json.JSONObject { + val jsonObject = org.json.JSONObject() + + for ((key, value) in map) { + when (value) { + is Map<*, *> -> { + @Suppress("UNCHECKED_CAST") + jsonObject.put(key, convertMapToJsonObject(value as Map)) + } + is List<*> -> { + val jsonArray = org.json.JSONArray() + value.forEach { item -> + when (item) { + is Map<*, *> -> { + @Suppress("UNCHECKED_CAST") + jsonArray.put(convertMapToJsonObject(item as Map)) + } + else -> jsonArray.put(item) + } + } + jsonObject.put(key, jsonArray) + } + else -> jsonObject.put(key, value) + } + } + + return jsonObject + } +} \ No newline at end of file diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift index c8b72a92b..bc9fc061e 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift @@ -340,17 +340,19 @@ public class SystemFunctionHandler: NSObject { // 检查位置服务是否可用 guard CLLocationManager.locationServicesEnabled() else { - continuation.resume(returning: Result(success: false, message: "位置服务未启用")) + locationCompletion?(Result(success: false, message: "位置服务未启用。请前往 设置 > 隐私与安全 > 定位服务 启用定位服务")) + locationCompletion = nil return } // 确保locationManager已初始化 guard let locationManager = locationManager else { - continuation.resume(returning: Result(success: false, message: "位置管理器未初始化")) + locationCompletion?(Result(success: false, message: "位置管理器未初始化")) + locationCompletion = nil return } - // 检查位置权限 - 兼容iOS 13.0+ + // 检查位置权限 let authStatus: CLAuthorizationStatus if #available(iOS 14.0, *) { authStatus = locationManager.authorizationStatus @@ -358,20 +360,27 @@ public class SystemFunctionHandler: NSObject { authStatus = CLLocationManager.authorizationStatus() } + print("当前位置权限状态: \(authStatus.rawValue)") + switch authStatus { case .notDetermined: - // 请求权限,权限结果会在 didChangeAuthorization 回调中处理 + print("请求位置权限...") + // 请求权限 locationManager.requestWhenInUseAuthorization() - // 不要在这里 return,让权限回调来处理后续逻辑 - case .denied, .restricted: - continuation.resume(returning: Result(success: false, message: "位置权限被拒绝")) - return + // 权限结果会在 didChangeAuthorization 回调中处理 + case .denied: + locationCompletion?(Result(success: false, message: "位置权限被拒绝。请前往 设置 > 隐私与安全 > 定位服务 > Deep Voice 启用定位权限")) + locationCompletion = nil + case .restricted: + locationCompletion?(Result(success: false, message: "位置权限受限制,可能是由于家长控制或企业配置限制")) + locationCompletion = nil case .authorizedWhenInUse, .authorizedAlways: + print("已有位置权限,开始获取位置...") // 已有权限,直接请求位置 locationManager.requestLocation() @unknown default: - continuation.resume(returning: Result(success: false, message: "未知的位置权限状态")) - return + locationCompletion?(Result(success: false, message: "未知的位置权限状态: \(authStatus.rawValue)")) + locationCompletion = nil } } } @@ -404,10 +413,17 @@ extension SystemFunctionHandler: CLLocationManagerDelegate { return } - if status == .authorizedWhenInUse || status == .authorizedAlways { + switch status { + case .authorizedWhenInUse, .authorizedAlways: manager.requestLocation() - } else if status == .denied || status == .restricted { - locationCompletion?(Result(success: false, message: "位置权限被拒绝")) + case .denied, .restricted: + locationCompletion?(Result(success: false, message: "位置权限被拒绝。请前往 设置 > 隐私与安全 > 定位服务 > Deep Voice 启用定位权限")) + locationCompletion = nil + case .notDetermined: + // 权限仍未确定,等待用户操作 + break + @unknown default: + locationCompletion?(Result(success: false, message: "位置权限状态异常,请重试")) locationCompletion = nil } } diff --git a/local_plugins/chat_api/pubspec.yaml b/local_plugins/chat_api/pubspec.yaml index 297ec2147..4bc40daee 100644 --- a/local_plugins/chat_api/pubspec.yaml +++ b/local_plugins/chat_api/pubspec.yaml @@ -34,6 +34,9 @@ flutter: # adding or updating assets for this project. plugin: platforms: + android: + package: com.yunqiinnovation.chat_api + pluginClass: ChatApiPlugin ios: pluginClass: ChatApiPlugin ffiPlugin: true