| <!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> |