From c97ae00426475225a36943a41ddda1f5f2f96b44 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Sat, 24 May 2025 10:50:33 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E8=A1=A5=E5=85=85=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../open_ai_service/OpenAIService.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt index 829e872c7..54cfa3347 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt @@ -514,10 +514,9 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { if (tools.length() > 0) { put("tools", tools) } - Log.i(TAG, "请求Ai对话: 携带工具数据 ${tools.length()}") - + Log.i(TAG, "liwei------------------- 请求Ai对话: context:${messages.getJSONObject(messages.length() - 1).get("content").toString()} 携带工具数据 ${tools.length()}") } - + val mediaType = "application/json".toMediaTypeOrNull() val request = Request.Builder() .url(baseUrl) @@ -678,6 +677,8 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { if (function.has("arguments")) { toolCallInfo.arguments += function.getString("arguments") } + Log.d(TAG, "liwei---------------------- AI发起工具调用: tool=$function") + } } } @@ -706,7 +707,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { put("arguments", firstToolCall.arguments) put("id", firstToolCall.id) } - Log.d(TAG, "工具调用: id=${firstToolCall.id}, name=${firstToolCall.name} arguments=${firstToolCall.arguments}") + Log.d(TAG, "liwei---------------------- 工具调用: id=${firstToolCall.id}, name=${firstToolCall.name} arguments=${firstToolCall.arguments}") // 通知上层工具调用事件 callback.onFunctionCall(functionCall) @@ -721,7 +722,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { val args = mcpClient?.parseJsonArguments(firstToolCall.arguments) ?: mapOf() mcpClient?.callTool(firstToolCall.name, args) } catch (e: Exception) { - Log.e(TAG, "工具调用错误: ${e.message}") + Log.e(TAG, "liwei----------------------- 工具调用错误: ${e.message}") JSONObject().put("context", "工具调用失败: ${e.message}") } @@ -733,7 +734,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { val context = resultObj.optString("context", "工具调用失败").let { if (it.isNullOrEmpty()) "工具调用失败" else it } - Log.d(TAG, "mcp工具调用完成: name=${firstToolCall.name}, context=$context") + Log.d(TAG, "liwei----------------------- mcp工具调用完成: name=${firstToolCall.name}, context=$context") // 将结果发送回OpenAI继续对话 sendFunctionCallResult( @@ -746,7 +747,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { } } catch (e: Exception) { if (!isCanceled) { - Log.e(TAG, "处理工具调用异常: ${e.message}") + Log.e(TAG, "liwei----------------------- 处理工具调用异常: ${e.message}") try { val errorMessage = "工具调用处理失败: ${e.message}" sendFunctionCallResult( From 650e89bbcedfe4df38440a36be4b57ca47abd4d6 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Sat, 24 May 2025 17:12:19 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=B8=8A=E4=BC=A0mcp=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/models/appconfig_model.dart | 4 +- lib/data/models/appconfig_model.g.dart | 6 +- lib/data/models/user_Info.dart | 5 +- .../login/controllers/login_controller.dart | 1 - .../controllers/pairing_controller.dart | 54 ++++++------- .../open_ai_service/OpenAIService.kt | 8 +- .../mcp/CustomSseClientTransport.kt | 76 ++++++++++++++++++- 7 files changed, 110 insertions(+), 44 deletions(-) diff --git a/lib/data/models/appconfig_model.dart b/lib/data/models/appconfig_model.dart index 54edc6f86..b95b5fae2 100644 --- a/lib/data/models/appconfig_model.dart +++ b/lib/data/models/appconfig_model.dart @@ -27,7 +27,7 @@ class DBAgent { final String? welcomeMessage; final String? voiceWelcomeMessage; final String? systemPrompt; - final List? tools; + final String? tools; DBAgent({ required this.id, @@ -52,7 +52,7 @@ class DBAgent { class DBMCPServer { final String servername; final String url; - final List? tools; + final String? tools; DBMCPServer({ required this.servername, required this.url, diff --git a/lib/data/models/appconfig_model.g.dart b/lib/data/models/appconfig_model.g.dart index 6b89333fa..6d47874e6 100644 --- a/lib/data/models/appconfig_model.g.dart +++ b/lib/data/models/appconfig_model.g.dart @@ -37,8 +37,7 @@ DBAgent _$DBAgentFromJson(Map json) => DBAgent( welcomeMessage: json['welcomeMessage'] as String?, voiceWelcomeMessage: json['voiceWelcomeMessage'] as String?, systemPrompt: json['systemPrompt'] as String?, - tools: - (json['tools'] as List?)?.map((e) => e as String).toList(), + tools: json['tools'] as String?, ); Map _$DBAgentToJson(DBAgent instance) => { @@ -58,8 +57,7 @@ Map _$DBAgentToJson(DBAgent instance) => { DBMCPServer _$DBMCPServerFromJson(Map json) => DBMCPServer( servername: json['servername'] as String, url: json['url'] as String, - tools: - (json['tools'] as List?)?.map((e) => e as String).toList(), + tools: json['tools'] as String?, ); Map _$DBMCPServerToJson(DBMCPServer instance) => diff --git a/lib/data/models/user_Info.dart b/lib/data/models/user_Info.dart index 13132bbf6..cf5853ee6 100644 --- a/lib/data/models/user_Info.dart +++ b/lib/data/models/user_Info.dart @@ -1,6 +1,3 @@ -import 'dart:convert'; -import 'dart:ffi'; - class User { // 单例实例 static User? _instance; @@ -24,7 +21,7 @@ class User { String phone; String phonemac; String password; - int membershipLevel=0;//添加会员等级字段,0表示普通用户 + int membershipLevel = 0; //添加会员等级字段,0表示普通用户 // final Int64 lastbettime; // final Int64 createtime; // final String wxopenid; diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index 4eb9c327c..951885884 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:logger/web.dart'; import '../../../routes/app_routes.dart'; import 'dart:convert'; -import 'dart:ffi'; import 'facebookLogin.dart'; import 'googleLogin.dart'; import 'weixinLogin.dart'; diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 7f11158bf..686db7ba8 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -201,10 +201,10 @@ class PairingController extends GetxController { // iOS需要请求的权限 permissionsToRequest.add(Permission.bluetooth); permissionsToRequest.add(Permission.location); - + // 如果需要麦克风权限(例如语音功能) // permissionsToRequest.add(Permission.microphone); - + try { // 先尝试请求权限 if (permissionsToRequest.isNotEmpty) { @@ -214,17 +214,18 @@ class PairingController extends GetxController { if (!status.isGranted) { allGranted = false; Logger.w(_tag, 'iOS权限 ${permission.toString()} 未被授予: $status'); - + // 特殊处理蓝牙权限 if (permission == Permission.bluetooth) { Logger.w(_tag, 'iOS蓝牙权限未授予,状态: $status'); } } }); - + // 如果有权限未授予,但不是永久拒绝,继续尝试 if (!allGranted) { - bool hasPermanentlyDenied = statuses.values.any((status) => status.isPermanentlyDenied); + bool hasPermanentlyDenied = + statuses.values.any((status) => status.isPermanentlyDenied); if (hasPermanentlyDenied) { // 有权限被永久拒绝,需要引导用户去设置 _showPermissionSettingsDialog('部分权限被永久拒绝,请在设置中启用'); @@ -232,15 +233,16 @@ class PairingController extends GetxController { } } } - + // 直接尝试使用蓝牙功能,这会触发iOS原生权限请求 bool isEnabled = await _bluetoothService.isBluetoothEnabled(); Logger.i(_tag, 'iOS蓝牙状态检查: ${isEnabled ? "已开启" : "未开启"}'); - + // 检测AccessorySetupKit支持情况(iOS 18+特性) bool companionSupported = await _bleService.isCompanionSupported(); - Logger.i(_tag, 'iOS Companion支持检查: ${companionSupported ? "支持" : "不支持"}'); - + Logger.i( + _tag, 'iOS Companion支持检查: ${companionSupported ? "支持" : "不支持"}'); + if (companionSupported) { // 针对iOS 18+,尝试预热AccessorySetupKit框架 try { @@ -251,23 +253,23 @@ class PairingController extends GetxController { Logger.w(_tag, 'iOS 18+ AccessorySetupKit初始化失败: ${e.toString()}'); } } - + // 在iOS上视为权限已授予 _hasPermissions.value = true; return true; } catch (e) { Logger.e(_tag, "iOS蓝牙初始化失败: ${e.toString()}"); - + // 显示自定义对话框引导用户 _showPermissionSettingsDialog('需要开启蓝牙权限才能配对设备'); return false; } } - + // 如果还有其他权限需要请求 if (permissionsToRequest.isNotEmpty) { statuses = await permissionsToRequest.request(); - + bool allGranted = true; statuses.forEach((permission, status) { if (!status.isGranted) { @@ -275,7 +277,7 @@ class PairingController extends GetxController { Logger.w(_tag, '权限 ${permission.toString()} 未被授予: $status'); } }); - + if (!allGranted) { return false; } @@ -284,7 +286,7 @@ class PairingController extends GetxController { _hasPermissions.value = true; return true; } - + /// 显示权限设置对话框 void _showPermissionSettingsDialog(String message) { Get.dialog( @@ -537,27 +539,27 @@ class PairingController extends GetxController { try { isLoading.value = true; statusDescription.value = '正在重新初始化蓝牙...'; - + // iOS上尝试重置权限状态 if (Platform.isIOS) { await Future.delayed(Duration(milliseconds: 500)); - + // 检查蓝牙权限 PermissionStatus bluetoothStatus = await Permission.bluetooth.status; Logger.i(_tag, "iOS蓝牙权限当前状态: $bluetoothStatus"); - + if (bluetoothStatus.isPermanentlyDenied) { Logger.i(_tag, "iOS蓝牙权限处于永久拒绝状态,提示用户手动开启"); _showPermissionSettingsDialog('蓝牙权限被拒绝,请在设置中手动开启'); return; } - + // 尝试重新请求权限 if (!bluetoothStatus.isGranted) { bluetoothStatus = await Permission.bluetooth.request(); Logger.i(_tag, "iOS蓝牙权限请求结果: $bluetoothStatus"); } - + // 尝试检查蓝牙状态,这会重新触发系统权限弹窗 try { bool isEnabled = await _bluetoothService.isBluetoothEnabled(); @@ -565,7 +567,7 @@ class PairingController extends GetxController { } catch (e) { Logger.e(_tag, "iOS蓝牙状态检查失败: ${e.toString()}"); } - + // 检查Companion功能(iOS 18+) try { bool isSupported = await _bleService.isCompanionSupported(); @@ -578,7 +580,7 @@ class PairingController extends GetxController { Logger.w(_tag, "iOS Companion功能检查失败: ${e.toString()}"); } } - + // 重新检查权限 if (await _checkAndRequestPermissions()) { // 重新检查蓝牙状态 @@ -591,7 +593,7 @@ class PairingController extends GetxController { _updateStatusDescription(); } } - + /// 打开应用设置 Future openAppSettings() async { try { @@ -617,10 +619,10 @@ class PairingController extends GetxController { // 即使没有设备连接,也启动BLE后台服务 await _startAgentService(); _isNavigatingToHome = true; - + // 记录跳过配对的日志 Logger.i(_tag, '用户跳过设备配对,直接跳转到主页'); - + Get.offAllNamed(Routes.home); } -} \ No newline at end of file +} diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt index 54cfa3347..d5464724e 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt @@ -677,10 +677,9 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { if (function.has("arguments")) { toolCallInfo.arguments += function.getString("arguments") } - Log.d(TAG, "liwei---------------------- AI发起工具调用: tool=$function") - } } + Log.d(TAG, "liwei---------------------- AI发起工具调用: tool=$toolCallsArray") } } @@ -722,11 +721,12 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { val args = mcpClient?.parseJsonArguments(firstToolCall.arguments) ?: mapOf() mcpClient?.callTool(firstToolCall.name, args) } catch (e: Exception) { - Log.e(TAG, "liwei----------------------- 工具调用错误: ${e.message}") + Log.e(TAG, "liwei----------------------- 工具调用错误: $e") JSONObject().put("context", "工具调用失败: ${e.message}") } if (!isCanceled) { + // 处理结果 val resultObj = result ?: JSONObject().put("context", "工具调用失败") callback.onFunctionCallResult(functionCall, resultObj) @@ -747,7 +747,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { } } catch (e: Exception) { if (!isCanceled) { - Log.e(TAG, "liwei----------------------- 处理工具调用异常: ${e.message}") + Log.e(TAG, "liwei----------------------- 处理工具调用异常: $e") try { val errorMessage = "工具调用处理失败: ${e.message}" sendFunctionCallResult( diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt index 247fd6cc0..6d6a5eb27 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt @@ -1,5 +1,7 @@ package com.yunqiinnovation.open_ai_service.mcp +import android.os.Parcel +import android.os.Parcelable import android.util.Log import io.ktor.client.* import io.ktor.client.plugins.sse.* @@ -7,6 +9,7 @@ import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport import kotlinx.coroutines.* import kotlinx.serialization.encodeToString @@ -16,6 +19,40 @@ import kotlin.properties.Delegates import kotlin.time.Duration import java.util.concurrent.atomic.AtomicBoolean import org.json.JSONObject +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive + + +//@Serializable +//data class JSONRPCRequest( +// val jsonrpc: String = "2.0", +// val method: String, +// val params: JsonElement? = null, +// val id: JsonPrimitive? = null +//) : JSONRPCMessage, Parcelable { +// +// constructor(parcel: Parcel) : this( +// parcel.readString() ?: "2.0", +// parcel.readString() ?: "", +// parcel.readString()?.let { Json.decodeFromString(it) }, +// parcel.readString()?.let { Json.decodeFromString(it) } +// ) +// +// override fun writeToParcel(parcel: Parcel, flags: Int) { +// parcel.writeString(jsonrpc) +// parcel.writeString(method) +// parcel.writeString(params?.let { Json.encodeToString(it) }) +// parcel.writeString(id?.let { Json.encodeToString(it) }) +// } +// +// override fun describeContents(): Int = 0 +// +// companion object CREATOR : Parcelable.Creator { +// override fun createFromParcel(parcel: Parcel): JSONRPCRequest = JSONRPCRequest(parcel) +// override fun newArray(size: Int): Array = arrayOfNulls(size) +// } +//} /** * 自定义SSE客户端传输层,修复原始SseClientTransport中的URL拼接问题 @@ -39,7 +76,12 @@ class CustomSseClientTransport( private val endpoint = CompletableDeferred() private var job: Job? = null - + + + private val heartbeatIntervalMs = 30_000L // 30秒 + private var heartbeatJob: Job? = null + + // 创建JSON解析器,增强灵活性设置 private val json = Json { ignoreUnknownKeys = true // 忽略未知字段 @@ -299,6 +341,9 @@ class CustomSseClientTransport( // 收集SSE事件 collectEvents() + // 启动心跳任务 + startHeartbeat() + // 等待endpoint就绪 endpoint.await() Log.d(TAG, "传输层启动完成,消息端点已就绪") @@ -346,7 +391,7 @@ class CustomSseClientTransport( Log.e(TAG, "关闭失败: 传输层未初始化") error("CustomSseClientTransport is not initialized!") } - + heartbeatJob?.cancel() session.cancel() _onClose() job?.cancelAndJoin() @@ -359,4 +404,29 @@ class CustomSseClientTransport( fun isInitialized(): Boolean { return initialized.get() } -} \ No newline at end of file + + + private fun startHeartbeat() { + heartbeatJob = scope.launch { + while (isActive) { + delay(heartbeatIntervalMs) + try { + sendHeartbeat() + } catch (e: Exception) { + Log.e(TAG, "心跳发送失败: ${e.message}", e) + _onError(e) + } + } + } + } + + private suspend fun sendHeartbeat() { + val pingMessage = JSONRPCRequest( + method = "ping", + jsonrpc = "2.0" + ) + send(pingMessage) + } +} + + \ No newline at end of file