From 264581caf4d6233f8d977432dfcd6108a6844290 Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 19 May 2026 02:42:59 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 43 +++++++++++++ main.py | 178 +++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 175 insertions(+), 46 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c87271 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +.venv/ +env/ +.env +.env.* +!.env.example + +# 本地配置与密钥(保留 *.example) +gateway.json +nodes.json + +# 运行时数据 +gateway_stats.db +gateway_stats.db-* +*.log +logs/ + +# 测试与工具缓存 +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +htmlcov/ +.coverage +coverage.xml + +# IDE / 系统 +.vscode/ +.idea/ +*.swp +*.swo +.DS_Store +Thumbs.db +desktop.ini + +# 临时脚本 +_patch_*.py +_fix_*.py diff --git a/main.py b/main.py index 2534824..7faadd6 100644 --- a/main.py +++ b/main.py @@ -922,6 +922,16 @@ SHELL_HEAD = f""" }} catch (e) {{}} return res && res.status ? ('HTTP ' + res.status + ' ' + (res.statusText || '')) : '网络或服务器错误'; }} + (function() {{ + if (!localStorage.getItem('web_token')) {{ location.replace(wgApi('/login')); return; }} + document.addEventListener('DOMContentLoaded', function() {{ + var btn = document.getElementById('wg-logout'); + if (btn) btn.addEventListener('click', function() {{ + localStorage.removeItem('web_token'); + location.href = wgApi('/login'); + }}); + }}); + }})();
@@ -929,16 +939,16 @@ SHELL_HEAD = f"""
@@ -956,6 +966,47 @@ SHELL_FOOT = """ """ +LOGIN_SHELL_HEAD = f""" + + + + + + {{title}} + + + + + + + +
+
+
+
+
+""" + + def page(title: str, inner: str) -> str: return ( SHELL_HEAD.replace("{title}", title) @@ -964,12 +1015,20 @@ def page(title: str, inner: str) -> str: ) +def page_login(title: str, inner: str) -> str: + return ( + LOGIN_SHELL_HEAD.replace("{title}", title) + + inner + + SHELL_FOOT + ) + + HOME_HTML = page( "中转网关", f""" - +
- +

模型分布

各节点模型状态(约每 5 秒刷新;主机默认 127.0.0.1,经 frp 映射端口)

