|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<title>Gemini API 代理服务</title> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<style> |
|
|
body { |
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; |
|
|
max-width: 1200px; |
|
|
margin: 0 auto; |
|
|
padding: 20px; |
|
|
line-height: 1.6; |
|
|
background-color: #f8f9fa; |
|
|
} |
|
|
h1, h2, h3 { |
|
|
color: #333; |
|
|
text-align: center; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
.info-box { |
|
|
background-color: #fff; |
|
|
border: 1px solid #dee2e6; |
|
|
border-radius: 8px; |
|
|
padding: 20px; |
|
|
margin-bottom: 20px; |
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05); |
|
|
} |
|
|
.status { |
|
|
color: #28a745; |
|
|
font-weight: bold; |
|
|
font-size: 18px; |
|
|
margin-bottom: 20px; |
|
|
text-align: center; |
|
|
} |
|
|
.stats-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(3, 1fr); |
|
|
gap: 15px; |
|
|
margin-top: 15px; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
.stat-card { |
|
|
background-color: #e9ecef; |
|
|
padding: 15px; |
|
|
border-radius: 8px; |
|
|
text-align: center; |
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05); |
|
|
transition: transform 0.2s; |
|
|
} |
|
|
.stat-card:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1); |
|
|
} |
|
|
.stat-value { |
|
|
font-size: 24px; |
|
|
font-weight: bold; |
|
|
color: #007bff; |
|
|
} |
|
|
.stat-label { |
|
|
font-size: 14px; |
|
|
color: #6c757d; |
|
|
margin-top: 5px; |
|
|
} |
|
|
.section-title { |
|
|
color: #495057; |
|
|
border-bottom: 1px solid #dee2e6; |
|
|
padding-bottom: 10px; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
.log-container { |
|
|
background-color: #f5f5f5; |
|
|
border: 1px solid #ddd; |
|
|
border-radius: 8px; |
|
|
padding: 15px; |
|
|
margin-top: 20px; |
|
|
max-height: 500px; |
|
|
overflow-y: auto; |
|
|
font-family: monospace; |
|
|
font-size: 14px; |
|
|
line-height: 1.5; |
|
|
} |
|
|
.log-entry { |
|
|
margin-bottom: 8px; |
|
|
padding: 8px; |
|
|
border-radius: 4px; |
|
|
} |
|
|
.log-entry.INFO { |
|
|
background-color: #e8f4f8; |
|
|
border-left: 4px solid #17a2b8; |
|
|
} |
|
|
.log-entry.WARNING { |
|
|
background-color: #fff3cd; |
|
|
border-left: 4px solid #ffc107; |
|
|
} |
|
|
.log-entry.ERROR { |
|
|
background-color: #f8d7da; |
|
|
border-left: 4px solid #dc3545; |
|
|
} |
|
|
.log-entry.DEBUG { |
|
|
background-color: #d1ecf1; |
|
|
border-left: 4px solid #17a2b8; |
|
|
} |
|
|
.log-timestamp { |
|
|
color: #6c757d; |
|
|
font-size: 12px; |
|
|
margin-right: 10px; |
|
|
} |
|
|
.log-level { |
|
|
font-weight: bold; |
|
|
margin-right: 10px; |
|
|
} |
|
|
.log-level.INFO { |
|
|
color: #17a2b8; |
|
|
} |
|
|
.log-level.WARNING { |
|
|
color: #ffc107; |
|
|
} |
|
|
.log-level.ERROR { |
|
|
color: #dc3545; |
|
|
} |
|
|
.log-level.DEBUG { |
|
|
color: #17a2b8; |
|
|
} |
|
|
.log-message { |
|
|
color: #212529; |
|
|
} |
|
|
.refresh-button { |
|
|
display: block; |
|
|
margin: 20px auto; |
|
|
padding: 10px 20px; |
|
|
background-color: #007bff; |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 4px; |
|
|
font-size: 16px; |
|
|
cursor: pointer; |
|
|
transition: background-color 0.2s; |
|
|
} |
|
|
.refresh-button:hover { |
|
|
background-color: #0069d9; |
|
|
} |
|
|
.log-filter { |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
margin-bottom: 15px; |
|
|
gap: 10px; |
|
|
} |
|
|
.log-filter button { |
|
|
padding: 5px 10px; |
|
|
border: 1px solid #ddd; |
|
|
border-radius: 4px; |
|
|
background-color: #f8f9fa; |
|
|
cursor: pointer; |
|
|
} |
|
|
.log-filter button.active { |
|
|
background-color: #007bff; |
|
|
color: white; |
|
|
border-color: #007bff; |
|
|
} |
|
|
|
|
|
|
|
|
.api-key-stats-container { |
|
|
margin-top: 20px; |
|
|
} |
|
|
|
|
|
.api-key-stats-list { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(3, 1fr); |
|
|
gap: 15px; |
|
|
margin-top: 15px; |
|
|
} |
|
|
|
|
|
|
|
|
@media (max-width: 992px) { |
|
|
.api-key-stats-list { |
|
|
grid-template-columns: repeat(2, 1fr); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@media (max-width: 576px) { |
|
|
.api-key-stats-list { |
|
|
grid-template-columns: 1fr; |
|
|
} |
|
|
} |
|
|
|
|
|
.api-key-item { |
|
|
background-color: #f8f9fa; |
|
|
border-radius: 8px; |
|
|
padding: 15px; |
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05); |
|
|
} |
|
|
|
|
|
.api-key-header { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
margin-bottom: 10px; |
|
|
} |
|
|
|
|
|
.api-key-name { |
|
|
font-weight: bold; |
|
|
color: #495057; |
|
|
} |
|
|
|
|
|
.api-key-usage { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.api-key-count { |
|
|
font-weight: bold; |
|
|
color: #007bff; |
|
|
} |
|
|
|
|
|
.progress-container { |
|
|
width: 100%; |
|
|
height: 10px; |
|
|
background-color: #e9ecef; |
|
|
border-radius: 5px; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.progress-bar { |
|
|
height: 100%; |
|
|
border-radius: 5px; |
|
|
transition: width 0.3s ease; |
|
|
} |
|
|
|
|
|
.progress-bar.low { |
|
|
background-color: #28a745; |
|
|
} |
|
|
|
|
|
.progress-bar.medium { |
|
|
background-color: #ffc107; |
|
|
} |
|
|
|
|
|
.progress-bar.high { |
|
|
background-color: #dc3545; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<h1>🤖 Gemini API 代理服务</h1> |
|
|
|
|
|
<div class="info-box"> |
|
|
<h2 class="section-title">🟢 运行状态</h2> |
|
|
<p class="status">服务运行中</p> |
|
|
|
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-value">{{ key_count }}</div> |
|
|
<div class="stat-label">可用API密钥数量</div> |
|
|
</div> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-value">{{ model_count }}</div> |
|
|
<div class="stat-label">可用模型数量</div> |
|
|
</div> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-value">{{ retry_count }}</div> |
|
|
<div class="stat-label">最大重试次数</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<h3 class="section-title">API调用统计</h3> |
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-value" id="last-24h-calls">{{ last_24h_calls }}</div> |
|
|
<div class="stat-label">24小时内调用次数</div> |
|
|
</div> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-value" id="hourly-calls">{{ hourly_calls }}</div> |
|
|
<div class="stat-label">一小时内调用次数</div> |
|
|
</div> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-value" id="minute-calls">{{ minute_calls }}</div> |
|
|
<div class="stat-label">一分钟内调用次数</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="api-key-stats-container"> |
|
|
<h3 class="section-title" style="cursor: pointer; user-select: none;" onclick="toggleApiKeyStats()"> |
|
|
API密钥使用统计 <span id="toggle-icon">▼</span> |
|
|
</h3> |
|
|
<div id="api-key-stats" style="display: block;"> |
|
|
<div class="api-key-stats-list" id="api-key-stats-list"> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="info-box"> |
|
|
<h2 class="section-title">⚙️ 环境配置</h2> |
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-value">{{ max_requests_per_minute }}</div> |
|
|
<div class="stat-label">每分钟请求限制</div> |
|
|
</div> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-value">{{ max_requests_per_day_per_ip }}</div> |
|
|
<div class="stat-label">每IP每日请求限制</div> |
|
|
</div> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-value" id="current-time">{{ current_time }}</div> |
|
|
<div class="stat-label">当前服务器时间</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="info-box"> |
|
|
<h2 class="section-title">📦 版本信息</h2> |
|
|
<div class="version-info" style="text-align: center; margin-bottom: 15px;"> |
|
|
<div style="font-size: 18px; margin-bottom: 10px;"> |
|
|
当前版本: <span style="font-weight: bold; color: #007bff;">{{ local_version }}</span> |
|
|
</div> |
|
|
{% if has_update %} |
|
|
<div style="display: flex; align-items: center; justify-content: center; margin-top: 15px;"> |
|
|
<div style="background-color: #fef6e0; border: 1px solid #ffeeba; border-radius: 4px; padding: 10px 15px; display: inline-flex; align-items: center;"> |
|
|
<span style="color: #ff9800; margin-right: 10px;"> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
|
|
<circle cx="12" cy="12" r="10"></circle> |
|
|
<line x1="12" y1="8" x2="12" y2="12"></line> |
|
|
<line x1="12" y1="16" x2="12.01" y2="16"></line> |
|
|
</svg> |
|
|
</span> |
|
|
<span> |
|
|
<strong>发现新版本!</strong> 最新版本: <span style="font-weight: bold; color: #28a745;">{{ remote_version }}</span> |
|
|
</span> |
|
|
</div> |
|
|
</div> |
|
|
{% endif %} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="info-box"> |
|
|
<h2 class="section-title"> 系统日志</h2> |
|
|
<div class="log-filter"> |
|
|
<button class="active" data-level="ALL">全部</button> |
|
|
<button data-level="INFO">信息</button> |
|
|
<button data-level="WARNING">警告</button> |
|
|
<button data-level="ERROR">错误</button> |
|
|
</div> |
|
|
<div class="log-container" id="log-container"> |
|
|
{% for log in logs %} |
|
|
<div class="log-entry {{ log.level }}" data-level="{{ log.level }}"> |
|
|
<span class="log-timestamp">{{ log.timestamp }}</span> |
|
|
<span class="log-level {{ log.level }}">{{ log.level }}</span> |
|
|
<span class="log-message"> |
|
|
{% if log.key != 'N/A' %}[{{ log.key }}]{% endif %} |
|
|
{% if log.request_type != 'N/A' %}{{ log.request_type }}{% endif %} |
|
|
{% if log.model != 'N/A' %}[{{ log.model }}]{% endif %} |
|
|
{% if log.status_code != 'N/A' %}{{ log.status_code }}{% endif %} |
|
|
: {{ log.message }} |
|
|
{% if log.error_message %} |
|
|
- {{ log.error_message }} |
|
|
{% endif %} |
|
|
</span> |
|
|
</div> |
|
|
{% endfor %} |
|
|
</div> |
|
|
<button class="refresh-button" id="refresh-button">刷新数据</button> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
document.querySelectorAll('.log-filter button').forEach(button => { |
|
|
button.addEventListener('click', function() { |
|
|
|
|
|
document.querySelectorAll('.log-filter button').forEach(btn => { |
|
|
btn.classList.remove('active'); |
|
|
}); |
|
|
|
|
|
|
|
|
this.classList.add('active'); |
|
|
|
|
|
const level = this.getAttribute('data-level'); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.log-entry').forEach(entry => { |
|
|
if (level === 'ALL' || entry.getAttribute('data-level') === level) { |
|
|
entry.style.display = 'block'; |
|
|
} else { |
|
|
entry.style.display = 'none'; |
|
|
} |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
let refreshInterval; |
|
|
const refreshRate = 1000; |
|
|
let isRefreshing = false; |
|
|
|
|
|
|
|
|
function startAutoRefresh() { |
|
|
if (!refreshInterval) { |
|
|
refreshInterval = setInterval(fetchDashboardData, refreshRate); |
|
|
console.log('自动刷新已启动'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function stopAutoRefresh() { |
|
|
if (refreshInterval) { |
|
|
clearInterval(refreshInterval); |
|
|
refreshInterval = null; |
|
|
console.log('自动刷新已停止'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function fetchDashboardData() { |
|
|
if (isRefreshing) return; |
|
|
|
|
|
isRefreshing = true; |
|
|
try { |
|
|
const response = await fetch('/api/dashboard-data'); |
|
|
if (!response.ok) { |
|
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
|
} |
|
|
const data = await response.json(); |
|
|
updateDashboard(data); |
|
|
} catch (error) { |
|
|
console.error('获取数据失败:', error); |
|
|
} finally { |
|
|
isRefreshing = false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function updateDashboard(data) { |
|
|
|
|
|
document.getElementById('current-time').textContent = data.current_time; |
|
|
|
|
|
|
|
|
document.getElementById('last-24h-calls').textContent = data.last_24h_calls; |
|
|
document.getElementById('hourly-calls').textContent = data.hourly_calls; |
|
|
document.getElementById('minute-calls').textContent = data.minute_calls; |
|
|
|
|
|
|
|
|
if (data.api_key_stats) { |
|
|
updateApiKeyStats(data.api_key_stats); |
|
|
} |
|
|
|
|
|
|
|
|
updateLogs(data.logs); |
|
|
} |
|
|
|
|
|
|
|
|
function updateApiKeyStats(apiKeyStats) { |
|
|
const container = document.getElementById('api-key-stats-list'); |
|
|
container.innerHTML = ''; |
|
|
|
|
|
if (!apiKeyStats || apiKeyStats.length === 0) { |
|
|
container.innerHTML = '<div class="api-key-item">没有API密钥使用数据</div>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
apiKeyStats.forEach(stat => { |
|
|
const item = document.createElement('div'); |
|
|
item.className = 'api-key-item'; |
|
|
|
|
|
|
|
|
let barClass = 'low'; |
|
|
if (stat.usage_percent > 75) { |
|
|
barClass = 'high'; |
|
|
} else if (stat.usage_percent > 50) { |
|
|
barClass = 'medium'; |
|
|
} |
|
|
|
|
|
item.innerHTML = ` |
|
|
<div class="api-key-header"> |
|
|
<div class="api-key-name">API密钥: ${stat.api_key}</div> |
|
|
<div class="api-key-usage"> |
|
|
<span class="api-key-count">${stat.calls_24h}</span> / |
|
|
<span class="api-key-limit">${stat.limit}</span> |
|
|
<span class="api-key-percent">(${stat.usage_percent}%)</span> |
|
|
</div> |
|
|
</div> |
|
|
<div class="progress-container"> |
|
|
<div class="progress-bar ${barClass}" style="width: ${Math.min(stat.usage_percent, 100)}%"></div> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
container.appendChild(item); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function toggleApiKeyStats() { |
|
|
const statsDiv = document.getElementById('api-key-stats'); |
|
|
const toggleIcon = document.getElementById('toggle-icon'); |
|
|
|
|
|
if (statsDiv.style.display === 'none') { |
|
|
statsDiv.style.display = 'block'; |
|
|
toggleIcon.textContent = '▼'; |
|
|
} else { |
|
|
statsDiv.style.display = 'none'; |
|
|
toggleIcon.textContent = '▶'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function updateLogs(logs) { |
|
|
const logContainer = document.getElementById('log-container'); |
|
|
const currentFilter = document.querySelector('.log-filter button.active').getAttribute('data-level'); |
|
|
|
|
|
|
|
|
const wasScrolledToBottom = logContainer.scrollHeight - logContainer.clientHeight <= logContainer.scrollTop + 5; |
|
|
|
|
|
|
|
|
logContainer.innerHTML = ''; |
|
|
|
|
|
|
|
|
logs.forEach(log => { |
|
|
const logEntry = document.createElement('div'); |
|
|
logEntry.className = `log-entry ${log.level}`; |
|
|
logEntry.setAttribute('data-level', log.level); |
|
|
|
|
|
|
|
|
if (currentFilter !== 'ALL' && log.level !== currentFilter) { |
|
|
logEntry.style.display = 'none'; |
|
|
} |
|
|
|
|
|
const timestampSpan = document.createElement('span'); |
|
|
timestampSpan.className = 'log-timestamp'; |
|
|
timestampSpan.textContent = log.timestamp; |
|
|
|
|
|
const levelSpan = document.createElement('span'); |
|
|
levelSpan.className = `log-level ${log.level}`; |
|
|
levelSpan.textContent = log.level; |
|
|
|
|
|
const messageSpan = document.createElement('span'); |
|
|
messageSpan.className = 'log-message'; |
|
|
|
|
|
|
|
|
let messageContent = ''; |
|
|
if (log.key !== 'N/A') messageContent += `[${log.key}] `; |
|
|
if (log.request_type !== 'N/A') messageContent += log.request_type + ' '; |
|
|
if (log.model !== 'N/A') messageContent += `[${log.model}] `; |
|
|
if (log.status_code !== 'N/A') messageContent += log.status_code + ' '; |
|
|
messageContent += ': ' + log.message; |
|
|
if (log.error_message) messageContent += ' - ' + log.error_message; |
|
|
|
|
|
messageSpan.textContent = messageContent; |
|
|
|
|
|
logEntry.appendChild(timestampSpan); |
|
|
logEntry.appendChild(levelSpan); |
|
|
logEntry.appendChild(messageSpan); |
|
|
|
|
|
logContainer.appendChild(logEntry); |
|
|
}); |
|
|
|
|
|
|
|
|
if (wasScrolledToBottom) { |
|
|
logContainer.scrollTop = logContainer.scrollHeight; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
window.onload = function() { |
|
|
const logContainer = document.getElementById('log-container'); |
|
|
logContainer.scrollTop = logContainer.scrollHeight; |
|
|
|
|
|
|
|
|
startAutoRefresh(); |
|
|
}; |
|
|
|
|
|
|
|
|
document.getElementById('refresh-button').addEventListener('click', fetchDashboardData); |
|
|
</script> |
|
|
</body> |
|
|
</html> |