n2r-dev / static /admin.js
cacodex's picture
Upload 13 files
9c1527e verified
const PANEL_ATTR = "data-panel";
const sidebarButtons = document.querySelectorAll(".sidebar-btn");
const panels = document.querySelectorAll(`.glass-panel[${PANEL_ATTR}]`);
const loginOverlay = document.getElementById("login-overlay");
const loginBtn = document.getElementById("login-btn");
const loginStatus = document.getElementById("login-status");
const overviewMetrics = document.getElementById("overview-metrics");
const recentChecks = document.getElementById("recent-checks");
const modelTable = document.getElementById("model-table");
const keyTable = document.getElementById("key-table");
const healthGrid = document.getElementById("health-grid");
const modelCount = document.getElementById("model-count");
const modelHealthy = document.getElementById("model-healthy");
const settingsStatus = document.getElementById("settings-status");
const testAllModelsBtn = document.getElementById("test-all-models");
const testAllKeysBtn = document.getElementById("test-all-keys");
const runHealthcheckBtn = document.getElementById("run-healthcheck");
const state = {
token: sessionStorage.getItem("nim_token"),
panel: "overview",
};
const STATUS_LABELS = {
healthy: "正常",
degraded: "波动",
down: "异常",
unknown: "未巡检",
};
const dateTimeFormatter = new Intl.DateTimeFormat("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
function formatDateTime(value) {
if (!value) return "--";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "--";
return dateTimeFormatter.format(date);
}
function showPanel(name) {
panels.forEach((panel) => panel.classList.toggle("hidden", panel.getAttribute(PANEL_ATTR) !== name));
sidebarButtons.forEach((button) => button.classList.toggle("active", button.dataset.panel === name));
state.panel = name;
}
sidebarButtons.forEach((button) => button.addEventListener("click", () => showPanel(button.dataset.panel)));
async function apiRequest(endpoint, opts = {}) {
const headers = { "Content-Type": "application/json", Accept: "application/json" };
if (state.token) headers.Authorization = `Bearer ${state.token}`;
const response = await fetch(`/admin/api/${endpoint}`, {
...opts,
headers: { ...headers, ...(opts.headers || {}) },
});
if (!response.ok) {
const payload = await response.json().catch(() => ({}));
throw new Error(payload.message || payload.detail || payload.error?.message || "请求失败");
}
return response.json();
}
function metricCard(label, value, detail = "") {
const div = document.createElement("div");
div.className = "metric-card";
div.innerHTML = `<h3>${label}</h3><strong>${value}</strong><p>${detail}</p>`;
return div;
}
function pill(status) {
const normalized = status || "unknown";
return `<span class="pill ${normalized}">${STATUS_LABELS[normalized] || normalized}</span>`;
}
async function renderOverview() {
const payload = await apiRequest("overview");
const totals = payload.totals || {};
overviewMetrics.innerHTML = "";
overviewMetrics.appendChild(metricCard("启用模型", totals.enabled_models ?? "--", `总数 ${totals.total_models ?? "--"}`));
overviewMetrics.appendChild(metricCard("启用 Key", totals.enabled_keys ?? "--", `总数 ${totals.total_keys ?? "--"}`));
overviewMetrics.appendChild(metricCard("代理请求", totals.total_requests ?? "--", `成功 ${totals.total_success ?? 0}`));
overviewMetrics.appendChild(metricCard("失败次数", totals.total_failures ?? "--", "累计转发失败或上游返回错误"));
recentChecks.innerHTML = "";
(payload.recent_checks || []).forEach((check) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${formatDateTime(check.time)}</td>
<td>${check.model}</td>
<td>${pill(check.status)}</td>
<td>${check.latency ? `${check.latency} ms` : "--"}</td>
`;
recentChecks.appendChild(row);
});
}
async function renderModels() {
const payload = await apiRequest("models");
const items = payload.items || [];
modelCount.textContent = items.length;
modelHealthy.textContent = items.filter((item) => item.status === "healthy").length;
modelTable.innerHTML = "";
items.forEach((item) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>
<strong>${item.display_name || item.model_id}</strong><br />
<span class="status-text mono">${item.model_id}</span>
</td>
<td>${pill(item.status)}</td>
<td>${item.request_count}</td>
<td>${item.healthcheck_success_count}/${item.healthcheck_count}</td>
<td>
<div class="inline-actions">
<button class="secondary-btn" data-action="test-model" data-id="${item.model_id}">测试</button>
<button class="secondary-btn danger-btn" data-action="remove-model" data-id="${item.model_id}">删除</button>
</div>
</td>
`;
modelTable.appendChild(row);
});
}
async function renderKeys() {
const payload = await apiRequest("keys");
const items = payload.items || [];
keyTable.innerHTML = "";
items.forEach((item) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${item.label}</td>
<td class="mono">${item.masked_key}</td>
<td>${item.request_count}</td>
<td>${formatDateTime(item.last_tested)}</td>
<td>${pill(item.status)}</td>
<td>
<div class="inline-actions">
<button class="secondary-btn" data-action="test-key" data-id="${item.name}">测试</button>
<button class="secondary-btn danger-btn" data-action="remove-key" data-id="${item.name}">删除</button>
</div>
</td>
`;
keyTable.appendChild(row);
});
}
async function renderHealth() {
const payload = await apiRequest("healthchecks");
const items = payload.items || [];
healthGrid.innerHTML = "";
if (items.length === 0) {
const empty = document.createElement("div");
empty.className = "empty-card";
empty.textContent = "暂无巡检记录,执行一次巡检后这里会显示最新结果。";
healthGrid.appendChild(empty);
return;
}
items.slice(0, 12).forEach((item) => {
const card = document.createElement("article");
card.className = "health-record";
card.innerHTML = `
<div class="toolbar-row">
<div>
<h4>${item.model}</h4>
<span class="status-text mono">${item.model_id}</span>
</div>
${pill(item.status)}
</div>
<p class="status-text">${item.detail || "暂无详情"}</p>
<div class="record-meta">
<span>Key: ${item.api_key || "未记录"}</span>
<span>时延: ${item.latency ? `${item.latency} ms` : "--"}</span>
<span>时间: ${formatDateTime(item.checked_at)}</span>
</div>
`;
healthGrid.appendChild(card);
});
}
async function renderSettings() {
const payload = await apiRequest("settings");
document.getElementById("healthcheck-enabled").checked = Boolean(payload.healthcheck_enabled);
document.getElementById("healthcheck-interval").value = payload.healthcheck_interval_minutes || 60;
document.getElementById("public-history-hours").value = payload.public_history_hours || 48;
document.getElementById("healthcheck-prompt").value = payload.healthcheck_prompt || "请只回复 OK。";
}
async function loadAll() {
await Promise.all([renderOverview(), renderModels(), renderKeys(), renderHealth(), renderSettings()]);
}
async function runAllModelChecks() {
const payload = await apiRequest("healthchecks/run", { method: "POST", body: JSON.stringify({}) });
const items = payload.items || [];
const success = items.filter((item) => item.status === "healthy").length;
alert(`已完成全部模型巡检,共 ${items.length} 个模型,其中 ${success} 个正常。`);
showPanel("health");
await loadAll();
}
async function runAllKeyChecks() {
const payload = await apiRequest("keys/test-all", { method: "POST", body: JSON.stringify({}) });
const items = payload.items || [];
const success = items.filter((item) => item.status === "healthy").length;
alert(`已完成全部 Key 测试,共 ${items.length} 个 Key,其中 ${success} 个正常。`);
showPanel("keys");
await loadAll();
}
async function testModel(modelId) {
const payload = await apiRequest(`models/${encodeURIComponent(modelId)}/test`, { method: "POST", body: JSON.stringify({}) });
alert(`${payload.display_name || payload.model} 当前状态:${STATUS_LABELS[payload.status] || payload.status}`);
await loadAll();
}
async function removeModel(modelId) {
await apiRequest("models/remove", { method: "POST", body: JSON.stringify({ value: modelId }) });
await loadAll();
}
async function testKey(keyName) {
const payload = await apiRequest("keys/test", { method: "POST", body: JSON.stringify({ value: keyName }) });
alert(`Key ${payload.api_key} 当前状态:${STATUS_LABELS[payload.status] || payload.status}`);
await loadAll();
}
async function removeKey(keyName) {
await apiRequest("keys/remove", { method: "POST", body: JSON.stringify({ value: keyName }) });
await loadAll();
}
modelTable.addEventListener("click", (event) => {
const button = event.target.closest("button[data-action]");
if (!button) return;
if (button.dataset.action === "test-model") testModel(button.dataset.id);
if (button.dataset.action === "remove-model") removeModel(button.dataset.id);
});
keyTable.addEventListener("click", (event) => {
const button = event.target.closest("button[data-action]");
if (!button) return;
if (button.dataset.action === "test-key") testKey(button.dataset.id);
if (button.dataset.action === "remove-key") removeKey(button.dataset.id);
});
document.getElementById("model-add")?.addEventListener("click", async () => {
const modelId = document.getElementById("model-id").value.trim();
const displayName = document.getElementById("model-display-name").value.trim();
const description = document.getElementById("model-description").value.trim();
if (!modelId) {
alert("请先填写模型 ID。");
return;
}
await apiRequest("models", {
method: "POST",
body: JSON.stringify({ model_id: modelId, display_name: displayName || modelId, description }),
});
document.getElementById("model-id").value = "";
document.getElementById("model-display-name").value = "";
document.getElementById("model-description").value = "";
await renderModels();
});
document.getElementById("key-add")?.addEventListener("click", async () => {
const name = document.getElementById("key-label").value.trim();
const apiKey = document.getElementById("key-value").value.trim();
if (!name || !apiKey) {
alert("请填写 Key 名称和内容。");
return;
}
await apiRequest("keys", { method: "POST", body: JSON.stringify({ name, api_key: apiKey }) });
document.getElementById("key-label").value = "";
document.getElementById("key-value").value = "";
await renderKeys();
});
testAllModelsBtn?.addEventListener("click", runAllModelChecks);
testAllKeysBtn?.addEventListener("click", runAllKeyChecks);
runHealthcheckBtn?.addEventListener("click", runAllModelChecks);
document.getElementById("settings-save")?.addEventListener("click", async () => {
try {
const payload = {
healthcheck_enabled: document.getElementById("healthcheck-enabled").checked,
healthcheck_interval_minutes: Number(document.getElementById("healthcheck-interval").value || 60),
public_history_hours: Number(document.getElementById("public-history-hours").value || 48),
healthcheck_prompt: document.getElementById("healthcheck-prompt").value.trim(),
};
await apiRequest("settings", { method: "PUT", body: JSON.stringify(payload) });
settingsStatus.textContent = "设置已保存。";
await loadAll();
} catch (error) {
settingsStatus.textContent = error.message;
}
});
document.getElementById("refresh-now")?.addEventListener("click", loadAll);
loginBtn.addEventListener("click", async () => {
const password = document.getElementById("admin-password").value.trim();
if (!password) {
loginStatus.textContent = "请输入后台密码。";
return;
}
try {
loginStatus.textContent = "正在验证身份...";
const response = await fetch("/admin/api/login", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ password }),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.detail || payload.message || "登录失败");
state.token = payload.access_token || payload.token;
sessionStorage.setItem("nim_token", state.token);
loginOverlay.classList.add("hidden");
await loadAll();
} catch (error) {
loginStatus.textContent = error.message;
}
});
window.addEventListener("DOMContentLoaded", async () => {
showPanel(state.panel);
if (!state.token) return;
loginOverlay.classList.add("hidden");
try {
await loadAll();
setInterval(loadAll, 90 * 1000);
} catch (_error) {
sessionStorage.removeItem("nim_token");
loginOverlay.classList.remove("hidden");
}
});