feat(engine): 实现同步引擎与 engine_status 快照
apps/engine 独立进程(syncRound + 防重叠调度);上游 log/token;QPU 采样; server 读 engine_status;时刻出参 toUnixSec;中文日志;zod-openapi 扩展顺序修复。
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { OpenAPIHono, createRoute } from "@hono/zod-openapi";
|
||||
import { unixToIso, type AuditLogItem } from "@new-api-key-usage/core";
|
||||
import { toUnixSec, type AuditLogItem } from "@new-api-key-usage/core";
|
||||
import {
|
||||
requireAdminSession,
|
||||
type AppVars,
|
||||
@@ -56,7 +56,7 @@ export function adminAuditRoutes() {
|
||||
});
|
||||
const items: AuditLogItem[] = rows.map((row) => ({
|
||||
id: row.id,
|
||||
created_at: unixToIso(row.created_at)!,
|
||||
created_at: toUnixSec(row.created_at)!,
|
||||
operator: row.operator,
|
||||
domain: row.domain as AuditLogItem["domain"],
|
||||
action: row.action,
|
||||
|
||||
@@ -3,11 +3,22 @@ import type { EngineStatus } from "@new-api-key-usage/core";
|
||||
import { requireAdminSession, type AppVars } from "../middleware.ts";
|
||||
import { engineStatusSchema, err401 } from "../api-schemas.ts";
|
||||
|
||||
/** 状态存储归同步引擎设计;此处只暴露契约字段 */
|
||||
let engineStatus: EngineStatus = {
|
||||
running: false,
|
||||
last_round: null,
|
||||
};
|
||||
export type EngineStatusView = EngineStatus & { sync_interval_ms: number };
|
||||
|
||||
export function toEngineStatusView(row: {
|
||||
running: boolean;
|
||||
sync_interval_ms: number;
|
||||
last_round: unknown;
|
||||
} | null): EngineStatusView {
|
||||
if (!row) {
|
||||
return { running: false, sync_interval_ms: 300_000, last_round: null };
|
||||
}
|
||||
return {
|
||||
running: row.running,
|
||||
sync_interval_ms: row.sync_interval_ms,
|
||||
last_round: (row.last_round as EngineStatus["last_round"]) ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function adminEngineRoutes() {
|
||||
const r = new OpenAPIHono<{ Variables: AppVars }>();
|
||||
@@ -19,7 +30,7 @@ export function adminEngineRoutes() {
|
||||
path: "/engine",
|
||||
summary: "引擎状态",
|
||||
description:
|
||||
"最近轮次摘要;last_round 为 null 表示引擎尚未跑过。状态存储细节归同步引擎设计。",
|
||||
"同步引擎单行快照(engine_status);last_round 为 null 表示尚未跑过。sync_interval_ms 只读。",
|
||||
tags: ["引擎"],
|
||||
security: [{ adminSession: [] }],
|
||||
responses: {
|
||||
@@ -31,17 +42,10 @@ export function adminEngineRoutes() {
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(engineStatus, 200);
|
||||
const row = await c.get("repos").engine.getSnapshot();
|
||||
return c.json(toEngineStatusView(row), 200);
|
||||
},
|
||||
);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
export function setEngineStatus(next: EngineStatus) {
|
||||
engineStatus = next;
|
||||
}
|
||||
|
||||
export function getEngineStatus(): EngineStatus {
|
||||
return engineStatus;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ERR,
|
||||
KeyCrypto,
|
||||
normalizeExpiresAt,
|
||||
unixToIso,
|
||||
toUnixSec,
|
||||
type KeyItem,
|
||||
} from "@new-api-key-usage/core";
|
||||
import type { ApiKeyRow } from "@new-api-key-usage/db";
|
||||
@@ -30,14 +30,14 @@ function toKeyItem(row: ApiKeyRow & { member_name?: string | null }): KeyItem {
|
||||
status: row.status as "active" | "disabled",
|
||||
member_id: row.member_id,
|
||||
member_name: row.member_name ?? null,
|
||||
registered_at: unixToIso(row.registered_at)!,
|
||||
log_synced_at: unixToIso(row.log_synced_at),
|
||||
usage_synced_at: unixToIso(row.usage_synced_at),
|
||||
registered_at: toUnixSec(row.registered_at)!,
|
||||
log_synced_at: toUnixSec(row.log_synced_at),
|
||||
usage_synced_at: toUnixSec(row.usage_synced_at),
|
||||
total_used: row.total_used,
|
||||
total_available: row.total_available,
|
||||
total_granted: row.total_granted,
|
||||
unlimited_quota: row.unlimited_quota,
|
||||
expires_at: unixToIso(row.expires_at),
|
||||
expires_at: toUnixSec(row.expires_at),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,8 +85,11 @@ export function adminKeyRoutes() {
|
||||
}
|
||||
let rows = await c.get("repos").keys.listAll(filter);
|
||||
if (q.recent_failed === "true") {
|
||||
const { getEngineStatus } = await import("./admin-engine.ts");
|
||||
const failed = new Set(getEngineStatus().last_round?.failed_api_key_ids ?? []);
|
||||
const snap = await c.get("repos").engine.getSnapshot();
|
||||
const failed = new Set(
|
||||
((snap?.last_round as { failed_api_key_ids?: number[] } | null)
|
||||
?.failed_api_key_ids) ?? [],
|
||||
);
|
||||
rows = rows.filter((r) => failed.has(r.id));
|
||||
}
|
||||
return c.json(rows.map(toKeyItem), 200);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OpenAPIHono, createRoute } from "@hono/zod-openapi";
|
||||
import { ERR, generateToken, unixToIso } from "@new-api-key-usage/core";
|
||||
import { ERR, generateToken, toUnixSec } from "@new-api-key-usage/core";
|
||||
import type { MemberRow } from "@new-api-key-usage/db";
|
||||
import {
|
||||
jsonError,
|
||||
@@ -25,8 +25,8 @@ function toMemberItem(row: MemberRow, key_count: number) {
|
||||
name: row.name,
|
||||
remark: row.remark,
|
||||
status: row.status as "active" | "disabled",
|
||||
token_issued_at: unixToIso(row.token_issued_at),
|
||||
created_at: unixToIso(row.created_at)!,
|
||||
token_issued_at: toUnixSec(row.token_issued_at),
|
||||
created_at: toUnixSec(row.created_at)!,
|
||||
key_count,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
qpuAt,
|
||||
quotaToUsdStr,
|
||||
resolveQpuTimeline,
|
||||
unixToIso,
|
||||
toUnixSec,
|
||||
type PaymentItem,
|
||||
} from "@new-api-key-usage/core";
|
||||
import type { PaymentRow } from "@new-api-key-usage/db";
|
||||
@@ -31,11 +31,11 @@ function toPaymentItem(row: PaymentRow, quota_usd: string): PaymentItem {
|
||||
return {
|
||||
id: row.id,
|
||||
kind: row.kind as PaymentItem["kind"],
|
||||
paid_at: unixToIso(row.paid_at)!,
|
||||
paid_at: toUnixSec(row.paid_at)!,
|
||||
paid_amount: row.paid_amount,
|
||||
remark: row.remark,
|
||||
period_start: unixToIso(row.period_start),
|
||||
period_end: unixToIso(row.period_end),
|
||||
period_start: toUnixSec(row.period_start),
|
||||
period_end: toUnixSec(row.period_end),
|
||||
subscription_id: row.subscription_id,
|
||||
quota_granted: row.quota_granted,
|
||||
quota_usd,
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ERR,
|
||||
resolveQpuTimeline,
|
||||
sumUsage,
|
||||
unixToIso,
|
||||
toUnixSec,
|
||||
type LogItem,
|
||||
} from "@new-api-key-usage/core";
|
||||
import {
|
||||
@@ -25,7 +25,7 @@ const usageTotals = z.object({
|
||||
});
|
||||
|
||||
const overviewSchema = z.object({
|
||||
data_cutoff: z.string().nullable(),
|
||||
data_cutoff: z.number().int().nullable(),
|
||||
unassigned_subscription: usageTotals,
|
||||
by_source: z.array(
|
||||
z.object({
|
||||
@@ -40,7 +40,7 @@ const overviewSchema = z.object({
|
||||
member_id: z.number().int().nullable(),
|
||||
member_name: z.string().nullable(),
|
||||
key_count: z.number().int(),
|
||||
data_cutoff: z.string().nullable(),
|
||||
data_cutoff: z.number().int().nullable(),
|
||||
usage: usageTotals,
|
||||
}),
|
||||
),
|
||||
@@ -50,7 +50,7 @@ const overviewSchema = z.object({
|
||||
token_name: z.string(),
|
||||
member_id: z.number().int().nullable(),
|
||||
status: z.string(),
|
||||
data_cutoff: z.string().nullable(),
|
||||
data_cutoff: z.number().int().nullable(),
|
||||
usage: usageTotals,
|
||||
}),
|
||||
),
|
||||
@@ -58,7 +58,7 @@ const overviewSchema = z.object({
|
||||
|
||||
const qpuLatestSchema = z.object({
|
||||
qpu: z.number(),
|
||||
effective_from: z.string(),
|
||||
effective_from: z.number().int(),
|
||||
});
|
||||
|
||||
const timeRange = z.object({
|
||||
@@ -106,14 +106,14 @@ export function adminUsageRoutes() {
|
||||
const latest = await c.get("repos").qpu.latest();
|
||||
if (!latest) {
|
||||
return c.json(
|
||||
{ qpu: 500_000, effective_from: "1970-01-01T00:00:00Z" },
|
||||
{ qpu: 500_000, effective_from: 0 },
|
||||
200,
|
||||
);
|
||||
}
|
||||
return c.json(
|
||||
{
|
||||
qpu: latest.qpu,
|
||||
effective_from: unixToIso(latest.effective_from)!,
|
||||
effective_from: toUnixSec(latest.effective_from)!,
|
||||
},
|
||||
200,
|
||||
);
|
||||
@@ -208,7 +208,7 @@ export function adminUsageRoutes() {
|
||||
token_name: v.token_name,
|
||||
member_id: v.member_id,
|
||||
status: v.status,
|
||||
data_cutoff: unixToIso(v.log_synced_at),
|
||||
data_cutoff: toUnixSec(v.log_synced_at),
|
||||
usage: sumUsage(v.rows, timeline),
|
||||
}))
|
||||
.sort((a, b) => b.usage.quota - a.usage.quota);
|
||||
@@ -248,13 +248,13 @@ export function adminUsageRoutes() {
|
||||
member_id: b.member_id,
|
||||
member_name: b.member_name,
|
||||
key_count: b.key_count,
|
||||
data_cutoff: unixToIso(b.cutoff),
|
||||
data_cutoff: toUnixSec(b.cutoff),
|
||||
usage: sumUsage(b.rows, timeline),
|
||||
}));
|
||||
|
||||
return c.json(
|
||||
{
|
||||
data_cutoff: unixToIso(data_cutoff),
|
||||
data_cutoff: toUnixSec(data_cutoff),
|
||||
unassigned_subscription,
|
||||
by_source,
|
||||
by_member,
|
||||
@@ -326,7 +326,7 @@ export function adminUsageRoutes() {
|
||||
return {
|
||||
id: row.id,
|
||||
api_key_id: row.api_key_id,
|
||||
created_at: unixToIso(row.created_at)!,
|
||||
created_at: toUnixSec(row.created_at)!,
|
||||
token_name: row.token_name,
|
||||
model_name: row.model_name,
|
||||
type: row.type,
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ERR,
|
||||
resolveQpuTimeline,
|
||||
sumUsage,
|
||||
unixToIso,
|
||||
toUnixSec,
|
||||
type LogItem,
|
||||
type PaymentItem,
|
||||
} from "@new-api-key-usage/core";
|
||||
@@ -156,7 +156,7 @@ export function memberViewRoutes() {
|
||||
const keys = [...keyMap.entries()].map(([key_id, v]) => ({
|
||||
key_id,
|
||||
token_name: v.token_name,
|
||||
log_synced_at: unixToIso(v.log_synced_at),
|
||||
log_synced_at: toUnixSec(v.log_synced_at),
|
||||
usage: sumUsage(v.rows, timeline),
|
||||
}));
|
||||
|
||||
@@ -185,11 +185,11 @@ export function memberViewRoutes() {
|
||||
payments.push({
|
||||
id: p.id,
|
||||
kind: p.kind as PaymentItem["kind"],
|
||||
paid_at: unixToIso(p.paid_at)!,
|
||||
paid_at: toUnixSec(p.paid_at)!,
|
||||
paid_amount: p.paid_amount,
|
||||
remark: p.remark,
|
||||
period_start: unixToIso(p.period_start),
|
||||
period_end: unixToIso(p.period_end),
|
||||
period_start: toUnixSec(p.period_start),
|
||||
period_end: toUnixSec(p.period_end),
|
||||
subscription_id: p.subscription_id,
|
||||
quota_granted: p.quota_granted,
|
||||
quota_usd:
|
||||
@@ -272,7 +272,7 @@ export function memberViewRoutes() {
|
||||
return {
|
||||
id: row.id,
|
||||
api_key_id: row.api_key_id,
|
||||
created_at: unixToIso(row.created_at)!,
|
||||
created_at: toUnixSec(row.created_at)!,
|
||||
token_name: row.token_name ?? "",
|
||||
model_name: row.model_name,
|
||||
type: row.type,
|
||||
|
||||
Reference in New Issue
Block a user