2
0
Просмотр исходного кода

feat(mcp): 区分服务器健康状态和登录状态

- 新增 MCP 服务器健康检查功能
- 后端添加 /api/mcp/health/:mcpType 端点
- 前端 McpTokenManager 添加 checkHealth() 方法
- McpServerCard 显示逻辑改进:
  - ✅ 已连接 (服务器健康 + 已登录)
  - 🟢 在线(未登录)(服务器健康 + 未登录)
  - 🔴 离线 (服务器不健康)
- 添加延迟显示 (ms)
- 每 30 秒自动重新检查健康状态

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude AI 4 часов назад
Родитель
Сommit
7bf55dc4a9

+ 90 - 0
backend/app_fastapi.py

@@ -518,6 +518,96 @@ async def list_mcp_tools(
         )
 
 
+@app.get("/api/mcp/health/{mcp_type}")
+async def check_mcp_health(mcp_type: str):
+    """
+    检查 MCP 服务器健康状态
+
+    通过调用 MCP 服务器的 SSE 端点来检查服务器是否在线
+    返回健康状态和响应延迟
+    """
+    import time
+
+    try:
+        # 查找 MCP 服务器配置
+        target_server = MCP_SERVERS.get(mcp_type)
+        if not target_server:
+            return JSONResponse(
+                status_code=404,
+                content={"status": "error", "message": f"Unknown MCP type: {mcp_type}"}
+            )
+
+        # 获取 MCP URL
+        mcp_url = target_server.get('url', '')
+        if not mcp_url:
+            return JSONResponse(
+                status_code=400,
+                content={"status": "error", "message": f"No URL configured for {mcp_type}"}
+            )
+
+        # 调用 MCP 服务器的 SSE 端点检查健康状态
+        start_time = time.time()
+
+        try:
+            async with httpx.AsyncClient(timeout=10.0) as http_client:
+                # 尝试访问 MCP 端点
+                response = await http_client.get(
+                    mcp_url,
+                    headers={"Accept": "text/event-stream"},
+                    timeout=10.0
+                )
+
+            latency = int((time.time() - start_time) * 1000)
+
+            # SSE 端点返回 200 表示服务器在线
+            if response.status_code == 200:
+                return {
+                    "status": "healthy",
+                    "healthy": True,
+                    "mcp_type": mcp_type,
+                    "latency": latency,
+                    "url": mcp_url
+                }
+            else:
+                return {
+                    "status": "unhealthy",
+                    "healthy": False,
+                    "mcp_type": mcp_type,
+                    "error": f"HTTP {response.status_code}",
+                    "latency": latency
+                }
+
+        except httpx.TimeoutException:
+            return {
+                "status": "timeout",
+                "healthy": False,
+                "mcp_type": mcp_type,
+                "error": "Connection timeout",
+                "latency": 10000
+            }
+        except httpx.ConnectError as e:
+            return {
+                "status": "unreachable",
+                "healthy": False,
+                "mcp_type": mcp_type,
+                "error": f"Connection error: {str(e)}",
+                "latency": 0
+            }
+
+    except Exception as e:
+        import traceback
+        return JSONResponse(
+            status_code=500,
+            content={
+                "status": "error",
+                "healthy": False,
+                "mcp_type": mcp_type,
+                "error": str(e),
+                "traceback": traceback.format_exc()
+            }
+        )
+
+
 # ========== 认证 API ==========
 
 @app.post("/api/auth/login")

+ 56 - 8
frontend-v2/components/McpServerCard.tsx

