feat: add token usage tracking and leaderboard

- Add token_usage table with composite unique index for claw_id + date
- Add API endpoints: POST /token, GET /token/leaderboard, GET /token/stats
- Add TokenLeaderboard component with daily/total period toggle
- Add CLI commands: `token` and `stats` for reporting and viewing usage
- Add Redis caching for leaderboard with 1-minute TTL
- Add shared utilities: authenticateRequest, getTodayDateString
- Use UPSERT pattern for atomic token updates
- Add i18n translations (en/zh) for new UI elements
This commit is contained in:
richarjiang
2026-03-15 15:17:10 +08:00
parent 8d094ad5cc
commit 36f10954cf
22 changed files with 3140 additions and 11 deletions

55
app/api/v1/token/route.ts Normal file
View File

@@ -0,0 +1,55 @@
import { NextRequest, NextResponse } from "next/server";
import { sql } from "drizzle-orm";
import { db } from "@/lib/db";
import { invalidateTokenLeaderboardCache } from "@/lib/redis";
import { authenticateRequest, getTodayDateString } from "@/lib/utils";
import { tokenSchema } from "@/lib/validators/schemas";
export async function POST(req: NextRequest) {
try {
const auth = await authenticateRequest(req);
if (auth instanceof NextResponse) {
return auth;
}
const { claw } = auth;
const body = await req.json();
const parsed = tokenSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: "Validation failed", details: parsed.error.flatten() },
{ status: 400 }
);
}
const { inputTokens, outputTokens, date } = parsed.data;
const targetDate = date || getTodayDateString();
// Use UPSERT (INSERT ... ON DUPLICATE KEY UPDATE) for atomic operation
await db.execute(sql`
INSERT INTO token_usage (claw_id, date, input_tokens, output_tokens)
VALUES (${claw.id}, ${targetDate}, ${inputTokens}, ${outputTokens})
ON DUPLICATE KEY UPDATE
input_tokens = ${inputTokens},
output_tokens = ${outputTokens},
updated_at = NOW()
`);
// Invalidate leaderboard cache
await invalidateTokenLeaderboardCache();
return NextResponse.json({
ok: true,
date: targetDate,
inputTokens,
outputTokens,
totalTokens: inputTokens + outputTokens,
});
} catch (error) {
console.error("Token report error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}