| <!DOCTYPE html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Gemi2Api Server 管理面板</title> |
| <style> |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| background: #0f0f0f; |
| color: #e0e0e0; |
| min-height: 100vh; |
| } |
| .header { |
| background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); |
| padding: 16px 30px; |
| border-bottom: 1px solid #333; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| position: relative; |
| } |
| .header-left { |
| display: flex; |
| align-items: center; |
| gap: 4px; |
| } |
| .header-left h1 { |
| font-size: 22px; |
| color: #4fc3f7; |
| font-weight: 600; |
| } |
| .header-right { |
| position: absolute; |
| right: 30px; |
| display: flex; |
| align-items: center; |
| gap: 16px; |
| } |
| .github-link { |
| display: flex; |
| align-items: center; |
| opacity: 0.7; |
| transition: opacity 0.2s; |
| } |
| .github-link:hover { |
| opacity: 1; |
| } |
| .header .status { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| } |
| .status-dot { |
| width: 12px; |
| height: 12px; |
| border-radius: 50%; |
| background: #4caf50; |
| animation: pulse 2s infinite; |
| } |
| @keyframes pulse { |
| 0%, 100% { opacity: 1; } |
| 50% { opacity: 0.5; } |
| } |
| .container { |
| max-width: 1400px; |
| margin: 0 auto; |
| padding: 20px; |
| } |
| .grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); |
| gap: 20px; |
| margin-bottom: 20px; |
| } |
| .card { |
| background: #1a1a1a; |
| border-radius: 12px; |
| padding: 20px; |
| border: 1px solid #333; |
| } |
| .card h3 { |
| color: #4fc3f7; |
| margin-bottom: 15px; |
| font-size: 16px; |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| } |
| .stat-value { |
| font-size: 32px; |
| font-weight: bold; |
| color: #fff; |
| } |
| .stat-label { |
| color: #888; |
| font-size: 14px; |
| margin-top: 5px; |
| } |
| .config-item { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| padding: 12px 0; |
| border-bottom: 1px solid #333; |
| } |
| .config-item:last-child { border-bottom: none; } |
| .config-label { color: #aaa; } |
| .config-value { color: #fff; font-family: monospace; } |
| .toggle { |
| position: relative; |
| width: 50px; |
| height: 26px; |
| } |
| .toggle input { opacity: 0; width: 0; height: 0; } |
| .toggle .slider { |
| position: absolute; |
| cursor: pointer; |
| top: 0; left: 0; right: 0; bottom: 0; |
| background: #333; |
| border-radius: 26px; |
| transition: 0.3s; |
| } |
| .toggle .slider:before { |
| content: ""; |
| position: absolute; |
| height: 20px; |
| width: 20px; |
| left: 3px; |
| bottom: 3px; |
| background: #fff; |
| border-radius: 50%; |
| transition: 0.3s; |
| } |
| .toggle input:checked + .slider { background: #4caf50; } |
| .toggle input:checked + .slider:before { transform: translateX(24px); } |
| .btn { |
| padding: 10px 20px; |
| border: none; |
| border-radius: 6px; |
| cursor: pointer; |
| font-size: 14px; |
| transition: all 0.2s; |
| } |
| .btn-primary { |
| background: #4fc3f7; |
| color: #000; |
| } |
| .btn-primary:hover { background: #29b6f6; } |
| .btn-danger { |
| background: #f44336; |
| color: #fff; |
| } |
| .btn-danger:hover { background: #d32f2f; } |
| .btn-success { |
| background: #4caf50; |
| color: #fff; |
| } |
| .btn-success:hover { background: #388e3c; } |
| .input-group { |
| display: flex; |
| gap: 10px; |
| margin-top: 10px; |
| } |
| .input-group input, .input-group select { |
| flex: 1; |
| padding: 10px; |
| background: #2a2a2a; |
| border: 1px solid #444; |
| border-radius: 6px; |
| color: #fff; |
| font-size: 14px; |
| } |
| .input-group input:focus, .input-group select:focus { |
| outline: none; |
| border-color: #4fc3f7; |
| } |
| .model-list { |
| max-height: 300px; |
| overflow-y: auto; |
| } |
| .model-item { |
| padding: 10px; |
| background: #2a2a2a; |
| border-radius: 6px; |
| margin-bottom: 8px; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| .model-name { font-family: monospace; color: #4fc3f7; } |
| .log-container { |
| max-height: 400px; |
| overflow-y: auto; |
| font-family: monospace; |
| font-size: 13px; |
| background: #0a0a0a; |
| padding: 15px; |
| border-radius: 8px; |
| } |
| .log-entry { |
| padding: 8px 0; |
| border-bottom: 1px solid #222; |
| display: flex; |
| gap: 15px; |
| } |
| .log-time { color: #666; min-width: 150px; } |
| .log-method { color: #4caf50; min-width: 50px; } |
| .log-path { color: #fff; } |
| .log-status { min-width: 40px; } |
| .log-status.ok { color: #4caf50; } |
| .log-status.error { color: #f44336; } |
| .chat-area { |
| display: flex; |
| flex-direction: column; |
| height: 400px; |
| } |
| .chat-messages { |
| flex: 1; |
| overflow-y: auto; |
| padding: 15px; |
| background: #0a0a0a; |
| border-radius: 8px; |
| margin-bottom: 10px; |
| } |
| .chat-msg { |
| margin-bottom: 15px; |
| padding: 10px 15px; |
| border-radius: 8px; |
| max-width: 80%; |
| } |
| .chat-msg.user { |
| background: #1a3a5c; |
| margin-left: auto; |
| } |
| .chat-msg.assistant { |
| background: #2a2a2a; |
| } |
| .chat-input { |
| display: flex; |
| gap: 10px; |
| } |
| .chat-input input { |
| flex: 1; |
| padding: 12px; |
| background: #2a2a2a; |
| border: 1px solid #444; |
| border-radius: 8px; |
| color: #fff; |
| font-size: 14px; |
| } |
| .chat-input input:focus { outline: none; border-color: #4fc3f7; } |
| .modal { |
| display: none; |
| position: fixed; |
| top: 0; left: 0; right: 0; bottom: 0; |
| background: rgba(0,0,0,0.8); |
| z-index: 1000; |
| justify-content: center; |
| align-items: center; |
| } |
| .modal.active { display: flex; } |
| .modal-content { |
| background: #1a1a1a; |
| padding: 30px; |
| border-radius: 12px; |
| max-width: 500px; |
| width: 90%; |
| border: 1px solid #333; |
| } |
| .modal h3 { margin-bottom: 20px; color: #4fc3f7; } |
| .form-group { |
| margin-bottom: 15px; |
| } |
| .form-group label { |
| display: block; |
| margin-bottom: 5px; |
| color: #aaa; |
| } |
| .form-group input { |
| width: 100%; |
| padding: 10px; |
| background: #2a2a2a; |
| border: 1px solid #444; |
| border-radius: 6px; |
| color: #fff; |
| } |
| .form-group input:focus { outline: none; border-color: #4fc3f7; } |
| .modal-buttons { |
| display: flex; |
| gap: 10px; |
| justify-content: flex-end; |
| margin-top: 20px; |
| } |
| .section-title { |
| font-size: 18px; |
| color: #fff; |
| margin-bottom: 15px; |
| padding-bottom: 10px; |
| border-bottom: 1px solid #333; |
| } |
| .warning { |
| background: #3a2a1a; |
| border: 1px solid #ff9800; |
| border-radius: 8px; |
| padding: 15px; |
| margin-bottom: 20px; |
| color: #ffb74d; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="header"> |
| <div class="header-left"> |
| <svg viewBox="0 0 1024 1024" width="32" height="32" style="vertical-align: middle; margin-right: 10px;"> |
| <path d="M960 512.896A477.248 477.248 0 0 0 512.896 960h-1.792A477.184 477.184 0 0 0 64 512.896v-1.792A477.184 477.184 0 0 0 511.104 64h1.792A477.248 477.248 0 0 0 960 511.104z" fill="#448AFF"></path> |
| </svg> |
| <h1>Gemi2Api Server</h1> |
| </div> |
| <div class="header-right"> |
| <div class="status"> |
| <div class="status-dot" id="statusDot"></div> |
| <span id="statusText">运行中</span> |
| </div> |
| <a href="https://github.com/zhiyu1998/Gemi2Api-Server" target="_blank" rel="noopener noreferrer" class="github-link" title="GitHub"> |
| <svg viewBox="0 0 1024 1024" width="28" height="28"> |
| <path d="M64 512c0 195.2 124.8 361.6 300.8 422.4 22.4 6.4 19.2-9.6 19.2-22.4v-76.8c-134.4 16-140.8-73.6-150.4-89.6-19.2-32-60.8-38.4-48-54.4 32-16 64 3.2 99.2 57.6 25.6 38.4 76.8 32 105.6 25.6 6.4-22.4 19.2-44.8 35.2-60.8-144-22.4-201.6-108.8-201.6-211.2 0-48 16-96 48-131.2-22.4-60.8 0-115.2 3.2-121.6 57.6-6.4 118.4 41.6 124.8 44.8 32-9.6 70.4-12.8 112-12.8 41.6 0 80 6.4 112 12.8 12.8-9.6 67.2-48 121.6-44.8 3.2 6.4 25.6 57.6 6.4 118.4 32 38.4 48 83.2 48 131.2 0 102.4-57.6 188.8-201.6 214.4 22.4 22.4 38.4 54.4 38.4 92.8v112c0 9.6 0 19.2 16 19.2C832 876.8 960 710.4 960 512c0-246.4-201.6-448-448-448S64 265.6 64 512z" fill="#e0e0e0"></path> |
| </svg> |
| </a> |
| </div> |
| </div> |
|
|
| |
| <div class="container" id="loginPage" style="display: none;"> |
| <div style="max-width: 400px; margin: 80px auto;"> |
| <div class="card" style="text-align: center;"> |
| <div style="margin-bottom: 20px;"> |
| <svg viewBox="0 0 1024 1024" width="64" height="64" style="margin-bottom: 10px;"> |
| <path d="M960 512.896A477.248 477.248 0 0 0 512.896 960h-1.792A477.184 477.184 0 0 0 64 512.896v-1.792A477.184 477.184 0 0 0 511.104 64h1.792A477.248 477.248 0 0 0 960 511.104z" fill="#448AFF"></path> |
| </svg> |
| <h2 style="color: #4fc3f7; margin-bottom: 5px;">Gemi2Api Server</h2> |
| <p style="color: #888; font-size: 14px;">管理面板登录</p> |
| </div> |
| <div class="form-group" style="text-align: left;"> |
| <label>API_KEY</label> |
| <input type="password" id="loginApiKey" placeholder="输入 API_KEY" style="width: 100%; padding: 12px; background: #2a2a2a; border: 1px solid #444; border-radius: 8px; color: #fff; font-size: 14px;" onkeypress="if(event.key==='Enter')adminLogin()"> |
| </div> |
| <button class="btn btn-primary" style="width: 100%; padding: 12px; font-size: 16px; margin-top: 10px;" onclick="adminLogin()">登录</button> |
| <div id="loginMessage" style="margin-top: 15px; display: none; padding: 10px; border-radius: 6px; font-size: 14px;"></div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="container" id="mainPage"> |
| |
| <div class="grid"> |
| <div class="card"> |
| <h3>📊 服务状态</h3> |
| <div class="stat-value" id="uptime">--</div> |
| <div class="stat-label">运行时间</div> |
| </div> |
| <div class="card"> |
| <h3>📈 请求统计</h3> |
| <div class="stat-value" id="totalRequests">0</div> |
| <div class="stat-label">总请求数</div> |
| </div> |
| <div class="card"> |
| <h3>⏱️ 平均响应</h3> |
| <div class="stat-value" id="avgResponseTime">--ms</div> |
| <div class="stat-label">响应时间</div> |
| </div> |
| <div class="card"> |
| <h3>🎯 错误率</h3> |
| <div class="stat-value" id="errorRate">0%</div> |
| <div class="stat-label">请求错误率</div> |
| </div> |
| </div> |
|
|
| |
| <div class="grid"> |
| <div class="card"> |
| <h3>⚙️ 服务配置</h3> |
| <div class="config-item"> |
| <span class="config-label">监听地址</span> |
| <span class="config-value" id="currentHost">--</span> |
| </div> |
| <div class="config-item"> |
| <span class="config-label">监听端口</span> |
| <span class="config-value" id="currentPort">--</span> |
| </div> |
| <div class="config-item"> |
| <span class="config-label">API_KEY</span> |
| <span class="config-value" id="apiKeyStatus">--</span> |
| </div> |
| <div class="config-item"> |
| <span class="config-label">Cookie 状态</span> |
| <span class="config-value" id="cookieStatus">--</span> |
| </div> |
| <button class="btn btn-primary" onclick="showEditConfig()">修改配置</button> |
| </div> |
|
|
| <div class="card"> |
| <h3>🍪 Gemini Cookie 配置</h3> |
| <div class="warning"> |
| 💡 登录 <a href="https://gemini.google.com/" target="_blank" rel="noopener noreferrer" style="color: #4fc3f7;">gemini.google.com</a>,F12 → Application → Cookies 复制以下两个值。 |
| </div> |
| <div class="form-group"> |
| <label>一键粘贴(从浏览器复制的整段 Cookie,自动提取)</label> |
| <textarea id="cookieRaw" rows="3" placeholder="粘贴整段 Cookie,例如:__Secure-1PSID=xxx; __Secure-1PSIDTS=yyy; ..." style="width: 100%; padding: 10px; background: #2a2a2a; border: 1px solid #444; border-radius: 6px; color: #fff; font-family: monospace; font-size: 12px; resize: vertical;" oninput="parseRawCookie()"></textarea> |
| <button class="btn btn-primary" style="margin-top: 8px;" onclick="parseRawCookie(true)">🔍 提取 Cookie</button> |
| </div> |
| <div class="form-group"> |
| <label>__Secure-1PSID</label> |
| <input type="text" id="cookie1psid" placeholder="粘贴 __Secure-1PSID 的值" style="font-family: monospace; font-size: 12px;"> |
| </div> |
| <div class="form-group"> |
| <label>__Secure-1PSIDTS</label> |
| <input type="text" id="cookie1psidts" placeholder="粘贴 __Secure-1PSIDTS 的值" style="font-family: monospace; font-size: 12px;"> |
| </div> |
| <div style="display: flex; gap: 10px; margin-top: 10px;"> |
| <button class="btn btn-success" onclick="saveCookiesAndReinit()">💾 保存并重新连接 Gemini</button> |
| </div> |
| <div id="cookieMessage" style="margin-top: 10px; display: none; padding: 10px; border-radius: 6px;"></div> |
| </div> |
| </div> |
|
|
| <div class="grid"> |
| <div class="card"> |
| <h3>🔧 功能开关</h3> |
| <div class="config-item"> |
| <span class="config-label">思考模式</span> |
| <label class="toggle"> |
| <input type="checkbox" id="toggleThinking" onchange="toggleFeature('thinking')"> |
| <span class="slider"></span> |
| </label> |
| </div> |
| <div class="config-item"> |
| <span class="config-label">临时对话</span> |
| <label class="toggle"> |
| <input type="checkbox" id="toggleTemporary" onchange="toggleFeature('temporary')"> |
| <span class="slider"></span> |
| </label> |
| </div> |
| <div class="config-item"> |
| <span class="config-label">自动删除对话</span> |
| <label class="toggle"> |
| <input type="checkbox" id="toggleAutoDelete" onchange="toggleFeature('autoDelete')"> |
| <span class="slider"></span> |
| </label> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="card" style="margin-bottom: 20px;"> |
| <h3>📋 可用模型</h3> |
| <div class="model-list" id="modelList"> |
| <div style="color: #666;">加载中...</div> |
| </div> |
| </div> |
|
|
| |
| <div class="grid"> |
| <div class="card"> |
| <h3>📝 最近日志</h3> |
| <div class="log-container" id="logContainer"> |
| <div style="color: #666;">暂无日志</div> |
| </div> |
| <button class="btn btn-primary" style="margin-top: 10px;" onclick="refreshLogs()">刷新</button> |
| </div> |
|
|
| <div class="card"> |
| <h3>💬 快速测试</h3> |
| <div class="chat-area"> |
| <div class="chat-messages" id="chatMessages"> |
| <div class="chat-msg assistant">你好!我是 Gemini,有什么可以帮你的?</div> |
| </div> |
| <div class="chat-input"> |
| <select id="chatModel" style="background: #2a2a2a; color: #e0e0e0; border: 1px solid #444; border-radius: 6px; padding: 8px 12px; font-size: 14px; min-width: 180px;"> |
| <option value="">加载模型中...</option> |
| </select> |
| <input type="text" id="chatInput" placeholder="输入消息..." onkeypress="if(event.key==='Enter')sendTestMessage()"> |
| <button class="btn btn-primary" onclick="sendTestMessage()">发送</button> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="modal" id="configModal"> |
| <div class="modal-content"> |
| <h3>修改服务配置</h3> |
| <div class="warning"> |
| ⚠️ 修改监听地址或端口后需要重启服务才能生效。 |
| </div> |
| <div class="form-group"> |
| <label>监听地址</label> |
| <input type="text" id="editHost" placeholder="0.0.0.0"> |
| </div> |
| <div class="form-group"> |
| <label>监听端口</label> |
| <input type="number" id="editPort" placeholder="8000"> |
| </div> |
| <div class="form-group"> |
| <label>API_KEY(留空表示禁用鉴权)</label> |
| <input type="text" id="editApiKey" placeholder="可选"> |
| </div> |
| <div class="modal-buttons"> |
| <button class="btn" style="background: #444; color: #fff;" onclick="closeModal()">取消</button> |
| <button class="btn btn-primary" onclick="saveConfigAndRestart()">💾 保存并重启服务</button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const API_BASE = '/admin/api'; |
| |
| |
| let adminToken = localStorage.getItem('adminToken') || ''; |
| |
| |
| function authFetch(url, options = {}) { |
| if (!options.headers) options.headers = {}; |
| if (adminToken) options.headers['X-Admin-Token'] = adminToken; |
| return fetch(url, options); |
| } |
| |
| |
| async function checkAuth() { |
| if (!adminToken) { |
| showLogin(); |
| return false; |
| } |
| try { |
| const res = await fetch(`${API_BASE}/check?token=${adminToken}`); |
| if (res.ok) return true; |
| } catch (e) {} |
| |
| adminToken = ''; |
| localStorage.removeItem('adminToken'); |
| showLogin(); |
| return false; |
| } |
| |
| function showLogin() { |
| document.getElementById('loginPage').style.display = 'block'; |
| document.getElementById('mainPage').style.display = 'none'; |
| } |
| |
| function showMain() { |
| document.getElementById('loginPage').style.display = 'none'; |
| document.getElementById('mainPage').style.display = 'block'; |
| } |
| |
| |
| async function adminLogin() { |
| const apiKey = document.getElementById('loginApiKey').value.trim(); |
| const msgEl = document.getElementById('loginMessage'); |
| |
| if (!apiKey) { |
| msgEl.style.display = 'block'; |
| msgEl.style.background = '#3a1a1a'; |
| msgEl.style.color = '#f44336'; |
| msgEl.textContent = '请输入 API_KEY'; |
| return; |
| } |
| |
| try { |
| const res = await fetch(`${API_BASE}/login`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ api_key: apiKey }) |
| }); |
| |
| const data = await res.json(); |
| if (res.ok) { |
| adminToken = data.token; |
| localStorage.setItem('adminToken', adminToken); |
| showMain(); |
| initDashboard(); |
| } else { |
| msgEl.style.display = 'block'; |
| msgEl.style.background = '#3a1a1a'; |
| msgEl.style.color = '#f44336'; |
| msgEl.textContent = '❌ ' + (data.detail || '登录失败'); |
| } |
| } catch (e) { |
| msgEl.style.display = 'block'; |
| msgEl.style.background = '#3a1a1a'; |
| msgEl.style.color = '#f44336'; |
| msgEl.textContent = '❌ 网络错误: ' + e.message; |
| } |
| } |
| |
| |
| function adminLogout() { |
| adminToken = ''; |
| localStorage.removeItem('adminToken'); |
| showLogin(); |
| } |
| |
| |
| let refreshInterval = null; |
| |
| |
| async function initDashboard() { |
| fetchStatus(); |
| fetchModels(); |
| fetchLogs(); |
| if (refreshInterval) clearInterval(refreshInterval); |
| refreshInterval = setInterval(() => { |
| fetchStatus(); |
| fetchLogs(); |
| }, 10000); |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', async () => { |
| const valid = await checkAuth(); |
| if (valid) { |
| showMain(); |
| initDashboard(); |
| } |
| }); |
| |
| |
| async function fetchStatus() { |
| try { |
| const res = await authFetch(`${API_BASE}/status`); |
| const data = await res.json(); |
| |
| document.getElementById('uptime').textContent = data.uptime || '--'; |
| document.getElementById('totalRequests').textContent = data.total_requests || 0; |
| document.getElementById('avgResponseTime').textContent = (data.avg_response_time || 0) + 'ms'; |
| document.getElementById('errorRate').textContent = (data.error_rate || 0).toFixed(1) + '%'; |
| |
| document.getElementById('currentHost').textContent = data.host || '--'; |
| document.getElementById('currentPort').textContent = data.port || '--'; |
| document.getElementById('apiKeyStatus').textContent = data.api_key_enabled ? '已启用' : '未设置'; |
| document.getElementById('cookieStatus').textContent = data.cookie_valid ? '有效' : '无效'; |
| |
| |
| document.getElementById('cookie1psid').placeholder = data.secure_1psid_masked || '粘贴 __Secure-1PSID 的值'; |
| document.getElementById('cookie1psidts').placeholder = data.secure_1psidts_masked || '粘贴 __Secure-1PSIDTS 的值'; |
| |
| |
| document.getElementById('toggleThinking').checked = data.thinking_enabled || false; |
| document.getElementById('toggleTemporary').checked = data.temporary_chat || false; |
| document.getElementById('toggleAutoDelete').checked = data.auto_delete_chat || false; |
| |
| |
| document.getElementById('statusDot').style.background = data.running ? '#4caf50' : '#f44336'; |
| document.getElementById('statusText').textContent = data.running ? '运行中' : '已停止'; |
| } catch (e) { |
| console.error('获取状态失败:', e); |
| document.getElementById('statusDot').style.background = '#f44336'; |
| document.getElementById('statusText').textContent = '连接失败'; |
| } |
| } |
| |
| |
| async function fetchModels() { |
| try { |
| const res = await fetch('/v1/models'); |
| const data = await res.json(); |
| |
| const list = document.getElementById('modelList'); |
| const select = document.getElementById('chatModel'); |
| if (data.data && data.data.length > 0) { |
| list.innerHTML = data.data.map(m => ` |
| <div class="model-item"> |
| <span class="model-name">${escapeHtml(m.id)}</span> |
| <span style="color: #888;">${escapeHtml(m.owned_by)}</span> |
| </div> |
| `).join(''); |
| |
| const models = data.data.filter(m => m.id !== 'unspecified'); |
| select.innerHTML = models.map(m => `<option value="${escapeHtml(m.id)}">${escapeHtml(m.id)}</option>`).join(''); |
| if (select.options.length > 0) select.selectedIndex = 0; |
| } else { |
| list.innerHTML = '<div style="color: #666;">暂无可用模型</div>'; |
| select.innerHTML = '<option value="">无可用模型</option>'; |
| } |
| } catch (e) { |
| console.error('获取模型列表失败:', e); |
| } |
| } |
| |
| |
| async function fetchLogs() { |
| try { |
| const res = await authFetch(`${API_BASE}/logs`); |
| const data = await res.json(); |
| |
| const container = document.getElementById('logContainer'); |
| if (data.logs && data.logs.length > 0) { |
| container.innerHTML = data.logs.map(log => ` |
| <div class="log-entry"> |
| <span class="log-time">${escapeHtml(String(log.time))}</span> |
| <span class="log-method">${escapeHtml(log.method)}</span> |
| <span class="log-path">${escapeHtml(log.path)}</span> |
| <span class="log-status ${log.status < 400 ? 'ok' : 'error'}">${Number(log.status)}</span> |
| </div> |
| `).join(''); |
| } else { |
| container.innerHTML = '<div style="color: #666;">暂无日志</div>'; |
| } |
| } catch (e) { |
| console.error('获取日志失败:', e); |
| } |
| } |
| |
| |
| function refreshLogs() { |
| fetchLogs(); |
| } |
| |
| |
| async function toggleFeature(feature) { |
| const checkbox = document.getElementById(`toggle${feature.charAt(0).toUpperCase() + feature.slice(1)}`); |
| try { |
| await authFetch(`${API_BASE}/config`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ feature, enabled: checkbox.checked }) |
| }); |
| } catch (e) { |
| console.error('切换功能失败:', e); |
| checkbox.checked = !checkbox.checked; |
| } |
| } |
| |
| |
| function showEditConfig() { |
| fetchStatus().then(() => { |
| document.getElementById('editHost').value = document.getElementById('currentHost').textContent; |
| document.getElementById('editPort').value = document.getElementById('currentPort').textContent; |
| document.getElementById('configModal').classList.add('active'); |
| }); |
| } |
| |
| |
| function closeModal() { |
| document.getElementById('configModal').classList.remove('active'); |
| } |
| |
| |
| async function saveConfigAndRestart() { |
| if (!confirm('确定要保存配置并重启服务吗?')) return; |
| |
| const config = { |
| host: document.getElementById('editHost').value, |
| port: parseInt(document.getElementById('editPort').value), |
| api_key: document.getElementById('editApiKey').value |
| }; |
| |
| try { |
| const res = await authFetch(`${API_BASE}/config-save-restart`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(config) |
| }); |
| |
| if (res.ok) { |
| alert('配置已保存,服务正在重启...'); |
| closeModal(); |
| setTimeout(fetchStatus, 3000); |
| } else { |
| alert('保存失败'); |
| } |
| } catch (e) { |
| console.error('保存配置失败:', e); |
| alert('保存失败: ' + e.message); |
| } |
| } |
| |
| |
| async function saveCookiesAndReinit() { |
| const psid = document.getElementById('cookie1psid').value.trim(); |
| const psidts = document.getElementById('cookie1psidts').value.trim(); |
| const msgEl = document.getElementById('cookieMessage'); |
| |
| if (!psid || !psidts) { |
| msgEl.style.display = 'block'; |
| msgEl.style.background = '#3a1a1a'; |
| msgEl.style.color = '#f44336'; |
| msgEl.textContent = '请填写 __Secure-1PSID 和 __Secure-1PSIDTS'; |
| return; |
| } |
| |
| try { |
| msgEl.style.display = 'block'; |
| msgEl.style.background = '#1a2a3a'; |
| msgEl.style.color = '#4fc3f7'; |
| msgEl.textContent = '⏳ 正在保存 Cookie 并重新连接 Gemini...'; |
| |
| const res = await authFetch(`${API_BASE}/cookies-save-reinit`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ secure_1psid: psid, secure_1psidts: psidts }) |
| }); |
| |
| const data = await res.json(); |
| if (res.ok && data.success) { |
| msgEl.style.background = '#1a3a1a'; |
| msgEl.style.color = '#4caf50'; |
| msgEl.textContent = '✅ ' + data.message; |
| document.getElementById('cookie1psid').value = ''; |
| document.getElementById('cookie1psidts').value = ''; |
| document.getElementById('cookieRaw').value = ''; |
| } else { |
| msgEl.style.background = '#3a2a1a'; |
| msgEl.style.color = '#ff9800'; |
| msgEl.textContent = '⚠️ ' + (data.message || '操作失败'); |
| } |
| fetchStatus(); |
| } catch (e) { |
| msgEl.style.display = 'block'; |
| msgEl.style.background = '#3a1a1a'; |
| msgEl.style.color = '#f44336'; |
| msgEl.textContent = '❌ 网络错误: ' + e.message; |
| } |
| } |
| |
| |
| function parseRawCookie(showMessage = false) { |
| const raw = document.getElementById('cookieRaw').value.trim(); |
| const msgEl = document.getElementById('cookieMessage'); |
| |
| if (!raw) { |
| if (showMessage) { |
| msgEl.style.display = 'block'; |
| msgEl.style.background = '#3a2a1a'; |
| msgEl.style.color = '#ffb74d'; |
| msgEl.textContent = '⚠️ 请先粘贴 Cookie 字符串'; |
| } |
| return; |
| } |
| |
| |
| const cookies = {}; |
| |
| const regex = /([\w\-]+)=([^;]+)/g; |
| let match; |
| while ((match = regex.exec(raw)) !== null) { |
| cookies[match[1].trim()] = match[2].trim(); |
| } |
| |
| const psid = cookies['__Secure-1PSID'] || ''; |
| const psidts = cookies['__Secure-1PSIDTS'] || ''; |
| |
| |
| document.getElementById('cookie1psid').value = psid; |
| document.getElementById('cookie1psidts').value = psidts; |
| |
| if (showMessage) { |
| msgEl.style.display = 'block'; |
| if (psid && psidts) { |
| msgEl.style.background = '#1a3a1a'; |
| msgEl.style.color = '#4caf50'; |
| msgEl.textContent = `✅ 成功提取!共识别 ${Object.keys(cookies).length} 个 Cookie,已填入 __Secure-1PSID (${psid.substring(0, 20)}...) 和 __Secure-1PSIDTS (${psidts.substring(0, 20)}...)`; |
| } else if (psid || psidts) { |
| msgEl.style.background = '#3a2a1a'; |
| msgEl.style.color = '#ffb74d'; |
| const missing = !psid ? '__Secure-1PSID' : '__Secure-1PSIDTS'; |
| msgEl.textContent = `⚠️ 只找到 ${psid ? '__Secure-1PSID' : '__Secure-1PSIDTS'},缺少 ${missing},请手动补充`; |
| } else { |
| msgEl.style.background = '#3a1a1a'; |
| msgEl.style.color = '#f44336'; |
| msgEl.textContent = `❌ 未找到 __Secure-1PSID 或 __Secure-1PSIDTS(共识别 ${Object.keys(cookies).length} 个 Cookie)`; |
| } |
| } |
| } |
| |
| |
| async function sendTestMessage() { |
| const input = document.getElementById('chatInput'); |
| const message = input.value.trim(); |
| if (!message) return; |
| |
| |
| const chatMessages = document.getElementById('chatMessages'); |
| chatMessages.innerHTML += `<div class="chat-msg user">${escapeHtml(message)}</div>`; |
| input.value = ''; |
| |
| |
| const loadingId = 'loading-' + Date.now(); |
| chatMessages.innerHTML += `<div class="chat-msg assistant" id="${loadingId}">思考中...</div>`; |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| |
| try { |
| const model = document.getElementById('chatModel').value || 'gemini-3-flash'; |
| const res = await fetch('/v1/chat/completions', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Authorization': `Bearer ${adminToken}` |
| }, |
| body: JSON.stringify({ |
| model: model, |
| messages: [{ role: 'user', content: message }], |
| stream: false |
| }) |
| }); |
| |
| if (!res.ok) { |
| let errMsg; |
| try { |
| const errBody = await res.json(); |
| errMsg = errBody.error?.message || errBody.detail || JSON.stringify(errBody); |
| } catch { errMsg = res.statusText || `HTTP ${res.status}`; } |
| document.getElementById(loadingId).innerHTML = ''; |
| const span = document.createElement('span'); |
| span.style.color = '#f44336'; |
| span.textContent = `请求失败 (${res.status}): `; |
| const el = document.getElementById(loadingId); |
| el.appendChild(span); |
| el.appendChild(document.createTextNode(errMsg)); |
| } else { |
| const data = await res.json(); |
| const reply = data.choices?.[0]?.message?.content || '抱歉,没有收到回复。'; |
| document.getElementById(loadingId).innerHTML = escapeHtml(reply); |
| } |
| } catch (e) { |
| const el = document.getElementById(loadingId); |
| el.innerHTML = '<span style="color: #f44336;">错误: </span>'; |
| el.appendChild(document.createTextNode(e.message)); |
| } |
| |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| } |
| |
| |
| function escapeHtml(text) { |
| const div = document.createElement('div'); |
| div.textContent = text; |
| return div.innerHTML; |
| } |
| </script> |
| </body> |
| </html> |