修改bug
This commit is contained in:
+43
@@ -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
|
||||||
@@ -922,6 +922,16 @@ SHELL_HEAD = f"""
|
|||||||
}} catch (e) {{}}
|
}} catch (e) {{}}
|
||||||
return res && res.status ? ('HTTP ' + res.status + ' ' + (res.statusText || '')) : '网络或服务器错误';
|
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');
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
}})();
|
||||||
</script>
|
</script>
|
||||||
<div class="pointer-events-none fixed inset-0 overflow-hidden">
|
<div class="pointer-events-none fixed inset-0 overflow-hidden">
|
||||||
<div class="absolute -left-32 top-20 h-72 w-72 rounded-full bg-indigo-500/20 blur-3xl"></div>
|
<div class="absolute -left-32 top-20 h-72 w-72 rounded-full bg-indigo-500/20 blur-3xl"></div>
|
||||||
@@ -929,16 +939,16 @@ SHELL_HEAD = f"""
|
|||||||
</div>
|
</div>
|
||||||
<header class="relative z-10 border-b border-white/10 bg-black/20 backdrop-blur-md">
|
<header class="relative z-10 border-b border-white/10 bg-black/20 backdrop-blur-md">
|
||||||
<div class="mx-auto flex max-w-5xl items-center justify-between px-6 py-4">
|
<div class="mx-auto flex max-w-5xl items-center justify-between px-6 py-4">
|
||||||
<a href="{app_url('/')}" class="flex items-center gap-2 text-lg font-semibold tracking-tight text-white hover:text-indigo-200 transition">
|
<a href="{app_url('/home')}" class="flex items-center gap-2 text-lg font-semibold tracking-tight text-white hover:text-indigo-200 transition">
|
||||||
<span class="inline-flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-indigo-400 to-cyan-400 text-slate-950 font-bold">AI</span>
|
<span class="inline-flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-indigo-400 to-cyan-400 text-slate-950 font-bold">AI</span>
|
||||||
<span>中转网关</span>
|
<span>中转网关</span>
|
||||||
</a>
|
</a>
|
||||||
<nav class="flex items-center gap-6 text-sm font-medium text-slate-300">
|
<nav class="flex items-center gap-6 text-sm font-medium text-slate-300">
|
||||||
<a href="{app_url('/')}" class="hover:text-white transition">首页</a>
|
<a href="{app_url('/home')}" class="hover:text-white transition">首页</a>
|
||||||
<a href="{app_url('/login')}" class="hover:text-white transition">登录</a>
|
|
||||||
<a href="{app_url('/stats')}" class="hover:text-white transition">流量统计</a>
|
<a href="{app_url('/stats')}" class="hover:text-white transition">流量统计</a>
|
||||||
<a href="{app_url('/settings')}" class="hover:text-white transition">系统设置</a>
|
<a href="{app_url('/settings')}" class="hover:text-white transition">系统设置</a>
|
||||||
<a href="{app_url('/user')}" class="rounded-full bg-white/10 px-4 py-2 text-white ring-1 ring-white/15 hover:bg-white/15 transition">用户中心</a>
|
<a href="{app_url('/user')}" class="rounded-full bg-white/10 px-4 py-2 text-white ring-1 ring-white/15 hover:bg-white/15 transition">用户中心</a>
|
||||||
|
<button type="button" id="wg-logout" class="text-slate-400 hover:text-white transition">退出</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -956,6 +966,47 @@ SHELL_FOOT = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
LOGIN_SHELL_HEAD = f"""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<title>{{title}}</title>
|
||||||
|
<script src="{TW_SCRIPT}"></script>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen bg-gradient-to-br from-slate-950 via-indigo-950 to-slate-900 text-slate-100 antialiased">
|
||||||
|
<script>
|
||||||
|
window.__APP_ROOT__ = {_APP_ROOT_JSON};
|
||||||
|
function wgApi(path) {{
|
||||||
|
if (!path || path.charAt(0) !== '/') path = '/' + (path || '');
|
||||||
|
var root = (window.__APP_ROOT__ || '').replace(/\\/+$/, '');
|
||||||
|
return root + path;
|
||||||
|
}}
|
||||||
|
function wgFmtErr(res, data) {{
|
||||||
|
try {{
|
||||||
|
if (data && data.detail !== undefined) {{
|
||||||
|
var d = data.detail;
|
||||||
|
if (typeof d === 'string') return d;
|
||||||
|
if (Array.isArray(d)) return d.map(function(e) {{ return (e.loc?e.loc.join('.'):'') + (e.msg||''); }}).join(';');
|
||||||
|
if (typeof d === 'object') return JSON.stringify(d);
|
||||||
|
}}
|
||||||
|
}} catch (e) {{}}
|
||||||
|
return res && res.status ? ('HTTP ' + res.status) : '网络或服务器错误';
|
||||||
|
}}
|
||||||
|
if (localStorage.getItem('web_token')) location.replace(wgApi('/home'));
|
||||||
|
</script>
|
||||||
|
<div class="pointer-events-none fixed inset-0 overflow-hidden">
|
||||||
|
<div class="absolute -left-32 top-20 h-72 w-72 rounded-full bg-indigo-500/20 blur-3xl"></div>
|
||||||
|
<div class="absolute -right-20 bottom-10 h-96 w-96 rounded-full bg-cyan-500/15 blur-3xl"></div>
|
||||||
|
</div>
|
||||||
|
<main class="relative z-10 mx-auto flex min-h-screen max-w-5xl items-center justify-center px-6 py-12">
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def page(title: str, inner: str) -> str:
|
def page(title: str, inner: str) -> str:
|
||||||
return (
|
return (
|
||||||
SHELL_HEAD.replace("{title}", title)
|
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(
|
HOME_HTML = page(
|
||||||
"中转网关",
|
"中转网关",
|
||||||
f"""
|
f"""
|
||||||
<motion-wrap class="space-y-12">
|
<div class="space-y-12">
|
||||||
<div>
|
<div>
|
||||||
<motion-wrap class="flex flex-wrap items-end justify-between gap-4">
|
<div class="flex flex-wrap items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold text-white">模型分布</h1>
|
<h1 class="text-2xl font-bold text-white">模型分布</h1>
|
||||||
<p class="mt-2 text-sm text-slate-400">各节点模型状态(约每 5 秒刷新;主机默认 127.0.0.1,经 frp 映射端口)</p>
|
<p class="mt-2 text-sm text-slate-400">各节点模型状态(约每 5 秒刷新;主机默认 127.0.0.1,经 frp 映射端口)</p>
|
||||||
@@ -977,10 +1036,10 @@ HOME_HTML = page(
|
|||||||
<a href="{app_url('/settings')}" class="text-sm text-indigo-300 hover:text-white transition">管理节点与模型 →</a>
|
<a href="{app_url('/settings')}" class="text-sm text-indigo-300 hover:text-white transition">管理节点与模型 →</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="cards-loading" class="mt-6 text-slate-400">加载中…</div>
|
<div id="cards-loading" class="mt-6 text-slate-400">加载中…</div>
|
||||||
<motion-wrap id="cards-grid" class="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3 hidden"></div>
|
<div id="cards-grid" class="mt-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-3"></div>
|
||||||
<p id="cards-empty" class="mt-6 hidden text-sm text-slate-500">暂无模型卡片,请登录后在系统设置中添加节点与模型。</p>
|
<p id="cards-empty" class="mt-6 hidden text-sm text-slate-500">暂无模型卡片,请登录后在系统设置中添加节点与模型。</p>
|
||||||
</div>
|
</div>
|
||||||
<motion-wrap class="rounded-3xl bg-white/5 p-8 ring-1 ring-white/10 backdrop-blur">
|
<div class="rounded-3xl bg-white/5 p-8 ring-1 ring-white/10 backdrop-blur">
|
||||||
<h2 class="text-lg font-semibold text-white">使用说明</h2>
|
<h2 class="text-lg font-semibold text-white">使用说明</h2>
|
||||||
<ul class="mt-4 space-y-3 text-sm leading-relaxed text-slate-300">
|
<ul class="mt-4 space-y-3 text-sm leading-relaxed text-slate-300">
|
||||||
<li>· 对外统一访问本网关(宝塔反代端口 <span class="text-cyan-200">8150</span>),请求 <code class="rounded bg-white/10 px-1 text-cyan-200">/v1/chat/completions</code>,Header 携带 <code class="rounded bg-white/10 px-1 text-cyan-200">Authorization: Bearer sk-...</code>。</li>
|
<li>· 对外统一访问本网关(宝塔反代端口 <span class="text-cyan-200">8150</span>),请求 <code class="rounded bg-white/10 px-1 text-cyan-200">/v1/chat/completions</code>,Header 携带 <code class="rounded bg-white/10 px-1 text-cyan-200">Authorization: Bearer sk-...</code>。</li>
|
||||||
@@ -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' }};
|
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,'<').replace(/>/g,'>'); }}
|
function esc(s) {{ return String(s == null ? '' : s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }}
|
||||||
function fmt(n) {{ return Number(n || 0).toLocaleString(); }}
|
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) {{
|
function renderCards(cards) {{
|
||||||
const grid = document.getElementById('cards-grid');
|
const grid = document.getElementById('cards-grid');
|
||||||
const empty = document.getElementById('cards-empty');
|
const empty = document.getElementById('cards-empty');
|
||||||
grid.innerHTML = '';
|
if (!cards.length) {{ grid.innerHTML = ''; empty.classList.remove('hidden'); return; }}
|
||||||
if (!cards.length) {{ grid.classList.add('hidden'); empty.classList.remove('hidden'); return; }}
|
empty.classList.add('hidden');
|
||||||
empty.classList.add('hidden'); grid.classList.remove('hidden');
|
grid.innerHTML = cards.map(function(c) {{
|
||||||
cards.forEach(c => {{
|
|
||||||
const st = c.status || 'offline';
|
const st = c.status || 'offline';
|
||||||
const el = document.createElement('article');
|
const err = (c.last_error && (st === 'error' || st === 'offline'))
|
||||||
el.className = 'rounded-2xl bg-white/5 p-5 ring-1 ' + (statusRing[st]||statusRing.offline) + ' backdrop-blur flex flex-col gap-3';
|
? '<p class="text-xs text-rose-300/90 truncate mt-2" title="'+esc(c.last_error)+'">'+esc(c.last_error)+'</p>' : '';
|
||||||
el.innerHTML = '<motion-wrap class="flex items-start justify-between gap-2"><h3 class="font-semibold text-white leading-snug">'+esc(c.model_label||c.model_id||'未命名')+'</h3><span class="shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium '+(statusBadge[st]||statusBadge.offline)+'">'+esc(c.status_label)+'</span></div>'
|
return '<article class="rounded-2xl bg-white/5 p-5 ring-1 '+(statusRing[st]||statusRing.offline)+' flex flex-col min-h-[190px]">'
|
||||||
+ '<p class="text-xs text-slate-400 font-mono">'+esc(c.model_id||'—')+'</p>'
|
+ '<div class="flex items-start justify-between gap-2 mb-2"><h3 class="font-semibold text-white text-base break-words flex-1">'+esc(c.model_label||c.model_id||'未命名')+'</h3>'
|
||||||
+ '<p class="text-sm text-slate-300">'+esc(c.node_name)+' · <span class="text-cyan-200/90">'+esc(c.endpoint)+'</span></p>'
|
+ '<span class="shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium '+(statusBadge[st]||statusBadge.offline)+'">'+esc(c.status_label)+'</span></div>'
|
||||||
+ '<motion-wrap class="grid grid-cols-2 gap-2 text-xs text-slate-400 mt-auto pt-2 border-t border-white/10"><div>进行中 <span class="text-white font-medium">'+esc(c.in_flight)+'/'+esc(c.max_concurrent)+'</span></div><div>今日 Token <span class="text-cyan-200 font-medium">'+fmt(c.today_tokens)+'</span></div><div>今日请求 <span class="text-white font-medium">'+fmt(c.today_requests)+'</span></div><div></div></div>';
|
+ '<p class="text-xs text-slate-500 font-mono truncate mb-1">'+esc(c.model_id||'—')+'</p>'
|
||||||
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); }}
|
+ '<p class="text-sm text-slate-300 truncate mb-3">'+esc(c.node_name)+' · <span class="text-cyan-200">'+esc(c.endpoint)+'</span></p>'
|
||||||
grid.appendChild(el);
|
+ '<dl class="grid grid-cols-2 gap-x-4 gap-y-2 text-xs mt-auto pt-3 border-t border-white/10">'
|
||||||
}});
|
+ '<div><dt class="text-slate-500">进行中</dt><dd class="text-white font-medium">'+esc(c.in_flight)+' / '+esc(c.max_concurrent)+'</dd></div>'
|
||||||
|
+ '<div><dt class="text-slate-500">今日 Token</dt><dd class="text-cyan-200 font-medium">'+fmt(c.today_tokens)+'</dd></div>'
|
||||||
|
+ '<div><dt class="text-slate-500">今日请求</dt><dd class="text-white font-medium">'+fmt(c.today_requests)+'</dd></div>'
|
||||||
|
+ '<div></div></dl>' + err + '</article>';
|
||||||
|
}}).join('');
|
||||||
}}
|
}}
|
||||||
async function refreshCards() {{
|
async function refreshCards() {{
|
||||||
|
const hdrs = wgAuthHeaders();
|
||||||
|
if (!hdrs) return;
|
||||||
try {{
|
try {{
|
||||||
const res = await fetch(wgApi('/api/models/cards'));
|
const res = await fetch(wgApi('/api/models/cards'), {{ headers: hdrs }});
|
||||||
const data = await res.json().catch(() => ({{}}));
|
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');
|
document.getElementById('cards-loading').classList.add('hidden');
|
||||||
if (res.ok) renderCards(Array.isArray(data) ? data : (data.cards || []));
|
if (res.ok) {{
|
||||||
}} catch (_) {{}}
|
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);
|
||||||
</script>
|
</script>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
LOGIN_HTML = page(
|
LOGIN_HTML = page_login(
|
||||||
"登录",
|
"登录",
|
||||||
f"""
|
f"""
|
||||||
<div class="mx-auto max-w-md">
|
<div class="mx-auto max-w-md">
|
||||||
@@ -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 (!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; }}
|
if (!data.access_token) {{ err.textContent = '服务端未返回令牌'; err.classList.remove('hidden'); return; }}
|
||||||
localStorage.setItem('web_token', data.access_token);
|
localStorage.setItem('web_token', data.access_token);
|
||||||
window.location.href = wgApi('/user');
|
window.location.href = wgApi('/home');
|
||||||
}} catch (ex) {{
|
}} catch (ex) {{
|
||||||
err.textContent = '无法连接服务器:' + (ex && ex.message ? ex.message : String(ex));
|
err.textContent = '无法连接服务器:' + (ex && ex.message ? ex.message : String(ex));
|
||||||
err.classList.remove('hidden');
|
err.classList.remove('hidden');
|
||||||
@@ -1151,31 +1230,31 @@ SETTINGS_HTML = page(
|
|||||||
<p class="mt-2 text-sm text-slate-400">管理节点主机、端口与模型(默认主机 127.0.0.1,对应 frp 映射)</p>
|
<p class="mt-2 text-sm text-slate-400">管理节点主机、端口与模型(默认主机 127.0.0.1,对应 frp 映射)</p>
|
||||||
</div>
|
</div>
|
||||||
<p id="msg" class="hidden text-sm"></p>
|
<p id="msg" class="hidden text-sm"></p>
|
||||||
<motion-wrap id="nodes-list" class="space-y-4"></motion-wrap>
|
<div id="nodes-list" class="space-y-4"></div>
|
||||||
<motion-wrap class="rounded-3xl bg-white/5 p-6 ring-1 ring-white/10">
|
<div class="rounded-3xl bg-white/5 p-6 ring-1 ring-white/10">
|
||||||
<h2 class="text-lg font-semibold text-white">添加节点</h2>
|
<h2 class="text-lg font-semibold text-white">添加节点</h2>
|
||||||
<form id="add-form" class="mt-4 space-y-4">
|
<form id="add-form" class="mt-4 space-y-4">
|
||||||
<motion-wrap class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
<div><label class="text-sm text-slate-300">节点名称</label>
|
<div><label class="text-sm text-slate-300">节点名称</label>
|
||||||
<input name="name" required class="mt-1 w-full rounded-xl border border-white/10 bg-black/30 px-3 py-2 text-white"/></motion-wrap>
|
<input name="name" required class="mt-1 w-full rounded-xl border border-white/10 bg-black/30 px-3 py-2 text-white"/></div>
|
||||||
<div><label class="text-sm text-slate-300">主机</label>
|
<div><label class="text-sm text-slate-300">主机</label>
|
||||||
<input name="host" value="127.0.0.1" class="mt-1 w-full rounded-xl border border-white/10 bg-black/30 px-3 py-2 text-white"/></motion-wrap>
|
<input name="host" value="127.0.0.1" class="mt-1 w-full rounded-xl border border-white/10 bg-black/30 px-3 py-2 text-white"/></div>
|
||||||
<div><label class="text-sm text-slate-300">端口</label>
|
<div><label class="text-sm text-slate-300">端口</label>
|
||||||
<input name="port" type="number" required min="1" max="65535" placeholder="3313" class="mt-1 w-full rounded-xl border border-white/10 bg-black/30 px-3 py-2 text-white"/></motion-wrap>
|
<input name="port" type="number" required min="1" max="65535" placeholder="3313" class="mt-1 w-full rounded-xl border border-white/10 bg-black/30 px-3 py-2 text-white"/></div>
|
||||||
<div><label class="text-sm text-slate-300">最大并发</label>
|
<div><label class="text-sm text-slate-300">最大并发</label>
|
||||||
<input name="max_concurrent" type="number" value="1" min="1" max="32" class="mt-1 w-full rounded-xl border border-white/10 bg-black/30 px-3 py-2 text-white"/></motion-wrap>
|
<input name="max_concurrent" type="number" value="1" min="1" max="32" class="mt-1 w-full rounded-xl border border-white/10 bg-black/30 px-3 py-2 text-white"/></div>
|
||||||
</motion-wrap>
|
</div>
|
||||||
<div><label class="text-sm text-slate-300">模型列表 <span class="text-slate-500">(每行:模型ID|显示名,显示名可省略)</span></label>
|
<div><label class="text-sm text-slate-300">模型列表 <span class="text-slate-500">(每行:模型ID|显示名,显示名可省略)</span></label>
|
||||||
<textarea name="models" rows="4" placeholder="qwen2.5:14b|千问14B llama3|Llama3" class="mt-1 w-full rounded-xl border border-white/10 bg-black/30 px-3 py-2 text-sm text-white font-mono"></textarea>
|
<textarea name="models" rows="4" placeholder="qwen2.5:14b|千问14B llama3|Llama3" class="mt-1 w-full rounded-xl border border-white/10 bg-black/30 px-3 py-2 text-sm text-white font-mono"></textarea>
|
||||||
</motion-wrap>
|
</div>
|
||||||
<label class="flex items-center gap-2 text-sm text-slate-300"><input name="enabled" type="checkbox" checked class="rounded"/> 启用节点</label>
|
<label class="flex items-center gap-2 text-sm text-slate-300"><input name="enabled" type="checkbox" checked class="rounded"/> 启用节点</label>
|
||||||
<button type="submit" class="rounded-xl bg-gradient-to-r from-indigo-500 to-cyan-500 px-5 py-2.5 text-sm font-semibold text-slate-950">添加节点</button>
|
<button type="submit" class="rounded-xl bg-gradient-to-r from-indigo-500 to-cyan-500 px-5 py-2.5 text-sm font-semibold text-slate-950">添加节点</button>
|
||||||
</form>
|
</form>
|
||||||
</motion-wrap>
|
</div>
|
||||||
</motion-wrap>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
const token = localStorage.getItem('web_token');
|
const token = localStorage.getItem('web_token');
|
||||||
if (!token) location.href = wgApi('/login');
|
if (!token) {{ location.replace(wgApi('/login')); }}
|
||||||
const hdrs = {{ 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }};
|
const hdrs = {{ 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }};
|
||||||
function esc(s) {{ return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<'); }}
|
function esc(s) {{ return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<'); }}
|
||||||
function showMsg(text, ok) {{
|
function showMsg(text, ok) {{
|
||||||
@@ -1207,12 +1286,12 @@ SETTINGS_HTML = page(
|
|||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'rounded-2xl bg-white/5 p-5 ring-1 ring-white/10 space-y-3';
|
card.className = 'rounded-2xl bg-white/5 p-5 ring-1 ring-white/10 space-y-3';
|
||||||
const modelsTxt = modelsToText(n.models);
|
const modelsTxt = modelsToText(n.models);
|
||||||
card.innerHTML = '<motion-wrap class="flex flex-wrap items-center justify-between gap-2"><div><span class="font-semibold text-white">'+esc(n.name)+'</span> <span class="text-xs rounded-full px-2 py-0.5 bg-white/10 text-slate-300">'+esc(n.status_label)+'</span></motion-wrap><motion-wrap class="flex gap-2"><button data-test="'+esc(n.id)+'" class="text-xs text-indigo-300 hover:underline">测试</button><button data-del="'+esc(n.id)+'" class="text-xs text-rose-400 hover:underline">删除</button></motion-wrap></motion-wrap>'
|
card.innerHTML = '<div class="flex flex-wrap items-center justify-between gap-2"><div><span class="font-semibold text-white">'+esc(n.name)+'</span> <span class="text-xs rounded-full px-2 py-0.5 bg-white/10 text-slate-300">'+esc(n.status_label)+'</span></div><div class="flex gap-2"><button data-test="'+esc(n.id)+'" class="text-xs text-indigo-300 hover:underline">测试</button><button data-del="'+esc(n.id)+'" class="text-xs text-rose-400 hover:underline">删除</button></div></div>'
|
||||||
+ '<p class="text-sm text-cyan-200/90 font-mono">'+esc(n.host)+':'+esc(n.port)+' · 进行中 '+esc(n.in_flight)+'/'+esc(n.max_concurrent)+'</p>'
|
+ '<p class="text-sm text-cyan-200/90 font-mono">'+esc(n.host)+':'+esc(n.port)+' · 进行中 '+esc(n.in_flight)+'/'+esc(n.max_concurrent)+'</p>'
|
||||||
+ '<motion-wrap class="grid gap-2 sm:grid-cols-2 text-sm"><input data-f="name" value="'+esc(n.name)+'" class="rounded-lg bg-black/30 border border-white/10 px-2 py-1 text-white"/>'
|
+ '<div class="grid gap-2 sm:grid-cols-2 text-sm"><input data-f="name" value="'+esc(n.name)+'" class="rounded-lg bg-black/30 border border-white/10 px-2 py-1 text-white"/>'
|
||||||
+ '<input data-f="host" value="'+esc(n.host)+'" class="rounded-lg bg-black/30 border border-white/10 px-2 py-1 text-white"/>'
|
+ '<input data-f="host" value="'+esc(n.host)+'" class="rounded-lg bg-black/30 border border-white/10 px-2 py-1 text-white"/>'
|
||||||
+ '<input data-f="port" type="number" value="'+esc(n.port)+'" class="rounded-lg bg-black/30 border border-white/10 px-2 py-1 text-white"/>'
|
+ '<input data-f="port" type="number" value="'+esc(n.port)+'" class="rounded-lg bg-black/30 border border-white/10 px-2 py-1 text-white"/>'
|
||||||
+ '<input data-f="max_concurrent" type="number" value="'+esc(n.max_concurrent)+'" class="rounded-lg bg-black/30 border border-white/10 px-2 py-1 text-white"/></motion-wrap>'
|
+ '<input data-f="max_concurrent" type="number" value="'+esc(n.max_concurrent)+'" class="rounded-lg bg-black/30 border border-white/10 px-2 py-1 text-white"/></div>'
|
||||||
+ '<textarea data-f="models" rows="3" class="w-full rounded-lg bg-black/30 border border-white/10 px-2 py-1 text-xs text-white font-mono">'+esc(modelsTxt)+'</textarea>'
|
+ '<textarea data-f="models" rows="3" class="w-full rounded-lg bg-black/30 border border-white/10 px-2 py-1 text-xs text-white font-mono">'+esc(modelsTxt)+'</textarea>'
|
||||||
+ '<label class="flex items-center gap-2 text-sm"><input data-f="enabled" type="checkbox" '+(n.enabled?'checked':'')+'> 启用</label>'
|
+ '<label class="flex items-center gap-2 text-sm"><input data-f="enabled" type="checkbox" '+(n.enabled?'checked':'')+'> 启用</label>'
|
||||||
+ '<button data-save="'+esc(n.id)+'" class="rounded-lg bg-white/10 px-3 py-1.5 text-sm text-white ring-1 ring-white/15">保存修改</button>';
|
+ '<button data-save="'+esc(n.id)+'" class="rounded-lg bg-white/10 px-3 py-1.5 text-sm text-white ring-1 ring-white/15">保存修改</button>';
|
||||||
@@ -1254,7 +1333,7 @@ SETTINGS_HTML = page(
|
|||||||
load().catch(e => showMsg(e.message, false));
|
load().catch(e => showMsg(e.message, false));
|
||||||
</script>
|
</script>
|
||||||
""",
|
""",
|
||||||
).replace("<div>", "<div>").replace("</motion-wrap>", "</motion-wrap>")
|
).replace("<div>", "<div>").replace("</div>", "</div>")
|
||||||
|
|
||||||
|
|
||||||
STATS_HTML = page(
|
STATS_HTML = page(
|
||||||
@@ -1519,8 +1598,8 @@ app.add_middleware(
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def home() -> str:
|
async def root() -> str:
|
||||||
return HOME_HTML
|
return LOGIN_HTML
|
||||||
|
|
||||||
|
|
||||||
@app.get("/login", response_class=HTMLResponse)
|
@app.get("/login", response_class=HTMLResponse)
|
||||||
@@ -1528,6 +1607,11 @@ async def login_page() -> str:
|
|||||||
return LOGIN_HTML
|
return LOGIN_HTML
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/home", response_class=HTMLResponse)
|
||||||
|
async def home() -> str:
|
||||||
|
return HOME_HTML
|
||||||
|
|
||||||
|
|
||||||
@app.get("/user", response_class=HTMLResponse)
|
@app.get("/user", response_class=HTMLResponse)
|
||||||
async def user_page() -> str:
|
async def user_page() -> str:
|
||||||
return USER_HTML
|
return USER_HTML
|
||||||
@@ -1563,7 +1647,9 @@ async def api_me(
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/api/models/cards")
|
@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)
|
return await run_in_thread(build_model_cards)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user