route-claudecode
Version:
Advanced routing and transformation system for Claude Code outputs to multiple AI providers
804 lines (690 loc) • 30.2 kB
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>