feat(mcp): 完善健康检查和连接统计功能
- 使用 HEAD 请求检测 MCP 服务器可达性,解决 405 错误 - 修复连接统计逻辑:Novel Translator 无需登录即可算已连接 - 显示服务器响应延迟(毫秒) - 修复无限循环更新问题:使用 useCallback 稳定回调引用 - 使用 ref 跟踪上次报告状态,避免重复报告相同值 - 开发环境测试账号快速复制功能 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -523,10 +523,11 @@ async def check_mcp_health(mcp_type: str):
|
||||
"""
|
||||
检查 MCP 服务器健康状态
|
||||
|
||||
通过调用 MCP 服务器的 SSE 端点来检查服务器是否在线
|
||||
使用 HEAD 请求检查 MCP 服务器是否在线
|
||||
返回健康状态和响应延迟
|
||||
"""
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
try:
|
||||
# 查找 MCP 服务器配置
|
||||
@@ -545,37 +546,29 @@ async def check_mcp_health(mcp_type: str):
|
||||
content={"status": "error", "message": f"No URL configured for {mcp_type}"}
|
||||
)
|
||||
|
||||
# 调用 MCP 服务器的 SSE 端点检查健康状态
|
||||
# 使用 HEAD 请求检查服务器是否在线
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as http_client:
|
||||
# 尝试访问 MCP 端点
|
||||
response = await http_client.get(
|
||||
# 使用 HEAD 请求检查服务器可达性
|
||||
response = await http_client.head(
|
||||
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
|
||||
}
|
||||
# 只要能收到响应(无论什么状态码),说明服务器在线
|
||||
# MCP SSE 端点可能返回 405 (不允许 HEAD),但服务器仍健康
|
||||
return {
|
||||
"status": "healthy",
|
||||
"healthy": True,
|
||||
"mcp_type": mcp_type,
|
||||
"latency": latency,
|
||||
"url": mcp_url,
|
||||
"http_status": response.status_code
|
||||
}
|
||||
|
||||
except httpx.TimeoutException:
|
||||
return {
|
||||
@@ -593,6 +586,14 @@ async def check_mcp_health(mcp_type: str):
|
||||
"error": f"Connection error: {str(e)}",
|
||||
"latency": 0
|
||||
}
|
||||
except httpx.ConnectTimeout as e:
|
||||
return {
|
||||
"status": "timeout",
|
||||
"healthy": False,
|
||||
"mcp_type": mcp_type,
|
||||
"error": f"Connection timeout: {str(e)}",
|
||||
"latency": 10000
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
@@ -5,27 +5,60 @@
|
||||
* - Web UI 是 MCP 测试工具,不需要全局登录
|
||||
* - 每个 MCP 有独立的认证状态
|
||||
* - 可以同时管理多个 MCP 的登录
|
||||
*
|
||||
* 连接统计逻辑:
|
||||
* - Novel Translator (authType='none'): 健康就算已连接
|
||||
* - User/Admin MCP (authType='jwt'): 健康 + 已登录才算已连接
|
||||
*/
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { McpServerCard } from '@/components/McpServerCard';
|
||||
import { MCP_SERVERS, mcpTokenManager } from '@/lib/mcp-token-manager';
|
||||
import { MCP_SERVERS } from '@/lib/mcp-token-manager';
|
||||
|
||||
// 每个服务器的连接状态
|
||||
interface ServerConnectionStatus {
|
||||
healthy: boolean;
|
||||
loggedIn: boolean;
|
||||
}
|
||||
|
||||
export default function McpPage() {
|
||||
const [updateTrigger, setUpdateTrigger] = useState(0);
|
||||
const [loggedInCount, setLoggedInCount] = useState(0); // SSR 安全的初始值
|
||||
const [connectionStatuses, setConnectionStatuses] = useState<Record<string, ServerConnectionStatus>>({});
|
||||
|
||||
const totalCount = Object.keys(MCP_SERVERS).length;
|
||||
|
||||
// 页面加载时从 localStorage 读取登录状态
|
||||
useEffect(() => {
|
||||
setLoggedInCount(mcpTokenManager.getLoggedInMcpList().length);
|
||||
}, [updateTrigger]);
|
||||
// 计算已连接数量
|
||||
// - authType='none': 只要健康就算已连接
|
||||
// - authType='jwt': 健康 + 已登录才算已连接
|
||||
const connectedCount = Object.entries(MCP_SERVERS).reduce((count, [mcpType, config]) => {
|
||||
const status = connectionStatuses[mcpType];
|
||||
if (!status) return count; // 状态未知,不计入
|
||||
|
||||
// 强制刷新组件状态
|
||||
const refresh = () => setUpdateTrigger(prev => prev + 1);
|
||||
if (config.authType === 'none') {
|
||||
// 无需登录,只要健康就算已连接
|
||||
return status.healthy ? count + 1 : count;
|
||||
} else {
|
||||
// 需要登录,健康 + 已登录才算已连接
|
||||
return (status.healthy && status.loggedIn) ? count + 1 : count;
|
||||
}
|
||||
}, 0);
|
||||
|
||||
// 处理子组件报告的连接状态变化
|
||||
// 使用 useCallback 避免每次渲染创建新函数引用,防止子组件 useEffect 无限循环
|
||||
const handleConnectionStatusChange = useCallback((mcpType: string, status: ServerConnectionStatus) => {
|
||||
setConnectionStatuses(prev => {
|
||||
// 只在状态真正改变时才更新,避免不必要的重新渲染
|
||||
const current = prev[mcpType];
|
||||
if (current && current.healthy === status.healthy && current.loggedIn === status.loggedIn) {
|
||||
return prev; // 状态未改变,返回原对象
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[mcpType]: status
|
||||
};
|
||||
});
|
||||
}, []); // 空依赖数组,函数引用永远不变
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
@@ -62,7 +95,7 @@ export default function McpPage() {
|
||||
MCP 服务器管理
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
管理和配置您的 MCP 服务器连接。已连接 {loggedInCount}/{totalCount} 个服务器。
|
||||
管理和配置您的 MCP 服务器连接。已连接 {connectedCount}/{totalCount} 个服务器。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -73,8 +106,7 @@ export default function McpPage() {
|
||||
key={mcpType}
|
||||
mcpType={mcpType}
|
||||
config={config}
|
||||
onLoginSuccess={refresh}
|
||||
onLogoutSuccess={refresh}
|
||||
onConnectionStatusChange={handleConnectionStatusChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { MCP_SERVERS, mcpTokenManager, type McpServerConfig } from '@/lib/mcp-token-manager';
|
||||
|
||||
/**
|
||||
@@ -75,11 +75,10 @@ function DevTestAccounts({ mcpType }: { mcpType: string }) {
|
||||
interface McpServerCardProps {
|
||||
mcpType: string;
|
||||
config: McpServerConfig;
|
||||
onLoginSuccess?: () => void;
|
||||
onLogoutSuccess?: () => void;
|
||||
onConnectionStatusChange?: (mcpType: string, status: { healthy: boolean; loggedIn: boolean }) => void;
|
||||
}
|
||||
|
||||
export function McpServerCard({ mcpType, config, onLoginSuccess, onLogoutSuccess }: McpServerCardProps) {
|
||||
export function McpServerCard({ mcpType, config, onConnectionStatusChange }: McpServerCardProps) {
|
||||
const [showLoginForm, setShowLoginForm] = useState(false);
|
||||
const [showTokenForm, setShowTokenForm] = useState(false);
|
||||
const [email, setEmail] = useState('');
|
||||
@@ -152,6 +151,27 @@ export function McpServerCard({ mcpType, config, onLoginSuccess, onLogoutSuccess
|
||||
const remainingTime = mcpTokenManager.getTokenRemainingTime(mcpType);
|
||||
const hoursRemaining = Math.floor(remainingTime / (1000 * 60 * 60));
|
||||
|
||||
// 使用 ref 跟踪上次报告的状态,避免重复报告相同状态
|
||||
const lastReportedStatusRef = useRef<{ healthy: boolean | null; loggedIn: boolean }>({
|
||||
healthy: null,
|
||||
loggedIn: false
|
||||
});
|
||||
|
||||
// 向父组件报告连接状态变化
|
||||
useEffect(() => {
|
||||
if (isHealthy !== null) {
|
||||
const lastReported = lastReportedStatusRef.current;
|
||||
// 只在状态真正改变时才报告
|
||||
if (lastReported.healthy !== isHealthy || lastReported.loggedIn !== isLoggedIn) {
|
||||
lastReportedStatusRef.current = { healthy: isHealthy, loggedIn: isLoggedIn };
|
||||
onConnectionStatusChange?.(mcpType, {
|
||||
healthy: isHealthy,
|
||||
loggedIn: isLoggedIn
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [isHealthy, isLoggedIn, mcpType, onConnectionStatusChange]);
|
||||
|
||||
// 计算连接状态显示
|
||||
const getConnectionStatus = () => {
|
||||
if (isHealthy === null) {
|
||||
@@ -183,7 +203,6 @@ export function McpServerCard({ mcpType, config, onLoginSuccess, onLogoutSuccess
|
||||
setPassword('');
|
||||
// 强制刷新组件状态
|
||||
setUpdateTrigger(prev => prev + 1);
|
||||
onLoginSuccess?.();
|
||||
} else {
|
||||
setError(result.error || '登录失败');
|
||||
}
|
||||
@@ -195,7 +214,6 @@ export function McpServerCard({ mcpType, config, onLoginSuccess, onLogoutSuccess
|
||||
mcpTokenManager.logoutMcp(mcpType);
|
||||
// 强制刷新组件状态
|
||||
setUpdateTrigger(prev => prev + 1);
|
||||
onLogoutSuccess?.();
|
||||
};
|
||||
|
||||
const handleSetToken = () => {
|
||||
@@ -208,7 +226,6 @@ export function McpServerCard({ mcpType, config, onLoginSuccess, onLogoutSuccess
|
||||
setManualToken('');
|
||||
setManualUsername('');
|
||||
setUpdateTrigger(prev => prev + 1);
|
||||
onLoginSuccess?.();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user