feat(logs): persist business events

This commit is contained in:
2026-08-19 21:03:56 +08:00
parent 62c811fea2
commit 4474069a29
19 changed files with 623 additions and 32 deletions
+3 -2
View File
@@ -15,7 +15,8 @@ export const routes = Object.freeze({
upstreams: managementBase + "/upstreams",
models: managementBase + "/model-suggestions",
billingReset: managementBase + "/billing-reset",
billingLedger: managementBase + "/billing-ledger"
billingLedger: managementBase + "/billing-ledger",
events: managementBase + "/events"
});
export const keyInput = document.querySelector("#key");
@@ -145,7 +146,7 @@ function readOnlyURL(managementURL) {
const views = new Map([
[routes.usage, "usage"], [routes.usageSummary, "usage-summary"], [routes.prices, "prices"],
[routes.catalog, "price-catalog"], [routes.keys, "keys"], [routes.keyStats, "key-stats"],
[routes.upstreams, "upstreams"], [routes.models, "model-suggestions"]
[routes.upstreams, "upstreams"], [routes.models, "model-suggestions"], [routes.events, "events"]
]);
const view = views.get(source.pathname);
if (!view) throw new Error("该数据不支持匿名读取");
+9 -10
View File
@@ -352,16 +352,15 @@ async function resetAllManagedBilling() {
if (!targets.length || !confirm(`重置全部 ${targets.length} 个用户的额度?当前剩余余额不会结转。`)) return;
resetAllBillingNode.disabled = true;
keyStatusNode.textContent = `正在重置 ${targets.length} 个用户`;
const results = await Promise.allSettled(targets.map(key => managedFetch(routes.billingReset, {
method: "POST",
body: JSON.stringify({ id: key.id })
})));
const failed = results.filter(result => result.status === "rejected");
await loadKeys();
resetAllBillingNode.disabled = false;
keyStatusNode.textContent = failed.length
? `已重置 ${targets.length - failed.length} 个用户,${failed.length} 个失败`
: `已重置全部 ${targets.length} 个用户的额度`;
try {
const result = await managedFetch(routes.billingReset, { method: "POST", body: JSON.stringify({ all: true }) });
await loadKeys();
keyStatusNode.textContent = `已重置全部 ${result.reset_count || 0} 个用户的额度`;
} catch (error) {
keyStatusNode.textContent = "重置失败:" + error.message;
} finally {
resetAllBillingNode.disabled = false;
}
}
async function archiveManagedKey(key) {
+48
View File
@@ -0,0 +1,48 @@
import { dataFetch, routes } from "../core/runtime.js";
const rowsNode = document.querySelector("#event-rows");
export async function loadEvents() {
rowsNode.replaceChildren(emptyRow("正在读取"));
try {
const payload = await dataFetch(routes.events + "?limit=200");
const events = payload.events || [];
rowsNode.replaceChildren(...(events.length ? events.map(eventRow) : [emptyRow("暂无日志")]));
} catch (error) {
rowsNode.replaceChildren(emptyRow("读取失败:" + error.message));
}
}
function eventRow(item) {
const row = document.createElement("tr");
const occurredAt = document.createElement("td");
occurredAt.className = "left";
occurredAt.textContent = formatTime(item.occurred_at);
const event = document.createElement("td");
event.className = "left";
event.textContent = item.event || "-";
const status = document.createElement("td");
status.className = "left event-status " + (String(item.status || "").startsWith("成功") ? "ok" : "failed");
status.textContent = item.status || "-";
row.append(occurredAt, event, status);
return row;
}
function emptyRow(message) {
const row = document.createElement("tr");
const cell = document.createElement("td");
cell.className = "left empty";
cell.colSpan = 3;
cell.textContent = message;
row.append(cell);
return row;
}
function formatTime(value) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "-";
return new Intl.DateTimeFormat("zh-CN", {
year: "numeric", month: "2-digit", day: "2-digit",
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false
}).format(date);
}
+2 -1
View File
@@ -2,8 +2,9 @@ import { initializeRuntime, onAccessChange } from "./core/runtime.js";
import { closeKeyDrawer, initializeKeys, loadKeys } from "./features/keys.js";
import { initializePricing, loadPricing } from "./features/pricing.js";
import { activateUsage, initializeUsage, refreshVisibleUsage, resetUsage } from "./features/usage.js";
import { loadEvents } from "./features/logs.js";
const loaders = Object.freeze({ stats: loadKeys, usage: activateUsage, pricing: loadPricing, logs: () => {}, users: loadKeys });
const loaders = Object.freeze({ stats: loadKeys, usage: activateUsage, pricing: loadPricing, logs: loadEvents, users: loadKeys });
initializeKeys();
initializeUsage();
+5
View File
@@ -28,3 +28,8 @@ tbody tr:hover { background: color-mix(in srgb, var(--text-primary) 3%, transpar
.usage-filter input, .usage-filter select { height: 32px; padding: 6px 8px; font-size: 11px; }
.usage-filter.request-filter { grid-column: span 2; }
.usage-filter-actions { display: flex; align-items: end; justify-content: flex-end; gap: 7px; grid-column: span 2; }
.event-table { table-layout: fixed; }
.event-table th:nth-child(1), .event-table td:nth-child(1) { width: 190px; }
.event-table th:nth-child(3), .event-table td:nth-child(3) { width: 220px; }
.event-table td { height: 42px; white-space: normal; }
.event-status { font-weight: 650; }
+1 -1
View File
@@ -91,7 +91,7 @@
</section>
<section id="view-logs" class="view">
<div class="surface"><div class="empty">暂无日志</div></div>
<div class="surface"><div class="table-wrap"><table class="event-table"><thead><tr><th class="left">时间</th><th class="left">事件</th><th class="left">状态</th></tr></thead><tbody id="event-rows"></tbody></table></div></div>
</section>
</main>
</div>