Spaces:
Running
Running
nacho commited on
Commit ·
251d72f
1
Parent(s): a3f3a8e
feat: 面板登录密码 + 启动自动预登录账号
Browse files- 新增登录遮罩层,需输入 admin key 才能进入控制台
- admin key 验证通过 /admin/verify 接口
- localStorage 持久化密钥,下次自动登录
- 启动时自动预登录所有账号(后台异步)
- 移除面板中多余的 admin key 输入框
- main.py +26 -0
- static/index.html +75 -18
main.py
CHANGED
|
@@ -268,6 +268,16 @@ async def list_accounts(admin_key: str = Header(...)):
|
|
| 268 |
return {"accounts": accounts, "total": len(accounts)}
|
| 269 |
|
| 270 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
@app.get("/")
|
| 272 |
async def admin_panel():
|
| 273 |
return RedirectResponse(url="/static/index.html")
|
|
@@ -285,6 +295,22 @@ async def startup():
|
|
| 285 |
|
| 286 |
logger.info("Loaded %d accounts", len(config.accounts))
|
| 287 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
def main():
|
| 290 |
import uvicorn
|
|
|
|
| 268 |
return {"accounts": accounts, "total": len(accounts)}
|
| 269 |
|
| 270 |
|
| 271 |
+
@app.post("/admin/verify")
|
| 272 |
+
async def admin_verify(request: Request):
|
| 273 |
+
"""Verify admin key for panel login."""
|
| 274 |
+
body = await request.json()
|
| 275 |
+
key = body.get("key", "")
|
| 276 |
+
if key != config.server.admin_key:
|
| 277 |
+
raise HTTPException(status_code=401, detail="Invalid admin key")
|
| 278 |
+
return {"ok": True}
|
| 279 |
+
|
| 280 |
+
|
| 281 |
@app.get("/")
|
| 282 |
async def admin_panel():
|
| 283 |
return RedirectResponse(url="/static/index.html")
|
|
|
|
| 295 |
|
| 296 |
logger.info("Loaded %d accounts", len(config.accounts))
|
| 297 |
|
| 298 |
+
# Pre-login all accounts in background so they show online immediately
|
| 299 |
+
asyncio.create_task(_prelogin_all())
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
async def _prelogin_all():
|
| 303 |
+
"""Pre-login all accounts at startup for instant readiness."""
|
| 304 |
+
for email, account in manager.accounts.items():
|
| 305 |
+
try:
|
| 306 |
+
logger.info("Pre-logging in %s...", email)
|
| 307 |
+
await manager.get_or_create_browser_with_retry(
|
| 308 |
+
account, headless=config.browser.headless
|
| 309 |
+
)
|
| 310 |
+
logger.info("Pre-login OK: %s (muted=%s)", email, account.is_muted)
|
| 311 |
+
except Exception as e:
|
| 312 |
+
logger.error("Pre-login FAILED for %s: %s", email, e)
|
| 313 |
+
|
| 314 |
|
| 315 |
def main():
|
| 316 |
import uvicorn
|
static/index.html
CHANGED
|
@@ -120,10 +120,37 @@ select{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%
|
|
| 120 |
.empty{color:var(--text-muted);padding:24px 0;text-align:center;font-size:12px}
|
| 121 |
.key-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
| 122 |
@media(max-width:600px){.key-row{grid-template-columns:1fr}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
</style>
|
| 124 |
</head>
|
| 125 |
<body>
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
<div class="topbar">
|
| 128 |
<span class="logo">▸ DS2API</span>
|
| 129 |
<span class="badge-mode">浏览器模式</span>
|
|
@@ -157,15 +184,9 @@ select{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%
|
|
| 157 |
<span class="hint">/v1/chat/completions</span>
|
| 158 |
</div>
|
| 159 |
<div class="card-body">
|
| 160 |
-
<div class="
|
| 161 |
-
<
|
| 162 |
-
|
| 163 |
-
<input type="password" id="apiKey" placeholder="sk-xxx">
|
| 164 |
-
</div>
|
| 165 |
-
<div class="form-group">
|
| 166 |
-
<label>Admin Key</label>
|
| 167 |
-
<input type="password" id="adminKey" placeholder="admin key">
|
| 168 |
-
</div>
|
| 169 |
</div>
|
| 170 |
<div class="row" style="gap:10px;margin-bottom:14px">
|
| 171 |
<div class="form-group" style="flex:1;margin-bottom:0">
|
|
@@ -229,6 +250,7 @@ select{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%
|
|
| 229 |
|
| 230 |
</div>
|
| 231 |
</div>
|
|
|
|
| 232 |
|
| 233 |
<script>
|
| 234 |
const H=location.origin;
|
|
@@ -237,16 +259,48 @@ const LS={
|
|
| 237 |
set(k,v){try{localStorage.setItem('ds2_'+k,v)}catch(e){}}
|
| 238 |
};
|
| 239 |
|
| 240 |
-
|
| 241 |
-
document.getElementById('apiKey').value=LS.get('apiKey','');
|
| 242 |
-
document.getElementById('adminKey').value=LS.get('adminKey','');
|
| 243 |
|
| 244 |
-
//
|
| 245 |
-
|
| 246 |
-
document.getElementById('
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
|
| 248 |
function getApiKey(){return document.getElementById('apiKey').value.trim()||'sk-default'}
|
| 249 |
-
function getAdminKey(){return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
function toast(m,ok){
|
| 252 |
const e=document.createElement('div');
|
|
@@ -396,8 +450,11 @@ async function loadAll(){await loadStats();await loadAccounts()}
|
|
| 396 |
// Ctrl+Enter to send
|
| 397 |
document.getElementById('prompt').addEventListener('keydown',e=>{if(e.ctrlKey&&e.key==='Enter')sendMsg()});
|
| 398 |
|
| 399 |
-
|
| 400 |
-
|
|
|
|
|
|
|
|
|
|
| 401 |
</script>
|
| 402 |
</body>
|
| 403 |
</html>
|
|
|
|
| 120 |
.empty{color:var(--text-muted);padding:24px 0;text-align:center;font-size:12px}
|
| 121 |
.key-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
| 122 |
@media(max-width:600px){.key-row{grid-template-columns:1fr}}
|
| 123 |
+
|
| 124 |
+
/* ── login overlay ── */
|
| 125 |
+
.login-overlay{position:fixed;inset:0;z-index:200;background:var(--bg);display:flex;align-items:center;justify-content:center;transition:opacity .4s}
|
| 126 |
+
.login-overlay.hidden{opacity:0;pointer-events:none}
|
| 127 |
+
.login-box{background:var(--surface);backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:var(--radius);padding:40px;width:100%;max-width:380px;text-align:center;animation:fadeUp .5s ease-out}
|
| 128 |
+
.login-box .logo-big{font-size:24px;font-weight:800;background:linear-gradient(135deg,var(--accent),hsl(280,80%,65%));-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:8px}
|
| 129 |
+
.login-box .sub{color:var(--text-dim);font-size:12px;margin-bottom:28px}
|
| 130 |
+
.login-box .form-group{text-align:left;margin-bottom:16px}
|
| 131 |
+
.login-box .btn-primary{width:100%;padding:12px;font-size:14px}
|
| 132 |
+
.login-box .err-msg{color:var(--red);font-size:12px;margin-top:10px;min-height:18px}
|
| 133 |
+
.app-wrap{transition:filter .3s}
|
| 134 |
+
.app-wrap.blur{filter:blur(8px);pointer-events:none}
|
| 135 |
</style>
|
| 136 |
</head>
|
| 137 |
<body>
|
| 138 |
|
| 139 |
+
<!-- Login Overlay -->
|
| 140 |
+
<div class="login-overlay" id="loginOverlay">
|
| 141 |
+
<div class="login-box">
|
| 142 |
+
<div class="logo-big">▸ DS2API</div>
|
| 143 |
+
<div class="sub">输入管理密钥进入控制台</div>
|
| 144 |
+
<div class="form-group">
|
| 145 |
+
<label>Admin Key</label>
|
| 146 |
+
<input type="password" id="loginKey" placeholder="请输入管理密钥" autofocus>
|
| 147 |
+
</div>
|
| 148 |
+
<button class="btn btn-primary" onclick="doLogin()" id="loginBtn">进入控制台</button>
|
| 149 |
+
<div class="err-msg" id="loginErr"></div>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
|
| 153 |
+
<div class="app-wrap" id="appWrap">
|
| 154 |
<div class="topbar">
|
| 155 |
<span class="logo">▸ DS2API</span>
|
| 156 |
<span class="badge-mode">浏览器模式</span>
|
|
|
|
| 184 |
<span class="hint">/v1/chat/completions</span>
|
| 185 |
</div>
|
| 186 |
<div class="card-body">
|
| 187 |
+
<div class="form-group">
|
| 188 |
+
<label>API Key</label>
|
| 189 |
+
<input type="password" id="apiKey" placeholder="sk-xxx">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
</div>
|
| 191 |
<div class="row" style="gap:10px;margin-bottom:14px">
|
| 192 |
<div class="form-group" style="flex:1;margin-bottom:0">
|
|
|
|
| 250 |
|
| 251 |
</div>
|
| 252 |
</div>
|
| 253 |
+
</div><!-- /app-wrap -->
|
| 254 |
|
| 255 |
<script>
|
| 256 |
const H=location.origin;
|
|
|
|
| 259 |
set(k,v){try{localStorage.setItem('ds2_'+k,v)}catch(e){}}
|
| 260 |
};
|
| 261 |
|
| 262 |
+
let _adminKey='';
|
|
|
|
|
|
|
| 263 |
|
| 264 |
+
// ── Auth Gate ──
|
| 265 |
+
async function doLogin(){
|
| 266 |
+
const key=document.getElementById('loginKey').value.trim();
|
| 267 |
+
const err=document.getElementById('loginErr');
|
| 268 |
+
const btn=document.getElementById('loginBtn');
|
| 269 |
+
if(!key){err.textContent='请输入密钥';return}
|
| 270 |
+
btn.disabled=true;btn.textContent='验证中…';err.textContent='';
|
| 271 |
+
try{
|
| 272 |
+
const r=await fetch(H+'/admin/verify',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({key})});
|
| 273 |
+
if(!r.ok)throw new Error('密钥错误');
|
| 274 |
+
_adminKey=key;
|
| 275 |
+
LS.set('adminKey',key);
|
| 276 |
+
document.getElementById('loginOverlay').classList.add('hidden');
|
| 277 |
+
document.getElementById('appWrap').classList.remove('blur');
|
| 278 |
+
initApp();
|
| 279 |
+
}catch(e){err.textContent=e.message}
|
| 280 |
+
btn.disabled=false;btn.textContent='进入控制台';
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
document.getElementById('loginKey').addEventListener('keydown',e=>{if(e.key==='Enter')doLogin()});
|
| 284 |
+
|
| 285 |
+
// Auto-login if key saved
|
| 286 |
+
(async()=>{
|
| 287 |
+
const saved=LS.get('adminKey','');
|
| 288 |
+
if(saved){
|
| 289 |
+
document.getElementById('loginKey').value=saved;
|
| 290 |
+
try{
|
| 291 |
+
const r=await fetch(H+'/admin/verify',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({key:saved})});
|
| 292 |
+
if(r.ok){_adminKey=saved;document.getElementById('loginOverlay').classList.add('hidden');document.getElementById('appWrap').classList.remove('blur');initApp();return}
|
| 293 |
+
}catch(e){}
|
| 294 |
+
}
|
| 295 |
+
document.getElementById('appWrap').classList.add('blur');
|
| 296 |
+
})();
|
| 297 |
|
| 298 |
function getApiKey(){return document.getElementById('apiKey').value.trim()||'sk-default'}
|
| 299 |
+
function getAdminKey(){return _adminKey||'admin'}
|
| 300 |
+
|
| 301 |
+
// Restore API key
|
| 302 |
+
document.getElementById('apiKey').value=LS.get('apiKey','');
|
| 303 |
+
document.getElementById('apiKey').addEventListener('input',e=>LS.set('apiKey',e.target.value));
|
| 304 |
|
| 305 |
function toast(m,ok){
|
| 306 |
const e=document.createElement('div');
|
|
|
|
| 450 |
// Ctrl+Enter to send
|
| 451 |
document.getElementById('prompt').addEventListener('keydown',e=>{if(e.ctrlKey&&e.key==='Enter')sendMsg()});
|
| 452 |
|
| 453 |
+
let _pollTimer=null;
|
| 454 |
+
function initApp(){
|
| 455 |
+
loadAll();
|
| 456 |
+
if(!_pollTimer)_pollTimer=setInterval(loadAll,12000);
|
| 457 |
+
}
|
| 458 |
</script>
|
| 459 |
</body>
|
| 460 |
</html>
|