gmn2a / admin.html
longxing's picture
Upload admin.html
1aeb39d verified
Raw
History Blame Contribute Delete
39 kB
<!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>
// API 基础路径
const API_BASE = '/admin/api';
// 会话管理
let adminToken = localStorage.getItem('adminToken') || '';
// 带 token 的 fetch 封装
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) {}
// token 无效,清除并显示登录
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 ? '有效' : '无效';
// 显示脱敏的 Cookie 值
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('');
// 填充快速测试模型下拉框,过滤掉 'unspecified'
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);
}
}
// 保存 Gemini Cookie 并重新连接
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;
}
}
// 从整段 Cookie 字符串中提取 __Secure-1PSID 和 __Secure-1PSIDTS
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;
}
// 解析 key=value; key=value; 格式
const cookies = {};
// 匹配 key=value 对,支持空格和分号分隔
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;
}
// HTML 转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>