/* AI Balance Monitor 前端逻辑 */ (() => { "use strict"; const TOKEN_KEY = "abm_token"; let token = localStorage.getItem(TOKEN_KEY) || ""; let accounts = []; let platforms = []; let settings = null; let refreshTimer = null; /* ---------- 工具 ---------- */ async function api(path, opts = {}) { const headers = Object.assign({ "Content-Type": "application/json" }, opts.headers || {}); if (token) headers["Authorization"] = "Bearer " + token; const resp = await fetch("/api" + path, Object.assign({}, opts, { headers })); if (resp.status === 401 && !path.startsWith("/login")) { logout(); throw new Error("登录已失效"); } const data = await resp.json().catch(() => ({})); if (!resp.ok) throw new Error(data.detail || ("HTTP " + resp.status)); return data; } function toast(msg, kind = "ok") { const el = document.createElement("div"); el.className = "toast " + kind; el.innerHTML = `${escapeHtml(msg)}`; document.getElementById("toast-root").appendChild(el); requestAnimationFrame(() => el.classList.add("show")); setTimeout(() => { el.classList.remove("show"); setTimeout(() => el.remove(), 250); }, 2600); } function escapeHtml(s) { return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }[c])); } function fmtBalance(v) { if (v === null || v === undefined) return null; const n = Number(v); if (!Number.isFinite(n)) return null; return n >= 100 ? n.toFixed(2) : n >= 1 ? n.toFixed(3) : n.toFixed(4); } function fmtTime(s) { if (!s) return "—"; return s; } /* ---------- 品牌图标映射(@lobehub/icons 键 → 品牌色) ---------- */ const BRAND_COLORS = { OpenAI: "#10A37F", GPT: "#10A37F", AzureOpenAI: "#0078D4", Anthropic: "#D97757", Claude: "#D97757", Google: "#4285F4", Gemini: "#4285F4", GoogleAIStudio: "#4285F4", DeepSeek: "#4D6BFE", MoonshotAI: "#1E1E1E", Kimi: "#1E1E1E", ZhipuAI: "#3859FF", ChatGLM: "#3859FF", Qwen: "#615CED", Tongyi: "#615CED", Aliyun: "#FF6A00", Groq: "#F55036", MistralAI: "#FF7000", Mistral: "#FF7000", Meta: "#0668E1", Llama: "#0668E1", XAI: "#1B1B1B", XAIGrok: "#1B1B1B", Grok: "#1B1B1B", OpenRouter: "#B35CFF", Perplexity: "#1FB8CD", Cohere: "#39594D", HuggingFace: "#FFD21E", TogetherAI: "#FFB1D9", Replicate: "#F26E1E", Baidu: "#2932E1", Qianfan: "#2932E1", MiniMax: "#FFC80A", SiliconFlow: "#3E9E6B", FireworksAI: "#FF2D20", Cerebras: "#00D1B2", NVIDIA: "#76B900", Volcengine: "#3370FF", Doubao: "#3370FF", TencentCloud: "#006EFF", Hunyuan: "#006EFF", }; function brandStyle(iconKey) { const color = BRAND_COLORS[iconKey] || "#8A8F98"; const abbr = (iconKey || "").slice(0, 2).toUpperCase() || "AI"; return { color, abbr }; } /* ---------- 视图/状态 ---------- */ function switchView(name) { document.querySelectorAll(".view").forEach((v) => v.classList.add("hidden")); document.getElementById("view-" + name).classList.remove("hidden"); document.querySelectorAll(".tab").forEach((t) => { t.classList.toggle("active", t.dataset.view === name); }); if (name === "platforms") renderPlatforms(); if (name === "settings") loadSettings(); } async function refresh() { try { const [acc, plat] = await Promise.all([api("/accounts"), api("/platforms")]); accounts = acc; platforms = plat; renderAccounts(); if (!document.getElementById("view-platforms").classList.contains("hidden")) renderPlatforms(); const d = new Date(); document.getElementById("refresh-time").textContent = d.toLocaleTimeString("zh-CN", { hour12: false }); } catch (e) { if (e.message !== "登录已失效") console.warn("刷新失败", e); } } /* ---------- 渲染:账号 ---------- */ function statusInfo(a) { if (!a.enabled) return { cls: "disabled", text: "已禁用" }; if (a.last_status === "disabled") return { cls: "disabled", text: "已禁用" }; if (a.last_status === "error") return { cls: "err", text: "异常" }; if (a.last_status === "ok") { if (a.last_balance !== null && a.last_balance < a.threshold) { return { cls: "warn", text: "低于阈值" }; } return { cls: "ok", text: "正常" }; } return { cls: "pending", text: "待检查" }; } function accountCard(a, idx) { const st = statusInfo(a); const plat = platforms.find((p) => p.id === a.platform_id) || {}; const b = brandStyle(plat.icon || ""); const bal = fmtBalance(a.last_balance); const th = fmtBalance(a.threshold); const below = a.last_status === "ok" && bal !== null && Number(a.last_balance) < a.threshold; const cardCls = ["account-card", st.cls === "warn" ? "below" : "", st.cls === "err" ? "error" : "", st.cls === "disabled" ? "disabled" : ""].join(" ").trim(); const pct = th && bal !== null ? Math.min(100, (Number(a.last_balance) / Number(th)) * 100) : 100; const fillCls = below ? "threshold-fill below" : "threshold-fill"; return `
${escapeHtml(b.abbr)}
${escapeHtml(plat.name || "未知平台")} · ${escapeHtml(plat.currency || "")}
${escapeHtml(a.name)}
${bal !== null ? `${bal}${escapeHtml(plat.currency || "")}` : ``}
${st.text} 上次检查:${fmtTime(a.last_check_at)} ${a.last_error ? `${escapeHtml(a.last_error)}` : ""}
`; } function renderAccounts() { const grid = document.getElementById("account-grid"); const stats = { total: accounts.length, ok: 0, below: 0, error: 0, disabled: 0, pending: 0 }; accounts.forEach((a) => { const st = statusInfo(a); if (st.cls === "ok") stats.ok++; else if (st.cls === "warn") stats.below++; else if (st.cls === "err") stats.error++; else if (st.cls === "disabled") stats.disabled++; else stats.pending++; }); document.getElementById("stat-total").textContent = stats.total; document.getElementById("stat-ok").textContent = stats.ok; document.getElementById("stat-below").textContent = stats.below; document.getElementById("stat-error").textContent = stats.error; document.getElementById("stat-disabled").textContent = stats.disabled; document.getElementById("stat-pending").textContent = stats.pending; grid.innerHTML = accounts.map(accountCard).join(""); document.getElementById("empty-hint").classList.toggle("hidden", accounts.length > 0); } /* ---------- 渲染:平台 ---------- */ function renderPlatforms() { const list = document.getElementById("platform-list"); if (platforms.length === 0) { list.innerHTML = `
还没有平台,点击右上角「+ 添加平台」创建。
`; return; } list.innerHTML = platforms.map((p, i) => { const b = brandStyle(p.icon || ""); return `
${escapeHtml(b.abbr)}
${escapeHtml(p.name)} ${p.enabled ? "" : '已停用'}
${escapeHtml(p.method)} ${escapeHtml(p.url)} · 提取 ${escapeHtml(p.balance_path)} · ${escapeHtml(p.currency)} · 间隔 ${p.interval_seconds || "全局"}s · ${p.account_count} 个账号
`; }).join(""); } /* ---------- 模态框 ---------- */ function openModal(html, onMount) { const root = document.getElementById("modal-root"); const mask = document.createElement("div"); mask.className = "modal-mask"; mask.innerHTML = html; mask.addEventListener("click", (e) => { if (e.target === mask) closeModal(mask); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeModal(mask); }, { once: true }); root.appendChild(mask); if (onMount) onMount(mask); return mask; } function closeModal(mask) { if (mask) mask.remove(); } function confirmDialog(text, onOk) { const mask = openModal(` `); mask.querySelector("[data-cancel]").onclick = () => closeModal(mask); mask.querySelector("[data-ok]").onclick = () => { closeModal(mask); onOk(); }; } /* 平台表单 */ function platformForm(p) { const isEdit = !!p; const v = p || {}; const headersStr = Object.keys(v.headers || {}).length ? JSON.stringify(v.headers, null, 2) : '{\n "Authorization": "Bearer {{apiKey}}"\n}'; openModal(` `, (mask) => { mask.querySelector("[data-cancel]").onclick = () => closeModal(mask); mask.querySelector("#pf-save").onclick = async () => { const payload = { name: val("#pf-name"), currency: val("#pf-currency"), icon: val("#pf-icon"), method: val("#pf-method"), url: val("#pf-url"), balance_path: val("#pf-path"), note: val("#pf-note"), interval_seconds: numOrNull("#pf-interval"), retry_count: numOrNull("#pf-retry"), timeout_seconds: numOrNull("#pf-timeout"), enabled: p ? p.enabled : true, }; let headers = {}; try { headers = JSON.parse(val("#pf-headers") || "{}"); } catch (e) { err("#pf-error", "Headers 不是合法 JSON"); return; } payload.headers = headers; if (!payload.name || !payload.url || !payload.balance_path) { err("#pf-error", "名称 / URL / 提取路径必填"); return; } if (payload.method === "POST") { try { JSON.parse(val("#pf-body") || "{}"); } catch (e) { err("#pf-error", "Body 不是合法 JSON"); return; } } payload.body = val("#pf-body"); try { if (isEdit) await api("/platforms/" + p.id, { method: "PUT", body: JSON.stringify(payload) }); else await api("/platforms", { method: "POST", body: JSON.stringify(payload) }); closeModal(mask); toast(isEdit ? "平台已更新" : "平台已添加"); await refresh(); switchView("platforms"); } catch (e) { err("#pf-error", e.message); } }; }); } /* 账号表单 */ function accountForm(a) { const isEdit = !!a; const v = a || {}; const opts = platforms.map((p) => ``).join(""); openModal(` `, (mask) => { mask.querySelector("[data-cancel]").onclick = () => closeModal(mask); mask.querySelector("#ac-save").onclick = async () => { const payload = { platform_id: Number(val("#ac-platform")), name: val("#ac-name"), api_key: val("#ac-key"), threshold: Number(val("#ac-threshold") || 0), enabled: val("#ac-enabled") === "1", note: val("#ac-note"), }; if (!payload.platform_id || !payload.name || !payload.api_key) { err("#ac-error", "平台 / 名称 / API Key 必填"); return; } try { if (isEdit) await api("/accounts/" + a.id, { method: "PUT", body: JSON.stringify(payload) }); else await api("/accounts", { method: "POST", body: JSON.stringify(payload) }); closeModal(mask); toast(isEdit ? "账号已更新" : "账号已添加"); await refresh(); switchView("monitor"); } catch (e) { err("#ac-error", e.message); } }; }); } /* 历史弹窗 */ async function showHistory(accountId) { const acc = accounts.find((a) => a.id === accountId); if (!acc) return; const plat = platforms.find((p) => p.id === acc.platform_id) || {}; const mask = openModal(` `); try { const hist = await api(`/accounts/${accountId}/history?limit=30`); const body = mask.querySelector("#hist-body"); if (hist.length === 0) { body.innerHTML = `
暂无记录(检查成功后自动记录)
`; return; } const max = Math.max(...hist.map((h) => h.balance), 1e-9); const bars = hist.map((h) => ``).join(""); const items = [...hist].reverse().map((h) => `
${escapeHtml(h.checked_at)}${fmtBalance(h.balance)} ${escapeHtml(plat.currency || "")}
`).join(""); body.innerHTML = `
${bars}
${items}`; } catch (e) { mask.querySelector("#hist-body").textContent = "加载失败:" + e.message; } } /* ---------- 设置 ---------- */ async function loadSettings() { try { settings = await api("/settings"); document.getElementById("set-interval").value = settings.global_interval_seconds; document.getElementById("set-retry").value = settings.retry_count; document.getElementById("set-timeout").value = settings.timeout_seconds; document.getElementById("set-workers").value = settings.max_workers; document.getElementById("set-token").value = settings.telegram_bot_token; document.getElementById("set-chatid").value = settings.telegram_chat_id; } catch (e) { toast(e.message, "err"); } } /* ---------- 事件 ---------- */ function val(id) { return document.getElementById(id).value.trim(); } function numOrNull(id) { const s = val(id); return s === "" ? null : Number(s); } function err(id, msg) { document.getElementById(id).textContent = msg; } function bindEvents() { document.getElementById("login-btn").onclick = doLogin; document.getElementById("login-password").addEventListener("keydown", (e) => { if (e.key === "Enter") doLogin(); }); document.getElementById("logout-btn").onclick = logout; document.querySelectorAll(".tab").forEach((t) => t.addEventListener("click", () => switchView(t.dataset.view))); document.getElementById("add-platform-btn").onclick = () => platformForm(null); document.getElementById("save-settings-btn").onclick = saveSettings; document.getElementById("change-pwd-btn").onclick = changePassword; document.getElementById("account-grid").addEventListener("click", onAccountAction); document.getElementById("platform-list").addEventListener("click", onPlatformAction); } async function doLogin() { const pw = val("login-password"); if (!pw) return; try { const data = await api("/login", { method: "POST", body: JSON.stringify({ password: pw }) }); token = data.token; localStorage.setItem(TOKEN_KEY, token); document.getElementById("login-error").textContent = ""; document.getElementById("login-view").classList.add("hidden"); document.getElementById("app-view").classList.remove("hidden"); await refresh(); startRefreshTimer(); } catch (e) { document.getElementById("login-error").textContent = e.message; } } function logout() { if (token) api("/logout", { method: "POST" }).catch(() => {}); token = ""; localStorage.removeItem(TOKEN_KEY); stopRefreshTimer(); document.getElementById("app-view").classList.add("hidden"); document.getElementById("login-view").classList.remove("hidden"); document.getElementById("login-password").value = ""; } function startRefreshTimer() { stopRefreshTimer(); refreshTimer = setInterval(refresh, 10000); } function stopRefreshTimer() { if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; } } async function onAccountAction(e) { const btn = e.target.closest("[data-act]"); if (!btn) return; const id = Number(btn.dataset.id); const act = btn.dataset.act; if (act === "edit") accountForm(accounts.find((a) => a.id === id)); else if (act === "del") { const a = accounts.find((x) => x.id === id); confirmDialog(`确定删除账号「${escapeHtml(a ? a.name : id)}」?其历史记录将一并删除,此操作不可恢复。`, async () => { try { await api("/accounts/" + id, { method: "DELETE" }); toast("账号已删除"); await refresh(); } catch (err2) { toast(err2.message, "err"); } }); } else if (act === "check") { btn.disabled = true; try { await api(`/accounts/${id}/check`, { method: "POST" }); toast("已开始检查"); } catch (err2) { toast(err2.message, "err"); btn.disabled = false; } setTimeout(refresh, 1500); } else if (act === "history") { showHistory(id); } } async function onPlatformAction(e) { const btn = e.target.closest("[data-act]"); if (!btn) return; const id = Number(btn.dataset.id); const act = btn.dataset.act; const p = platforms.find((x) => x.id === id); if (act === "edit") platformForm(p); else if (act === "toggle") { try { await api("/platforms/" + id, { method: "PUT", body: JSON.stringify({ enabled: !p.enabled }) }); toast(p.enabled ? "平台已停用" : "平台已启用"); await refresh(); renderPlatforms(); } catch (err2) { toast(err2.message, "err"); } } else if (act === "del") { confirmDialog(`确定删除平台「${escapeHtml(p ? p.name : id)}」?
其下 ${p ? p.account_count : 0} 个账号及历史记录将一并删除,此操作不可恢复。`, async () => { try { await api("/platforms/" + id, { method: "DELETE" }); toast("平台已删除"); await refresh(); switchView("platforms"); } catch (err2) { toast(err2.message, "err"); } }); } } async function saveSettings() { try { await api("/settings", { method: "PUT", body: JSON.stringify({ global_interval_seconds: Number(val("set-interval")), retry_count: Number(val("set-retry")), timeout_seconds: Number(val("set-timeout")), max_workers: Number(val("set-workers")), telegram_bot_token: val("set-token"), telegram_chat_id: val("set-chatid"), }), }); toast("设置已保存"); await loadSettings(); } catch (e) { toast(e.message, "err"); } } async function changePassword() { try { await api("/settings/password", { method: "POST", body: JSON.stringify({ old_password: val("set-oldpwd"), new_password: val("set-newpwd") }), }); document.getElementById("set-oldpwd").value = ""; document.getElementById("set-newpwd").value = ""; toast("密码已修改"); } catch (e) { toast(e.message, "err"); } } /* ---------- 启动 ---------- */ function init() { bindEvents(); if (token) { document.getElementById("login-view").classList.add("hidden"); document.getElementById("app-view").classList.remove("hidden"); refresh().then(() => startRefreshTimer()).catch(() => logout()); } } init(); })();