UNPKG

route-claudecode

Version:

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

811 lines (709 loc) 26.9 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 - Statistics Dashboard</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: 1400px; 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; } .refresh-info { text-align: center; color: #888; margin-bottom: 20px; font-size: 0.9rem; } /* Bento Grid Layout */ .bento-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-auto-rows: auto; gap: 20px; grid-template-areas: "summary summary summary" "realtime providers models" "distribution distribution distribution" "performance failures-stats failures-trends" "daily history trends"; } .card { background: rgba(45, 45, 45, 0.9); backdrop-filter: blur(10px); border-radius: 16px; padding: 24px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); border: 1px solid rgba(128, 128, 128, 0.2); transition: transform 0.2s ease, box-shadow 0.2s ease; position: relative; overflow: hidden; } .card:hover { transform: translateY(-2px); box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6); } .card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: linear-gradient(90deg, #dc2626, #b91c1c, #991b1b); } .card-title { font-size: 1.2rem; font-weight: 600; margin-bottom: 16px; color: #f5f5f5; display: flex; align-items: center; gap: 8px; } .card-icon { font-size: 1.5rem; } /* Specific card styling */ .summary-card { grid-area: summary; background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%); color: white; } .summary-card .card-title { color: white; } .realtime-card { grid-area: realtime; } .providers-card { grid-area: providers; } .models-card { grid-area: models; } .distribution-card { grid-area: distribution; } .performance-card { grid-area: performance; } .daily-card { grid-area: daily; } .history-card { grid-area: history; } .failures-stats-card { grid-area: failures-stats; } .failures-trends-card { grid-area: failures-trends; } .trends-card { grid-area: trends; } /* Stats display */ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; margin-top: 16px; } .stat-item { text-align: center; padding: 16px; background: rgba(128, 128, 128, 0.1); border-radius: 12px; backdrop-filter: blur(5px); } .stat-value { font-size: 2rem; font-weight: 700; color: #dc2626; margin-bottom: 4px; } .summary-card .stat-value { color: white; } .stat-label { font-size: 0.85rem; color: #b0b0b0; text-transform: uppercase; letter-spacing: 0.5px; } .summary-card .stat-label { color: rgba(255,255,255,0.8); } /* Progress bars */ .progress-bar { width: 100%; height: 8px; background: #404040; border-radius: 4px; overflow: hidden; margin: 8px 0; } .progress-fill { height: 100%; background: linear-gradient(90deg, #dc2626, #b91c1c); border-radius: 4px; transition: width 0.3s ease; } /* Provider list */ .provider-list { margin-top: 16px; } .provider-item { display: flex; justify-content: space-between; align-items: center; padding: 12px; margin: 8px 0; background: #3a3a3a; border-radius: 8px; border-left: 4px solid #dc2626; } .provider-name { font-weight: 600; color: #f5f5f5; } .provider-stats { font-size: 0.9rem; color: #b0b0b0; } /* Status indicators */ .status-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; } .status-healthy { background: #10b981; } .status-warning { background: #f59e0b; } .status-error { background: #dc2626; } /* Failure Analysis specific styles */ .failure-trend { display: flex; align-items: center; gap: 8px; margin: 8px 0; } .trend-up { color: #dc2626; } .trend-down { color: #10b981; } .trend-stable { color: #888; } .error-category { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; margin: 4px 0; background: #3a3a3a; border-radius: 6px; border-left: 3px solid #dc2626; } .error-category.warning { border-left-color: #f59e0b; } .error-category.info { border-left-color: #888; } /* Chart placeholder */ .chart-placeholder { height: 200px; background: #3a3a3a; border-radius: 8px; display: flex; align-items: center; justify-content: center; color: #b0b0b0; font-style: italic; margin-top: 16px; } /* Loading state */ .loading { display: flex; justify-content: center; align-items: center; height: 100px; color: #b0b0b0; } .spinner { width: 20px; height: 20px; border: 2px solid #404040; border-top: 2px solid #dc2626; border-radius: 50%; animation: spin 1s linear infinite; margin-right: 10px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* Responsive design */ @media (max-width: 768px) { .bento-grid { grid-template-columns: 1fr; grid-template-areas: "summary" "realtime" "providers" "models" "distribution" "performance" "failures-stats" "failures-trends" "daily" "history"; } .header h1 { font-size: 2rem; } .stats-grid { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); } } </style> </head> <body> <div class="dashboard"> <div class="header"> <h1>📊 Claude Code Router Dashboard</h1> <p>实时监控统计 · 负载均衡分析 · 性能指标</p> </div> <div class="refresh-info"> <span id="lastUpdate">最后更新: --</span> | <span id="nextRefresh">下次刷新: 5秒</span> </div> <div class="bento-grid"> <!-- 总览卡片 --> <div class="card summary-card"> <div class="card-title"> <span class="card-icon">🎯</span> 系统总览 </div> <div class="stats-grid"> <div class="stat-item"> <div class="stat-value" id="totalRequests">--</div> <div class="stat-label">总请求数</div> </div> <div class="stat-item"> <div class="stat-value" id="totalProviders">--</div> <div class="stat-label">活跃Providers</div> </div> <div class="stat-item"> <div class="stat-value" id="totalModels">--</div> <div class="stat-label">使用模型数</div> </div> <div class="stat-item"> <div class="stat-value" id="successRate">--%</div> <div class="stat-label">成功率</div> </div> </div> </div> <!-- 实时状态 --> <div class="card realtime-card"> <div class="card-title"> <span class="card-icon"></span> 实时状态 </div> <div id="realtimeContent" class="loading"> <div class="spinner"></div> 加载中... </div> </div> <!-- Provider统计 --> <div class="card providers-card"> <div class="card-title"> <span class="card-icon">🏭</span> Provider分布 </div> <div id="providersContent" class="loading"> <div class="spinner"></div> 加载中... </div> </div> <!-- 模型统计 --> <div class="card models-card"> <div class="card-title"> <span class="card-icon">🤖</span> 模型使用 </div> <div id="modelsContent" class="loading"> <div class="spinner"></div> 加载中... </div> </div> <!-- 权重分布 --> <div class="card distribution-card"> <div class="card-title"> <span class="card-icon">⚖️</span> 权重效果分析 </div> <div id="distributionContent" class="loading"> <div class="spinner"></div> 加载中... </div> </div> <!-- 性能指标 --> <div class="card performance-card"> <div class="card-title"> <span class="card-icon">📈</span> 性能指标 </div> <div id="performanceContent" class="loading"> <div class="spinner"></div> 加载中... </div> </div> <!-- 每日统计 --> <div class="card daily-card"> <div class="card-title"> <span class="card-icon">📅</span> 今日统计 </div> <div class="chart-placeholder"> 今日请求趋势图 (开发中) </div> </div> <!-- 失败统计 --> <div class="card failures-stats-card"> <div class="card-title"> <span class="card-icon">⚠️</span> 失败分析 </div> <div id="failuresStatsContent" class="loading"> <div class="spinner"></div> 加载中... </div> </div> <!-- 失败趋势 --> <div class="card failures-trends-card"> <div class="card-title"> <span class="card-icon">📉</span> 失败趋势 </div> <div id="failuresTrendsContent" class="loading"> <div class="spinner"></div> 加载中... </div> </div> <!-- 历史记录 --> <div class="card history-card"> <div class="card-title"> <span class="card-icon">📚</span> 历史记录 </div> <div class="chart-placeholder"> 历史统计图表 (开发中) </div> </div> </div> </div> <script> let refreshTimer; let nextRefreshCountdown = 5; // 更新倒计时 function updateCountdown() { document.getElementById('nextRefresh').textContent = `下次刷新: ${nextRefreshCountdown}秒`; if (nextRefreshCountdown > 0) { nextRefreshCountdown--; setTimeout(updateCountdown, 1000); } } // 获取统计数据 async function fetchStats() { try { const response = await fetch('/api/stats'); const data = await response.json(); updateDashboard(data); document.getElementById('lastUpdate').textContent = `最后更新: ${new Date().toLocaleTimeString()}`; nextRefreshCountdown = 5; updateCountdown(); } catch (error) { console.error('Failed to fetch stats:', error); showError('获取统计数据失败'); } } // 更新仪表板 function updateDashboard(data) { // 更新总览数据 if (data.summary) { document.getElementById('totalRequests').textContent = data.summary.totalRequests || 0; document.getElementById('totalProviders').textContent = data.summary.totalProviders || 0; document.getElementById('totalModels').textContent = data.summary.totalModels || 0; document.getElementById('successRate').textContent = data.summary.overallSuccessRate ? `${data.summary.overallSuccessRate.toFixed(1)}%` : '0%'; } // 更新实时状态 updateRealtimeStatus(data.concurrency); // 更新Provider分布 updateProvidersContent(data.providers); // 更新模型使用 updateModelsContent(data.models); // 更新权重分布 updateDistributionContent(data.distribution); // 更新性能指标 updatePerformanceContent(data.performance); // 更新失败分析 updateFailuresContent(data.failures); } // 更新实时状态 function updateRealtimeStatus(concurrency) { const container = document.getElementById('realtimeContent'); if (!concurrency) { container.innerHTML = '<div style="text-align: center; color: #b0b0b0;">无并发数据</div>'; return; } let html = ''; for (const [providerId, state] of Object.entries(concurrency)) { const statusClass = state.isAvailable ? 'status-healthy' : 'status-error'; html += ` <div class="provider-item"> <div> <span class="status-indicator ${statusClass}"></span> <span class="provider-name">${providerId}</span> </div> <div class="provider-stats"> ${state.activeConnections}/${state.maxConcurrency} (${state.utilizationRate}) </div> </div> `; } container.innerHTML = html || '<div style="text-align: center; color: #b0b0b0;">暂无活动</div>'; } // 更新Provider内容 function updateProvidersContent(providers) { const container = document.getElementById('providersContent'); if (!providers || Object.keys(providers).length === 0) { container.innerHTML = '<div style="text-align: center; color: #b0b0b0;">暂无数据</div>'; return; } let html = '<div class="provider-list">'; for (const [providerId, count] of Object.entries(providers)) { html += ` <div class="provider-item"> <div class="provider-name">${providerId}</div> <div class="provider-stats">${count} 次请求</div> </div> `; } html += '</div>'; container.innerHTML = html; } // 更新模型内容 function updateModelsContent(models) { const container = document.getElementById('modelsContent'); if (!models || Object.keys(models).length === 0) { container.innerHTML = '<div style="text-align: center; color: #b0b0b0;">暂无数据</div>'; return; } let html = '<div class="provider-list">'; for (const [modelName, count] of Object.entries(models)) { const shortName = modelName.length > 30 ? modelName.substring(0, 30) + '...' : modelName; html += ` <div class="provider-item"> <div class="provider-name" title="${modelName}">${shortName}</div> <div class="provider-stats">${count} 次使用</div> </div> `; } html += '</div>'; container.innerHTML = html; } // 更新权重分布内容 function updateDistributionContent(distribution) { const container = document.getElementById('distributionContent'); if (!distribution || Object.keys(distribution).length === 0) { container.innerHTML = '<div style="text-align: center; color: #b0b0b0;">暂无权重数据</div>'; return; } let html = '<div class="provider-list">'; const total = Object.values(distribution).reduce((sum, count) => sum + count, 0); for (const [key, count] of Object.entries(distribution)) { const percentage = total > 0 ? ((count / total) * 100).toFixed(1) : 0; html += ` <div class="provider-item"> <div class="provider-name">${key}</div> <div class="provider-stats"> ${count} 次 (${percentage}%) <div class="progress-bar"> <div class="progress-fill" style="width: ${percentage}%"></div> </div> </div> </div> `; } html += '</div>'; container.innerHTML = html; } // 更新性能指标 function updatePerformanceContent(performance) { const container = document.getElementById('performanceContent'); if (!performance) { container.innerHTML = '<div style="text-align: center; color: #b0b0b0;">暂无性能数据</div>'; return; } const html = ` <div class="stats-grid"> <div class="stat-item"> <div class="stat-value">${performance.avgResponseTime || 0}ms</div> <div class="stat-label">平均响应时间</div> </div> <div class="stat-item"> <div class="stat-value">${performance.requestsPerMinute || 0}</div> <div class="stat-label">每分钟请求</div> </div> </div> `; container.innerHTML = html; } // 更新失败分析内容 function updateFailuresContent(failuresData) { updateFailuresStats(failuresData?.stats); updateFailuresTrends(failuresData?.trends); } // 更新失败统计 function updateFailuresStats(stats) { const container = document.getElementById('failuresStatsContent'); if (!stats) { container.innerHTML = '<div style="text-align: center; color: #b0b0b0;">暂无失败数据</div>'; return; } let html = ` <div class="stats-grid"> <div class="stat-item"> <div class="stat-value">${stats.totalFailures || 0}</div> <div class="stat-label">24H失败总数</div> </div> <div class="stat-item"> <div class="stat-value">${stats.avgFailureDuration || 0}ms</div> <div class="stat-label">平均失败时长</div> </div> </div> `; // 按错误类型显示失败 if (stats.failuresByError && Object.keys(stats.failuresByError).length > 0) { html += '<div style="margin-top: 16px;"><strong>错误类型分布:</strong></div>'; for (const [errorType, count] of Object.entries(stats.failuresByError)) { const severity = errorType.includes('Server') ? 'error' : errorType.includes('Rate') ? 'warning' : 'info'; html += ` <div class="error-category ${severity}"> <span>${errorType}</span> <span>${count} 次</span> </div> `; } } container.innerHTML = html; } // 更新失败趋势 function updateFailuresTrends(trends) { const container = document.getElementById('failuresTrendsContent'); if (!trends) { container.innerHTML = '<div style="text-align: center; color: #b0b0b0;">暂无趋势数据</div>'; return; } const trendIcon = trends.trendAnalysis?.isIncreasing ? '📈' : '📉'; const trendClass = trends.trendAnalysis?.isIncreasing ? 'trend-up' : 'trend-down'; const changePercentage = trends.trendAnalysis?.changePercentage || 0; let html = ` <div class="failure-trend"> <span style="font-size: 1.5rem;">${trendIcon}</span> <div> <div class="${trendClass}" style="font-weight: 600;"> ${changePercentage > 0 ? '+' : ''}${changePercentage}% </div> <div style="font-size: 0.8rem; color: #b0b0b0;">7天趋势变化</div> </div> </div> <div style="margin-top: 12px; padding: 12px; background: #3a3a3a; border-radius: 8px; font-size: 0.9rem;"> <strong>建议:</strong> ${trends.trendAnalysis?.recommendation || '无建议'} </div> `; // 显示每日失败数据 if (trends.dailyFailures) { html += '<div style="margin-top: 16px;"><strong>每日失败统计:</strong></div>'; const dates = Object.keys(trends.dailyFailures).sort().slice(-3); // 显示最近3天 for (const date of dates) { const count = trends.dailyFailures[date]; const shortDate = date.substring(5); // MM-DD html += ` <div class="provider-item"> <span>${shortDate}</span> <span>${count} 次失败</span> </div> `; } } container.innerHTML = html; } // 显示错误 function showError(message) { const containers = ['realtimeContent', 'providersContent', 'modelsContent', 'distributionContent', 'failuresStatsContent', 'failuresTrendsContent']; containers.forEach(id => { const element = document.getElementById(id); if (element && element.classList.contains('loading')) { element.innerHTML = `<div style="text-align: center; color: #dc2626;">${message}</div>`; } }); } // 启动定时刷新 function startAutoRefresh() { fetchStats(); // 立即加载一次 refreshTimer = setInterval(fetchStats, 5000); // 每5秒刷新 } // 页面加载完成后启动 window.addEventListener('load', startAutoRefresh); // 页面关闭前清理定时器 window.addEventListener('beforeunload', () => { if (refreshTimer) { clearInterval(refreshTimer); } }); </script> </body> </html>