From ff30bfab825f94a18349c4f84ea9097d895092b3 Mon Sep 17 00:00:00 2001
From: atoz03
Date: Fri, 5 Dec 2025 14:23:25 +0800
Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E8=B4=A6=E6=88=B7=E6=97=B6?=
=?UTF-8?q?=E9=97=B4=E7=BA=BF=E8=AF=A6=E6=83=85=E9=A1=B5=E4=B8=8E=E6=8E=A5?=
=?UTF-8?q?=E5=8F=A3=E5=AE=8C=E5=96=84=20=20=20-=20=E5=90=8E=E7=AB=AF?=
=?UTF-8?q?=E6=96=B0=E5=A2=9E=20/admin/accounts/:accountId/usage-records?=
=?UTF-8?q?=20=E6=8E=A5=E5=8F=A3=EF=BC=8C=E6=94=AF=E6=8C=81=E6=8C=89?=
=?UTF-8?q?=E8=B4=A6=E6=88=B7=E8=81=9A=E5=90=88=E5=A4=9A=20Key=20=E8=AE=B0?=
=?UTF-8?q?=E5=BD=95=E5=B9=B6=E5=88=86=E9=A1=B5=E7=AD=9B=E9=80=89=E3=80=81?=
=?UTF-8?q?=E6=B1=87=E6=80=BB=E7=BB=9F=E8=AE=A1=20=20=20-=20=E4=BF=AE?=
=?UTF-8?q?=E5=A4=8D=20API=20Key=20=E6=97=B6=E9=97=B4=E7=BA=BF=E8=B4=A6?=
=?UTF-8?q?=E6=88=B7=E7=AD=9B=E9=80=89=E8=B7=B3=E8=BF=87=E5=B7=B2=E5=88=A0?=
=?UTF-8?q?=E9=99=A4=E8=B4=A6=E5=8F=B7=EF=BC=8C=E8=A1=A5=E5=85=85=E8=B4=A6?=
=?UTF-8?q?=E6=88=B7/Key=20=E8=BE=85=E5=8A=A9=E8=A7=A3=E6=9E=90=20=20=20-?=
=?UTF-8?q?=20=E5=89=8D=E7=AB=AF=E6=96=B0=E5=A2=9E=20AccountUsageRecordsVi?=
=?UTF-8?q?ew=E3=80=81=E8=B7=AF=E7=94=B1=E5=8F=8A=E8=B4=A6=E6=88=B7?=
=?UTF-8?q?=E5=88=97=E8=A1=A8=E2=80=9C=E6=97=B6=E9=97=B4=E7=BA=BF=E2=80=9D?=
=?UTF-8?q?=E5=85=A5=E5=8F=A3=EF=BC=8C=E6=94=AF=E6=8C=81=E6=A8=A1=E5=9E=8B?=
=?UTF-8?q?/API=20Key=20=E7=AD=9B=E9=80=89=E4=B8=8E=20CSV=20=E5=AF=BC?=
=?UTF-8?q?=E5=87=BA=20=20=20-=20=E8=A1=A5=E8=A3=85=20prettier-plugin-tail?=
=?UTF-8?q?windcss=20=E5=B9=B6=E5=AE=8C=E6=88=90=E7=9B=B8=E5=85=B3?=
=?UTF-8?q?=E6=96=87=E4=BB=B6=E6=A0=BC=E5=BC=8F=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
package-lock.json | 80 +++
package.json | 1 +
src/routes/admin/usageStats.js | 371 ++++++++++-
web/admin-spa/src/router/index.js | 13 +
.../src/views/AccountUsageRecordsView.vue | 574 ++++++++++++++++++
web/admin-spa/src/views/AccountsView.vue | 33 +
6 files changed, 1053 insertions(+), 19 deletions(-)
create mode 100644 web/admin-spa/src/views/AccountUsageRecordsView.vue
diff --git a/package-lock.json b/package-lock.json
index 551062b7..c6dccd11 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,6 +44,7 @@
"jest": "^29.7.0",
"nodemon": "^3.0.1",
"prettier": "^3.6.2",
+ "prettier-plugin-tailwindcss": "^0.7.2",
"supertest": "^6.3.3"
},
"engines": {
@@ -7605,6 +7606,85 @@
"node": ">=6.0.0"
}
},
+ "node_modules/prettier-plugin-tailwindcss": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.2.tgz",
+ "integrity": "sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19"
+ },
+ "peerDependencies": {
+ "@ianvs/prettier-plugin-sort-imports": "*",
+ "@prettier/plugin-hermes": "*",
+ "@prettier/plugin-oxc": "*",
+ "@prettier/plugin-pug": "*",
+ "@shopify/prettier-plugin-liquid": "*",
+ "@trivago/prettier-plugin-sort-imports": "*",
+ "@zackad/prettier-plugin-twig": "*",
+ "prettier": "^3.0",
+ "prettier-plugin-astro": "*",
+ "prettier-plugin-css-order": "*",
+ "prettier-plugin-jsdoc": "*",
+ "prettier-plugin-marko": "*",
+ "prettier-plugin-multiline-arrays": "*",
+ "prettier-plugin-organize-attributes": "*",
+ "prettier-plugin-organize-imports": "*",
+ "prettier-plugin-sort-imports": "*",
+ "prettier-plugin-svelte": "*"
+ },
+ "peerDependenciesMeta": {
+ "@ianvs/prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "@prettier/plugin-hermes": {
+ "optional": true
+ },
+ "@prettier/plugin-oxc": {
+ "optional": true
+ },
+ "@prettier/plugin-pug": {
+ "optional": true
+ },
+ "@shopify/prettier-plugin-liquid": {
+ "optional": true
+ },
+ "@trivago/prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "@zackad/prettier-plugin-twig": {
+ "optional": true
+ },
+ "prettier-plugin-astro": {
+ "optional": true
+ },
+ "prettier-plugin-css-order": {
+ "optional": true
+ },
+ "prettier-plugin-jsdoc": {
+ "optional": true
+ },
+ "prettier-plugin-marko": {
+ "optional": true
+ },
+ "prettier-plugin-multiline-arrays": {
+ "optional": true
+ },
+ "prettier-plugin-organize-attributes": {
+ "optional": true
+ },
+ "prettier-plugin-organize-imports": {
+ "optional": true
+ },
+ "prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "prettier-plugin-svelte": {
+ "optional": true
+ }
+ }
+ },
"node_modules/pretty-format": {
"version": "29.7.0",
"resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-29.7.0.tgz",
diff --git a/package.json b/package.json
index 72ea4720..2b7ffa25 100644
--- a/package.json
+++ b/package.json
@@ -83,6 +83,7 @@
"jest": "^29.7.0",
"nodemon": "^3.0.1",
"prettier": "^3.6.2",
+ "prettier-plugin-tailwindcss": "^0.7.2",
"supertest": "^6.3.3"
},
"engines": {
diff --git a/src/routes/admin/usageStats.js b/src/routes/admin/usageStats.js
index 18d6b436..25b8c260 100644
--- a/src/routes/admin/usageStats.js
+++ b/src/routes/admin/usageStats.js
@@ -16,6 +16,65 @@ const pricingService = require('../../services/pricingService')
const router = express.Router()
+const accountTypeNames = {
+ claude: 'Claude官方',
+ 'claude-console': 'Claude Console',
+ ccr: 'Claude Console Relay',
+ openai: 'OpenAI',
+ 'openai-responses': 'OpenAI Responses',
+ gemini: 'Gemini',
+ 'gemini-api': 'Gemini API',
+ droid: 'Droid',
+ unknown: '未知渠道'
+}
+
+const resolveAccountByPlatform = async (accountId, platform) => {
+ const serviceMap = {
+ claude: claudeAccountService,
+ 'claude-console': claudeConsoleAccountService,
+ gemini: geminiAccountService,
+ 'gemini-api': geminiApiAccountService,
+ openai: openaiAccountService,
+ 'openai-responses': openaiResponsesAccountService,
+ droid: droidAccountService,
+ ccr: ccrAccountService
+ }
+
+ if (platform && serviceMap[platform]) {
+ try {
+ const account = await serviceMap[platform].getAccount(accountId)
+ if (account) {
+ return { ...account, platform }
+ }
+ } catch (error) {
+ logger.debug(`⚠️ Failed to get account ${accountId} from ${platform}: ${error.message}`)
+ }
+ }
+
+ for (const [platformName, service] of Object.entries(serviceMap)) {
+ try {
+ const account = await service.getAccount(accountId)
+ if (account) {
+ return { ...account, platform: platformName }
+ }
+ } catch (error) {
+ logger.debug(`⚠️ Failed to get account ${accountId} from ${platformName}: ${error.message}`)
+ }
+ }
+
+ return null
+}
+
+const getApiKeyName = async (keyId) => {
+ try {
+ const keyData = await redis.getApiKey(keyId)
+ return keyData?.name || keyData?.label || keyId
+ } catch (error) {
+ logger.debug(`⚠️ Failed to get API key name for ${keyId}: ${error.message}`)
+ return keyId
+ }
+}
+
// 📊 账户使用统计
// 获取所有账户的使用统计
@@ -1861,18 +1920,6 @@ router.get('/api-keys/:keyId/usage-records', authenticateAdmin, async (req, res)
const rawRecords = await redis.getUsageRecords(keyId, 5000)
- const accountTypeNames = {
- claude: 'Claude官方',
- 'claude-console': 'Claude Console',
- ccr: 'Claude Console Relay',
- openai: 'OpenAI',
- 'openai-responses': 'OpenAI Responses',
- gemini: 'Gemini',
- 'gemini-api': 'Gemini API',
- droid: 'Droid',
- unknown: '未知渠道'
- }
-
const accountServices = [
{ type: 'claude', getter: (id) => claudeAccountService.getAccount(id) },
{ type: 'claude-console', getter: (id) => claudeConsoleAccountService.getAccount(id) },
@@ -2088,13 +2135,16 @@ router.get('/api-keys/:keyId/usage-records', authenticateAdmin, async (req, res)
const accountOptions = []
for (const option of accountOptionMap.values()) {
const info = await resolveAccountInfo(option.id, option.accountType)
- const resolvedType = info?.type || option.accountType || 'unknown'
- accountOptions.push({
- id: option.id,
- name: info?.name || option.id,
- accountType: resolvedType,
- accountTypeName: accountTypeNames[resolvedType] || '未知渠道'
- })
+ if (info && info.name) {
+ accountOptions.push({
+ id: option.id,
+ name: info.name,
+ accountType: info.type,
+ accountTypeName: accountTypeNames[info.type] || '未知渠道'
+ })
+ } else {
+ logger.warn(`⚠️ Skipping deleted/invalid account in filter options: ${option.id}`)
+ }
}
return res.json({
@@ -2146,4 +2196,287 @@ router.get('/api-keys/:keyId/usage-records', authenticateAdmin, async (req, res)
}
})
+// 获取账户的请求记录时间线
+router.get('/accounts/:accountId/usage-records', authenticateAdmin, async (req, res) => {
+ try {
+ const { accountId } = req.params
+ const {
+ platform,
+ page = 1,
+ pageSize = 50,
+ startDate,
+ endDate,
+ model,
+ apiKeyId,
+ sortOrder = 'desc'
+ } = req.query
+
+ const pageNumber = Math.max(parseInt(page, 10) || 1, 1)
+ const pageSizeNumber = Math.min(Math.max(parseInt(pageSize, 10) || 50, 1), 200)
+ const normalizedSortOrder = sortOrder === 'asc' ? 'asc' : 'desc'
+
+ const startTime = startDate ? new Date(startDate) : null
+ const endTime = endDate ? new Date(endDate) : null
+
+ if (
+ (startDate && Number.isNaN(startTime?.getTime())) ||
+ (endDate && Number.isNaN(endTime?.getTime()))
+ ) {
+ return res.status(400).json({ success: false, error: 'Invalid date range' })
+ }
+
+ if (startTime && endTime && startTime > endTime) {
+ return res
+ .status(400)
+ .json({ success: false, error: 'Start date must be before or equal to end date' })
+ }
+
+ const accountInfo = await resolveAccountByPlatform(accountId, platform)
+ if (!accountInfo) {
+ return res.status(404).json({ success: false, error: 'Account not found' })
+ }
+
+ const allApiKeys = await apiKeyService.getAllApiKeys(true)
+ const apiKeyNameCache = new Map(
+ allApiKeys.map((key) => [key.id, key.name || key.label || key.id])
+ )
+
+ let keysToUse = apiKeyId ? allApiKeys.filter((key) => key.id === apiKeyId) : allApiKeys
+ if (apiKeyId && keysToUse.length === 0) {
+ keysToUse = [{ id: apiKeyId }]
+ }
+
+ const toUsageObject = (record) => ({
+ input_tokens: record.inputTokens || 0,
+ output_tokens: record.outputTokens || 0,
+ cache_creation_input_tokens: record.cacheCreateTokens || 0,
+ cache_read_input_tokens: record.cacheReadTokens || 0,
+ cache_creation: record.cacheCreation || record.cache_creation || null
+ })
+
+ const withinRange = (record) => {
+ if (!record.timestamp) {
+ return false
+ }
+ const ts = new Date(record.timestamp)
+ if (Number.isNaN(ts.getTime())) {
+ return false
+ }
+ if (startTime && ts < startTime) {
+ return false
+ }
+ if (endTime && ts > endTime) {
+ return false
+ }
+ return true
+ }
+
+ const filteredRecords = []
+ const modelSet = new Set()
+ const apiKeyOptionMap = new Map()
+ let earliestTimestamp = null
+ let latestTimestamp = null
+
+ const batchSize = 10
+ for (let i = 0; i < keysToUse.length; i += batchSize) {
+ const batch = keysToUse.slice(i, i + batchSize)
+ const batchResults = await Promise.all(
+ batch.map(async (key) => {
+ try {
+ const records = await redis.getUsageRecords(key.id, 5000)
+ return { keyId: key.id, records: records || [] }
+ } catch (error) {
+ logger.debug(`⚠️ Failed to get usage records for key ${key.id}: ${error.message}`)
+ return { keyId: key.id, records: [] }
+ }
+ })
+ )
+
+ for (const { keyId, records } of batchResults) {
+ const apiKeyName = apiKeyNameCache.get(keyId) || (await getApiKeyName(keyId))
+ for (const record of records) {
+ if (record.accountId !== accountId) {
+ continue
+ }
+ if (!withinRange(record)) {
+ continue
+ }
+ if (model && record.model !== model) {
+ continue
+ }
+
+ const accountType = record.accountType || accountInfo.platform || 'unknown'
+ const normalizedModel = record.model || 'unknown'
+
+ modelSet.add(normalizedModel)
+ apiKeyOptionMap.set(keyId, { id: keyId, name: apiKeyName })
+
+ if (record.timestamp) {
+ const ts = new Date(record.timestamp)
+ if (!Number.isNaN(ts.getTime())) {
+ if (!earliestTimestamp || ts < earliestTimestamp) {
+ earliestTimestamp = ts
+ }
+ if (!latestTimestamp || ts > latestTimestamp) {
+ latestTimestamp = ts
+ }
+ }
+ }
+
+ filteredRecords.push({
+ ...record,
+ model: normalizedModel,
+ accountType,
+ apiKeyId: keyId,
+ apiKeyName
+ })
+ }
+ }
+ }
+
+ filteredRecords.sort((a, b) => {
+ const aTime = new Date(a.timestamp).getTime()
+ const bTime = new Date(b.timestamp).getTime()
+ if (Number.isNaN(aTime) || Number.isNaN(bTime)) {
+ return 0
+ }
+ return normalizedSortOrder === 'asc' ? aTime - bTime : bTime - aTime
+ })
+
+ const summary = {
+ totalRequests: 0,
+ inputTokens: 0,
+ outputTokens: 0,
+ cacheCreateTokens: 0,
+ cacheReadTokens: 0,
+ totalTokens: 0,
+ totalCost: 0
+ }
+
+ for (const record of filteredRecords) {
+ const usage = toUsageObject(record)
+ const costData = CostCalculator.calculateCost(usage, record.model || 'unknown')
+ const computedCost =
+ typeof record.cost === 'number' ? record.cost : costData?.costs?.total || 0
+ const totalTokens =
+ record.totalTokens ||
+ usage.input_tokens +
+ usage.output_tokens +
+ usage.cache_creation_input_tokens +
+ usage.cache_read_input_tokens
+
+ summary.totalRequests += 1
+ summary.inputTokens += usage.input_tokens
+ summary.outputTokens += usage.output_tokens
+ summary.cacheCreateTokens += usage.cache_creation_input_tokens
+ summary.cacheReadTokens += usage.cache_read_input_tokens
+ summary.totalTokens += totalTokens
+ summary.totalCost += computedCost
+ }
+
+ const totalRecords = filteredRecords.length
+ const totalPages = totalRecords > 0 ? Math.ceil(totalRecords / pageSizeNumber) : 0
+ const safePage = totalPages > 0 ? Math.min(pageNumber, totalPages) : 1
+ const startIndex = (safePage - 1) * pageSizeNumber
+ const pageRecords =
+ totalRecords === 0 ? [] : filteredRecords.slice(startIndex, startIndex + pageSizeNumber)
+
+ const enrichedRecords = []
+ for (const record of pageRecords) {
+ const usage = toUsageObject(record)
+ const costData = CostCalculator.calculateCost(usage, record.model || 'unknown')
+ const computedCost =
+ typeof record.cost === 'number' ? record.cost : costData?.costs?.total || 0
+ const totalTokens =
+ record.totalTokens ||
+ usage.input_tokens +
+ usage.output_tokens +
+ usage.cache_creation_input_tokens +
+ usage.cache_read_input_tokens
+
+ enrichedRecords.push({
+ timestamp: record.timestamp,
+ model: record.model || 'unknown',
+ apiKeyId: record.apiKeyId,
+ apiKeyName: record.apiKeyName,
+ accountId,
+ accountName: accountInfo.name || accountInfo.email || accountId,
+ accountType: record.accountType,
+ accountTypeName: accountTypeNames[record.accountType] || '未知渠道',
+ inputTokens: usage.input_tokens,
+ outputTokens: usage.output_tokens,
+ cacheCreateTokens: usage.cache_creation_input_tokens,
+ cacheReadTokens: usage.cache_read_input_tokens,
+ ephemeral5mTokens: record.ephemeral5mTokens || 0,
+ ephemeral1hTokens: record.ephemeral1hTokens || 0,
+ totalTokens,
+ isLongContextRequest: record.isLongContext || record.isLongContextRequest || false,
+ cost: Number(computedCost.toFixed(6)),
+ costFormatted:
+ record.costFormatted ||
+ costData?.formatted?.total ||
+ CostCalculator.formatCost(computedCost),
+ costBreakdown: record.costBreakdown || {
+ input: costData?.costs?.input || 0,
+ output: costData?.costs?.output || 0,
+ cacheCreate: costData?.costs?.cacheWrite || 0,
+ cacheRead: costData?.costs?.cacheRead || 0,
+ total: costData?.costs?.total || computedCost
+ },
+ responseTime: record.responseTime || null
+ })
+ }
+
+ return res.json({
+ success: true,
+ data: {
+ records: enrichedRecords,
+ pagination: {
+ currentPage: safePage,
+ pageSize: pageSizeNumber,
+ totalRecords,
+ totalPages,
+ hasNextPage: totalPages > 0 && safePage < totalPages,
+ hasPreviousPage: totalPages > 0 && safePage > 1
+ },
+ filters: {
+ startDate: startTime ? startTime.toISOString() : null,
+ endDate: endTime ? endTime.toISOString() : null,
+ model: model || null,
+ apiKeyId: apiKeyId || null,
+ platform: accountInfo.platform,
+ sortOrder: normalizedSortOrder
+ },
+ accountInfo: {
+ id: accountId,
+ name: accountInfo.name || accountInfo.email || accountId,
+ platform: accountInfo.platform || platform || 'unknown',
+ status: accountInfo.status ?? accountInfo.isActive ?? null
+ },
+ summary: {
+ ...summary,
+ totalCost: Number(summary.totalCost.toFixed(6)),
+ avgCost:
+ summary.totalRequests > 0
+ ? Number((summary.totalCost / summary.totalRequests).toFixed(6))
+ : 0
+ },
+ availableFilters: {
+ models: Array.from(modelSet),
+ apiKeys: Array.from(apiKeyOptionMap.values()),
+ dateRange: {
+ earliest: earliestTimestamp ? earliestTimestamp.toISOString() : null,
+ latest: latestTimestamp ? latestTimestamp.toISOString() : null
+ }
+ }
+ }
+ })
+ } catch (error) {
+ logger.error('❌ Failed to get account usage records:', error)
+ return res
+ .status(500)
+ .json({ error: 'Failed to get account usage records', message: error.message })
+ }
+})
+
module.exports = router
diff --git a/web/admin-spa/src/router/index.js b/web/admin-spa/src/router/index.js
index 3375c906..feb67aa1 100644
--- a/web/admin-spa/src/router/index.js
+++ b/web/admin-spa/src/router/index.js
@@ -13,6 +13,7 @@ const DashboardView = () => import('@/views/DashboardView.vue')
const ApiKeysView = () => import('@/views/ApiKeysView.vue')
const ApiKeyUsageRecordsView = () => import('@/views/ApiKeyUsageRecordsView.vue')
const AccountsView = () => import('@/views/AccountsView.vue')
+const AccountUsageRecordsView = () => import('@/views/AccountUsageRecordsView.vue')
const TutorialView = () => import('@/views/TutorialView.vue')
const SettingsView = () => import('@/views/SettingsView.vue')
const ApiStatsView = () => import('@/views/ApiStatsView.vue')
@@ -110,6 +111,18 @@ const routes = [
}
]
},
+ {
+ path: '/accounts/:accountId/usage-records',
+ component: MainLayout,
+ meta: { requiresAuth: true },
+ children: [
+ {
+ path: '',
+ name: 'AccountUsageRecords',
+ component: AccountUsageRecordsView
+ }
+ ]
+ },
{
path: '/tutorial',
component: MainLayout,
diff --git a/web/admin-spa/src/views/AccountUsageRecordsView.vue b/web/admin-spa/src/views/AccountUsageRecordsView.vue
new file mode 100644
index 00000000..c813e143
--- /dev/null
+++ b/web/admin-spa/src/views/AccountUsageRecordsView.vue
@@ -0,0 +1,574 @@
+
+
+
+
+
+
+
+ 账户请求详情时间线
+
+
+ {{ accountDisplayName }}
+
+
ID: {{ accountId }}
+
渠道:{{ platformDisplayName }}
+
+
+
+
+ {{ dateRangeHint }}
+ 显示近 5000 条记录
+
+
+
+
+
+
总请求
+
+ {{ formatNumber(summary.totalRequests) }}
+
+
+
+
总 Token
+
+ {{ formatNumber(summary.totalTokens) }}
+
+
+
+
总费用
+
+ {{ formatCost(summary.totalCost) }}
+
+
+
+
平均费用/次
+
+ {{ formatCost(summary.avgCost) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 重置
+
+ 导出 CSV
+
+
+
+
+
+
+ 加载中...
+
+
+
+
+
+
+
+
+ |
+ 时间
+ |
+
+ API Key
+ |
+
+ 模型
+ |
+
+ 输入
+ |
+
+ 输出
+ |
+
+ 缓存(创/读)
+ |
+
+ 总 Token
+ |
+
+ 费用
+ |
+
+ 操作
+ |
+
+
+
+
+ |
+ {{ formatDate(record.timestamp) }}
+ |
+
+
+
+ {{ record.apiKeyName || record.apiKeyId || '未知 Key' }}
+
+
+ ID: {{ record.apiKeyId }}
+
+
+ |
+
+ {{ record.model }}
+ |
+
+ {{ formatNumber(record.inputTokens) }}
+ |
+
+ {{ formatNumber(record.outputTokens) }}
+ |
+
+ {{ formatNumber(record.cacheCreateTokens) }} /
+ {{ formatNumber(record.cacheReadTokens) }}
+ |
+
+ {{ formatNumber(record.totalTokens) }}
+ |
+
+ {{ record.costFormatted || formatCost(record.cost) }}
+ |
+
+ 详情
+ |
+
+
+
+
+
+
+
+
+
+
+ {{ record.apiKeyName || record.apiKeyId || '未知 Key' }}
+
+
+ ID: {{ record.apiKeyId }} · {{ formatDate(record.timestamp) }}
+
+
+ 渠道:{{ platformDisplayName }}
+
+
+
详情
+
+
+
模型:{{ record.model }}
+
总 Token:{{ formatNumber(record.totalTokens) }}
+
输入:{{ formatNumber(record.inputTokens) }}
+
输出:{{ formatNumber(record.outputTokens) }}
+
+ 缓存创/读:{{ formatNumber(record.cacheCreateTokens) }} /
+ {{ formatNumber(record.cacheReadTokens) }}
+
+
+ 费用:{{ record.costFormatted || formatCost(record.cost) }}
+
+
+
+
+
+
+
+ 共 {{ pagination.totalRecords }} 条记录
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue
index c2a31013..da60e249 100644
--- a/web/admin-spa/src/views/AccountsView.vue
+++ b/web/admin-spa/src/views/AccountsView.vue
@@ -1199,6 +1199,15 @@
详情
+
详情
+
-
+
+
+
+
@@ -325,6 +333,7 @@
diff --git a/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue
index da60e249..88aa858a 100644
--- a/web/admin-spa/src/views/AccountsView.vue
+++ b/web/admin-spa/src/views/AccountsView.vue
@@ -1199,15 +1199,6 @@
详情
-
详情
-
-