| """ | |
| Web界面模板模块 | |
| 包含管理界面的HTML模板 | |
| """ | |
| def get_admin_login_template(): | |
| """返回管理员登录页面模板""" | |
| return """ | |
| <!DOCTYPE html> | |
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>🔐 管理员登录 - Warp API 管理中心</title> | |
| <style> | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| margin: 0; | |
| padding: 0; | |
| background: linear-gradient(135deg, #0f1419 0%, #1a2332 25%, #2d3748 50%, #1a2332 75%, #0f1419 100%); | |
| background-attachment: fixed; | |
| min-height: 100vh; | |
| color: #e2e8f0; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .login-container { | |
| max-width: 400px; | |
| width: 90%; | |
| background: rgba(30, 41, 59, 0.9); | |
| backdrop-filter: blur(20px); | |
| border-radius: 20px; | |
| padding: 40px; | |
| box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05); | |
| border: 1px solid rgba(148, 163, 184, 0.1); | |
| text-align: center; | |
| } | |
| h1 { | |
| color: #cbd5e1; | |
| margin-bottom: 30px; | |
| font-size: 2rem; | |
| text-shadow: 0 2px 10px rgba(59, 130, 246, 0.3); | |
| } | |
| .form-group { | |
| margin-bottom: 25px; | |
| text-align: left; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 8px; | |
| color: #cbd5e1; | |
| font-weight: 600; | |
| } | |
| input { | |
| width: 100%; | |
| padding: 12px 16px; | |
| background: rgba(30, 41, 59, 0.7); | |
| border: 2px solid rgba(71, 85, 105, 0.3); | |
| border-radius: 10px; | |
| box-sizing: border-box; | |
| color: #e2e8f0; | |
| font-size: 14px; | |
| transition: all 0.3s ease; | |
| } | |
| input:focus { | |
| outline: none; | |
| border-color: rgba(59, 130, 246, 0.5); | |
| background: rgba(30, 41, 59, 0.9); | |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); | |
| } | |
| button { | |
| background: linear-gradient(135deg, rgba(59, 130, 246, 0.8) 0%, rgba(29, 78, 216, 0.8) 100%); | |
| color: white; | |
| padding: 14px 28px; | |
| border: none; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| font-size: 16px; | |
| width: 100%; | |
| transition: all 0.3s ease; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(59, 130, 246, 0.3); | |
| } | |
| button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.4); | |
| background: linear-gradient(135deg, rgba(59, 130, 246, 1) 0%, rgba(29, 78, 216, 1) 100%); | |
| } | |
| .status { | |
| padding: 12px 16px; | |
| margin: 15px 0; | |
| border-radius: 8px; | |
| font-weight: 500; | |
| display: none; | |
| } | |
| .status.success { | |
| background: rgba(34, 197, 94, 0.15); | |
| color: #86efac; | |
| border: 1px solid rgba(34, 197, 94, 0.3); | |
| } | |
| .status.error { | |
| background: rgba(239, 68, 68, 0.15); | |
| color: #fca5a5; | |
| border: 1px solid rgba(239, 68, 68, 0.3); | |
| } | |
| .note { | |
| margin-top: 20px; | |
| padding: 15px; | |
| background: rgba(79, 70, 229, 0.1); | |
| border: 1px solid rgba(79, 70, 229, 0.2); | |
| border-radius: 8px; | |
| font-size: 14px; | |
| color: #a5b4fc; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="login-container"> | |
| <h1>🔐 管理员登录</h1> | |
| <form id="loginForm"> | |
| <div class="form-group"> | |
| <label for="adminKey">管理员密钥</label> | |
| <input type="password" id="adminKey" name="adminKey" placeholder="请输入管理员密钥" required> | |
| </div> | |
| <button type="submit">🚀 登录</button> | |
| <div id="status" class="status"></div> | |
| </form> | |
| <div class="note"> | |
| 💡 管理员密钥通过环境变量 <code>ADMIN_KEY</code> 设置 | |
| </div> | |
| </div> | |
| <script> | |
| document.getElementById('loginForm').addEventListener('submit', async function(e) { | |
| e.preventDefault(); | |
| const adminKey = document.getElementById('adminKey').value; | |
| const statusDiv = document.getElementById('status'); | |
| if (!adminKey) { | |
| showStatus('请输入管理员密钥', 'error'); | |
| return; | |
| } | |
| try { | |
| const response = await fetch('/admin/auth', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ admin_key: adminKey }) | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| showStatus('登录成功,正在跳转...', 'success'); | |
| setTimeout(() => { | |
| window.location.href = data.redirect || '/'; | |
| }, 1000); | |
| } else { | |
| showStatus(data.message || '登录失败', 'error'); | |
| } | |
| } catch (error) { | |
| showStatus('登录请求失败: ' + error.message, 'error'); | |
| } | |
| }); | |
| function showStatus(message, type) { | |
| const statusDiv = document.getElementById('status'); | |
| statusDiv.textContent = message; | |
| statusDiv.className = `status ${type}`; | |
| statusDiv.style.display = 'block'; | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| def get_html_template(): | |
| """返回优化的HTML模板""" | |
| return """ | |
| <!DOCTYPE html> | |
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>🚀 Warp API 管理中心</title> | |
| <style> | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| margin: 0; | |
| padding: 20px; | |
| background: linear-gradient(135deg, #0f1419 0%, #1a2332 25%, #2d3748 50%, #1a2332 75%, #0f1419 100%); | |
| background-attachment: fixed; | |
| min-height: 100vh; | |
| color: #e2e8f0; | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| background: rgba(30, 41, 59, 0.85); | |
| backdrop-filter: blur(20px); | |
| border-radius: 20px; | |
| padding: 40px; | |
| box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05); | |
| border: 1px solid rgba(148, 163, 184, 0.1); | |
| } | |
| .header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 40px; | |
| } | |
| h1 { | |
| color: #cbd5e1; | |
| margin: 0; | |
| font-size: 2.5rem; | |
| text-shadow: 0 2px 10px rgba(59, 130, 246, 0.3); | |
| letter-spacing: -0.025em; | |
| } | |
| .logout-btn { | |
| background: linear-gradient(135deg, rgba(239, 68, 68, 0.8) 0%, rgba(185, 28, 28, 0.8) 100%); | |
| color: white; | |
| padding: 10px 20px; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| font-size: 14px; | |
| transition: all 0.3s ease; | |
| border: 1px solid rgba(239, 68, 68, 0.3); | |
| } | |
| .logout-btn:hover { | |
| background: linear-gradient(135deg, rgba(239, 68, 68, 1) 0%, rgba(185, 28, 28, 1) 100%); | |
| transform: translateY(-1px); | |
| } | |
| .section { | |
| margin-bottom: 40px; | |
| padding: 30px; | |
| background: rgba(51, 65, 85, 0.6); | |
| backdrop-filter: blur(10px); | |
| border-radius: 16px; | |
| border: 1px solid rgba(148, 163, 184, 0.1); | |
| box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| } | |
| .section h2 { | |
| color: #f1f5f9; | |
| margin-top: 0; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| font-size: 1.5rem; | |
| margin-bottom: 25px; | |
| } | |
| .status-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 20px; | |
| margin-bottom: 30px; | |
| } | |
| .status-card { | |
| padding: 20px; | |
| border-radius: 12px; | |
| text-align: center; | |
| font-weight: 600; | |
| background: rgba(59, 130, 246, 0.15); | |
| border: 1px solid rgba(59, 130, 246, 0.2); | |
| backdrop-filter: blur(10px); | |
| color: #e2e8f0; | |
| transition: all 0.3s ease; | |
| } | |
| .status-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.2); | |
| background: rgba(59, 130, 246, 0.2); | |
| } | |
| .status-card.total { background: rgba(59, 130, 246, 0.15); border-color: rgba(59, 130, 246, 0.3); } | |
| .status-card.active { background: rgba(34, 197, 94, 0.15); border-color: rgba(34, 197, 94, 0.3); } | |
| .status-card.with-access { background: rgba(168, 85, 247, 0.15); border-color: rgba(168, 85, 247, 0.3); } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin-bottom: 25px; | |
| background: rgba(30, 41, 59, 0.5); | |
| border-radius: 12px; | |
| overflow: hidden; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| } | |
| th, td { | |
| padding: 16px; | |
| text-align: left; | |
| border-bottom: 1px solid rgba(71, 85, 105, 0.3); | |
| } | |
| th { | |
| background: rgba(51, 65, 85, 0.8); | |
| color: #f1f5f9; | |
| font-weight: 600; | |
| font-size: 0.875rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| } | |
| tr:hover { | |
| background-color: rgba(71, 85, 105, 0.3); | |
| transition: background-color 0.2s ease; | |
| } | |
| input, textarea { | |
| width: 100%; | |
| padding: 12px 16px; | |
| background: rgba(30, 41, 59, 0.7); | |
| border: 2px solid rgba(71, 85, 105, 0.3); | |
| border-radius: 10px; | |
| box-sizing: border-box; | |
| color: #e2e8f0; | |
| font-size: 14px; | |
| transition: all 0.3s ease; | |
| } | |
| input:focus, textarea:focus { | |
| outline: none; | |
| border-color: rgba(59, 130, 246, 0.5); | |
| background: rgba(30, 41, 59, 0.9); | |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); | |
| } | |
| input::placeholder, textarea::placeholder { | |
| color: #94a3b8; | |
| } | |
| button { | |
| background: linear-gradient(135deg, rgba(59, 130, 246, 0.8) 0%, rgba(29, 78, 216, 0.8) 100%); | |
| color: white; | |
| padding: 12px 24px; | |
| border: none; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| margin: 6px; | |
| font-weight: 600; | |
| font-size: 14px; | |
| transition: all 0.3s ease; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(59, 130, 246, 0.3); | |
| } | |
| button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.4); | |
| background: linear-gradient(135deg, rgba(59, 130, 246, 1) 0%, rgba(29, 78, 216, 1) 100%); | |
| } | |
| button:active { | |
| transform: translateY(0); | |
| } | |
| .btn-success { | |
| background: linear-gradient(135deg, rgba(34, 197, 94, 0.8) 0%, rgba(21, 128, 61, 0.8) 100%); | |
| border-color: rgba(34, 197, 94, 0.3); | |
| } | |
| .btn-success:hover { | |
| background: linear-gradient(135deg, rgba(34, 197, 94, 1) 0%, rgba(21, 128, 61, 1) 100%); | |
| box-shadow: 0 10px 25px -5px rgba(34, 197, 94, 0.4); | |
| } | |
| .btn-danger { | |
| background: linear-gradient(135deg, rgba(239, 68, 68, 0.8) 0%, rgba(185, 28, 28, 0.8) 100%); | |
| border-color: rgba(239, 68, 68, 0.3); | |
| } | |
| .btn-danger:hover { | |
| background: linear-gradient(135deg, rgba(239, 68, 68, 1) 0%, rgba(185, 28, 28, 1) 100%); | |
| box-shadow: 0 10px 25px -5px rgba(239, 68, 68, 0.4); | |
| } | |
| .btn-info { | |
| background: linear-gradient(135deg, rgba(6, 182, 212, 0.8) 0%, rgba(8, 145, 178, 0.8) 100%); | |
| border-color: rgba(6, 182, 212, 0.3); | |
| } | |
| .btn-info:hover { | |
| background: linear-gradient(135deg, rgba(6, 182, 212, 1) 0%, rgba(8, 145, 178, 1) 100%); | |
| box-shadow: 0 10px 25px -5px rgba(6, 182, 212, 0.4); | |
| } | |
| .btn-warning { | |
| background: linear-gradient(135deg, rgba(245, 158, 11, 0.8) 0%, rgba(217, 119, 6, 0.8) 100%); | |
| border-color: rgba(245, 158, 11, 0.3); | |
| } | |
| .btn-warning:hover { | |
| background: linear-gradient(135deg, rgba(245, 158, 11, 1) 0%, rgba(217, 119, 6, 1) 100%); | |
| box-shadow: 0 10px 25px -5px rgba(245, 158, 11, 0.4); | |
| } | |
| .btn-small { | |
| padding: 8px 16px; | |
| font-size: 12px; | |
| margin: 2px; | |
| } | |
| .status { | |
| padding: 16px 20px; | |
| margin: 20px 0; | |
| border-radius: 12px; | |
| font-weight: 600; | |
| backdrop-filter: blur(10px); | |
| } | |
| .status.success { | |
| background: rgba(34, 197, 94, 0.15); | |
| color: #86efac; | |
| border: 1px solid rgba(34, 197, 94, 0.3); | |
| } | |
| .status.error { | |
| background: rgba(239, 68, 68, 0.15); | |
| color: #fca5a5; | |
| border: 1px solid rgba(239, 68, 68, 0.3); | |
| } | |
| .api-info { | |
| background: linear-gradient(135deg, rgba(79, 70, 229, 0.15) 0%, rgba(59, 130, 246, 0.15) 100%); | |
| border: 1px solid rgba(79, 70, 229, 0.2); | |
| color: #e2e8f0; | |
| padding: 25px; | |
| border-radius: 16px; | |
| margin-bottom: 30px; | |
| backdrop-filter: blur(10px); | |
| } | |
| .api-info code { | |
| background: rgba(30, 41, 59, 0.6); | |
| padding: 6px 12px; | |
| border-radius: 8px; | |
| color: #a78bfa; | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | |
| border: 1px solid rgba(71, 85, 105, 0.3); | |
| } | |
| .tabs { | |
| display: flex; | |
| margin-bottom: 30px; | |
| background: rgba(30, 41, 59, 0.5); | |
| border-radius: 12px; | |
| padding: 4px; | |
| backdrop-filter: blur(10px); | |
| } | |
| .tab { | |
| padding: 14px 28px; | |
| cursor: pointer; | |
| border: none; | |
| background: none; | |
| color: #94a3b8; | |
| font-weight: 600; | |
| border-radius: 8px; | |
| transition: all 0.3s ease; | |
| flex: 1; | |
| text-align: center; | |
| } | |
| .tab.active { | |
| color: #f1f5f9; | |
| background: rgba(59, 130, 246, 0.3); | |
| backdrop-filter: blur(10px); | |
| } | |
| .tab:hover:not(.active) { | |
| color: #cbd5e1; | |
| background: rgba(71, 85, 105, 0.3); | |
| } | |
| .tab-content { | |
| display: none; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| .loading { | |
| display: none; | |
| text-align: center; | |
| padding: 30px; | |
| color: #94a3b8; | |
| } | |
| .spinner { | |
| border: 4px solid rgba(71, 85, 105, 0.3); | |
| border-top: 4px solid #3b82f6; | |
| border-radius: 50%; | |
| width: 40px; | |
| height: 40px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 15px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .action-buttons { | |
| display: flex; | |
| gap: 8px; | |
| justify-content: center; | |
| } | |
| .token-id { | |
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | |
| background: rgba(30, 41, 59, 0.6); | |
| padding: 4px 8px; | |
| border-radius: 6px; | |
| font-size: 0.85rem; | |
| color: #a78bfa; | |
| } | |
| .env-info { | |
| background: rgba(34, 197, 94, 0.1); | |
| border: 1px solid rgba(34, 197, 94, 0.2); | |
| color: #86efac; | |
| padding: 15px; | |
| border-radius: 12px; | |
| margin-bottom: 20px; | |
| font-size: 14px; | |
| } | |
| .export-section { | |
| background: rgba(245, 158, 11, 0.1); | |
| border: 1px solid rgba(245, 158, 11, 0.2); | |
| padding: 20px; | |
| border-radius: 12px; | |
| margin-bottom: 20px; | |
| } | |
| .export-form { | |
| display: flex; | |
| gap: 15px; | |
| align-items: end; | |
| } | |
| .export-form input { | |
| flex: 1; | |
| } | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| z-index: 1000; | |
| left: 0; | |
| top: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| backdrop-filter: blur(10px); | |
| } | |
| .modal-content { | |
| background: rgba(30, 41, 59, 0.95); | |
| margin: 15% auto; | |
| padding: 30px; | |
| border-radius: 16px; | |
| width: 80%; | |
| max-width: 500px; | |
| border: 1px solid rgba(148, 163, 184, 0.2); | |
| box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8); | |
| } | |
| .close { | |
| color: #94a3b8; | |
| float: right; | |
| font-size: 28px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| line-height: 1; | |
| } | |
| .close:hover { | |
| color: #e2e8f0; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>🚀 Warp API 管理中心</h1> | |
| <button class="logout-btn" onclick="logout()">🚪 退出登录</button> | |
| </div> | |
| <div class="api-info"> | |
| <h3>📡 API 端点信息</h3> | |
| <p><strong>Base URL:</strong> <code>http://localhost:7860</code></p> | |
| <p><strong>Chat:</strong> <code>/v1/chat/completions</code> | <strong>Models:</strong> <code>/v1/models</code></p> | |
| <div class="env-info"> | |
| 🔐 <strong>安全提示:</strong> API密钥和敏感信息已隐藏,仅在服务器端可见 | |
| </div> | |
| </div> | |
| <div class="tabs"> | |
| <button class="tab active" onclick="showTab('token-status')">🔑 Token状态</button> | |
| <button class="tab" onclick="showTab('add-tokens')">➕ 添加Token</button> | |
| <button class="tab" onclick="showTab('get-tokens')">📧 获取Token</button> | |
| </div> | |
| <div id="token-status" class="tab-content active"> | |
| <div class="section"> | |
| <h2>🔑 Token 状态管理</h2> | |
| <div class="env-info" id="tokenEnvInfo" style="display: none;"> | |
| 🎯 <strong>环境变量Token:</strong> 已通过 <code>WARP_REFRESH_TOKEN</code> 设置<br> | |
| 💡 <strong>多Token支持:</strong> 可使用分号(;)分割多个token:<code>token1;token2;token3</code> | |
| </div> | |
| <div class="export-section"> | |
| <h3>🔒 导出 Refresh Token (超级管理员功能)</h3> | |
| <p>导出所有refresh token为分号分割的文本文件,需要超级管理员密钥验证</p> | |
| <div class="export-form"> | |
| <div style="flex: 1;"> | |
| <label>超级管理员密钥</label> | |
| <input type="password" id="superAdminKey" placeholder="请输入超级管理员密钥"> | |
| </div> | |
| <button onclick="exportTokens()" class="btn-warning">📥 导出Token</button> | |
| </div> | |
| <div class="env-info" style="margin-top: 15px;"> | |
| 💡 <strong>提示:</strong> 点击导出后,浏览器会提示您选择保存位置<br> | |
| 📁 <strong>文件格式:</strong> 文本文件,token间用分号(;)分割,文件名包含时间戳 | |
| </div> | |
| <div id="exportStatus"></div> | |
| </div> | |
| <div class="status-grid"> | |
| <div class="status-card total"><div>总Token数</div><div id="totalTokens">-</div></div> | |
| <div class="status-card active"><div>活跃Token</div><div id="activeTokens">-</div></div> | |
| <div class="status-card with-access"><div>可用Token</div><div id="tokensWithAccess">-</div></div> | |
| </div> | |
| <button onclick="refreshTokenStatus()" class="btn-info">🔄 刷新状态</button> | |
| <button onclick="refreshAllTokens()" class="btn-success">⚡ 刷新所有Token</button> | |
| <div style="max-height: 500px; overflow-y: auto; margin-top: 25px;"> | |
| <table> | |
| <thead><tr><th>Token ID</th><th>状态</th><th>访问Token</th><th>刷新次数</th><th>使用次数</th><th>操作</th></tr></thead> | |
| <tbody id="tokenTableBody"></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="add-tokens" class="tab-content"> | |
| <div class="section"> | |
| <h2>➕ 添加 Refresh Token</h2> | |
| <div class="env-info"> | |
| 💡 <strong>提示:</strong> 也可通过环境变量 <code>WARP_REFRESH_TOKEN</code> 设置refresh token<br> | |
| 🔀 <strong>多Token支持:</strong> 环境变量中可使用分号(;)分割多个token:<code>token1;token2;token3</code> | |
| </div> | |
| <textarea id="tokensInput" rows="5" placeholder="请输入refresh token,每行一个 或者在环境变量中使用分号分割:token1;token2;token3"></textarea> | |
| <div style="margin-top: 20px;"> | |
| <button onclick="addTokens()" class="btn-success">➕ 添加Token</button> | |
| <button onclick="clearTokensInput()" class="btn-danger">🗑️ 清空</button> | |
| </div> | |
| <div id="addTokensStatus"></div> | |
| </div> | |
| </div> | |
| <div id="get-tokens" class="tab-content"> | |
| <div class="section"> | |
| <h2>📧 批量获取 Refresh Token</h2> | |
| <div style="margin-bottom: 25px;"> | |
| <label style="color: #cbd5e1; margin-right: 10px;">并发线程数:</label> | |
| <input type="number" id="maxWorkers" min="1" max="20" value="5" style="width: 100px; display: inline-block;"> | |
| <button onclick="addEmailRow()">➕ 添加邮箱</button> | |
| <button onclick="clearEmails()" class="btn-danger">🗑️ 清空</button> | |
| </div> | |
| <table id="emailTable"> | |
| <thead><tr><th>邮箱地址</th><th>登录URL</th><th>操作</th></tr></thead> | |
| <tbody id="emailTableBody"> | |
| <tr> | |
| <td><input type="email" placeholder="example@gmail.com"></td> | |
| <td><input type="url" placeholder="https://..."></td> | |
| <td><button onclick="removeEmailRow(this)" class="btn-danger btn-small">❌</button></td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| <button onclick="processEmails()" class="btn-success">🚀 开始处理</button> | |
| <div class="loading" id="emailLoading"><div class="spinner"></div><p id="loadingText">正在处理邮箱,获取Token并创建用户...</p></div> | |
| <div id="emailResults"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| function showTab(tabName) { | |
| document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active')); | |
| document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); | |
| event.target.classList.add('active'); | |
| document.getElementById(tabName).classList.add('active'); | |
| if (tabName === 'token-status') refreshTokenStatus(); | |
| } | |
| async function refreshTokenStatus() { | |
| try { | |
| const response = await fetch('/token/status'); | |
| const data = await response.json(); | |
| if (data.success !== false) { | |
| document.getElementById('totalTokens').textContent = data.total_tokens; | |
| document.getElementById('activeTokens').textContent = data.active_tokens; | |
| document.getElementById('tokensWithAccess').textContent = data.tokens_with_access; | |
| // 如果有token,检查是否是环境变量设置的 | |
| if (data.total_tokens > 0) { | |
| document.getElementById('tokenEnvInfo').style.display = 'block'; | |
| } | |
| const tbody = document.getElementById('tokenTableBody'); | |
| tbody.innerHTML = ''; | |
| data.tokens.forEach(token => { | |
| const row = tbody.insertRow(); | |
| row.innerHTML = ` | |
| <td><span class="token-id">${token.refresh_token}</span></td> | |
| <td>${token.is_active ? '✅ 活跃' : '❌ 失效'}</td> | |
| <td>${token.has_access_token ? '✅ 有效' : '❌ 无效'}</td> | |
| <td>${token.refresh_count}</td> | |
| <td>${token.usage_count}</td> | |
| <td> | |
| <div class="action-buttons"> | |
| <button onclick="deleteToken('${token.refresh_token}')" class="btn-danger btn-small">🗑️ 删除</button> | |
| </div> | |
| </td> | |
| `; | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('刷新状态失败:', error); | |
| showStatus('刷新状态失败: ' + error.message, 'error'); | |
| } | |
| } | |
| async function deleteToken(tokenId) { | |
| if (!confirm('确定要删除这个Token吗?')) return; | |
| try { | |
| const response = await fetch('/token/remove', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ refresh_token: tokenId }) | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| showStatus('Token删除成功', 'success'); | |
| refreshTokenStatus(); | |
| } else { | |
| showStatus(data.message || 'Token删除失败', 'error'); | |
| } | |
| } catch (error) { | |
| showStatus('删除请求失败: ' + error.message, 'error'); | |
| } | |
| } | |
| async function exportTokens() { | |
| const superAdminKey = document.getElementById('superAdminKey').value; | |
| if (!superAdminKey) { | |
| showStatus('请输入超级管理员密钥', 'error', 'exportStatus'); | |
| return; | |
| } | |
| try { | |
| showStatus('正在准备导出...', 'success', 'exportStatus'); | |
| const response = await fetch('/token/export', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ super_admin_key: superAdminKey }) | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| // 使用浏览器下载API | |
| downloadTextFile(data.content, data.suggested_filename); | |
| showStatus(`导出成功!包含 ${data.token_count} 个token`, 'success', 'exportStatus'); | |
| document.getElementById('superAdminKey').value = ''; | |
| } else { | |
| showStatus(data.message || '导出失败', 'error', 'exportStatus'); | |
| } | |
| } catch (error) { | |
| showStatus('导出请求失败: ' + error.message, 'error', 'exportStatus'); | |
| } | |
| } | |
| function downloadTextFile(content, filename) { | |
| try { | |
| // 创建Blob对象 | |
| const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); | |
| // 创建下载链接 | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement('a'); | |
| link.href = url; | |
| link.download = filename; | |
| // 触发下载 | |
| document.body.appendChild(link); | |
| link.click(); | |
| // 清理 | |
| document.body.removeChild(link); | |
| URL.revokeObjectURL(url); | |
| console.log(`文件 ${filename} 下载完成`); | |
| } catch (error) { | |
| console.error('下载文件时出错:', error); | |
| showStatus('下载文件时出错: ' + error.message, 'error', 'exportStatus'); | |
| } | |
| } | |
| async function refreshAllTokens() { | |
| try { | |
| const response = await fetch('/token/refresh', { method: 'POST' }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| showStatus('所有Token刷新已触发', 'success'); | |
| setTimeout(refreshTokenStatus, 2000); | |
| } else { | |
| showStatus(data.message, 'error'); | |
| } | |
| } catch (error) { showStatus('刷新失败: ' + error.message, 'error'); } | |
| } | |
| async function addTokens() { | |
| const tokens = document.getElementById('tokensInput').value.split('\\n').map(t => t.trim()).filter(t => t); | |
| if (tokens.length === 0) { showStatus('请输入至少一个token', 'error', 'addTokensStatus'); return; } | |
| try { | |
| const response = await fetch('/token/add', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ tokens }) | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| showStatus(`成功添加 ${data.added_tokens} 个token`, 'success', 'addTokensStatus'); | |
| document.getElementById('tokensInput').value = ''; | |
| } else { | |
| showStatus(data.message, 'error', 'addTokensStatus'); | |
| } | |
| } catch (error) { showStatus('添加失败: ' + error.message, 'error', 'addTokensStatus'); } | |
| } | |
| function clearTokensInput() { document.getElementById('tokensInput').value = ''; } | |
| function addEmailRow() { | |
| const tbody = document.getElementById('emailTableBody'); | |
| const row = tbody.insertRow(); | |
| row.innerHTML = `<td><input type="email" placeholder="example@gmail.com"></td><td><input type="url" placeholder="https://..."></td><td><button onclick="removeEmailRow(this)" class="btn-danger btn-small">❌</button></td>`; | |
| } | |
| function removeEmailRow(button) { button.closest('tr').remove(); } | |
| function clearEmails() { | |
| document.getElementById('emailTableBody').innerHTML = `<tr><td><input type="email" placeholder="example@gmail.com"></td><td><input type="url" placeholder="https://..."></td><td><button onclick="removeEmailRow(this)" class="btn-danger btn-small">❌</button></td></tr>`; | |
| } | |
| async function processEmails() { | |
| const rows = document.querySelectorAll('#emailTableBody tr'); | |
| const emailUrlPairs = []; | |
| rows.forEach(row => { | |
| const inputs = row.querySelectorAll('input'); | |
| const email = inputs[0].value.trim(); | |
| const url = inputs[1].value.trim(); | |
| if (email && url) emailUrlPairs.push({email, url}); | |
| }); | |
| if (emailUrlPairs.length === 0) { showStatus('请至少添加一个邮箱和URL', 'error', 'emailResults'); return; } | |
| document.getElementById('emailLoading').style.display = 'block'; | |
| document.getElementById('emailResults').innerHTML = ''; | |
| try { | |
| const response = await fetch('/login/batch', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| email_url_pairs: emailUrlPairs, | |
| max_workers: parseInt(document.getElementById('maxWorkers').value) | |
| }) | |
| }); | |
| const data = await response.json(); | |
| document.getElementById('emailLoading').style.display = 'none'; | |
| if (data.success) { | |
| let resultsHtml = `<div class="status success">批量处理完成!成功: ${data.success_count}/${data.total_count}</div>`; | |
| resultsHtml += '<table><tr><th>邮箱</th><th>状态</th><th>Token</th><th>用户创建</th></tr>'; | |
| Object.entries(data.results).forEach(([email, result]) => { | |
| let userCreationStatus = ''; | |
| if (result.status.includes('成功并已创建用户')) { | |
| userCreationStatus = '✅ 已创建'; | |
| } else if (result.status.includes('创建用户失败')) { | |
| userCreationStatus = '❌ 创建失败'; | |
| } else if (result.status.includes('获取access_token失败')) { | |
| userCreationStatus = '⚠️ Token失败'; | |
| } else if (result.refresh_token) { | |
| userCreationStatus = '🔄 未尝试'; | |
| } else { | |
| userCreationStatus = '-'; | |
| } | |
| resultsHtml += `<tr><td>${email}</td><td>${result.status}</td><td>${result.refresh_token ? '✅ 已获取' : '❌ 失败'}</td><td>${userCreationStatus}</td></tr>`; | |
| }); | |
| resultsHtml += '</table>'; | |
| document.getElementById('emailResults').innerHTML = resultsHtml; | |
| } else { | |
| showStatus(data.message, 'error', 'emailResults'); | |
| } | |
| } catch (error) { | |
| document.getElementById('emailLoading').style.display = 'none'; | |
| showStatus('处理失败: ' + error.message, 'error', 'emailResults'); | |
| } | |
| } | |
| async function logout() { | |
| if (!confirm('确定要退出登录吗?')) return; | |
| try { | |
| const response = await fetch('/admin/logout', { method: 'POST' }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| window.location.href = '/admin/login'; | |
| } | |
| } catch (error) { | |
| console.error('登出失败:', error); | |
| } | |
| } | |
| function showStatus(message, type, targetId = null) { | |
| const status = `<div class="status ${type}">${message}</div>`; | |
| if (targetId) { | |
| document.getElementById(targetId).innerHTML = status; | |
| } else { | |
| const container = document.querySelector('.container'); | |
| const statusDiv = document.createElement('div'); | |
| statusDiv.innerHTML = status; | |
| container.insertBefore(statusDiv, container.firstChild); | |
| setTimeout(() => statusDiv.remove(), 5000); | |
| } | |
| } | |
| // 页面加载时刷新状态 | |
| document.addEventListener('DOMContentLoaded', function() { | |
| refreshTokenStatus(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| """ |