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) }}
-
-
-
{{ row.request_id }}
-
-
- -
+
+ {{ formatUserAgent(row.user_agent) }}
+ -
@@ -228,7 +222,6 @@
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { formatDateTime } from '@/utils/format'
-import { useAppStore } from '@/stores/app'
import DataTable from '@/components/common/DataTable.vue'
import EmptyState from '@/components/common/EmptyState.vue'
import Icon from '@/components/icons/Icon.vue'
@@ -236,8 +229,6 @@ import type { UsageLog } from '@/types'
defineProps(['data', 'loading'])
const { t } = useI18n()
-const appStore = useAppStore()
-const copiedRequestId = ref(null)
// Tooltip state - cost
const tooltipVisible = ref(false)
@@ -262,7 +253,7 @@ const cols = computed(() => [
{ 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: 'request_id', label: t('admin.usage.requestId'), sortable: false }
+ { key: 'user_agent', label: t('usage.userAgent'), sortable: false }
])
const formatCacheTokens = (tokens: number): string => {
@@ -271,23 +262,25 @@ const formatCacheTokens = (tokens: number): string => {
return tokens.toString()
}
+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 formatDuration = (ms: number | null | undefined): string => {
if (ms == null) return '-'
if (ms < 1000) return `${ms}ms`
return `${(ms / 1000).toFixed(2)}s`
}
-const copyRequestId = async (requestId: string) => {
- try {
- await navigator.clipboard.writeText(requestId)
- copiedRequestId.value = requestId
- appStore.showSuccess(t('admin.usage.requestIdCopied'))
- setTimeout(() => { copiedRequestId.value = null }, 2000)
- } catch {
- appStore.showError(t('common.copyFailed'))
- }
-}
-
// Cost tooltip functions
const showTooltip = (event: MouseEvent, row: UsageLog) => {
const target = event.currentTarget as HTMLElement
diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts
index f0cc4e14..1a24ea95 100644
--- a/frontend/src/i18n/locales/en.ts
+++ b/frontend/src/i18n/locales/en.ts
@@ -424,7 +424,8 @@ export default {
billingType: 'Billing',
balance: 'Balance',
subscription: 'Subscription',
- imageUnit: ' images'
+ imageUnit: ' images',
+ userAgent: 'User-Agent'
},
// Redeem
diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts
index ef9d2e39..2a58e74b 100644
--- a/frontend/src/i18n/locales/zh.ts
+++ b/frontend/src/i18n/locales/zh.ts
@@ -421,7 +421,8 @@ export default {
billingType: '消费类型',
balance: '余额',
subscription: '订阅',
- imageUnit: '张'
+ imageUnit: '张',
+ userAgent: 'User-Agent'
},
// Redeem
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index b16c66ef..360d20c4 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -576,6 +576,9 @@ export interface UsageLog {
image_count: number
image_size: string | null
+ // User-Agent
+ user_agent: string | null
+
created_at: string
user?: User
diff --git a/frontend/src/views/admin/UsageView.vue b/frontend/src/views/admin/UsageView.vue
index 522f1b00..47af8141 100644
--- a/frontend/src/views/admin/UsageView.vue
+++ b/frontend/src/views/admin/UsageView.vue
@@ -96,7 +96,7 @@ const exportToExcel = async () => {
t('admin.usage.cacheReadCost'), t('admin.usage.cacheCreationCost'),
t('usage.rate'), t('usage.original'), t('usage.billed'),
t('usage.billingType'), t('usage.firstToken'), t('usage.duration'),
- t('admin.usage.requestId')
+ t('admin.usage.requestId'), t('usage.userAgent')
]
const rows = all.map(log => [
log.created_at,
@@ -120,7 +120,8 @@ const exportToExcel = async () => {
log.billing_type === 1 ? t('usage.subscription') : t('usage.balance'),
log.first_token_ms ?? '',
log.duration_ms,
- log.request_id || ''
+ log.request_id || '',
+ log.user_agent || ''
])
const ws = XLSX.utils.aoa_to_sheet([headers, ...rows])
const wb = XLSX.utils.book_new()
diff --git a/frontend/src/views/user/UsageView.vue b/frontend/src/views/user/UsageView.vue
index 489e2726..0058c527 100644
--- a/frontend/src/views/user/UsageView.vue
+++ b/frontend/src/views/user/UsageView.vue
@@ -308,6 +308,11 @@
}}
+
+ {{ formatUserAgent(row.user_agent) }}
+ -
+
+
@@ -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`