feat(admin): 计费 & 用量合成一页 —— 顶部配规则、下方看结果(闭环)

按"配置和观测放一个页面"收口:删掉重复的「计价 & 预算」页(其预算区块是 mock),
把真·计价配置并入「计费 & 用量」,形成 规则→扣费→观测 一页闭环。
- 新组件 BillingRules:全局 token→积分汇率 + 每模型 单价/币种/积分权重(读写
  /admin/pricing + /admin/billing-config),改完对后续任务实时生效。
- UsagePage:顶部「计费规则」(配置端) + 下方「用量观测」(按规则折算后的实际消耗)。
- 删 PricingPage + /pricing 路由;api 加 credit_weight / getBillingConfig / saveBillingConfig。

live 验证(preview):改汇率保存→持久化→新任务按新汇率扣→用量观测反映;
tsc + 41 vitest 全过;无 PricingPage 残引。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-07 13:26:33 +08:00
parent e2fc2d366c
commit 11d321f218
5 changed files with 228 additions and 291 deletions
+19
View File
@@ -125,6 +125,7 @@ export interface Pricing {
model_id: string;
input_per_1k: number;
output_per_1k: number;
credit_weight: number; // 每模型积分权重(0/缺省=1.0,不加权)
currency: string;
}
@@ -142,6 +143,24 @@ export async function savePricing(p: Pricing): Promise<void> {
}
}
// —— 全局计费规则(token→积分汇率)——
export async function getBillingConfig(): Promise<{ tokens_per_credit: number }> {
const res = guard(await fetch(`${ADMIN}/billing-config`, { headers: authHeaders() }));
if (!res.ok) throw new Error(`billing config failed: ${res.status}`);
const d = (await res.json()) as { tokens_per_credit?: string };
return { tokens_per_credit: Number(d.tokens_per_credit) || 1000 }; // 空/未设 → 回退默认 1000
}
export async function saveBillingConfig(tokensPerCredit: number): Promise<void> {
const res = guard(
await fetch(`${ADMIN}/billing-config`, { method: "PUT", headers: authHeaders(true), body: JSON.stringify({ tokens_per_credit: tokensPerCredit }) }),
);
if (!res.ok) {
const d = (await res.json().catch(() => ({}))) as { error?: string };
throw new Error(d.error ?? `save billing config failed: ${res.status}`);
}
}
// gatewayOnline 用公开的 /healthz 探活(不受鉴权影响)。
export async function gatewayOnline(): Promise<boolean> {
try {