mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
Merge branch 'Wei-Shaw:dev' into dev
This commit is contained in:
@@ -123,7 +123,7 @@ router.get('/api-keys/:keyId/cost-debug', authenticateAdmin, async (req, res) =>
|
||||
// 获取所有API Keys
|
||||
router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { timeRange = 'all' } = req.query // all, 7days, monthly
|
||||
const { timeRange = 'all', startDate, endDate } = req.query // all, 7days, monthly, custom
|
||||
const apiKeys = await apiKeyService.getAllApiKeys()
|
||||
|
||||
// 获取用户服务来补充owner信息
|
||||
@@ -133,7 +133,32 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
const now = new Date()
|
||||
const searchPatterns = []
|
||||
|
||||
if (timeRange === 'today') {
|
||||
if (timeRange === 'custom' && startDate && endDate) {
|
||||
// 自定义日期范围
|
||||
const redisClient = require('../models/redis')
|
||||
const start = new Date(startDate)
|
||||
const end = new Date(endDate)
|
||||
|
||||
// 确保日期范围有效
|
||||
if (start > end) {
|
||||
return res.status(400).json({ error: 'Start date must be before or equal to end date' })
|
||||
}
|
||||
|
||||
// 限制最大范围为365天
|
||||
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
||||
if (daysDiff > 365) {
|
||||
return res.status(400).json({ error: 'Date range cannot exceed 365 days' })
|
||||
}
|
||||
|
||||
// 生成日期范围内每天的搜索模式
|
||||
const currentDate = new Date(start)
|
||||
while (currentDate <= end) {
|
||||
const tzDate = redisClient.getDateInTimezone(currentDate)
|
||||
const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}`
|
||||
searchPatterns.push(`usage:daily:*:${dateStr}`)
|
||||
currentDate.setDate(currentDate.getDate() + 1)
|
||||
}
|
||||
} else if (timeRange === 'today') {
|
||||
// 今日 - 使用时区日期
|
||||
const redisClient = require('../models/redis')
|
||||
const tzDate = redisClient.getDateInTimezone(now)
|
||||
@@ -234,7 +259,7 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
apiKey.usage.total.formattedCost = CostCalculator.formatCost(totalCost)
|
||||
}
|
||||
} else {
|
||||
// 7天或本月:重新计算统计数据
|
||||
// 7天、本月或自定义日期范围:重新计算统计数据
|
||||
const tempUsage = {
|
||||
requests: 0,
|
||||
tokens: 0,
|
||||
@@ -275,12 +300,28 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
const tzDate = redisClient.getDateInTimezone(now)
|
||||
const tzMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`
|
||||
|
||||
const modelKeys =
|
||||
timeRange === 'today'
|
||||
? await client.keys(`usage:${apiKey.id}:model:daily:*:${tzToday}`)
|
||||
: timeRange === '7days'
|
||||
? await client.keys(`usage:${apiKey.id}:model:daily:*:*`)
|
||||
: await client.keys(`usage:${apiKey.id}:model:monthly:*:${tzMonth}`)
|
||||
let modelKeys = []
|
||||
if (timeRange === 'custom' && startDate && endDate) {
|
||||
// 自定义日期范围:获取范围内所有日期的模型统计
|
||||
const start = new Date(startDate)
|
||||
const end = new Date(endDate)
|
||||
const currentDate = new Date(start)
|
||||
|
||||
while (currentDate <= end) {
|
||||
const tzDateForKey = redisClient.getDateInTimezone(currentDate)
|
||||
const dateStr = `${tzDateForKey.getUTCFullYear()}-${String(tzDateForKey.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDateForKey.getUTCDate()).padStart(2, '0')}`
|
||||
const dayKeys = await client.keys(`usage:${apiKey.id}:model:daily:*:${dateStr}`)
|
||||
modelKeys = modelKeys.concat(dayKeys)
|
||||
currentDate.setDate(currentDate.getDate() + 1)
|
||||
}
|
||||
} else {
|
||||
modelKeys =
|
||||
timeRange === 'today'
|
||||
? await client.keys(`usage:${apiKey.id}:model:daily:*:${tzToday}`)
|
||||
: timeRange === '7days'
|
||||
? await client.keys(`usage:${apiKey.id}:model:daily:*:*`)
|
||||
: await client.keys(`usage:${apiKey.id}:model:monthly:*:${tzMonth}`)
|
||||
}
|
||||
|
||||
const modelStatsMap = new Map()
|
||||
|
||||
@@ -296,8 +337,8 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else if (timeRange === 'today') {
|
||||
// today选项已经在查询时过滤了,不需要额外处理
|
||||
} else if (timeRange === 'today' || timeRange === 'custom') {
|
||||
// today和custom选项已经在查询时过滤了,不需要额外处理
|
||||
}
|
||||
|
||||
const modelMatch = key.match(
|
||||
@@ -948,10 +989,8 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
expiresAt,
|
||||
dailyCostLimit,
|
||||
weeklyOpusCostLimit,
|
||||
weeklyGPT5HighCostLimit,
|
||||
tags,
|
||||
ownerId, // 新增:所有者ID字段
|
||||
icon // 新增:图标(base64编码)
|
||||
ownerId // 新增:所有者ID字段
|
||||
} = req.body
|
||||
|
||||
// 只允许更新指定字段
|
||||
@@ -1114,22 +1153,6 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
updates.weeklyOpusCostLimit = costLimit
|
||||
}
|
||||
|
||||
// 处理 GPT-5 High 周费用限制
|
||||
if (
|
||||
weeklyGPT5HighCostLimit !== undefined &&
|
||||
weeklyGPT5HighCostLimit !== null &&
|
||||
weeklyGPT5HighCostLimit !== ''
|
||||
) {
|
||||
const costLimit = Number(weeklyGPT5HighCostLimit)
|
||||
// 明确验证非负数(0 表示禁用,负数无意义)
|
||||
if (isNaN(costLimit) || costLimit < 0) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Weekly GPT-5 High cost limit must be a non-negative number' })
|
||||
}
|
||||
updates.weeklyGPT5HighCostLimit = costLimit
|
||||
}
|
||||
|
||||
// 处理标签
|
||||
if (tags !== undefined) {
|
||||
if (!Array.isArray(tags)) {
|
||||
@@ -1141,19 +1164,6 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
updates.tags = tags
|
||||
}
|
||||
|
||||
// 处理图标
|
||||
if (icon !== undefined) {
|
||||
// icon 可以是空字符串(清除图标)或 base64 编码的字符串
|
||||
if (icon !== '' && typeof icon !== 'string') {
|
||||
return res.status(400).json({ error: 'Icon must be a string' })
|
||||
}
|
||||
// 简单验证 base64 格式(如果不为空)
|
||||
if (icon && !icon.startsWith('data:image/')) {
|
||||
return res.status(400).json({ error: 'Icon must be a valid base64 image' })
|
||||
}
|
||||
updates.icon = icon
|
||||
}
|
||||
|
||||
// 处理活跃/禁用状态状态, 放在过期处理后,以确保后续增加禁用key功能
|
||||
if (isActive !== undefined) {
|
||||
if (typeof isActive !== 'boolean') {
|
||||
@@ -4328,10 +4338,10 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => {
|
||||
return res.status(400).json({ error: 'Start date must be before or equal to end date' })
|
||||
}
|
||||
|
||||
// 限制最大范围为31天
|
||||
// 限制最大范围为365天
|
||||
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
||||
if (daysDiff > 31) {
|
||||
return res.status(400).json({ error: 'Date range cannot exceed 31 days' })
|
||||
if (daysDiff > 365) {
|
||||
return res.status(400).json({ error: 'Date range cannot exceed 365 days' })
|
||||
}
|
||||
|
||||
// 生成日期范围内所有日期的搜索模式
|
||||
@@ -4792,10 +4802,10 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) =
|
||||
return res.status(400).json({ error: 'Start date must be before or equal to end date' })
|
||||
}
|
||||
|
||||
// 限制最大范围为31天
|
||||
// 限制最大范围为365天
|
||||
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
||||
if (daysDiff > 31) {
|
||||
return res.status(400).json({ error: 'Date range cannot exceed 31 days' })
|
||||
if (daysDiff > 365) {
|
||||
return res.status(400).json({ error: 'Date range cannot exceed 365 days' })
|
||||
}
|
||||
|
||||
// 生成日期范围内所有日期的搜索模式
|
||||
|
||||
Reference in New Issue
Block a user