UNPKG

route-claudecode

Version:

Advanced routing and transformation system for Claude Code outputs to multiple AI providers

804 lines (690 loc) 30.2 kB
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Claude Code Router - Dual Server Monitoring</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #2d2d2d 0%, #1a1a1a 100%); min-height: 100vh; padding: 20px; color: #f5f5f5; } .dashboard { max-width: 1600px; margin: 0 auto; } .header { text-align: center; margin-bottom: 30px; } .header h1 { color: #f5f5f5; font-size: 2.5rem; font-weight: 700; text-shadow: 0 2px 4px rgba(0,0,0,0.3); margin-bottom: 10px; } .header p { color: #b0b0b0; font-size: 1.1rem; } .server-tabs { display: flex; justify-content: center; margin-bottom: 30px; gap: 10px; } .tab-button { padding: 12px 24px; background: rgba(45, 45, 45, 0.7); border: none; border-radius: 8px; color: #f5f5f5; cursor: pointer; font-size: 1rem; font-weight: 500; transition: all 0.2s ease; border: 1px solid rgba(128, 128, 128, 0.2); } .tab-button:hover { background: rgba(55, 55, 55, 0.9); transform: translateY(-1px); } .tab-button.active { background: linear-gradient(90deg, #dc2626, #b91c1c); box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3); } .server-section { display: none; } .server-section.active { display: block; } .server-status { display: flex; justify-content: center; gap: 30px; margin-bottom: 30px; flex-wrap: wrap; } .status-card { background: rgba(45, 45, 45, 0.9); border-radius: 12px; padding: 20px; text-align: center; min-width: 200px; border: 1px solid rgba(128, 128, 128, 0.2); } .status-indicator { width: 12px; height: 12px; border-radius: 50%; display: inline-block; margin-right: 8px; } .status-online { background: #10b981; box-shadow: 0 0 10px rgba(16, 185, 129, 0.5); } .status-offline { background: #ef4444; box-shadow: 0 0 10px rgba(239, 68, 68, 0.5); } .server-info { font-size: 0.9rem; color: #b0b0b0; margin-top: 8px; } .model-controls { background: rgba(45, 45, 45, 0.9); border-radius: 12px; padding: 24px; margin-bottom: 30px; border: 1px solid rgba(128, 128, 128, 0.2); } .model-controls h3 { margin-bottom: 20px; color: #f5f5f5; font-size: 1.3rem; } .category-section { margin-bottom: 20px; } .category-title { font-weight: 600; color: #10b981; margin-bottom: 10px; font-size: 1.1rem; } .model-toggle { display: flex; align-items: center; justify-content: space-between; padding: 12px; background: rgba(60, 60, 60, 0.5); border-radius: 8px; margin-bottom: 8px; border: 1px solid rgba(100, 100, 100, 0.3); } .model-info { flex: 1; } .model-name { font-weight: 500; color: #f5f5f5; } .provider-name { font-size: 0.85rem; color: #888; margin-top: 2px; } .toggle-switch { position: relative; width: 50px; height: 24px; background: #4b5563; border-radius: 12px; cursor: pointer; transition: background 0.2s ease; } .toggle-switch.enabled { background: #10b981; } .toggle-switch.disabled { background: #ef4444; } .toggle-switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; background: white; border-radius: 50%; transition: transform 0.2s ease; } .toggle-switch.enabled::after { transform: translateX(26px); } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; } .stats-card { background: rgba(45, 45, 45, 0.9); border-radius: 16px; padding: 24px; border: 1px solid rgba(128, 128, 128, 0.2); position: relative; overflow: hidden; } .stats-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: linear-gradient(90deg, #dc2626, #b91c1c, #991b1b); } .stats-title { font-size: 1.2rem; font-weight: 600; margin-bottom: 16px; color: #f5f5f5; } .stats-value { font-size: 2rem; font-weight: 700; color: #10b981; margin-bottom: 8px; } .stats-label { color: #b0b0b0; font-size: 0.9rem; } .refresh-info { text-align: center; color: #888; margin-bottom: 20px; font-size: 0.9rem; } .error-message { background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 8px; padding: 16px; color: #fca5a5; text-align: center; margin: 20px 0; } .loading { text-align: center; color: #888; font-style: italic; padding: 40px; } @media (max-width: 768px) { .server-status { flex-direction: column; align-items: center; } .stats-grid { grid-template-columns: 1fr; } .server-tabs { flex-direction: column; align-items: center; } } </style> </head> <body> <div class="dashboard"> <div class="header"> <h1>🚀 Claude Code Router</h1> <p>Dual Server Monitoring Dashboard</p> </div> <div class="refresh-info"> <span id="refresh-status">数据每30秒自动更新</span> | <span id="last-update">最后更新: --</span> </div> <div class="server-tabs"> <button class="tab-button active" onclick="switchServer('development', 3456)"> Development Server (3456) </button> <button class="tab-button" onclick="switchServer('release', 8888)"> Release Server (8888) </button> <button class="tab-button" onclick="switchServer('compare', 'both')"> Compare Both </button> </div> <div id="development-section" class="server-section active"> <div class="server-status"> <div class="status-card"> <h3> <span class="status-indicator" id="dev-status-indicator"></span> Development Server </h3> <div class="server-info" id="dev-server-info">检查中...</div> </div> </div> <div class="model-controls"> <h3>🎛️ Model Controls - Development</h3> <div id="dev-model-controls">加载中...</div> </div> <div class="stats-grid" id="dev-stats-grid"> <div class="loading">加载统计数据中...</div> </div> </div> <div id="release-section" class="server-section"> <div class="server-status"> <div class="status-card"> <h3> <span class="status-indicator" id="release-status-indicator"></span> Release Server </h3> <div class="server-info" id="release-server-info">检查中...</div> </div> </div> <div class="model-controls"> <h3>🎛️ Model Controls - Release</h3> <div id="release-model-controls">加载中...</div> </div> <div class="stats-grid" id="release-stats-grid"> <div class="loading">加载统计数据中...</div> </div> </div> <div id="compare-section" class="server-section"> <div class="server-status"> <div class="status-card"> <h3> <span class="status-indicator" id="both-dev-indicator"></span> Development (3456) </h3> <div class="server-info" id="both-dev-info">检查中...</div> </div> <div class="status-card"> <h3> <span class="status-indicator" id="both-release-indicator"></span> Release (8888) </h3> <div class="server-info" id="both-release-info">检查中...</div> </div> </div> <div class="stats-grid" id="compare-stats-grid"> <div class="loading">加载对比数据中...</div> </div> </div> </div> <script> let currentServer = 'development'; let currentPort = 3456; let refreshInterval; const serverConfigs = { development: { port: 3456, name: 'Development' }, release: { port: 8888, name: 'Release' } }; function switchServer(serverType, port) { // Remove active class from all tabs document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); // Add active class to clicked tab event.target.classList.add('active'); // Hide all sections document.querySelectorAll('.server-section').forEach(section => section.classList.remove('active')); // Show selected section document.getElementById(`${serverType}-section`).classList.add('active'); currentServer = serverType; currentPort = port; if (serverType !== 'compare') { loadServerData(serverType, port); } else { loadComparisonData(); } } async function loadServerData(serverType, port) { try { const [statusResponse, statsResponse] = await Promise.all([ fetch(`http://localhost:${port}/status`), fetch(`http://localhost:${port}/api/stats`) ]); const status = await statusResponse.json(); const stats = await statsResponse.json(); updateServerStatus(serverType, status, true); updateModelControls(serverType, status); updateServerStats(serverType, stats); } catch (error) { console.error(`Failed to load ${serverType} server data:`, error); updateServerStatus(serverType, null, false); showError(serverType, `无法连接到 ${serverType} 服务器 (端口 ${port})`); } } async function loadComparisonData() { const promises = Object.entries(serverConfigs).map(async ([type, config]) => { try { const [statusResponse, statsResponse] = await Promise.all([ fetch(`http://localhost:${config.port}/status`), fetch(`http://localhost:${config.port}/api/stats`) ]); return { type, status: await statusResponse.json(), stats: await statsResponse.json(), online: true }; } catch (error) { return { type, status: null, stats: null, online: false, error: error.message }; } }); const results = await Promise.all(promises); updateComparisonView(results); } function updateServerStatus(serverType, status, online) { const indicator = document.getElementById(`${serverType}-status-indicator`); const info = document.getElementById(`${serverType}-server-info`); if (online && status) { indicator.className = 'status-indicator status-online'; info.innerHTML = ` 端口: ${status.server === 'claude-code-router' ? currentPort : 'Unknown'}<br> 运行时间: ${Math.floor(status.uptime)}秒<br> Providers: ${status.providers.length}个<br> 调试模式: ${status.debug ? '开启' : '关闭'} `; } else { indicator.className = 'status-indicator status-offline'; info.innerHTML = '服务器离线'; } } function updateModelControls(serverType, status) { const container = document.getElementById(`${serverType}-model-controls`); if (!status || !status.routing) { container.innerHTML = '<div class="error-message">无法加载模型配置</div>'; return; } let html = ''; Object.entries(status.routing.routing).forEach(([category, config]) => { html += ` <div class="category-section"> <div class="category-title">${category.toUpperCase()}</div> `; if (config.providers) { config.providers.forEach((provider, index) => { const providerHealth = status.routing.providerHealth[provider.provider]; const isBlacklisted = providerHealth?.isTemporarilyBlacklisted; const isDisabledByUser = providerHealth?.isTemporarilyDisabledByUser; const isEnabled = !isBlacklisted && !isDisabledByUser; let statusText = ''; if (isDisabledByUser) { statusText = ' - 用户禁用'; } else if (isBlacklisted) { statusText = ' - 系统拉黑'; } html += ` <div class="model-toggle"> <div class="model-info"> <div class="model-name">${provider.model}</div> <div class="provider-name">${provider.provider} (权重: ${provider.weight || 100})${statusText}</div> </div> <div class="toggle-switch ${isEnabled ? 'enabled' : 'disabled'}" onclick="toggleModel('${serverType}', '${category}', '${provider.provider}', '${provider.model}', ${isEnabled})"> </div> </div> `; }); } else if (config.provider) { const providerHealth = status.routing.providerHealth[config.provider]; const isBlacklisted = providerHealth?.isTemporarilyBlacklisted; const isDisabledByUser = providerHealth?.isTemporarilyDisabledByUser; const isEnabled = !isBlacklisted && !isDisabledByUser; let statusText = ''; if (isDisabledByUser) { statusText = ' - 用户禁用'; } else if (isBlacklisted) { statusText = ' - 系统拉黑'; } html += ` <div class="model-toggle"> <div class="model-info"> <div class="model-name">${config.model}</div> <div class="provider-name">${config.provider}${statusText}</div> </div> <div class="toggle-switch ${isEnabled ? 'enabled' : 'disabled'}" onclick="toggleModel('${serverType}', '${category}', '${config.provider}', '${config.model}', ${isEnabled})"> </div> </div> `; } html += '</div>'; }); container.innerHTML = html; } function updateServerStats(serverType, stats) { const container = document.getElementById(`${serverType}-stats-grid`); if (!stats) { container.innerHTML = '<div class="error-message">无法加载统计数据</div>'; return; } const html = ` <div class="stats-card"> <div class="stats-title">📊 总请求数</div> <div class="stats-value">${stats.summary?.totalRequests || 0}</div> <div class="stats-label">自启动以来</div> </div> <div class="stats-card"> <div class="stats-title">✅ 成功率</div> <div class="stats-value">${(stats.summary?.overallSuccessRate || 0).toFixed(1)}%</div> <div class="stats-label">总体成功率</div> </div> <div class="stats-card"> <div class="stats-title">🚀 活跃Providers</div> <div class="stats-value">${stats.summary?.totalProviders || 0}</div> <div class="stats-label">当前可用</div> </div> <div class="stats-card"> <div class="stats-title">🤖 模型数量</div> <div class="stats-value">${stats.summary?.totalModels || 0}</div> <div class="stats-label">已配置模型</div> </div> <div class="stats-card"> <div class="stats-title">⚡ 平均响应</div> <div class="stats-value">${Math.round(stats.summary?.averageResponseTime || 0)}ms</div> <div class="stats-label">响应时间</div> </div> <div class="stats-card"> <div class="stats-title">🔄 运行时间</div> <div class="stats-value">${Math.floor((stats.summary?.runtimeMinutes || 0))}m</div> <div class="stats-label">持续运行</div> </div> `; container.innerHTML = html; } function updateComparisonView(results) { // Update individual server indicators in comparison view results.forEach(result => { const devIndicator = document.getElementById('both-dev-indicator'); const releaseIndicator = document.getElementById('both-release-indicator'); const devInfo = document.getElementById('both-dev-info'); const releaseInfo = document.getElementById('both-release-info'); if (result.type === 'development') { devIndicator.className = `status-indicator ${result.online ? 'status-online' : 'status-offline'}`; devInfo.textContent = result.online ? `运行中 - ${result.status.providers.length} providers` : '离线'; } else if (result.type === 'release') { releaseIndicator.className = `status-indicator ${result.online ? 'status-online' : 'status-offline'}`; releaseInfo.textContent = result.online ? `运行中 - ${result.status.providers.length} providers` : '离线'; } }); // Create comparison stats const container = document.getElementById('compare-stats-grid'); let html = ''; results.forEach(result => { if (result.online && result.stats) { html += ` <div class="stats-card"> <div class="stats-title">📊 ${result.type === 'development' ? 'Development' : 'Release'} Server</div> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 15px;"> <div> <div style="font-size: 1.2rem; color: #10b981;">${result.stats.summary?.totalRequests || 0}</div> <div style="font-size: 0.8rem; color: #888;">总请求</div> </div> <div> <div style="font-size: 1.2rem; color: #10b981;">${(result.stats.summary?.overallSuccessRate || 0).toFixed(1)}%</div> <div style="font-size: 0.8rem; color: #888;">成功率</div> </div> <div> <div style="font-size: 1.2rem; color: #10b981;">${result.stats.summary?.totalProviders || 0}</div> <div style="font-size: 0.8rem; color: #888;">Providers</div> </div> <div> <div style="font-size: 1.2rem; color: #10b981;">${Math.round(result.stats.summary?.averageResponseTime || 0)}ms</div> <div style="font-size: 0.8rem; color: #888;">平均响应</div> </div> </div> </div> `; } else { html += ` <div class="stats-card"> <div class="stats-title">❌ ${result.type === 'development' ? 'Development' : 'Release'} Server</div> <div class="error-message">服务器离线或无法连接</div> </div> `; } }); container.innerHTML = html; } async function toggleModel(serverType, category, provider, model, currentlyEnabled) { const port = serverType === 'development' ? 3456 : 8888; const action = currentlyEnabled ? 'disable' : 'enable'; // 防止重复点击 const toggleElement = event.target; if (toggleElement.classList.contains('loading')) { return; // 正在处理中,忽略重复点击 } // 取消自动刷新避免冲突 if (refreshInterval) { clearInterval(refreshInterval); } try { // 显示加载状态 const originalClass = toggleElement.className; toggleElement.className = 'toggle-switch loading'; toggleElement.style.opacity = '0.6'; toggleElement.style.pointerEvents = 'none'; // 禁用点击 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时 const response = await fetch(`http://localhost:${port}/api/providers/${provider}/${action}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = await response.json(); if (result.success) { // 成功后刷新数据 console.log(`✅ ${provider} ${action}d successfully:`, result.message); // 立即更新UI状态 const newEnabled = !currentlyEnabled; toggleElement.className = `toggle-switch ${newEnabled ? 'enabled' : 'disabled'}`; toggleElement.style.opacity = '1'; toggleElement.setAttribute('onclick', `toggleModel('${serverType}', '${category}', '${provider}', '${model}', ${newEnabled})`); // 更新provider状态文本 const providerNameElement = toggleElement.parentElement.querySelector('.provider-name'); let baseText = providerNameElement.textContent.replace(/ - (用户禁用|系统拉黑)/, ''); if (!newEnabled) { baseText += ' - 用户禁用'; } providerNameElement.textContent = baseText; // 1秒后刷新完整数据确保同步,然后重新启动自动刷新 setTimeout(() => { if (currentServer === serverType) { loadServerData(serverType, port); } else if (currentServer === 'compare') { loadComparisonData(); } // 重新启动自动刷新 startAutoRefresh(); }, 1000); } else { throw new Error(result.error || 'Unknown error'); } } catch (error) { console.error(`❌ Failed to ${action} ${provider}:`, error); // 恢复原始状态 toggleElement.className = originalClass; toggleElement.style.opacity = '1'; toggleElement.style.pointerEvents = 'auto'; // 重新启动自动刷新 startAutoRefresh(); // 处理不同类型的错误 let errorMessage = `操作失败: ${error.message}`; if (error.name === 'AbortError') { errorMessage = '操作超时,请检查网络连接'; } else if (error.message.includes('Failed to fetch')) { errorMessage = '网络连接失败,请检查服务器状态'; } // 使用更友好的错误提示 console.error(errorMessage); // 注释掉alert避免打断用户体验 // alert(`${errorMessage}\n\n服务器: ${serverType} (${port})\nProvider: ${provider}\n操作: ${action}`); } } function showError(serverType, message) { const container = document.getElementById(`${serverType}-stats-grid`); container.innerHTML = `<div class="error-message">${message}</div>`; } function updateLastUpdateTime() { document.getElementById('last-update').textContent = `最后更新: ${new Date().toLocaleTimeString()}`; } function startAutoRefresh() { // 清除现有的定时器 if (refreshInterval) { clearInterval(refreshInterval); } // 启动新的自动刷新 refreshInterval = setInterval(() => { if (currentServer === 'compare') { loadComparisonData(); } else if (currentServer !== 'compare') { loadServerData(currentServer, currentPort); } updateLastUpdateTime(); }, 30000); // Refresh every 30 seconds } // Initialize the dashboard function init() { loadServerData('development', 3456); updateLastUpdateTime(); startAutoRefresh(); } // Start the dashboard init(); </script> </body> </html>