- Extract callUpstream helper to deduplicate ~30 lines between chatCompletions and responses - Move HTML templates to internal/server/templates/ via go:embed (server.go 1749→1192 lines) - Consolidate 4 copies of firstNonEmpty into util.FirstNonEmpty - Extract chatStreamChunk named type shared by aggregateStream and aggregateResponsesStream - Fix tool-call ordering: iterate sorted map keys instead of sequential 0..N - Map upstream finish_reason to Responses API status (length/content_filter → incomplete) - Surface /v1/models errors as 401/502 instead of silently returning empty 200 - Add Secure cookie flag via isTLSRequest helper - forEachSSEChunk: use sseDataPayload parser, drop redundant json.Valid, distinguish bufio.ErrTooLong - Fix StreamIdleTimout typo → StreamIdleTimeout - sso.go: single Read → io.ReadAll(io.LimitReader), explicit unknown error fallback
81 lines
5.4 KiB
HTML
81 lines
5.4 KiB
HTML
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>湛卢代理登录</title>
|
|
<style>
|
|
:root{
|
|
color-scheme:light;
|
|
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,"PingFang SC","Microsoft YaHei",sans-serif;
|
|
--bg:#f6f7f9; --surface:#ffffff; --border:#e6e8ec; --border-strong:#d4d8df;
|
|
--ink:#111827; --body:#4b5563; --muted:#9ca3af;
|
|
--accent:#2563eb; --accent-hover:#1d4ed8; --accent-soft:#eff4ff;
|
|
--ok:#059669; --err:#dc2626;
|
|
--radius:16px; --radius-sm:10px;
|
|
}
|
|
*{box-sizing:border-box}
|
|
body{margin:0;min-height:100vh;display:grid;place-items:center;padding:32px 20px;color:var(--ink);background:var(--bg);-webkit-font-smoothing:antialiased}
|
|
@media(prefers-reduced-motion:no-preference){body{animation:fade .4s ease both}}
|
|
@keyframes fade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
|
.card{width:min(440px,100%);background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 1px 2px rgba(16,24,40,.04),0 12px 32px -12px rgba(16,24,40,.12);padding:40px 36px}
|
|
.brand{display:flex;align-items:center;gap:10px;margin-bottom:8px}
|
|
.dot{width:10px;height:10px;border-radius:3px;background:var(--accent);flex:none}
|
|
.eyebrow{font-size:12px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
|
|
h1{margin:0 0 10px;font-size:24px;font-weight:700;letter-spacing:-.02em}
|
|
.lede{margin:0 0 28px;color:var(--body);font-size:14px;line-height:1.6}
|
|
.lede code{font-family:"SF Mono",ui-monospace,Consolas,monospace;font-size:12.5px;color:var(--ink);background:var(--bg);padding:1px 6px;border-radius:5px;border:1px solid var(--border)}
|
|
form{display:grid;gap:18px}
|
|
label{display:grid;gap:7px;font-size:13px;font-weight:500;color:var(--body)}
|
|
input{width:100%;border:1px solid var(--border-strong);border-radius:var(--radius-sm);padding:11px 13px;background:var(--surface);color:var(--ink);outline:none;font:inherit;font-size:14px;transition:border-color .15s ease,box-shadow .15s ease}
|
|
input::placeholder{color:var(--muted)}
|
|
input:hover{border-color:#bdc2cc}
|
|
input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
|
|
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:1px solid transparent;border-radius:var(--radius-sm);padding:11px 18px;font:inherit;font-weight:600;font-size:14px;cursor:pointer;text-decoration:none;color:#fff;transition:background .15s ease,border-color .15s ease,box-shadow .15s ease,transform .05s ease}
|
|
.btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
.btn-primary{background:var(--accent);width:100%}
|
|
.btn-primary:hover{background:var(--accent-hover)}
|
|
.btn-primary:active{transform:translateY(1px)}
|
|
.btn-primary:disabled{background:#c2c9d4;cursor:not-allowed}
|
|
.status{min-height:20px;font-size:13px;line-height:1.6;color:var(--muted);display:flex;align-items:flex-start;gap:8px;margin-top:4px}
|
|
.status::before{content:"";flex:none;width:7px;height:7px;border-radius:50%;margin-top:6px;background:currentColor}
|
|
.status[data-state="ok"]{color:var(--ok)}
|
|
.status[data-state="err"]{color:var(--err)}
|
|
.status[data-state="busy"]{color:var(--accent)}
|
|
.status[data-state="busy"]::before{animation:pulse 1.1s ease-in-out infinite}
|
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
|
@media(prefers-reduced-motion:reduce){.status[data-state="busy"]::before{animation:none}}
|
|
.footer{margin-top:24px;padding-top:18px;border-top:1px solid var(--border);font-size:12px;color:var(--muted);line-height:1.7}
|
|
.footer code{font-family:"SF Mono",ui-monospace,Consolas,monospace;font-size:12px;color:var(--body);word-break:break-all}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main class="card">
|
|
<div class="brand"><span class="dot"></span><span class="eyebrow">Zhanlu Proxy</span></div>
|
|
<h1>湛卢代理登录</h1>
|
|
<p class="lede">请输入服务环境变量 <code>ZHANLU_LOGIN_PASSWORD</code> 配置的管理密码,验证后进入管理后台。</p>
|
|
<form id="password-form">
|
|
<label>登录密码<input name="password" type="password" autocomplete="current-password" placeholder="请输入服务访问密码" required></label>
|
|
<button class="btn btn-primary" type="submit">进入管理后台</button>
|
|
</form>
|
|
<div class="status" id="status" data-state="busy">需要登录后才能管理湛卢凭据。</div>
|
|
</main>
|
|
<script>
|
|
const statusEl = document.getElementById('status');
|
|
const passwordForm = document.getElementById('password-form');
|
|
function setStatus(text, state) { statusEl.textContent = text; statusEl.dataset.state = state || ''; }
|
|
if (passwordForm) {
|
|
passwordForm.addEventListener('submit', async (event) => {
|
|
event.preventDefault();
|
|
const password = passwordForm.password.value;
|
|
if (!password) { setStatus('请输入登录密码', 'err'); return; }
|
|
setStatus('正在登录...', 'busy');
|
|
const res = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) });
|
|
const data = await res.json();
|
|
if (!res.ok || !data.ok) { setStatus(data.error || '登录失败', 'err'); return; }
|
|
window.location.href = '/admin';
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |