|
|
|
@ -33,6 +33,14 @@ import okhttp3.Interceptor |
|
|
|
import io.ktor.client.request.* |
|
|
|
import io.ktor.http.* |
|
|
|
import com.yunqiinnovation.open_ai_service.mcp.CustomSseClientTransport |
|
|
|
import kotlinx.coroutines.CoroutineScope |
|
|
|
import kotlinx.coroutines.Dispatchers |
|
|
|
import kotlinx.coroutines.Job |
|
|
|
import kotlinx.coroutines.delay |
|
|
|
import kotlinx.coroutines.isActive |
|
|
|
import kotlinx.coroutines.launch |
|
|
|
import kotlinx.coroutines.sync.Mutex |
|
|
|
import kotlinx.coroutines.sync.withLock |
|
|
|
import okio.Buffer |
|
|
|
|
|
|
|
/** |
|
|
|
@ -68,11 +76,17 @@ class MCPSubClient( |
|
|
|
|
|
|
|
// 保存服务器URL |
|
|
|
private val serverUrl: String = serverUrl |
|
|
|
|
|
|
|
init { |
|
|
|
Log.d(TAG, "MCPSubClient初始化,URL: $serverUrl") |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 重连配置 |
|
|
|
private val maxRetryAttempts = 3 |
|
|
|
private val initialReconnectDelay = 1000L // 初始重连延迟1秒 |
|
|
|
private val maxReconnectDelay = 30000L // 最大重连延迟30秒 |
|
|
|
private var currentReconnectDelay = initialReconnectDelay |
|
|
|
private var retryCount = 0 |
|
|
|
|
|
|
|
// 连接状态锁 |
|
|
|
private val connectionLock = Mutex() |
|
|
|
|
|
|
|
// 初始化MCP客户端 |
|
|
|
private val mcp: Client = Client( |
|
|
|
clientInfo = Implementation( |
|
|
|
@ -95,7 +109,18 @@ class MCPSubClient( |
|
|
|
|
|
|
|
// 工具名称到工具的映射,用于快速查找 |
|
|
|
private val toolMapping = mutableMapOf<String, ToolUnion>() |
|
|
|
|
|
|
|
|
|
|
|
// 心跳检测 |
|
|
|
private var heartbeatJob: Job? = null |
|
|
|
private val heartbeatInterval = 30000L // 30秒 |
|
|
|
|
|
|
|
// 网络状态监听 |
|
|
|
private var networkStateListener: NetworkStateListener? = null |
|
|
|
|
|
|
|
init { |
|
|
|
Log.d(TAG, "MCPSubClient初始化,URL: $serverUrl") |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 将JsonObject转换为JsonValue |
|
|
|
*/ |
|
|
|
@ -110,6 +135,10 @@ class MCPSubClient( |
|
|
|
* @return 是否连接成功 |
|
|
|
*/ |
|
|
|
suspend fun connect(): Boolean { |
|
|
|
if (isConnected) { |
|
|
|
Log.d(TAG, "已经连接到服务器,跳过重复连接") |
|
|
|
return true |
|
|
|
} |
|
|
|
try { |
|
|
|
Log.d(TAG, "开始连接服务器: $serverUrl") |
|
|
|
|
|
|
|
@ -130,6 +159,11 @@ class MCPSubClient( |
|
|
|
processToolsList(toolsResult) |
|
|
|
|
|
|
|
isConnected = true |
|
|
|
retryCount = 0 |
|
|
|
currentReconnectDelay = initialReconnectDelay |
|
|
|
// 启动心跳检测 |
|
|
|
startHeartbeat() |
|
|
|
|
|
|
|
Log.d(TAG, "连接成功,可用工具: ${tools.size}个") |
|
|
|
return true |
|
|
|
} catch (e: Exception) { |
|
|
|
@ -368,9 +402,11 @@ class MCPSubClient( |
|
|
|
* 调用工具 |
|
|
|
*/ |
|
|
|
suspend fun callTool(name: String, arguments: Map<String, Any>): JSONObject? { |
|
|
|
if (!isConnected) { |
|
|
|
Log.e(TAG, "未连接到服务器") |
|
|
|
return JSONObject().apply { put("context","未连接到MCP服务器") } |
|
|
|
|
|
|
|
// 先检查连接状态 |
|
|
|
if (!checkConnection()) { |
|
|
|
Log.e(TAG, "无法连接到服务器,工具调用失败") |
|
|
|
return JSONObject().apply { put("context", "无法连接到MCP服务器") } |
|
|
|
} |
|
|
|
|
|
|
|
if (!containsTool(name)) { |
|
|
|
@ -395,32 +431,123 @@ class MCPSubClient( |
|
|
|
} |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "调用工具失败: ${e.message}") |
|
|
|
isConnected = false |
|
|
|
JSONObject().apply { put("context", "调用工具失败: ${e.message}") } |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 设置网络状态监听器 |
|
|
|
*/ |
|
|
|
fun setNetworkStateListener(listener: NetworkStateListener) { |
|
|
|
this.networkStateListener = listener |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 检查连接状态并自动重连 |
|
|
|
*/ |
|
|
|
suspend fun checkConnection(): Boolean { |
|
|
|
if (!isConnected) { |
|
|
|
Log.d(TAG, "当前未连接,尝试重新连接...") |
|
|
|
return connect() |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
// 简单ping测试连接状态 |
|
|
|
mcp.ping() |
|
|
|
return true |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "连接检查失败: ${e.message}") |
|
|
|
isConnected = false |
|
|
|
return false |
|
|
|
} |
|
|
|
} |
|
|
|
/** |
|
|
|
* 检查是否已连接 |
|
|
|
*/ |
|
|
|
fun isConnected(): Boolean { |
|
|
|
return isConnected |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理连接错误并尝试重连 |
|
|
|
*/ |
|
|
|
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),停止重连") |
|
|
|
// 可以在这里添加通知或回调,告知上层连接彻底失败 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 启动心跳检测 |
|
|
|
*/ |
|
|
|
private fun startHeartbeat() { |
|
|
|
heartbeatJob?.cancel() |
|
|
|
heartbeatJob = CoroutineScope(Dispatchers.IO).launch { |
|
|
|
while (isActive && isConnected) { |
|
|
|
try { |
|
|
|
delay(heartbeatInterval) |
|
|
|
|
|
|
|
// 发送心跳请求 |
|
|
|
val startTime = System.currentTimeMillis() |
|
|
|
val response = mcp.ping() |
|
|
|
val latency = System.currentTimeMillis() - startTime |
|
|
|
|
|
|
|
Log.d(TAG, "心跳检测成功,延迟: ${latency}ms") |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "心跳检测失败: ${e.message}") |
|
|
|
handleConnectionError(e) |
|
|
|
break |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 停止心跳检测 |
|
|
|
*/ |
|
|
|
private fun stopHeartbeat() { |
|
|
|
heartbeatJob?.cancel() |
|
|
|
heartbeatJob = null |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 关闭连接 |
|
|
|
*/ |
|
|
|
override fun close() { |
|
|
|
runBlocking { |
|
|
|
try { |
|
|
|
mcp.close() |
|
|
|
sseTransport?.close() |
|
|
|
isConnected = false |
|
|
|
Log.d(TAG, "已关闭MCP服务器连接") |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "关闭MCP服务器连接失败: ${e.message}", e) |
|
|
|
connectionLock.withLock { |
|
|
|
try { |
|
|
|
isConnected = false |
|
|
|
stopHeartbeat() |
|
|
|
mcp.close() |
|
|
|
sseTransport?.close() |
|
|
|
Log.d(TAG, "已关闭MCP服务器连接") |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "关闭MCP服务器连接失败: ${e.message}", e) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 网络状态监听接口 |
|
|
|
*/ |
|
|
|
interface NetworkStateListener { |
|
|
|
fun onNetworkAvailable() |
|
|
|
fun onNetworkLost() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
|