From 2d4e6285a45d2c8a8c18982adf0d11f26df72450 Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 25 Jan 2026 17:29:59 -0800 Subject: [PATCH] feat(admin): add global token usage statistics - Add token aggregation to statistics/overview endpoint - Include total tokens, cost, and API calls for all time - Include today's token usage and cost breakdown - Display token stats in ConversationsPage with 2 rows of cards - Add formatNumber helper for K/M number formatting - Export GlobalTokenStats and TodayTokenStats types Co-Authored-By: Claude Opus 4.5 --- .../application/useConversations.ts | 2 + .../infrastructure/conversations.api.ts | 20 +++++ .../presentation/pages/ConversationsPage.tsx | 86 ++++++++++++++++++- .../inbound/admin-conversation.controller.ts | 46 +++++++++- 4 files changed, 152 insertions(+), 2 deletions(-) diff --git a/packages/admin-client/src/features/conversations/application/useConversations.ts b/packages/admin-client/src/features/conversations/application/useConversations.ts index 3d7669e..2e5bb74 100644 --- a/packages/admin-client/src/features/conversations/application/useConversations.ts +++ b/packages/admin-client/src/features/conversations/application/useConversations.ts @@ -64,4 +64,6 @@ export type { ConversationStatistics, ConversationQueryParams, TokenDetails, + GlobalTokenStats, + TodayTokenStats, } from '../infrastructure/conversations.api'; diff --git a/packages/admin-client/src/features/conversations/infrastructure/conversations.api.ts b/packages/admin-client/src/features/conversations/infrastructure/conversations.api.ts index 269ddb1..4176b85 100644 --- a/packages/admin-client/src/features/conversations/infrastructure/conversations.api.ts +++ b/packages/admin-client/src/features/conversations/infrastructure/conversations.api.ts @@ -78,6 +78,24 @@ export interface PaginatedConversations { totalPages: number; } +export interface GlobalTokenStats { + totalInputTokens: number; + totalOutputTokens: number; + totalCacheCreationTokens: number; + totalCacheReadTokens: number; + totalTokens: number; + totalEstimatedCost: number; + totalApiCalls: number; +} + +export interface TodayTokenStats { + totalInputTokens: number; + totalOutputTokens: number; + totalTokens: number; + totalEstimatedCost: number; + totalApiCalls: number; +} + export interface ConversationStatistics { total: number; active: number; @@ -85,6 +103,8 @@ export interface ConversationStatistics { converted: number; todayCount: number; conversionRate: string; + tokenStats: GlobalTokenStats; + todayTokenStats: TodayTokenStats; } export interface ConversationQueryParams { diff --git a/packages/admin-client/src/features/conversations/presentation/pages/ConversationsPage.tsx b/packages/admin-client/src/features/conversations/presentation/pages/ConversationsPage.tsx index 4829850..a16b30c 100644 --- a/packages/admin-client/src/features/conversations/presentation/pages/ConversationsPage.tsx +++ b/packages/admin-client/src/features/conversations/presentation/pages/ConversationsPage.tsx @@ -22,6 +22,9 @@ import { GlobalOutlined, LaptopOutlined, BarChartOutlined, + DollarOutlined, + ApiOutlined, + ThunderboltOutlined, } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import dayjs from 'dayjs'; @@ -196,7 +199,7 @@ export function ConversationsPage() {
对话管理 - {/* Statistics Cards */} + {/* Statistics Cards - Conversations */} @@ -251,6 +254,76 @@ export function ConversationsPage() { + {/* Token Usage Statistics */} + + + + + } + valueStyle={{ color: '#13c2c2', fontSize: 20 }} + formatter={(value) => formatNumber(value as number)} + /> +
+ 输入: {formatNumber(stats?.tokenStats?.totalInputTokens ?? 0)} | + 输出: {formatNumber(stats?.tokenStats?.totalOutputTokens ?? 0)} +
+
+
+ + + + + } + precision={2} + valueStyle={{ color: '#eb2f96', fontSize: 20 }} + /> +
+ API 调用: {formatNumber(stats?.tokenStats?.totalApiCalls ?? 0)} 次 +
+
+
+ + + + + } + valueStyle={{ color: '#1890ff', fontSize: 20 }} + formatter={(value) => formatNumber(value as number)} + /> +
+ 输入: {formatNumber(stats?.todayTokenStats?.totalInputTokens ?? 0)} | + 输出: {formatNumber(stats?.todayTokenStats?.totalOutputTokens ?? 0)} +
+
+
+ + + + + } + precision={4} + valueStyle={{ color: '#fa8c16', fontSize: 20 }} + /> +
+ API 调用: {formatNumber(stats?.todayTokenStats?.totalApiCalls ?? 0)} 次 +
+
+
+ +
+ {/* Filters */} @@ -542,3 +615,14 @@ function parseUserAgent(ua: string): string { return `${browser} / ${os}`; } + +// Helper function to format large numbers (e.g., 1234567 -> 1.23M) +function formatNumber(num: number): string { + if (num >= 1000000) { + return (num / 1000000).toFixed(2) + 'M'; + } + if (num >= 1000) { + return (num / 1000).toFixed(1) + 'K'; + } + return num.toString(); +} diff --git a/packages/services/conversation-service/src/adapters/inbound/admin-conversation.controller.ts b/packages/services/conversation-service/src/adapters/inbound/admin-conversation.controller.ts index ec4bb3d..4d05189 100644 --- a/packages/services/conversation-service/src/adapters/inbound/admin-conversation.controller.ts +++ b/packages/services/conversation-service/src/adapters/inbound/admin-conversation.controller.ts @@ -323,7 +323,7 @@ export class AdminConversationController { } /** - * 获取对话统计 + * 获取对话统计(包含 Token 汇总) */ @Get('statistics/overview') async getStatistics(@Headers('authorization') auth: string) { @@ -342,6 +342,33 @@ export class AdminConversationController { .where('c.created_at >= :today', { today }) .getCount(); + // 全局 Token 统计 + const allTokenStats = await this.tokenUsageRepo + .createQueryBuilder('t') + .select([ + 'SUM(t.input_tokens) as "totalInputTokens"', + 'SUM(t.output_tokens) as "totalOutputTokens"', + 'SUM(t.cache_creation_tokens) as "totalCacheCreationTokens"', + 'SUM(t.cache_read_tokens) as "totalCacheReadTokens"', + 'SUM(t.total_tokens) as "totalTokens"', + 'SUM(t.estimated_cost) as "totalEstimatedCost"', + 'COUNT(*) as "totalApiCalls"', + ]) + .getRawOne(); + + // 今日 Token 统计 + const todayTokenStats = await this.tokenUsageRepo + .createQueryBuilder('t') + .select([ + 'SUM(t.input_tokens) as "totalInputTokens"', + 'SUM(t.output_tokens) as "totalOutputTokens"', + 'SUM(t.total_tokens) as "totalTokens"', + 'SUM(t.estimated_cost) as "totalEstimatedCost"', + 'COUNT(*) as "totalApiCalls"', + ]) + .where('t.created_at >= :today', { today }) + .getRawOne(); + return { success: true, data: { @@ -351,6 +378,23 @@ export class AdminConversationController { converted, todayCount, conversionRate: total > 0 ? ((converted / total) * 100).toFixed(1) : '0', + // Token 汇总统计 + tokenStats: { + totalInputTokens: parseInt(allTokenStats?.totalInputTokens || '0'), + totalOutputTokens: parseInt(allTokenStats?.totalOutputTokens || '0'), + totalCacheCreationTokens: parseInt(allTokenStats?.totalCacheCreationTokens || '0'), + totalCacheReadTokens: parseInt(allTokenStats?.totalCacheReadTokens || '0'), + totalTokens: parseInt(allTokenStats?.totalTokens || '0'), + totalEstimatedCost: parseFloat(allTokenStats?.totalEstimatedCost || '0'), + totalApiCalls: parseInt(allTokenStats?.totalApiCalls || '0'), + }, + todayTokenStats: { + totalInputTokens: parseInt(todayTokenStats?.totalInputTokens || '0'), + totalOutputTokens: parseInt(todayTokenStats?.totalOutputTokens || '0'), + totalTokens: parseInt(todayTokenStats?.totalTokens || '0'), + totalEstimatedCost: parseFloat(todayTokenStats?.totalEstimatedCost || '0'), + totalApiCalls: parseInt(todayTokenStats?.totalApiCalls || '0'), + }, }, }; }