@@ -89,6 +89,10 @@ export function McpServerCard({ mcpType, config, onLoginSuccess, onLogoutSuccess
   const [isLoading, setIsLoading] = useState(false);
   const [error, setError] = useState('');
   const [updateTrigger, setUpdateTrigger] = useState(0);
+  // 新增:健康检查状态
+  const [isHealthy, setIsHealthy] = useState<boolean | null>(null); // null=未检查, true=健康, false=不健康
+  const [isCheckingHealth, setIsCheckingHealth] = useState(false);
+  const [latency, setLatency] = useState<number | null>(null);
 
   // 监听 localStorage 变化和组件挂载
   useEffect(() => {
@@ -106,6 +110,24 @@ export function McpServerCard({ mcpType, config, onLoginSuccess, onLogoutSuccess
 
     checkLoginStatus();
 
+    // 检查服务器健康状态
+    const checkServerHealth = async () => {
+      setIsCheckingHealth(true);
+      try {
+        const result = await mcpTokenManager.checkHealth(mcpType);
+        setIsHealthy(result.healthy);
+        setLatency(result.latency ?? null);
+        console.log(`[McpServerCard ${mcpType}] Health check:`, result);
+      } catch (e) {
+        console.error(`[McpServerCard ${mcpType}] Health check error:`, e);
+        setIsHealthy(false);
+      } finally {
+        setIsCheckingHealth(false);
+      }
+    };
+
+    checkServerHealth();
+
     // 监听 storage 事件(其他标签页的更改)
     const handleStorageChange = (e: StorageEvent) => {
       if (e.key?.startsWith('mcp_token_') || e.key?.startsWith('mcp_username_')) {
@@ -115,7 +137,14 @@ export function McpServerCard({ mcpType, config, onLoginSuccess, onLogoutSuccess
     };
 
     window.addEventListener('storage', handleStorageChange);
-    return () => window.removeEventListener('storage', handleStorageChange);
+
+    // 定期重新检查健康状态(每 30 秒)
+    const healthInterval = setInterval(checkServerHealth, 30000);
+
+    return () => {
+      window.removeEventListener('storage', handleStorageChange);
+      clearInterval(healthInterval);
+    };
   }, [mcpType]);
 
   const isLoggedIn = mcpTokenManager.isLoggedIn(mcpType);
@@ -123,6 +152,24 @@ export function McpServerCard({ mcpType, config, onLoginSuccess, onLogoutSuccess
   const remainingTime = mcpTokenManager.getTokenRemainingTime(mcpType);
   const hoursRemaining = Math.floor(remainingTime / (1000 * 60 * 60));
 
+  // 计算连接状态显示
+  const getConnectionStatus = () => {
+    if (isHealthy === null) {
+      return { text: '检查中...', className: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400', dotColor: 'bg-gray-400' };
+    }
+    if (isHealthy) {
+      if (isLoggedIn) {
+        return { text: '已连接', className: 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200', dotColor: 'bg-green-500' };
+      } else {
+        return { text: '在线(未登录)', className: 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200', dotColor: 'bg-blue-500' };
+      }
+    } else {
+      return { text: '离线', className: 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200', dotColor: 'bg-red-500' };
+    }
+  };
+
+  const connectionStatus = getConnectionStatus();
+
   const handleLogin = async (e: React.FormEvent) => {
     e.preventDefault();
     setIsLoading(true);
@@ -173,15 +220,16 @@ export function McpServerCard({ mcpType, config, onLoginSuccess, onLogoutSuccess
           </h3>
           <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
             {config.authType === 'none' ? '无需认证' : '需要登录'}
+            {latency !== null && isHealthy && (
+              <span className="ml-2 text-xs text-gray-400">
+                ({latency}ms)
+              </span>
+            )}
           </p>
         </div>
-        <div className={`flex items-center gap-2 px-3 py-1 rounded-full text-sm ${
-          isLoggedIn
-            ? 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200'
-            : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
-        }`}>
-          <span className={`w-2 h-2 rounded-full ${isLoggedIn ? 'bg-green-500' : 'bg-gray-400'}`} />
-          {isLoggedIn ? '已连接' : '未连接'}
+        <div className={`flex items-center gap-2 px-3 py-1 rounded-full text-sm ${connectionStatus.className}`}>
+          <span className={`w-2 h-2 rounded-full ${connectionStatus.dotColor} ${isCheckingHealth ? 'animate-pulse' : ''}`} />
+          {connectionStatus.text}
         </div>
       </div>
 

+ 86 - 0
frontend-v2/lib/mcp-token-manager.ts

@@ -49,6 +49,15 @@ export interface McpLoginResponse {
   detail?: string;
 }
 
+/**
+ * MCP 健康状态
+ */
+export interface McpHealthStatus {
+  healthy: boolean;
+  latency?: number;  // 响应延迟(毫秒)
+  error?: string;
+}
+
 /**
  * MCP Token 管理器
  *
@@ -56,9 +65,15 @@ export interface McpLoginResponse {
  * - Web UI 是 MCP 测试工具,不需要全局登录
  * - 每个 MCP 有独立的认证状态
  * - 可以同时管理多个 MCP 的登录
+ *
+ * 状态区分:
+ * - isHealthy: MCP 服务器是否正常运行(health check)
+ * - isLoggedIn: 是否有有效的认证 token
  */
 export class McpTokenManager {
   private static instance: McpTokenManager;
+  private healthCache: Map<string, { healthy: boolean; timestamp: number }> = new Map();
+  private readonly HEALTH_CACHE_TTL = 10000; // 健康检查缓存 10 秒
 
   private constructor() {}
 
@@ -221,6 +236,77 @@ export class McpTokenManager {
    */
   logoutMcp(mcpType: string): void {
     this.clearToken(mcpType);
+    this.healthCache.delete(mcpType);
+  }
+
+  /**
+   * 检查 MCP 服务器健康状态
+   * 通过后端代理端点 /api/mcp/health/:mcpType 进行健康检查
+   */
+  async checkHealth(mcpType: string): Promise<McpHealthStatus> {
+    // 检查缓存
+    const cached = this.healthCache.get(mcpType);
+    if (cached && Date.now() - cached.timestamp < this.HEALTH_CACHE_TTL) {
+      return { healthy: cached.healthy };
+    }
+
+    const config = MCP_SERVERS[mcpType];
+    if (!config) {
+      return { healthy: false, error: '未知的 MCP 类型' };
+    }
+
+    try {
+      const startTime = Date.now();
+      // 使用后端代理端点进行健康检查
+      const response = await fetch(`/api/mcp/health/${mcpType}`, {
+        method: 'GET',
+        headers: { 'Content-Type': 'application/json' },
+      });
+      const latency = Date.now() - startTime;
+
+      if (response.ok) {
+        const data = await response.json();
+        const healthy = data.status === 'healthy' || data.healthy === true;
+
+        // 更新缓存
+        this.healthCache.set(mcpType, {
+          healthy,
+          timestamp: Date.now(),
+        });
+
+        return { healthy, latency };
+      } else {
+        const error = await response.text().catch(() => '未知错误');
+        this.healthCache.set(mcpType, { healthy: false, timestamp: Date.now() });
+        return { healthy: false, error: `HTTP ${response.status}: ${error}` };
+      }
+    } catch (e) {
+      const errorMsg = e instanceof Error ? e.message : '网络错误';
+      this.healthCache.set(mcpType, { healthy: false, timestamp: Date.now() });
+      return { healthy: false, error: errorMsg };
+    }
+  }
+
+  /**
+   * 检查 MCP 服务器是否健康(使用缓存,不发起网络请求)
+   */
+  isHealthy(mcpType: string): boolean {
+    const cached = this.healthCache.get(mcpType);
+    if (!cached) return false; // 尚未检查过,默认不健康
+    // 如果缓存过期,返回 false 以触发新的检查
+    if (Date.now() - cached.timestamp > this.HEALTH_CACHE_TTL) return false;
+    return cached.healthy;
+  }
+
+  /**
+   * 清除健康检查缓存
+   */
+  clearHealthCache(mcpType?: string): void {
+    if (mcpType) {
+      this.healthCache.delete(mcpType);
+    } else {
+      this.healthCache.clear();
+    }
   }
 }