diff --git a/lib/modules/meeting/views/ai_bottom_sheet.dart b/lib/modules/meeting/views/ai_bottom_sheet.dart new file mode 100644 index 000000000..9243acad8 --- /dev/null +++ b/lib/modules/meeting/views/ai_bottom_sheet.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +import '../controllers/meeting_details_controller.dart'; + +class AIBottomSheet extends StatefulWidget { + const AIBottomSheet({super.key}); + + @override + State createState() => _AIBottomSheetState(); +} + +class _AIBottomSheetState extends State { + final _controller = Get.find(); + + List _dataList = []; + + final List _cruxList = [ + '添加代办事项', + '提取结论', + '生成关键指标', + ]; + + @override + Widget build(BuildContext context) { + return Container( + height: 1.sh - 50.h, + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: Column( + children: [ + Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + 26.horizontalSpace, + Text( + 'Ask AI', + style: TextStyle( + fontSize: 16.sp, + color: Colors.black87, + fontWeight: FontWeight.bold, + ), + ), + GestureDetector( + onTap: () => Get.back(), + child: Container( + width: 26.w, + height: 26.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(26.r), + ), + child: Icon( + Icons.close, + size: 18.w, + color: Colors.grey[400], + ), + ), + ) + ], + ), + ), + Expanded( + child: _dataList.isNotEmpty ? SizedBox() : _empty(), + ), + _inputBox(), + ], + ), + ); + } + + Widget _empty() { + return Padding( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '今天我能帮您做点什么呢?', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + ), + ), + Padding( + padding: EdgeInsets.only(top: 10.w, bottom: 30.w), + child: Row( + children: [ + Icon( + Icons.audio_file_outlined, + size: 14.w, + color: Colors.grey[500], + ), + 2.horizontalSpace, + Text( + _controller.meetingData.value.title, + style: TextStyle( + color: Colors.grey[500], + fontSize: 12.sp, + ), + ), + ], + ), + ), + ...List.generate( + _cruxList.length, + (int index) => Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w), + margin: EdgeInsets.symmetric(vertical: 5.w), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(20.r), + border: Border.all( + color: Colors.grey[300]!, + ), + ), + child: Text( + _cruxList[index], + style: TextStyle(fontSize: 12.sp), + ), + ), + ), + ], + ), + ); + } + + Widget _inputBox() { + return Container( + height: 40.w, + padding: EdgeInsets.symmetric(horizontal: 10.w), + decoration: BoxDecoration(), + child: Row( + children: [], + ), + ); + } +} diff --git a/lib/modules/meeting/views/overview_tab.dart b/lib/modules/meeting/views/overview_tab.dart index 3f65ed1a9..0dde125c7 100644 --- a/lib/modules/meeting/views/overview_tab.dart +++ b/lib/modules/meeting/views/overview_tab.dart @@ -4,6 +4,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../controllers/meeting_details_controller.dart'; +import 'ai_bottom_sheet.dart'; import 'generate_bottom_sheet.dart'; class OverviewTab extends StatefulWidget { @@ -94,6 +95,23 @@ class _OverviewTabState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ + if (_controller.meetingDetails.value.tasktype >= 3) + _button( + Icons.auto_awesome, + 'Ask AI', + () { + Get.bottomSheet( + const AIBottomSheet(), + isScrollControlled: true, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ); + }, + ), if (_controller.meetingDetails.value.tasktype == 0) SizedBox( width: 150.w, diff --git a/lib/modules/meeting/views/speech_tab.dart b/lib/modules/meeting/views/speech_tab.dart index 8c043cfa5..c2530aa0d 100644 --- a/lib/modules/meeting/views/speech_tab.dart +++ b/lib/modules/meeting/views/speech_tab.dart @@ -5,6 +5,7 @@ import 'package:get/get.dart'; import 'package:lottie/lottie.dart'; import '../controllers/meeting_details_controller.dart'; +import 'ai_bottom_sheet.dart'; import 'generate_bottom_sheet.dart'; import 'speaker_all_bottom_sheet.dart'; import 'speaker_bottom_sheet.dart'; @@ -278,6 +279,24 @@ class SpeechTab extends GetView { ); }, ), + if (controller.meetingDetails.value.tasktype >= 2 && + !controller.isSpeakerText.value) + _button( + Icons.auto_awesome, + 'Ask AI', + () { + Get.bottomSheet( + const AIBottomSheet(), + isScrollControlled: true, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ); + }, + ), if (controller.meetingDetails.value.tasktype == 0) SizedBox( width: 150.w, diff --git a/lib/modules/meeting/views/summary_tab.dart b/lib/modules/meeting/views/summary_tab.dart index 65fe8b17e..42b050d3c 100644 --- a/lib/modules/meeting/views/summary_tab.dart +++ b/lib/modules/meeting/views/summary_tab.dart @@ -6,6 +6,7 @@ import 'package:get/get.dart'; import '../../../data/services/db/sqflite_api.dart'; import '../controllers/meeting_details_controller.dart'; +import 'ai_bottom_sheet.dart'; import 'generate_bottom_sheet.dart'; import 'speaker_all_bottom_sheet.dart'; @@ -310,6 +311,23 @@ class _SummaryTabState extends State { ); }, ), + if (_controller.meetingDetails.value.tasktype >= 3) + _button( + Icons.auto_awesome, + 'Ask AI', + () { + Get.bottomSheet( + const AIBottomSheet(), + isScrollControlled: true, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ); + }, + ), if (_controller.meetingDetails.value.tasktype == 0) SizedBox( width: 150.w, diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 6fb0d667e..6471a8223 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -569,28 +569,28 @@ class SettingsView extends GetView { // color: isDarkMode // ? Colors.white.withOpacity(0.1) // : Colors.grey[200]), - // // BLE测试 - // _buildSimpleNavigationSetting( - // title: 'opus解码测试', - // subtitle: '测试opus解码', - // icon: Icons.bluetooth_searching, - // iconBgColor: isDarkMode - // ? Colors.green[900]!.withOpacity(0.3) - // : Colors.green[100]!, - // iconColor: - // isDarkMode ? Colors.green[300]! : Colors.green[600]!, - // titleColor: isDarkMode ? Colors.white : null, - // subtitleColor: isDarkMode ? Colors.white70 : null, - // onTap: () { - // Get.toNamed(Routes.opusTest); - // }, - // isDarkMode: isDarkMode, - // ), - // Divider( - // height: 1, - // color: isDarkMode - // ? Colors.white.withOpacity(0.1) - // : Colors.grey[200]), + // BLE测试 + _buildSimpleNavigationSetting( + title: 'opus解码测试', + subtitle: '测试opus解码', + icon: Icons.bluetooth_searching, + iconBgColor: isDarkMode + ? Colors.green[900]!.withOpacity(0.3) + : Colors.green[100]!, + iconColor: + isDarkMode ? Colors.green[300]! : Colors.green[600]!, + titleColor: isDarkMode ? Colors.white : null, + subtitleColor: isDarkMode ? Colors.white70 : null, + onTap: () { + Get.toNamed(Routes.opusTest); + }, + isDarkMode: isDarkMode, + ), + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), // BLE测试 // _buildSimpleNavigationSetting( 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 e18de276f..adda12336 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 @@ -668,7 +668,7 @@ object AgentService : CoroutineScope { try { // 设置状态为正在流式输出 _isAiStreaming.set(true) - + audioPlayer?.playAudio(R.raw.await, true,0.3f) // 使用历史记录作为上下文发送到OpenAI val responseBuilder = StringBuilder() var aiMetadata:String = "" @@ -736,6 +736,9 @@ object AgentService : CoroutineScope { responseBuilder.append(token) if (speakResponse && broadcast) { ttsService?.speakStream(token) + if (token.length > 0){ + audioPlayer?.stopAudio() + } } if (broadcast){ // 发送流式回复token @@ -783,7 +786,7 @@ object AgentService : CoroutineScope { override fun onFunctionCall(call: JSONObject) { try { - audioPlayer?.playAudio(R.raw.calling, true) +// audioPlayer?.playAudio(R.raw.calling, true) val name = call.getString("name") sendEvent("function_call", mapOf( "name" to name, @@ -1120,34 +1123,32 @@ object AgentService : CoroutineScope { /** * 播放音频资源 * @param resId 资源ID + * @param isLooping 是否循环播放 + * @param volume 音量大小,范围0.0-1.0,默认1.0 */ - fun playAudio(resId: Int, isLooping: Boolean = false) { + fun playAudio(resId: Int, isLooping: Boolean = false, volume: Float = 1.0f) { try { // 释放之前的资源 release() - + // 创建播放器并设置资源 mediaPlayer = MediaPlayer().apply { // 设置资源 context.resources.openRawResourceFd(resId)?.use { fd -> setDataSource(fd.fileDescriptor, fd.startOffset, fd.length) } - - // 播放完成后自动释放资源 + + this.isLooping = isLooping // 设置循环属性 + setVolume(volume, volume) // 设置音量(左声道,右声道) + setOnCompletionListener { - release() - } - - // 准备并播放 - prepare() - if (isLooping) { - start() - } else { - start() - setOnCompletionListener { + if (!isLooping) { release() } } + + prepare() + start() } } catch (e: Exception) { Log.e(TAG, "播放音频资源异常: ${e.message}", e) diff --git a/local_plugins/agent_service/android/src/main/res/raw/await.mp3 b/local_plugins/agent_service/android/src/main/res/raw/await.mp3 new file mode 100644 index 000000000..ae1bfcc9d Binary files /dev/null and b/local_plugins/agent_service/android/src/main/res/raw/await.mp3 differ diff --git a/local_plugins/agent_service/ios/agent_service/Package.swift b/local_plugins/agent_service/ios/agent_service/Package.swift index a4cf49980..0e95c6bc6 100644 --- a/local_plugins/agent_service/ios/agent_service/Package.swift +++ b/local_plugins/agent_service/ios/agent_service/Package.swift @@ -31,6 +31,7 @@ let package = Package( path: "Sources/agent_service", resources: [ .copy("Resources/calling.mp3"), + .copy("Resources/await.mp3"), .copy("Resources/start.mp3"), .copy("Resources/stop.mp3") ] diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt index 29246eabf..9ae9dd608 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt @@ -10,11 +10,18 @@ object BleConst { /** 主服务UUID - 文档中定义为0000ABC0-0000-1111-2222-123456789ABC */ val PRIMARY_SERVICE_UUID: UUID = UUID.fromString("0000abc0-0000-1111-2222-123456789abc") - /** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */ - val AUDIO_SERVICE_UUID: UUID = UUID.fromString("0000ABC0-0001-1111-2222-123456789ABC") + + /** 音频服务UUID - 文档中定义为0000ABC0-0001-1111-2222-123456789ABC */ + val AUDIO_SERVICE_UUID1: UUID = UUID.fromString("0000ABC0-0001-1111-2222-123456789ABC") + + /** 接收音频特征UUID - 文档中定义为0000ABC2-0001-1111-2222-123456789ABC */ + val RECEIVE_AUDIO_CHAR_UUID1: UUID = UUID.fromString("0000ABC2-0001-1111-2222-123456789ABC") + + /** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */ + val AUDIO_SERVICE_UUID: UUID = UUID.fromString("0000ae00-0000-1000-8000-00805f9b34fb") - /** 接收音频特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ - val RECEIVE_AUDIO_CHAR_UUID: UUID = UUID.fromString("0000ABC2-0001-1111-2222-123456789ABC") + /** 接收音频特征UUID - 文档中定义为0000ABC2-0001-1111-2222-123456789ABC */ + val RECEIVE_AUDIO_CHAR_UUID: UUID = UUID.fromString("0000ae02-0000-1000-8000-00805f9b34fb") /** 写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ val WRITE_CHAR_UUID: UUID = UUID.fromString("0000abc1-0000-1111-2222-123456789abc") diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index 2e9d93f87..70c58d3d0 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -72,6 +72,7 @@ object BleService { private var notifyChar: BluetoothGattCharacteristic? = null private var writeChar: BluetoothGattCharacteristic? = null private var audioChar: BluetoothGattCharacteristic? = null + private var audioChar1: BluetoothGattCharacteristic? = null var recordfile: RecordingFile? = null // 扫描相关 @@ -419,6 +420,7 @@ object BleService { notifyChar = null writeChar = null audioChar = null + audioChar1 = null } } @@ -491,6 +493,10 @@ object BleService { val audioSvc = g.getService(BleConst.AUDIO_SERVICE_UUID) audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID) + + // 获取音频服务1特征 + val audioSvc1 = g.getService(BleConst.AUDIO_SERVICE_UUID1) + audioChar1 = audioSvc1?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID1) if (notifyChar == null || writeChar == null) { Log.e(TAG, "未找到主服务所需特征") updateConnectionState(BleConst.STATE_ERROR) @@ -505,6 +511,12 @@ object BleService { Log.i(TAG, "音频服务特征找到并设置通知") } else { Log.w(TAG, "音频服务特征未找到") + } + if (audioChar1 != null) { + setupNotifications(g, audioChar1) + Log.i(TAG, "音频服务特征找到并设置通知") + } else { + Log.w(TAG, "音频服务特征未找到") } // 设置主服务的通知 setupNotifications(g, notifyChar) @@ -518,10 +530,16 @@ object BleService { // 根据特征UUID区分处理 when (c.uuid) { + // 音频特征数据 + BleConst.RECEIVE_AUDIO_CHAR_UUID1 -> { + + Log.i(TAG, "RECEIVE_AUDIO_CHAR_UUID1") + processAudioData(data) + } // 音频特征数据 BleConst.RECEIVE_AUDIO_CHAR_UUID -> { - // Log.i(TAG, "收到音频特征数据") + Log.i(TAG, "RECEIVE_AUDIO_CHAR_UUID") processAudioData(data) } // 通知特征数据(命令和控制) diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt index 12982f0fb..aa469e62a 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt @@ -25,6 +25,7 @@ class CustomSseClientTransport( private val urlString: String?, private val reconnectionTime: Duration? = null, private val requestBuilder: HttpRequestBuilder.() -> Unit = {}, + private val onConnectionLost: (() -> Unit)? = null ) : AbstractTransport() { private val TAG = "CustomSseClientTransport" @@ -35,8 +36,10 @@ class CustomSseClientTransport( private val initialized = AtomicBoolean(false) private var session: ClientSSESession by Delegates.notNull() private val endpoint = CompletableDeferred() + private val isConnected = AtomicBoolean(false) private var job: Job? = null + private var connectionMonitorJob: Job? = null // 创建JSON解析器 private val json = Json { @@ -95,74 +98,118 @@ class CustomSseClientTransport( */ private suspend fun collectEvents() { job = scope.launch(CoroutineName("CustomSseMcpClientTransport.collect#${hashCode()}")) { - session.incoming.collect { event -> - when (event.event) { - "error" -> { - val e = IllegalStateException("SSE error: ${event.data}") - Log.e(TAG, "SSE错误: ${event.data}") - _onError(e) - throw e - } + try { + session.incoming.collect { event -> + when (event.event) { + "error" -> { + Log.e(TAG, "SSE错误: ${event.data}") + isConnected.set(false) + val exception = Exception("SSE Error: ${event.data}") + _onError(exception) + onConnectionLost?.invoke() + throw exception + } - "open" -> { - // SSE连接已打开 - } - "ping" -> { - // 心跳 - } - "endpoint" -> { - try { - val eventData = event.data ?: "" + "open" -> { + // SSE连接已打开 + Log.d(TAG, "SSE连接已打开") + isConnected.set(true) + } + "ping" -> { + // 心跳 + } + "endpoint" -> { + try { + val eventData = event.data ?: "" - // 构建完整的端点URL - val fullEndpoint = if (eventData.contains(hostPart)) { - eventData - } else if (eventData.startsWith("/")) { - "$hostPart$eventData" - } else { - eventData - } - - // 添加查询参数 - val endpointWithParams = if (queryParams.isNotEmpty()) { - if (fullEndpoint.contains("?")) { - val queryString = queryParams.entries.joinToString("&") { "${it.key}=${it.value}" } - "$fullEndpoint&$queryString" + // 构建完整的端点URL + val fullEndpoint = if (eventData.contains(hostPart)) { + eventData + } else if (eventData.startsWith("/")) { + "$hostPart$eventData" } else { - val queryString = queryParams.entries.joinToString("&") { "${it.key}=${it.value}" } - "$fullEndpoint?$queryString" + eventData } - } else { - fullEndpoint + + // 添加查询参数 + val endpointWithParams = if (queryParams.isNotEmpty()) { + if (fullEndpoint.contains("?")) { + val queryString = queryParams.entries.joinToString("&") { "${it.key}=${it.value}" } + "$fullEndpoint&$queryString" + } else { + val queryString = queryParams.entries.joinToString("&") { "${it.key}=${it.value}" } + "$fullEndpoint?$queryString" + } + } else { + fullEndpoint + } + + endpoint.complete(endpointWithParams) + } catch (e: Exception) { + Log.e(TAG, "处理endpoint事件失败: ${e.message}", e) + _onError(e) + close() + error(e) } - - endpoint.complete(endpointWithParams) - } catch (e: Exception) { - Log.e(TAG, "处理endpoint事件失败: ${e.message}", e) - _onError(e) - close() - error(e) } - } - else -> { - try { - val data = event.data - if (data != null) { - try { - val message = json.decodeFromString(data) - _onMessage(message) - } catch (e: Exception) { - Log.e(TAG, "解析JSON-RPC消息失败: ${e.message}", e) - _onError(e) + else -> { + try { + val data = event.data + if (data != null) { + try { + val message = json.decodeFromString(data) + _onMessage(message) + } catch (e: Exception) { + Log.e(TAG, "解析JSON-RPC消息失败: ${e.message}", e) + _onError(e) + } } + } catch (e: Exception) { + Log.e(TAG, "处理事件失败: ${e.message}", e) + _onError(e) } - } catch (e: Exception) { - Log.e(TAG, "处理事件失败: ${e.message}", e) - _onError(e) } } } + } catch (e: CancellationException) { + Log.d(TAG, "SSE事件收集被取消") + throw e + } catch (e: Exception) { + Log.e(TAG, "SSE连接异常断开: ${e.message}", e) + isConnected.set(false) + _onError(e) + onConnectionLost?.invoke() + throw e + } + } + + // 启动连接监控 + startConnectionMonitor() + } + + /** + * 启动连接监控,定期检查连接状态 + */ + private fun startConnectionMonitor() { + connectionMonitorJob = scope.launch { + while (isActive && isConnected.get()) { + try { + delay(10000) // 每10秒检查一次 + + // 检查session是否仍然活跃 + if (session.coroutineContext[Job]?.isCancelled == true) { + Log.w(TAG, "检测到SSE会话已取消") + isConnected.set(false) + onConnectionLost?.invoke() + break + } + } catch (e: Exception) { + Log.e(TAG, "连接监控异常: ${e.message}", e) + isConnected.set(false) + onConnectionLost?.invoke() + break + } } } } @@ -212,6 +259,8 @@ class CustomSseClientTransport( // 等待endpoint就绪 endpoint.await() + + Log.d(TAG, "CustomSseClientTransport启动完成") } /** @@ -245,6 +294,13 @@ class CustomSseClientTransport( } } + /** + * 检查连接状态 + */ + fun isConnectionActive(): Boolean { + return isConnected.get() && session.coroutineContext[Job]?.isActive == true + } + /** * 关闭传输层 */ @@ -253,8 +309,14 @@ class CustomSseClientTransport( Log.e(TAG, "关闭失败: 传输层未初始化") error("CustomSseClientTransport is not initialized!") } + + isConnected.set(false) + connectionMonitorJob?.cancel() session.cancel() _onClose() job?.cancelAndJoin() + connectionMonitorJob?.cancelAndJoin() + + Log.d(TAG, "CustomSseClientTransport已关闭") } -} \ No newline at end of file +} \ No newline at end of file diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt index 77393a44a..d04ce3bb6 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt @@ -44,13 +44,14 @@ class MCPSubClient( private var mcpClient: Client? = null private var isConnected = false private var availableTools = mutableListOf() + private var transport: CustomSseClientTransport? = null /** * 连接到MCP服务器 */ suspend fun connect(): Boolean = connectionMutex.withLock { if (isConnected) return true - + Log.e(TAG, "[$serverId] 开始连接mcp服务器: $serverUrl") return try { // 创建MCP客户端实例 val client = Client( @@ -61,13 +62,20 @@ class MCPSubClient( ) // 根据URL类型选择传输方式 - val transport = when { + val newTransport = when { serverUrl.startsWith("http://") || serverUrl.startsWith("https://") -> { // SSE传输 - 使用自定义的CustomSseClientTransport val mcpHttpClient = httpClient ?: createMcpHttpClient() CustomSseClientTransport( client = mcpHttpClient, - urlString = serverUrl + urlString = serverUrl, + onConnectionLost = { + // 连接断开回调 + Log.w(TAG, "[$serverId] 检测到连接断开") + scope.launch { + handleConnectionLost() + } + } ) } else -> { @@ -76,8 +84,10 @@ class MCPSubClient( } } + transport = newTransport + // 连接到服务器 - client.connect(transport) + client.connect(newTransport) // 获取可用工具列表 try { @@ -101,9 +111,9 @@ class MCPSubClient( retryCount = 0 currentReconnectDelay = initialReconnectDelay // 启动心跳检测 - startHeartbeat() +// startHeartbeat() + Log.e(TAG, "[$serverId] 连接mcp服务器成功: $serverUrl") true - } catch (e: Exception) { Log.e(TAG, "[$serverId] MCP连接失败: ${e.message}", e) false @@ -321,7 +331,15 @@ class MCPSubClient( */ suspend fun checkConnection(): Boolean { if (!isConnected) { - Log.d(TAG, "当前未连接,尝试重新连接...") + Log.d(TAG, "[$serverId] 当前未连接,尝试重新连接...") + return connect() + } + + // 检查传输层连接状态 + val transportActive = transport?.isConnectionActive() ?: false + if (!transportActive) { + Log.w(TAG, "[$serverId] 传输层连接已断开") + isConnected = false return connect() } @@ -330,11 +348,34 @@ class MCPSubClient( mcpClient?.ping() return true } catch (e: Exception) { - Log.e(TAG, "连接检查失败: ${e.message}") + Log.e(TAG, "[$serverId] 连接检查失败: ${e.message}") isConnected = false return false } } + + /** + * 处理连接断开事件 + */ + private suspend fun handleConnectionLost() { + connectionMutex.withLock { + if (isConnected) { + Log.w(TAG, "[$serverId] 连接已断开,更新状态") + isConnected = false + stopHeartbeat() + + // 可以在这里添加自动重连逻辑 + // 或者通知上层应用连接已断开 + } + } + } + + /** + * 获取连接状态 + */ + fun getConnectionStatus(): Boolean { + return isConnected && (transport?.isConnectionActive() ?: false) + } /** * 停止心跳检测 */ @@ -393,10 +434,14 @@ class MCPSubClient( scope.launch { connectionMutex.withLock { try { + stopHeartbeat() mcpClient?.close() + transport?.close() mcpClient = null + transport = null isConnected = false availableTools.clear() + Log.d(TAG, "[$serverId] MCP连接已关闭") } catch (e: Exception) { Log.e(TAG, "[$serverId] 关闭MCP连接时出错: ${e.message}", e) } @@ -404,4 +449,4 @@ class MCPSubClient( } scope.cancel() } -} \ No newline at end of file +} \ No newline at end of file diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/OTAManager.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/OTAManager.kt index 8001b29ee..733551f6c 100644 --- a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/OTAManager.kt +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/OTAManager.kt @@ -106,7 +106,7 @@ class OTAManager(context: Context) : BluetoothOTAManager(context) { "是否启用设备认证流程(与固件工程师确认) >>> ${configHelper.isUseDeviceAuth()} >>> ${bluetoothOption.isUseReconnect} " ) //是否启用设备认证流程(与固件工程师确认) - bluetoothOption.isUseAuthDevice = configHelper.isUseDeviceAuth() + bluetoothOption.isUseAuthDevice = false //设置BLE的MTU bluetoothOption.mtu = BluetoothConstant.BLE_MTU_MIN //是否需要改变BLE的MTU