diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go index 764a4132..bc5603f4 100644 --- a/backend/internal/handler/dto/mappers.go +++ b/backend/internal/handler/dto/mappers.go @@ -266,6 +266,7 @@ func UsageLogFromService(l *service.UsageLog) *UsageLog { FirstTokenMs: l.FirstTokenMs, ImageCount: l.ImageCount, ImageSize: l.ImageSize, + UserAgent: l.UserAgent, CreatedAt: l.CreatedAt, User: UserFromServiceShallow(l.User), APIKey: APIKeyFromService(l.APIKey), diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go index a11662fe..8826fd8b 100644 --- a/backend/internal/handler/dto/types.go +++ b/backend/internal/handler/dto/types.go @@ -180,6 +180,9 @@ type UsageLog struct { ImageCount int `json:"image_count"` ImageSize *string `json:"image_size"` + // User-Agent + UserAgent *string `json:"user_agent"` + CreatedAt time.Time `json:"created_at"` User *User `json:"user,omitempty"` diff --git a/frontend/src/components/admin/usage/UsageTable.vue b/frontend/src/components/admin/usage/UsageTable.vue index 79465bb7..c53b8c90 100644 --- a/frontend/src/components/admin/usage/UsageTable.vue +++ b/frontend/src/components/admin/usage/UsageTable.vue @@ -115,15 +115,9 @@ {{ formatDateTime(value) }} - + + @@ -480,7 +485,8 @@ const columns = computed(() => [ { key: 'billing_type', label: t('usage.billingType'), sortable: false }, { key: 'first_token', label: t('usage.firstToken'), sortable: false }, { key: 'duration', label: t('usage.duration'), sortable: false }, - { key: 'created_at', label: t('usage.time'), sortable: true } + { key: 'created_at', label: t('usage.time'), sortable: true }, + { key: 'user_agent', label: t('usage.userAgent'), sortable: false } ]) const usageLogs = ref([]) @@ -545,6 +551,19 @@ const formatDuration = (ms: number): string => { return `${(ms / 1000).toFixed(2)}s` } +const formatUserAgent = (ua: string): string => { + // 提取主要客户端标识 + if (ua.includes('claude-cli')) return ua.match(/claude-cli\/[\d.]+/)?.[0] || 'Claude CLI' + if (ua.includes('Cursor')) return 'Cursor' + if (ua.includes('VSCode') || ua.includes('vscode')) return 'VS Code' + if (ua.includes('Continue')) return 'Continue' + if (ua.includes('Cline')) return 'Cline' + if (ua.includes('OpenAI')) return 'OpenAI SDK' + if (ua.includes('anthropic')) return 'Anthropic SDK' + // 截断过长的 UA + return ua.length > 30 ? ua.substring(0, 30) + '...' : ua +} + const formatTokens = (value: number): string => { if (value >= 1_000_000_000) { return `${(value / 1_000_000_000).toFixed(2)}B`