@@ -977,10 +1036,10 @@ HOME_HTML = page( 管理节点与模型 →
加载中…
-
+
- +

使用说明

  • · 对外统一访问本网关(宝塔反代端口 8150),请求 /v1/chat/completions,Header 携带 Authorization: Bearer sk-...
  • @@ -995,39 +1054,59 @@ HOME_HTML = page( const statusBadge = {{ idle: 'bg-emerald-500/20 text-emerald-200', busy: 'bg-amber-500/20 text-amber-200', offline: 'bg-slate-500/20 text-slate-300', disabled: 'bg-slate-600/20 text-slate-400', error: 'bg-rose-500/20 text-rose-200' }}; function esc(s) {{ return String(s == null ? '' : s).replace(/&/g,'&').replace(//g,'>'); }} function fmt(n) {{ return Number(n || 0).toLocaleString(); }} + function wgAuthHeaders() {{ + var t = localStorage.getItem('web_token'); + if (!t) {{ location.replace(wgApi('/login')); return null; }} + return {{ 'Authorization': 'Bearer ' + t }}; + }} + let lastCards = []; function renderCards(cards) {{ const grid = document.getElementById('cards-grid'); const empty = document.getElementById('cards-empty'); - grid.innerHTML = ''; - if (!cards.length) {{ grid.classList.add('hidden'); empty.classList.remove('hidden'); return; }} - empty.classList.add('hidden'); grid.classList.remove('hidden'); - cards.forEach(c => {{ + if (!cards.length) {{ grid.innerHTML = ''; empty.classList.remove('hidden'); return; }} + empty.classList.add('hidden'); + grid.innerHTML = cards.map(function(c) {{ const st = c.status || 'offline'; - const el = document.createElement('article'); - el.className = 'rounded-2xl bg-white/5 p-5 ring-1 ' + (statusRing[st]||statusRing.offline) + ' backdrop-blur flex flex-col gap-3'; - el.innerHTML = '

    '+esc(c.model_label||c.model_id||'未命名')+'

    '+esc(c.status_label)+'
' - + '

'+esc(c.model_id||'—')+'

' - + '

'+esc(c.node_name)+' · '+esc(c.endpoint)+'

' - + '
进行中 '+esc(c.in_flight)+'/'+esc(c.max_concurrent)+'
今日 Token '+fmt(c.today_tokens)+'
今日请求 '+fmt(c.today_requests)+'
'; - if (c.last_error && (st==='error'||st==='offline')) {{ const e=document.createElement('p'); e.className='text-xs text-rose-300/90 truncate'; e.title=c.last_error; e.textContent=c.last_error; el.appendChild(e); }} - grid.appendChild(el); - }}); + const err = (c.last_error && (st === 'error' || st === 'offline')) + ? '

'+esc(c.last_error)+'

' : ''; + return '
' + + '

'+esc(c.model_label||c.model_id||'未命名')+'

' + + ''+esc(c.status_label)+'
' + + '

'+esc(c.model_id||'—')+'

' + + '

'+esc(c.node_name)+' · '+esc(c.endpoint)+'

' + + '
' + + '
进行中
'+esc(c.in_flight)+' / '+esc(c.max_concurrent)+'
' + + '
今日 Token
'+fmt(c.today_tokens)+'
' + + '
今日请求
'+fmt(c.today_requests)+'
' + + '
' + err + '
'; + }}).join(''); }} async function refreshCards() {{ + const hdrs = wgAuthHeaders(); + if (!hdrs) return; try {{ - const res = await fetch(wgApi('/api/models/cards')); - const data = await res.json().catch(() => ({{}})); + const res = await fetch(wgApi('/api/models/cards'), {{ headers: hdrs }}); + if (res.status === 401) {{ localStorage.removeItem('web_token'); location.replace(wgApi('/login')); return; }} + const data = await res.json().catch(function() {{ return []; }}); document.getElementById('cards-loading').classList.add('hidden'); - if (res.ok) renderCards(Array.isArray(data) ? data : (data.cards || [])); - }} catch (_) {{}} + if (res.ok) {{ + lastCards = Array.isArray(data) ? data : (data.cards || []); + renderCards(lastCards); + }} else if (lastCards.length) {{ + renderCards(lastCards); + }} + }} catch (e) {{ + if (lastCards.length) renderCards(lastCards); + }} }} - refreshCards(); setInterval(refreshCards, 5000); + refreshCards(); + setInterval(refreshCards, 5000); """, ) -LOGIN_HTML = page( +LOGIN_HTML = page_login( "登录", f"""
@@ -1067,7 +1146,7 @@ LOGIN_HTML = page( if (!res.ok) {{ err.textContent = wgFmtErr(res, data) || raw.slice(0, 200) || '登录失败'; err.classList.remove('hidden'); return; }} if (!data.access_token) {{ err.textContent = '服务端未返回令牌'; err.classList.remove('hidden'); return; }} localStorage.setItem('web_token', data.access_token); - window.location.href = wgApi('/user'); + window.location.href = wgApi('/home'); }} catch (ex) {{ err.textContent = '无法连接服务器:' + (ex && ex.message ? ex.message : String(ex)); err.classList.remove('hidden'); @@ -1151,31 +1230,31 @@ SETTINGS_HTML = page(

管理节点主机、端口与模型(默认主机 127.0.0.1,对应 frp 映射)

- - +
+

添加节点

- +
- +
- +
- +
- - +
+
- +
-
- +
+ """, -).replace("
", "
").replace("", "") +).replace("
", "
").replace("
", "
") STATS_HTML = page( @@ -1519,8 +1598,8 @@ app.add_middleware( @app.get("/", response_class=HTMLResponse) -async def home() -> str: - return HOME_HTML +async def root() -> str: + return LOGIN_HTML @app.get("/login", response_class=HTMLResponse) @@ -1528,6 +1607,11 @@ async def login_page() -> str: return LOGIN_HTML +@app.get("/home", response_class=HTMLResponse) +async def home() -> str: + return HOME_HTML + + @app.get("/user", response_class=HTMLResponse) async def user_page() -> str: return USER_HTML @@ -1563,7 +1647,9 @@ async def api_me( @app.get("/api/models/cards") -async def api_model_cards() -> List[Dict[str, Any]]: +async def api_model_cards( + _: Annotated[GateSessionUser, Depends(get_current_web_user)], +) -> List[Dict[str, Any]]: return await run_in_thread(build_model_cards)