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
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user