Files
zhanlu_proxy/internal/server/templates/login.html
T
m1saka 9aba511abe Refactor server: extract shared helpers, fix tool-call ordering and finish_reason mapping
- 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
2026-08-20 15:46:47 +08:00

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>