/** * MCP 服务器卡片组件 */ 'use client'; import { useState, useEffect, useRef } from 'react'; import { MCP_SERVERS, mcpTokenManager, type McpServerConfig } from '@/lib/mcp-token-manager'; /** * 开发环境测试账号辅助组件 */ function DevTestAccounts({ mcpType }: { mcpType: string }) { const [copied, setCopied] = useState(null); // 测试账号配置 const testAccounts: Record = { 'novel-platform-user': { email: 'reader-test@example.com', password: 'ReaderTest2026@', }, 'novel-platform-admin': { email: 'admin-test@example.com', password: 'AdminTest2026@', }, }; const account = testAccounts[mcpType]; if (!account) return null; const copyToClipboard = async (text: string, type: string) => { try { await navigator.clipboard.writeText(text); setCopied(type); setTimeout(() => setCopied(null), 2000); } catch (err) { console.error('复制失败:', err); } }; return (

🛠️ 开发环境测试账号

邮箱: {account.email}
密码: ••••••••••••
); } interface McpServerCardProps { mcpType: string; config: McpServerConfig; onConnectionStatusChange?: (mcpType: string, status: { healthy: boolean; loggedIn: boolean }) => void; onEnabledChange?: (mcpType: string, enabled: boolean) => void; } export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEnabledChange }: McpServerCardProps) { const [showLoginForm, setShowLoginForm] = useState(false); const [showTokenForm, setShowTokenForm] = useState(false); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [manualToken, setManualToken] = useState(''); const [manualUsername, setManualUsername] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); const [updateTrigger, setUpdateTrigger] = useState(0); // 客户端挂载标志 - 用于避免 hydration 错误 const [mounted, setMounted] = useState(false); // 启用/禁用状态 - 初始值统一为 true,客户端挂载后从 localStorage 读取真实值 const [isEnabled, setIsEnabled] = useState(true); // 健康检查状态 const [isHealthy, setIsHealthy] = useState(null); const [isCheckingHealth, setIsCheckingHealth] = useState(false); const [latency, setLatency] = useState(null); // 监听 localStorage 变化和组件挂载 useEffect(() => { // 设置客户端挂载标志 setMounted(true); // 从 localStorage 读取真实的启用状态 const storedEnabled = mcpTokenManager.isEnabled(mcpType); console.log(`[McpServerCard ${mcpType}] Client mounted, isEnabled from localStorage: ${storedEnabled}`); setIsEnabled(storedEnabled); // 初始化时检查登录状态 const checkLoginStatus = () => { const token = localStorage.getItem(`mcp_token_${mcpType}`); const username = localStorage.getItem(`mcp_username_${mcpType}`); console.log(`[McpServerCard ${mcpType}] Initial check:`, { hasToken: !!token, tokenLength: token?.length, username }); setUpdateTrigger(prev => prev + 1); }; 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_')) { console.log(`[McpServerCard ${mcpType}] Storage changed:`, e.key); setUpdateTrigger(prev => prev + 1); } // 监听启用/禁用状态变化 if (e.key === `mcp_enabled_${mcpType}`) { const newState = e.newValue === 'true'; setIsEnabled(newState); onEnabledChange?.(mcpType, newState); } }; window.addEventListener('storage', handleStorageChange); // 定期重新检查健康状态(每 30 秒) const healthInterval = setInterval(checkServerHealth, 30000); return () => { window.removeEventListener('storage', handleStorageChange); clearInterval(healthInterval); }; }, [mcpType]); const isLoggedIn = mcpTokenManager.isLoggedIn(mcpType); const username = mcpTokenManager.getUsername(mcpType); 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 = () => { // 未挂载时显示加载状态,避免 hydration 错误 if (!mounted) { return { text: '加载中...', className: 'bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400', dotColor: 'bg-gray-300' }; } // 禁用状态优先,显示灰色 if (!isEnabled) { return { text: '已禁用', className: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400', dotColor: 'bg-gray-400' }; } 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); setError(''); const result = await mcpTokenManager.loginMcp(mcpType, email, password); if (result.success) { setShowLoginForm(false); setEmail(''); setPassword(''); // 强制刷新组件状态 setUpdateTrigger(prev => prev + 1); } else { setError(result.error || '登录失败'); } setIsLoading(false); }; const handleLogout = () => { mcpTokenManager.logoutMcp(mcpType); // 强制刷新组件状态 setUpdateTrigger(prev => prev + 1); }; const handleSetToken = () => { if (!manualToken.trim()) { setError('Token 不能为空'); return; } mcpTokenManager.saveToken(mcpType, manualToken, manualUsername || 'Token User'); setShowTokenForm(false); setManualToken(''); setManualUsername(''); setUpdateTrigger(prev => prev + 1); }; const handleToggleEnabled = () => { console.log(`[McpServerCard.handleToggleEnabled] Called for ${mcpType}, current isEnabled=${isEnabled}`); const newState = mcpTokenManager.toggleEnabled(mcpType); console.log(`[McpServerCard.handleToggleEnabled] toggleEnabled returned: ${newState}`); setIsEnabled(newState); onEnabledChange?.(mcpType, newState); // 触发自定义事件,通知 Header 等组件更新状态 window.dispatchEvent(new CustomEvent('mcp-enabled-change')); console.log(`[McpServerCard.handleToggleEnabled] Dispatched mcp-enabled-change event`); }; return (
{/* 头部区域 - 移动端优化 */}
{/* 第一行:名称 + 启用/禁用按钮 */}

{config.name}

{config.authType === 'none' ? '无需认证' : '需要登录'} {mounted && latency !== null && isHealthy && isEnabled && ( ({latency}ms) )}

{/* 启用/禁用按钮 - 移动端加大点击区域,只在客户端显示真实状态 */}
{/* 第二行:连接状态 */}
{connectionStatus.text}
{/* 描述(如果有) */} {config.description && (

{config.description}

)}
{/* 已登录状态显示 - 移动端优化,只在客户端渲染 */} {mounted && isLoggedIn && (

已登录: {username}

Token 有效期剩余约 {hoursRemaining} 小时

)} {/* 禁用提示 - 移动端优化,只在客户端渲染 */} {mounted && !isEnabled && (

此 MCP 服务器已禁用。点击右上角的"启用"按钮来启用它。

)} {mounted && showLoginForm && !isLoggedIn && isEnabled && (
setEmail(e.target.value)} required className="w-full px-3 py-2 border dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" placeholder="your@email.com" />
setPassword(e.target.value)} required className="w-full px-3 py-2 border dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" placeholder="••••••••" />
{error && (

{error}

)}
{/* 开发环境测试账号辅助信息 */} {process.env.NODE_ENV === 'development' && ( )} )} {mounted && showTokenForm && !isLoggedIn && (