feat: 在仪表盘添加使用记录展示功能

- 新增后端API端点 /admin/dashboard/usage-records
  - 支持分页查询所有API Key的使用记录
  - 自动关联API Key名称和账户名称
  - 按时间倒序排列(最新的在前)

- 新增仪表盘使用记录表格
  - 显示时间、API Key、账户、模型、输入/输出/缓存创建/缓存读取tokens、成本
  - 智能时间格式化(今天显示时分秒,昨天显示时间)
  - 支持加载更多记录,分页展示
  - 响应式设计,支持暗黑模式

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
IanShaw027
2025-12-03 19:41:37 -08:00
committed by IanShaw027
parent 69a1006f4c
commit 81971436e6
2 changed files with 346 additions and 1 deletions

View File

@@ -704,4 +704,106 @@ router.post('/cleanup', authenticateAdmin, async (req, res) => {
}
})
// 📊 获取最近的使用记录
router.get('/usage-records', authenticateAdmin, async (req, res) => {
try {
const { limit = 100, offset = 0 } = req.query
const limitNum = Math.min(parseInt(limit) || 100, 500) // 最多500条
const offsetNum = Math.max(parseInt(offset) || 0, 0)
// 获取所有API Keys
const apiKeys = await apiKeyService.getAllApiKeys()
if (!apiKeys || apiKeys.length === 0) {
return res.json({ success: true, data: { records: [], total: 0 } })
}
// 收集所有API Key的使用记录
const allRecords = []
for (const key of apiKeys) {
try {
const records = await redis.getUsageRecords(key.id, 100) // 每个key最多取100条
if (records && records.length > 0) {
// 为每条记录添加API Key信息
const enrichedRecords = records.map((record) => ({
...record,
apiKeyId: key.id,
apiKeyName: key.name || 'Unnamed Key'
}))
allRecords.push(...enrichedRecords)
}
} catch (error) {
logger.error(`Failed to get usage records for key ${key.id}:`, error)
continue
}
}
// 按时间戳倒序排序(最新的在前)
allRecords.sort((a, b) => {
const timeA = new Date(a.timestamp).getTime()
const timeB = new Date(b.timestamp).getTime()
return timeB - timeA
})
// 分页
const paginatedRecords = allRecords.slice(offsetNum, offsetNum + limitNum)
// 获取账户名称映射
const accountIds = [...new Set(paginatedRecords.map((r) => r.accountId).filter(Boolean))]
const accountNameMap = {}
// 并发获取所有账户名称
await Promise.all(
accountIds.map(async (accountId) => {
try {
// 尝试从不同类型的账户中获取
const claudeAcc = await redis.getAccount(accountId)
if (claudeAcc && claudeAcc.name) {
accountNameMap[accountId] = claudeAcc.name
return
}
const consoleAcc = await redis.getClaudeConsoleAccount(accountId)
if (consoleAcc && consoleAcc.name) {
accountNameMap[accountId] = consoleAcc.name
return
}
const geminiAcc = await redis.getGeminiAccount(accountId)
if (geminiAcc && geminiAcc.name) {
accountNameMap[accountId] = geminiAcc.name
return
}
// 其他平台账户...
accountNameMap[accountId] = accountId // 降级显示ID
} catch (error) {
accountNameMap[accountId] = accountId
}
})
)
// 为记录添加账户名称
const enrichedRecords = paginatedRecords.map((record) => ({
...record,
accountName: record.accountId ? accountNameMap[record.accountId] || record.accountId : '-'
}))
return res.json({
success: true,
data: {
records: enrichedRecords,
total: allRecords.length,
limit: limitNum,
offset: offsetNum
}
})
} catch (error) {
logger.error('❌ Failed to get usage records:', error)
return res.status(500).json({
error: 'Failed to get usage records',
message: error.message
})
}
})
module.exports = router