|
|
|
@ -1,6 +1,7 @@ |
|
|
|
package com.yunqiinnovation.chat_api |
|
|
|
|
|
|
|
import android.util.Log |
|
|
|
import com.google.gson.GsonBuilder |
|
|
|
import io.ktor.client.* |
|
|
|
import io.modelcontextprotocol.kotlin.sdk.* |
|
|
|
import io.modelcontextprotocol.kotlin.sdk.client.* |
|
|
|
@ -21,12 +22,22 @@ import kotlin.collections.mutableListOf |
|
|
|
class MCPSubClient( |
|
|
|
private val serverId: String, |
|
|
|
private val serverUrl: String, |
|
|
|
private val filterTools: String, |
|
|
|
private val httpClient: HttpClient? = null |
|
|
|
) : AutoCloseable { |
|
|
|
|
|
|
|
companion object { |
|
|
|
private const val TAG = "MCPSubClient" |
|
|
|
} |
|
|
|
// 心跳检测 |
|
|
|
private var heartbeatJob: Job? = null |
|
|
|
private val heartbeatInterval = 30000L // 30秒 |
|
|
|
// 重连配置 |
|
|
|
private val maxRetryAttempts = 3 |
|
|
|
private val initialReconnectDelay = 1000L // 初始重连延迟1秒 |
|
|
|
private val maxReconnectDelay = 30000L // 最大重连延迟30秒 |
|
|
|
private var currentReconnectDelay = initialReconnectDelay |
|
|
|
private var retryCount = 0 |
|
|
|
|
|
|
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) |
|
|
|
private val connectionMutex = Mutex() |
|
|
|
@ -66,14 +77,18 @@ class MCPSubClient( |
|
|
|
} |
|
|
|
|
|
|
|
// 连接到服务器 |
|
|
|
client.connect(transport) |
|
|
|
client?.connect(transport) |
|
|
|
|
|
|
|
// 获取可用工具列表 |
|
|
|
try { |
|
|
|
val toolsResult = client.listTools() |
|
|
|
val toolsResult = client?.listTools() |
|
|
|
if (toolsResult != null) { |
|
|
|
availableTools.clear() |
|
|
|
availableTools.addAll(toolsResult.tools) |
|
|
|
val filtered = toolsResult.tools.filter { tool -> |
|
|
|
filterTools.isEmpty() || filterTools.contains(tool.name) |
|
|
|
} |
|
|
|
Log.w(TAG, "[$serverId] [${filterTools}] 获取工具列表: ${filtered} 原始列表:${toolsResult.tools}") |
|
|
|
availableTools.addAll(filtered) |
|
|
|
} |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.w(TAG, "[$serverId] 获取工具列表失败: ${e.message}") |
|
|
|
@ -82,6 +97,11 @@ class MCPSubClient( |
|
|
|
|
|
|
|
mcpClient = client |
|
|
|
isConnected = true |
|
|
|
|
|
|
|
retryCount = 0 |
|
|
|
currentReconnectDelay = initialReconnectDelay |
|
|
|
// 启动心跳检测 |
|
|
|
startHeartbeat() |
|
|
|
true |
|
|
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
@ -129,8 +149,15 @@ class MCPSubClient( |
|
|
|
* 调用MCP工具 |
|
|
|
*/ |
|
|
|
suspend fun callTool(name: String, arguments: Map<String, Any>): Map<String, Any>? { |
|
|
|
val client = mcpClient ?: return null |
|
|
|
|
|
|
|
// 先检查连接状态 |
|
|
|
if (!checkConnection()) { |
|
|
|
Log.e(TAG, "无法连接到服务器,工具调用失败") |
|
|
|
return null |
|
|
|
} |
|
|
|
if (!containsTool(name)) { |
|
|
|
Log.w(TAG, "此客户端不包含工具: $name") |
|
|
|
return null |
|
|
|
} |
|
|
|
return try { |
|
|
|
// 创建工具调用请求 - 将Map转换为JsonObject |
|
|
|
val argumentsJson = kotlinx.serialization.json.buildJsonObject { |
|
|
|
@ -150,7 +177,7 @@ class MCPSubClient( |
|
|
|
) |
|
|
|
|
|
|
|
// 调用工具 |
|
|
|
val result = client.callTool(request) |
|
|
|
val result = mcpClient?.callTool(request) |
|
|
|
|
|
|
|
result?.let { callResult -> |
|
|
|
// 将结果转换为统一格式 |
|
|
|
@ -158,7 +185,7 @@ class MCPSubClient( |
|
|
|
// 根据不同的内容类型处理 |
|
|
|
mapOf( |
|
|
|
"type" to "text", |
|
|
|
"text" to (contentItem.toString()) |
|
|
|
"text" to (contentItem.toJsonString()) |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
@ -180,6 +207,20 @@ class MCPSubClient( |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 外部扩展函数 |
|
|
|
fun PromptMessageContent.toJsonString(indent: Int = 0): String { |
|
|
|
val gson = GsonBuilder() |
|
|
|
.setPrettyPrinting() |
|
|
|
.create() |
|
|
|
|
|
|
|
return if (indent > 0) { |
|
|
|
gson.toJson(this) |
|
|
|
} else { |
|
|
|
// 移除空格和换行符 |
|
|
|
gson.toJson(this).replace("\\s+".toRegex(), "") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 将Tool.Input转换为Map格式,供OpenAI使用 |
|
|
|
*/ |
|
|
|
@ -276,6 +317,76 @@ class MCPSubClient( |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 检查连接状态并自动重连 |
|
|
|
*/ |
|
|
|
suspend fun checkConnection(): Boolean { |
|
|
|
if (!isConnected) { |
|
|
|
Log.d(TAG, "当前未连接,尝试重新连接...") |
|
|
|
return connect() |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
// 简单ping测试连接状态 |
|
|
|
mcpClient?.ping() |
|
|
|
return true |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "连接检查失败: ${e.message}") |
|
|
|
isConnected = false |
|
|
|
return false |
|
|
|
} |
|
|
|
} |
|
|
|
/** |
|
|
|
* 停止心跳检测 |
|
|
|
*/ |
|
|
|
private fun stopHeartbeat() { |
|
|
|
heartbeatJob?.cancel() |
|
|
|
heartbeatJob = null |
|
|
|
} |
|
|
|
/** |
|
|
|
* 启动心跳检测 |
|
|
|
*/ |
|
|
|
private fun startHeartbeat() { |
|
|
|
heartbeatJob?.cancel() |
|
|
|
heartbeatJob = CoroutineScope(Dispatchers.IO).launch { |
|
|
|
while (isActive && isConnected) { |
|
|
|
try { |
|
|
|
delay(heartbeatInterval) |
|
|
|
|
|
|
|
// 发送心跳请求 |
|
|
|
val startTime = System.currentTimeMillis() |
|
|
|
val response = mcpClient?.ping() |
|
|
|
val latency = System.currentTimeMillis() - startTime |
|
|
|
|
|
|
|
Log.d(TAG, "心跳检测成功,延迟: ${latency}ms") |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "心跳检测失败: ${e.message}") |
|
|
|
handleConnectionError(e) |
|
|
|
break |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
/** |
|
|
|
* 处理连接错误并尝试重连 |
|
|
|
*/ |
|
|
|
private suspend fun handleConnectionError(e: Exception) { |
|
|
|
isConnected = false |
|
|
|
stopHeartbeat() |
|
|
|
|
|
|
|
if (retryCount < maxRetryAttempts) { |
|
|
|
retryCount++ |
|
|
|
currentReconnectDelay = minOf(currentReconnectDelay * 2, maxReconnectDelay) |
|
|
|
|
|
|
|
Log.w(TAG, "连接失败,将在 ${currentReconnectDelay}ms 后尝试重连 (尝试 $retryCount/$maxRetryAttempts)") |
|
|
|
|
|
|
|
delay(currentReconnectDelay) |
|
|
|
connect() |
|
|
|
} else { |
|
|
|
Log.e(TAG, "已达到最大重试次数($maxRetryAttempts),停止重连") |
|
|
|
// 可以在这里添加通知或回调,告知上层连接彻底失败 |
|
|
|
} |
|
|
|
} |
|
|
|
/** |
|
|
|
* 关闭连接 |
|
|
|
*/ |
|
|
|
|