mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
152
scripts/test-total-usage-limit.js
Normal file
152
scripts/test-total-usage-limit.js
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const path = require('path')
|
||||||
|
const Module = require('module')
|
||||||
|
|
||||||
|
const originalResolveFilename = Module._resolveFilename
|
||||||
|
Module._resolveFilename = function resolveConfig(request, parent, isMain, options) {
|
||||||
|
if (request.endsWith('/config/config')) {
|
||||||
|
return path.resolve(__dirname, '../config/config.example.js')
|
||||||
|
}
|
||||||
|
return originalResolveFilename.call(this, request, parent, isMain, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
const redis = require('../src/models/redis')
|
||||||
|
const apiKeyService = require('../src/services/apiKeyService')
|
||||||
|
const { authenticateApiKey } = require('../src/middleware/auth')
|
||||||
|
|
||||||
|
Module._resolveFilename = originalResolveFilename
|
||||||
|
|
||||||
|
function createMockReq(apiKey) {
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
'x-api-key': apiKey,
|
||||||
|
'user-agent': 'total-usage-limit-test'
|
||||||
|
},
|
||||||
|
query: {},
|
||||||
|
body: {},
|
||||||
|
ip: '127.0.0.1',
|
||||||
|
connection: {},
|
||||||
|
originalUrl: '/test-total-usage-limit',
|
||||||
|
once: () => {},
|
||||||
|
on: () => {},
|
||||||
|
get(header) {
|
||||||
|
return this.headers[header.toLowerCase()] || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockRes() {
|
||||||
|
const state = {
|
||||||
|
status: 200,
|
||||||
|
body: null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
once: () => {},
|
||||||
|
on: () => {},
|
||||||
|
status(code) {
|
||||||
|
state.status = code
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
json(payload) {
|
||||||
|
state.body = payload
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
getState() {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAuth(apiKey) {
|
||||||
|
const req = createMockReq(apiKey)
|
||||||
|
const res = createMockRes()
|
||||||
|
let nextCalled = false
|
||||||
|
|
||||||
|
await authenticateApiKey(req, res, () => {
|
||||||
|
nextCalled = true
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = res.getState()
|
||||||
|
if (nextCalled && result.status === 200) {
|
||||||
|
return { status: 200, body: null }
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupKey(keyId) {
|
||||||
|
const client = redis.getClient()
|
||||||
|
if (!client) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await redis.deleteApiKey(keyId)
|
||||||
|
const usageKeys = await client.keys(`usage:*:${keyId}*`)
|
||||||
|
if (usageKeys.length > 0) {
|
||||||
|
await client.del(...usageKeys)
|
||||||
|
}
|
||||||
|
const costKeys = await client.keys(`usage:cost:*:${keyId}*`)
|
||||||
|
if (costKeys.length > 0) {
|
||||||
|
await client.del(...costKeys)
|
||||||
|
}
|
||||||
|
await client.del(`usage:${keyId}`)
|
||||||
|
await client.del(`usage:records:${keyId}`)
|
||||||
|
await client.del(`usage:cost:total:${keyId}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to cleanup test key ${keyId}:`, error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await redis.connect()
|
||||||
|
|
||||||
|
const testName = `TotalUsageLimitTest-${Date.now()}`
|
||||||
|
const totalLimit = 1.0
|
||||||
|
const newKey = await apiKeyService.generateApiKey({
|
||||||
|
name: testName,
|
||||||
|
permissions: 'all',
|
||||||
|
totalUsageLimit: totalLimit
|
||||||
|
})
|
||||||
|
|
||||||
|
const keyId = newKey.id
|
||||||
|
const { apiKey } = newKey
|
||||||
|
|
||||||
|
console.log(`➕ Created test API key ${keyId} with total usage limit $${totalLimit}`)
|
||||||
|
|
||||||
|
let authResult = await runAuth(apiKey)
|
||||||
|
if (authResult.status !== 200) {
|
||||||
|
throw new Error(`Expected success before any usage, got status ${authResult.status}`)
|
||||||
|
}
|
||||||
|
console.log('✅ Authentication succeeds before consuming quota')
|
||||||
|
|
||||||
|
await redis.incrementDailyCost(keyId, 0.6)
|
||||||
|
authResult = await runAuth(apiKey)
|
||||||
|
if (authResult.status !== 200) {
|
||||||
|
throw new Error(`Expected success under quota, got status ${authResult.status}`)
|
||||||
|
}
|
||||||
|
console.log('✅ Authentication succeeds while still under quota ($0.60)')
|
||||||
|
|
||||||
|
await redis.incrementDailyCost(keyId, 0.5)
|
||||||
|
authResult = await runAuth(apiKey)
|
||||||
|
if (authResult.status !== 429) {
|
||||||
|
throw new Error(`Expected 429 after exceeding quota, got status ${authResult.status}`)
|
||||||
|
}
|
||||||
|
console.log('✅ Authentication returns 429 after exceeding total usage limit ($1.10)')
|
||||||
|
|
||||||
|
await cleanupKey(keyId)
|
||||||
|
await redis.disconnect()
|
||||||
|
|
||||||
|
console.log('🎉 Total usage limit test completed successfully')
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(async (error) => {
|
||||||
|
console.error('❌ Total usage limit test failed:', error)
|
||||||
|
try {
|
||||||
|
await redis.disconnect()
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore disconnect errors during cleanup
|
||||||
|
}
|
||||||
|
process.exitCode = 1
|
||||||
|
})
|
||||||
@@ -120,7 +120,9 @@ const authenticateApiKey = async (req, res, next) => {
|
|||||||
// 如果超过限制,立即减少计数
|
// 如果超过限制,立即减少计数
|
||||||
await redis.decrConcurrency(validation.keyData.id)
|
await redis.decrConcurrency(validation.keyData.id)
|
||||||
logger.security(
|
logger.security(
|
||||||
`🚦 Concurrency limit exceeded for key: ${validation.keyData.id} (${validation.keyData.name}), current: ${currentConcurrency - 1}, limit: ${concurrencyLimit}`
|
`🚦 Concurrency limit exceeded for key: ${validation.keyData.id} (${
|
||||||
|
validation.keyData.name
|
||||||
|
}), current: ${currentConcurrency - 1}, limit: ${concurrencyLimit}`
|
||||||
)
|
)
|
||||||
return res.status(429).json({
|
return res.status(429).json({
|
||||||
error: 'Concurrency limit exceeded',
|
error: 'Concurrency limit exceeded',
|
||||||
@@ -275,7 +277,9 @@ const authenticateApiKey = async (req, res, next) => {
|
|||||||
const remainingMinutes = Math.ceil((resetTime - now) / 60000)
|
const remainingMinutes = Math.ceil((resetTime - now) / 60000)
|
||||||
|
|
||||||
logger.security(
|
logger.security(
|
||||||
`💰 Rate limit exceeded (cost) for key: ${validation.keyData.id} (${validation.keyData.name}), cost: $${currentCost.toFixed(2)}/$${rateLimitCost}`
|
`💰 Rate limit exceeded (cost) for key: ${validation.keyData.id} (${
|
||||||
|
validation.keyData.name
|
||||||
|
}), cost: $${currentCost.toFixed(2)}/$${rateLimitCost}`
|
||||||
)
|
)
|
||||||
|
|
||||||
return res.status(429).json({
|
return res.status(429).json({
|
||||||
@@ -308,6 +312,33 @@ const authenticateApiKey = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查总额度限制(基于累计费用)
|
||||||
|
const totalUsageLimit = Number(validation.keyData.totalUsageLimit || 0)
|
||||||
|
if (totalUsageLimit > 0) {
|
||||||
|
const totalCost = Number(validation.keyData.totalCost || 0)
|
||||||
|
|
||||||
|
if (totalCost >= totalUsageLimit) {
|
||||||
|
logger.security(
|
||||||
|
`📉 Total usage limit exceeded for key: ${validation.keyData.id} (${
|
||||||
|
validation.keyData.name
|
||||||
|
}), cost: $${totalCost.toFixed(2)}/$${totalUsageLimit.toFixed(2)}`
|
||||||
|
)
|
||||||
|
|
||||||
|
return res.status(429).json({
|
||||||
|
error: 'Total usage limit exceeded',
|
||||||
|
message: `已达到总额度限制 ($${totalUsageLimit.toFixed(2)})`,
|
||||||
|
currentCost: totalCost,
|
||||||
|
costLimit: totalUsageLimit
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.api(
|
||||||
|
`📉 Total usage for key: ${validation.keyData.id} (${
|
||||||
|
validation.keyData.name
|
||||||
|
}), cost: $${totalCost.toFixed(2)}/$${totalUsageLimit.toFixed(2)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 检查每日费用限制
|
// 检查每日费用限制
|
||||||
const dailyCostLimit = validation.keyData.dailyCostLimit || 0
|
const dailyCostLimit = validation.keyData.dailyCostLimit || 0
|
||||||
if (dailyCostLimit > 0) {
|
if (dailyCostLimit > 0) {
|
||||||
@@ -315,7 +346,9 @@ const authenticateApiKey = async (req, res, next) => {
|
|||||||
|
|
||||||
if (dailyCost >= dailyCostLimit) {
|
if (dailyCost >= dailyCostLimit) {
|
||||||
logger.security(
|
logger.security(
|
||||||
`💰 Daily cost limit exceeded for key: ${validation.keyData.id} (${validation.keyData.name}), cost: $${dailyCost.toFixed(2)}/$${dailyCostLimit}`
|
`💰 Daily cost limit exceeded for key: ${validation.keyData.id} (${
|
||||||
|
validation.keyData.name
|
||||||
|
}), cost: $${dailyCost.toFixed(2)}/$${dailyCostLimit}`
|
||||||
)
|
)
|
||||||
|
|
||||||
return res.status(429).json({
|
return res.status(429).json({
|
||||||
@@ -329,7 +362,36 @@ const authenticateApiKey = async (req, res, next) => {
|
|||||||
|
|
||||||
// 记录当前费用使用情况
|
// 记录当前费用使用情况
|
||||||
logger.api(
|
logger.api(
|
||||||
`💰 Cost usage for key: ${validation.keyData.id} (${validation.keyData.name}), current: $${dailyCost.toFixed(2)}/$${dailyCostLimit}`
|
`💰 Cost usage for key: ${validation.keyData.id} (${
|
||||||
|
validation.keyData.name
|
||||||
|
}), current: $${dailyCost.toFixed(2)}/$${dailyCostLimit}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查总费用限制
|
||||||
|
const totalCostLimit = validation.keyData.totalCostLimit || 0
|
||||||
|
if (totalCostLimit > 0) {
|
||||||
|
const totalCost = validation.keyData.totalCost || 0
|
||||||
|
|
||||||
|
if (totalCost >= totalCostLimit) {
|
||||||
|
logger.security(
|
||||||
|
`💰 Total cost limit exceeded for key: ${validation.keyData.id} (${
|
||||||
|
validation.keyData.name
|
||||||
|
}), cost: $${totalCost.toFixed(2)}/$${totalCostLimit}`
|
||||||
|
)
|
||||||
|
|
||||||
|
return res.status(429).json({
|
||||||
|
error: 'Total cost limit exceeded',
|
||||||
|
message: `已达到总费用限制 ($${totalCostLimit})`,
|
||||||
|
currentCost: totalCost,
|
||||||
|
costLimit: totalCostLimit
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.api(
|
||||||
|
`💰 Total cost usage for key: ${validation.keyData.id} (${
|
||||||
|
validation.keyData.name
|
||||||
|
}), current: $${totalCost.toFixed(2)}/$${totalCostLimit}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +408,9 @@ const authenticateApiKey = async (req, res, next) => {
|
|||||||
|
|
||||||
if (weeklyOpusCost >= weeklyOpusCostLimit) {
|
if (weeklyOpusCost >= weeklyOpusCostLimit) {
|
||||||
logger.security(
|
logger.security(
|
||||||
`💰 Weekly Opus cost limit exceeded for key: ${validation.keyData.id} (${validation.keyData.name}), cost: $${weeklyOpusCost.toFixed(2)}/$${weeklyOpusCostLimit}`
|
`💰 Weekly Opus cost limit exceeded for key: ${validation.keyData.id} (${
|
||||||
|
validation.keyData.name
|
||||||
|
}), cost: $${weeklyOpusCost.toFixed(2)}/$${weeklyOpusCostLimit}`
|
||||||
)
|
)
|
||||||
|
|
||||||
// 计算下周一的重置时间
|
// 计算下周一的重置时间
|
||||||
@@ -368,7 +432,9 @@ const authenticateApiKey = async (req, res, next) => {
|
|||||||
|
|
||||||
// 记录当前 Opus 费用使用情况
|
// 记录当前 Opus 费用使用情况
|
||||||
logger.api(
|
logger.api(
|
||||||
`💰 Opus weekly cost usage for key: ${validation.keyData.id} (${validation.keyData.name}), current: $${weeklyOpusCost.toFixed(2)}/$${weeklyOpusCostLimit}`
|
`💰 Opus weekly cost usage for key: ${validation.keyData.id} (${
|
||||||
|
validation.keyData.name
|
||||||
|
}), current: $${weeklyOpusCost.toFixed(2)}/$${weeklyOpusCostLimit}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,6 +460,9 @@ const authenticateApiKey = async (req, res, next) => {
|
|||||||
allowedClients: validation.keyData.allowedClients,
|
allowedClients: validation.keyData.allowedClients,
|
||||||
dailyCostLimit: validation.keyData.dailyCostLimit,
|
dailyCostLimit: validation.keyData.dailyCostLimit,
|
||||||
dailyCost: validation.keyData.dailyCost,
|
dailyCost: validation.keyData.dailyCost,
|
||||||
|
totalUsageLimit: validation.keyData.totalUsageLimit,
|
||||||
|
totalCostLimit: validation.keyData.totalCostLimit,
|
||||||
|
totalCost: validation.keyData.totalCost,
|
||||||
usage: validation.keyData.usage
|
usage: validation.keyData.usage
|
||||||
}
|
}
|
||||||
req.usage = validation.keyData.usage
|
req.usage = validation.keyData.usage
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ function getDateInTimezone(date = new Date()) {
|
|||||||
function getDateStringInTimezone(date = new Date()) {
|
function getDateStringInTimezone(date = new Date()) {
|
||||||
const tzDate = getDateInTimezone(date)
|
const tzDate = getDateInTimezone(date)
|
||||||
// 使用UTC方法获取偏移后的日期部分
|
// 使用UTC方法获取偏移后的日期部分
|
||||||
return `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}`
|
return `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(
|
||||||
|
tzDate.getUTCDate()
|
||||||
|
).padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取配置时区的小时 (0-23)
|
// 获取配置时区的小时 (0-23)
|
||||||
@@ -219,7 +221,10 @@ class RedisClient {
|
|||||||
const now = new Date()
|
const now = new Date()
|
||||||
const today = getDateStringInTimezone(now)
|
const today = getDateStringInTimezone(now)
|
||||||
const tzDate = getDateInTimezone(now)
|
const tzDate = getDateInTimezone(now)
|
||||||
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`
|
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}`
|
||||||
const currentHour = `${today}:${String(getHourInTimezone(now)).padStart(2, '0')}` // 新增小时级别
|
const currentHour = `${today}:${String(getHourInTimezone(now)).padStart(2, '0')}` // 新增小时级别
|
||||||
|
|
||||||
const daily = `usage:daily:${keyId}:${today}`
|
const daily = `usage:daily:${keyId}:${today}`
|
||||||
@@ -414,7 +419,10 @@ class RedisClient {
|
|||||||
const now = new Date()
|
const now = new Date()
|
||||||
const today = getDateStringInTimezone(now)
|
const today = getDateStringInTimezone(now)
|
||||||
const tzDate = getDateInTimezone(now)
|
const tzDate = getDateInTimezone(now)
|
||||||
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`
|
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}`
|
||||||
const currentHour = `${today}:${String(getHourInTimezone(now)).padStart(2, '0')}`
|
const currentHour = `${today}:${String(getHourInTimezone(now)).padStart(2, '0')}`
|
||||||
|
|
||||||
// 账户级别统计的键
|
// 账户级别统计的键
|
||||||
@@ -551,7 +559,10 @@ class RedisClient {
|
|||||||
const today = getDateStringInTimezone()
|
const today = getDateStringInTimezone()
|
||||||
const dailyKey = `usage:daily:${keyId}:${today}`
|
const dailyKey = `usage:daily:${keyId}:${today}`
|
||||||
const tzDate = getDateInTimezone()
|
const tzDate = getDateInTimezone()
|
||||||
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`
|
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}`
|
||||||
const monthlyKey = `usage:monthly:${keyId}:${currentMonth}`
|
const monthlyKey = `usage:monthly:${keyId}:${currentMonth}`
|
||||||
|
|
||||||
const [total, daily, monthly] = await Promise.all([
|
const [total, daily, monthly] = await Promise.all([
|
||||||
@@ -636,6 +647,48 @@ class RedisClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addUsageRecord(keyId, record, maxRecords = 200) {
|
||||||
|
const listKey = `usage:records:${keyId}`
|
||||||
|
const client = this.getClientSafe()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client
|
||||||
|
.multi()
|
||||||
|
.lpush(listKey, JSON.stringify(record))
|
||||||
|
.ltrim(listKey, 0, Math.max(0, maxRecords - 1))
|
||||||
|
.expire(listKey, 86400 * 90) // 默认保留90天
|
||||||
|
.exec()
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Failed to append usage record for key ${keyId}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUsageRecords(keyId, limit = 50) {
|
||||||
|
const listKey = `usage:records:${keyId}`
|
||||||
|
const client = this.getClient()
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rawRecords = await client.lrange(listKey, 0, Math.max(0, limit - 1))
|
||||||
|
return rawRecords
|
||||||
|
.map((entry) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(entry)
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('⚠️ Failed to parse usage record entry:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Failed to load usage records for key ${keyId}:`, error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 💰 获取当日费用
|
// 💰 获取当日费用
|
||||||
async getDailyCost(keyId) {
|
async getDailyCost(keyId) {
|
||||||
const today = getDateStringInTimezone()
|
const today = getDateStringInTimezone()
|
||||||
@@ -652,7 +705,10 @@ class RedisClient {
|
|||||||
async incrementDailyCost(keyId, amount) {
|
async incrementDailyCost(keyId, amount) {
|
||||||
const today = getDateStringInTimezone()
|
const today = getDateStringInTimezone()
|
||||||
const tzDate = getDateInTimezone()
|
const tzDate = getDateInTimezone()
|
||||||
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`
|
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}`
|
||||||
const currentHour = `${today}:${String(getHourInTimezone(new Date())).padStart(2, '0')}`
|
const currentHour = `${today}:${String(getHourInTimezone(new Date())).padStart(2, '0')}`
|
||||||
|
|
||||||
const dailyKey = `usage:cost:daily:${keyId}:${today}`
|
const dailyKey = `usage:cost:daily:${keyId}:${today}`
|
||||||
@@ -682,7 +738,10 @@ class RedisClient {
|
|||||||
async getCostStats(keyId) {
|
async getCostStats(keyId) {
|
||||||
const today = getDateStringInTimezone()
|
const today = getDateStringInTimezone()
|
||||||
const tzDate = getDateInTimezone()
|
const tzDate = getDateInTimezone()
|
||||||
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`
|
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}`
|
||||||
const currentHour = `${today}:${String(getHourInTimezone(new Date())).padStart(2, '0')}`
|
const currentHour = `${today}:${String(getHourInTimezone(new Date())).padStart(2, '0')}`
|
||||||
|
|
||||||
const [daily, monthly, hourly, total] = await Promise.all([
|
const [daily, monthly, hourly, total] = await Promise.all([
|
||||||
@@ -785,7 +844,10 @@ class RedisClient {
|
|||||||
const today = getDateStringInTimezone()
|
const today = getDateStringInTimezone()
|
||||||
const accountDailyKey = `account_usage:daily:${accountId}:${today}`
|
const accountDailyKey = `account_usage:daily:${accountId}:${today}`
|
||||||
const tzDate = getDateInTimezone()
|
const tzDate = getDateInTimezone()
|
||||||
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`
|
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}`
|
||||||
const accountMonthlyKey = `account_usage:monthly:${accountId}:${currentMonth}`
|
const accountMonthlyKey = `account_usage:monthly:${accountId}:${currentMonth}`
|
||||||
|
|
||||||
const [total, daily, monthly] = await Promise.all([
|
const [total, daily, monthly] = await Promise.all([
|
||||||
@@ -1421,14 +1483,18 @@ class RedisClient {
|
|||||||
if (remainingTTL < renewalThreshold) {
|
if (remainingTTL < renewalThreshold) {
|
||||||
await this.client.expire(key, fullTTL)
|
await this.client.expire(key, fullTTL)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`🔄 Renewed sticky session TTL: ${sessionHash} (was ${Math.round(remainingTTL / 60)}min, renewed to ${ttlHours}h)`
|
`🔄 Renewed sticky session TTL: ${sessionHash} (was ${Math.round(
|
||||||
|
remainingTTL / 60
|
||||||
|
)}min, renewed to ${ttlHours}h)`
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 剩余时间充足,无需续期
|
// 剩余时间充足,无需续期
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`✅ Sticky session TTL sufficient: ${sessionHash} (remaining ${Math.round(remainingTTL / 60)}min)`
|
`✅ Sticky session TTL sufficient: ${sessionHash} (remaining ${Math.round(
|
||||||
|
remainingTTL / 60
|
||||||
|
)}min)`
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -155,7 +155,10 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
const currentDate = new Date(start)
|
const currentDate = new Date(start)
|
||||||
while (currentDate <= end) {
|
while (currentDate <= end) {
|
||||||
const tzDate = redisClient.getDateInTimezone(currentDate)
|
const tzDate = redisClient.getDateInTimezone(currentDate)
|
||||||
const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}`
|
const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}-${String(tzDate.getUTCDate()).padStart(2, '0')}`
|
||||||
searchPatterns.push(`usage:daily:*:${dateStr}`)
|
searchPatterns.push(`usage:daily:*:${dateStr}`)
|
||||||
currentDate.setDate(currentDate.getDate() + 1)
|
currentDate.setDate(currentDate.getDate() + 1)
|
||||||
}
|
}
|
||||||
@@ -163,7 +166,10 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
// 今日 - 使用时区日期
|
// 今日 - 使用时区日期
|
||||||
const redisClient = require('../models/redis')
|
const redisClient = require('../models/redis')
|
||||||
const tzDate = redisClient.getDateInTimezone(now)
|
const tzDate = redisClient.getDateInTimezone(now)
|
||||||
const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}`
|
const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}-${String(tzDate.getUTCDate()).padStart(2, '0')}`
|
||||||
searchPatterns.push(`usage:daily:*:${dateStr}`)
|
searchPatterns.push(`usage:daily:*:${dateStr}`)
|
||||||
} else if (timeRange === '7days') {
|
} else if (timeRange === '7days') {
|
||||||
// 最近7天
|
// 最近7天
|
||||||
@@ -172,14 +178,20 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
const date = new Date(now)
|
const date = new Date(now)
|
||||||
date.setDate(date.getDate() - i)
|
date.setDate(date.getDate() - i)
|
||||||
const tzDate = redisClient.getDateInTimezone(date)
|
const tzDate = redisClient.getDateInTimezone(date)
|
||||||
const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}`
|
const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}-${String(tzDate.getUTCDate()).padStart(2, '0')}`
|
||||||
searchPatterns.push(`usage:daily:*:${dateStr}`)
|
searchPatterns.push(`usage:daily:*:${dateStr}`)
|
||||||
}
|
}
|
||||||
} else if (timeRange === 'monthly') {
|
} else if (timeRange === 'monthly') {
|
||||||
// 本月
|
// 本月
|
||||||
const redisClient = require('../models/redis')
|
const redisClient = require('../models/redis')
|
||||||
const tzDate = redisClient.getDateInTimezone(now)
|
const tzDate = redisClient.getDateInTimezone(now)
|
||||||
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`
|
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}`
|
||||||
searchPatterns.push(`usage:monthly:*:${currentMonth}`)
|
searchPatterns.push(`usage:monthly:*:${currentMonth}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +311,10 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
const redisClient = require('../models/redis')
|
const redisClient = require('../models/redis')
|
||||||
const tzToday = redisClient.getDateStringInTimezone(now)
|
const tzToday = redisClient.getDateStringInTimezone(now)
|
||||||
const tzDate = redisClient.getDateInTimezone(now)
|
const tzDate = redisClient.getDateInTimezone(now)
|
||||||
const tzMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`
|
const tzMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}`
|
||||||
|
|
||||||
let modelKeys = []
|
let modelKeys = []
|
||||||
if (timeRange === 'custom' && startDate && endDate) {
|
if (timeRange === 'custom' && startDate && endDate) {
|
||||||
@@ -310,7 +325,9 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
|
|
||||||
while (currentDate <= end) {
|
while (currentDate <= end) {
|
||||||
const tzDateForKey = redisClient.getDateInTimezone(currentDate)
|
const tzDateForKey = redisClient.getDateInTimezone(currentDate)
|
||||||
const dateStr = `${tzDateForKey.getUTCFullYear()}-${String(tzDateForKey.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDateForKey.getUTCDate()).padStart(2, '0')}`
|
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}`)
|
const dayKeys = await client.keys(`usage:${apiKey.id}:model:daily:*:${dateStr}`)
|
||||||
modelKeys = modelKeys.concat(dayKeys)
|
modelKeys = modelKeys.concat(dayKeys)
|
||||||
currentDate.setDate(currentDate.getDate() + 1)
|
currentDate.setDate(currentDate.getDate() + 1)
|
||||||
@@ -533,6 +550,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
enableClientRestriction,
|
enableClientRestriction,
|
||||||
allowedClients,
|
allowedClients,
|
||||||
dailyCostLimit,
|
dailyCostLimit,
|
||||||
|
totalUsageLimit,
|
||||||
|
totalCostLimit,
|
||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
activationDays, // 新增:激活后有效天数
|
activationDays, // 新增:激活后有效天数
|
||||||
@@ -615,6 +634,31 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'All tags must be non-empty strings' })
|
return res.status(400).json({ error: 'All tags must be non-empty strings' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (totalUsageLimit !== undefined && totalUsageLimit !== null && totalUsageLimit !== '') {
|
||||||
|
const usageLimit = Number(totalUsageLimit)
|
||||||
|
if (Number.isNaN(usageLimit) || usageLimit < 0) {
|
||||||
|
return res.status(400).json({ error: 'Total usage limit must be a non-negative number' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
totalCostLimit !== undefined &&
|
||||||
|
totalCostLimit !== null &&
|
||||||
|
totalCostLimit !== '' &&
|
||||||
|
(Number.isNaN(Number(totalCostLimit)) || Number(totalCostLimit) < 0)
|
||||||
|
) {
|
||||||
|
return res.status(400).json({ error: 'Total cost limit must be a non-negative number' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
totalCostLimit !== undefined &&
|
||||||
|
totalCostLimit !== null &&
|
||||||
|
totalCostLimit !== '' &&
|
||||||
|
(Number.isNaN(Number(totalCostLimit)) || Number(totalCostLimit) < 0)
|
||||||
|
) {
|
||||||
|
return res.status(400).json({ error: 'Total cost limit must be a non-negative number' })
|
||||||
|
}
|
||||||
|
|
||||||
// 验证激活相关字段
|
// 验证激活相关字段
|
||||||
if (expirationMode && !['fixed', 'activation'].includes(expirationMode)) {
|
if (expirationMode && !['fixed', 'activation'].includes(expirationMode)) {
|
||||||
return res
|
return res
|
||||||
@@ -660,6 +704,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
enableClientRestriction,
|
enableClientRestriction,
|
||||||
allowedClients,
|
allowedClients,
|
||||||
dailyCostLimit,
|
dailyCostLimit,
|
||||||
|
totalUsageLimit,
|
||||||
|
totalCostLimit,
|
||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
activationDays,
|
activationDays,
|
||||||
@@ -699,6 +745,8 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
|||||||
enableClientRestriction,
|
enableClientRestriction,
|
||||||
allowedClients,
|
allowedClients,
|
||||||
dailyCostLimit,
|
dailyCostLimit,
|
||||||
|
totalUsageLimit,
|
||||||
|
totalCostLimit,
|
||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
activationDays,
|
activationDays,
|
||||||
@@ -748,6 +796,8 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
|||||||
enableClientRestriction,
|
enableClientRestriction,
|
||||||
allowedClients,
|
allowedClients,
|
||||||
dailyCostLimit,
|
dailyCostLimit,
|
||||||
|
totalUsageLimit,
|
||||||
|
totalCostLimit,
|
||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
activationDays,
|
activationDays,
|
||||||
@@ -865,6 +915,12 @@ router.put('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
|||||||
if (updates.dailyCostLimit !== undefined) {
|
if (updates.dailyCostLimit !== undefined) {
|
||||||
finalUpdates.dailyCostLimit = updates.dailyCostLimit
|
finalUpdates.dailyCostLimit = updates.dailyCostLimit
|
||||||
}
|
}
|
||||||
|
if (updates.totalUsageLimit !== undefined) {
|
||||||
|
finalUpdates.totalUsageLimit = updates.totalUsageLimit
|
||||||
|
}
|
||||||
|
if (updates.totalCostLimit !== undefined) {
|
||||||
|
finalUpdates.totalCostLimit = updates.totalCostLimit
|
||||||
|
}
|
||||||
if (updates.weeklyOpusCostLimit !== undefined) {
|
if (updates.weeklyOpusCostLimit !== undefined) {
|
||||||
finalUpdates.weeklyOpusCostLimit = updates.weeklyOpusCostLimit
|
finalUpdates.weeklyOpusCostLimit = updates.weeklyOpusCostLimit
|
||||||
}
|
}
|
||||||
@@ -993,6 +1049,8 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
allowedClients,
|
allowedClients,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
dailyCostLimit,
|
dailyCostLimit,
|
||||||
|
totalUsageLimit,
|
||||||
|
totalCostLimit,
|
||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
ownerId // 新增:所有者ID字段
|
ownerId // 新增:所有者ID字段
|
||||||
@@ -1142,6 +1200,22 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
updates.dailyCostLimit = costLimit
|
updates.dailyCostLimit = costLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (totalCostLimit !== undefined && totalCostLimit !== null && totalCostLimit !== '') {
|
||||||
|
const costLimit = Number(totalCostLimit)
|
||||||
|
if (isNaN(costLimit) || costLimit < 0) {
|
||||||
|
return res.status(400).json({ error: 'Total cost limit must be a non-negative number' })
|
||||||
|
}
|
||||||
|
updates.totalCostLimit = costLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalUsageLimit !== undefined && totalUsageLimit !== null && totalUsageLimit !== '') {
|
||||||
|
const usageLimit = Number(totalUsageLimit)
|
||||||
|
if (Number.isNaN(usageLimit) || usageLimit < 0) {
|
||||||
|
return res.status(400).json({ error: 'Total usage limit must be a non-negative number' })
|
||||||
|
}
|
||||||
|
updates.totalUsageLimit = usageLimit
|
||||||
|
}
|
||||||
|
|
||||||
// 处理 Opus 周费用限制
|
// 处理 Opus 周费用限制
|
||||||
if (
|
if (
|
||||||
weeklyOpusCostLimit !== undefined &&
|
weeklyOpusCostLimit !== undefined &&
|
||||||
@@ -1252,7 +1326,9 @@ router.patch('/api-keys/:keyId/expiration', authenticateAdmin, async (req, res)
|
|||||||
updates.expiresAt = newExpiresAt.toISOString()
|
updates.expiresAt = newExpiresAt.toISOString()
|
||||||
|
|
||||||
logger.success(
|
logger.success(
|
||||||
`🔓 API key manually activated by admin: ${keyId} (${keyData.name}), expires at ${newExpiresAt.toISOString()}`
|
`🔓 API key manually activated by admin: ${keyId} (${
|
||||||
|
keyData.name
|
||||||
|
}), expires at ${newExpiresAt.toISOString()}`
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -1317,7 +1393,11 @@ router.delete('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
|||||||
// 参数验证
|
// 参数验证
|
||||||
if (!keyIds || !Array.isArray(keyIds) || keyIds.length === 0) {
|
if (!keyIds || !Array.isArray(keyIds) || keyIds.length === 0) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`🚨 Invalid keyIds: ${JSON.stringify({ keyIds, type: typeof keyIds, isArray: Array.isArray(keyIds) })}`
|
`🚨 Invalid keyIds: ${JSON.stringify({
|
||||||
|
keyIds,
|
||||||
|
type: typeof keyIds,
|
||||||
|
isArray: Array.isArray(keyIds)
|
||||||
|
})}`
|
||||||
)
|
)
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: 'Invalid request',
|
error: 'Invalid request',
|
||||||
@@ -2338,7 +2418,9 @@ router.put(
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.success(
|
logger.success(
|
||||||
`🔄 Admin toggled Claude account schedulable status: ${accountId} -> ${newSchedulable ? 'schedulable' : 'not schedulable'}`
|
`🔄 Admin toggled Claude account schedulable status: ${accountId} -> ${
|
||||||
|
newSchedulable ? 'schedulable' : 'not schedulable'
|
||||||
|
}`
|
||||||
)
|
)
|
||||||
return res.json({ success: true, schedulable: newSchedulable })
|
return res.json({ success: true, schedulable: newSchedulable })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -2626,7 +2708,9 @@ router.put('/claude-console-accounts/:accountId/toggle', authenticateAdmin, asyn
|
|||||||
await claudeConsoleAccountService.updateAccount(accountId, { isActive: newStatus })
|
await claudeConsoleAccountService.updateAccount(accountId, { isActive: newStatus })
|
||||||
|
|
||||||
logger.success(
|
logger.success(
|
||||||
`🔄 Admin toggled Claude Console account status: ${accountId} -> ${newStatus ? 'active' : 'inactive'}`
|
`🔄 Admin toggled Claude Console account status: ${accountId} -> ${
|
||||||
|
newStatus ? 'active' : 'inactive'
|
||||||
|
}`
|
||||||
)
|
)
|
||||||
return res.json({ success: true, isActive: newStatus })
|
return res.json({ success: true, isActive: newStatus })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -2667,7 +2751,9 @@ router.put(
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.success(
|
logger.success(
|
||||||
`🔄 Admin toggled Claude Console account schedulable status: ${accountId} -> ${newSchedulable ? 'schedulable' : 'not schedulable'}`
|
`🔄 Admin toggled Claude Console account schedulable status: ${accountId} -> ${
|
||||||
|
newSchedulable ? 'schedulable' : 'not schedulable'
|
||||||
|
}`
|
||||||
)
|
)
|
||||||
return res.json({ success: true, schedulable: newSchedulable })
|
return res.json({ success: true, schedulable: newSchedulable })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -3052,7 +3138,9 @@ router.put('/ccr-accounts/:accountId/toggle-schedulable', authenticateAdmin, asy
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.success(
|
logger.success(
|
||||||
`🔄 Admin toggled CCR account schedulable status: ${accountId} -> ${newSchedulable ? 'schedulable' : 'not schedulable'}`
|
`🔄 Admin toggled CCR account schedulable status: ${accountId} -> ${
|
||||||
|
newSchedulable ? 'schedulable' : 'not schedulable'
|
||||||
|
}`
|
||||||
)
|
)
|
||||||
return res.json({ success: true, schedulable: newSchedulable })
|
return res.json({ success: true, schedulable: newSchedulable })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -3375,7 +3463,9 @@ router.put('/bedrock-accounts/:accountId/toggle', authenticateAdmin, async (req,
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.success(
|
logger.success(
|
||||||
`🔄 Admin toggled Bedrock account status: ${accountId} -> ${newStatus ? 'active' : 'inactive'}`
|
`🔄 Admin toggled Bedrock account status: ${accountId} -> ${
|
||||||
|
newStatus ? 'active' : 'inactive'
|
||||||
|
}`
|
||||||
)
|
)
|
||||||
return res.json({ success: true, isActive: newStatus })
|
return res.json({ success: true, isActive: newStatus })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -3424,7 +3514,9 @@ router.put(
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.success(
|
logger.success(
|
||||||
`🔄 Admin toggled Bedrock account schedulable status: ${accountId} -> ${newSchedulable ? 'schedulable' : 'not schedulable'}`
|
`🔄 Admin toggled Bedrock account schedulable status: ${accountId} -> ${
|
||||||
|
newSchedulable ? 'schedulable' : 'not schedulable'
|
||||||
|
}`
|
||||||
)
|
)
|
||||||
return res.json({ success: true, schedulable: newSchedulable })
|
return res.json({ success: true, schedulable: newSchedulable })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -3847,7 +3939,9 @@ router.put(
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.success(
|
logger.success(
|
||||||
`🔄 Admin toggled Gemini account schedulable status: ${accountId} -> ${actualSchedulable ? 'schedulable' : 'not schedulable'}`
|
`🔄 Admin toggled Gemini account schedulable status: ${accountId} -> ${
|
||||||
|
actualSchedulable ? 'schedulable' : 'not schedulable'
|
||||||
|
}`
|
||||||
)
|
)
|
||||||
|
|
||||||
// 返回实际的数据库值,确保前端状态与后端一致
|
// 返回实际的数据库值,确保前端状态与后端一致
|
||||||
@@ -4322,7 +4416,10 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => {
|
|||||||
const { period = 'daily', startDate, endDate } = req.query // daily, monthly, 支持自定义时间范围
|
const { period = 'daily', startDate, endDate } = req.query // daily, monthly, 支持自定义时间范围
|
||||||
const today = redis.getDateStringInTimezone()
|
const today = redis.getDateStringInTimezone()
|
||||||
const tzDate = redis.getDateInTimezone()
|
const tzDate = redis.getDateInTimezone()
|
||||||
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`
|
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}`
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`📊 Getting global model stats, period: ${period}, startDate: ${startDate}, endDate: ${endDate}, today: ${today}, currentMonth: ${currentMonth}`
|
`📊 Getting global model stats, period: ${period}, startDate: ${startDate}, endDate: ${endDate}, today: ${today}, currentMonth: ${currentMonth}`
|
||||||
@@ -4793,7 +4890,10 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) =
|
|||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
const today = redis.getDateStringInTimezone()
|
const today = redis.getDateStringInTimezone()
|
||||||
const tzDate = redis.getDateInTimezone()
|
const tzDate = redis.getDateInTimezone()
|
||||||
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`
|
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}`
|
||||||
|
|
||||||
let searchPatterns = []
|
let searchPatterns = []
|
||||||
|
|
||||||
@@ -5324,7 +5424,10 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
|||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
const today = redis.getDateStringInTimezone()
|
const today = redis.getDateStringInTimezone()
|
||||||
const tzDate = redis.getDateInTimezone()
|
const tzDate = redis.getDateInTimezone()
|
||||||
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`
|
const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}`
|
||||||
|
|
||||||
let pattern
|
let pattern
|
||||||
if (period === 'today') {
|
if (period === 'today') {
|
||||||
@@ -5340,7 +5443,9 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
|||||||
const date = new Date()
|
const date = new Date()
|
||||||
date.setDate(date.getDate() - i)
|
date.setDate(date.getDate() - i)
|
||||||
const currentTzDate = redis.getDateInTimezone(date)
|
const currentTzDate = redis.getDateInTimezone(date)
|
||||||
const dateStr = `${currentTzDate.getUTCFullYear()}-${String(currentTzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(currentTzDate.getUTCDate()).padStart(2, '0')}`
|
const dateStr = `${currentTzDate.getUTCFullYear()}-${String(
|
||||||
|
currentTzDate.getUTCMonth() + 1
|
||||||
|
).padStart(2, '0')}-${String(currentTzDate.getUTCDate()).padStart(2, '0')}`
|
||||||
const dayPattern = `usage:model:daily:*:${dateStr}`
|
const dayPattern = `usage:model:daily:*:${dateStr}`
|
||||||
|
|
||||||
const dayKeys = await client.keys(dayPattern)
|
const dayKeys = await client.keys(dayPattern)
|
||||||
@@ -5393,7 +5498,9 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
|||||||
totalCosts.totalCost += costResult.costs.total
|
totalCosts.totalCost += costResult.costs.total
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`💰 Model ${model} (7days): ${usage.inputTokens + usage.outputTokens + usage.cacheCreateTokens + usage.cacheReadTokens} tokens, cost: ${costResult.formatted.total}`
|
`💰 Model ${model} (7days): ${
|
||||||
|
usage.inputTokens + usage.outputTokens + usage.cacheCreateTokens + usage.cacheReadTokens
|
||||||
|
} tokens, cost: ${costResult.formatted.total}`
|
||||||
)
|
)
|
||||||
|
|
||||||
// 记录模型费用
|
// 记录模型费用
|
||||||
@@ -5481,7 +5588,12 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
|||||||
totalCosts.totalCost += costResult.costs.total
|
totalCosts.totalCost += costResult.costs.total
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`💰 Model ${model}: ${usage.inputTokens + usage.outputTokens + usage.cacheCreateTokens + usage.cacheReadTokens} tokens, cost: ${costResult.formatted.total}`
|
`💰 Model ${model}: ${
|
||||||
|
usage.inputTokens +
|
||||||
|
usage.outputTokens +
|
||||||
|
usage.cacheCreateTokens +
|
||||||
|
usage.cacheReadTokens
|
||||||
|
} tokens, cost: ${costResult.formatted.total}`
|
||||||
)
|
)
|
||||||
|
|
||||||
// 记录模型费用
|
// 记录模型费用
|
||||||
@@ -6844,7 +6956,9 @@ router.post('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
|
|
||||||
// 测试连接
|
// 测试连接
|
||||||
try {
|
try {
|
||||||
const testUrl = `${azureEndpoint}/openai/deployments/${deploymentName}?api-version=${apiVersion || '2024-02-01'}`
|
const testUrl = `${azureEndpoint}/openai/deployments/${deploymentName}?api-version=${
|
||||||
|
apiVersion || '2024-02-01'
|
||||||
|
}`
|
||||||
await axios.get(testUrl, {
|
await axios.get(testUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
'api-key': apiKey
|
'api-key': apiKey
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ router.post('/api/user-stats', async (req, res) => {
|
|||||||
|
|
||||||
// 获取当日费用统计
|
// 获取当日费用统计
|
||||||
const dailyCost = await redis.getDailyCost(keyId)
|
const dailyCost = await redis.getDailyCost(keyId)
|
||||||
|
const costStats = await redis.getCostStats(keyId)
|
||||||
|
|
||||||
// 处理数据格式,与 validateApiKey 返回的格式保持一致
|
// 处理数据格式,与 validateApiKey 返回的格式保持一致
|
||||||
// 解析限制模型数据
|
// 解析限制模型数据
|
||||||
@@ -140,7 +141,10 @@ router.post('/api/user-stats', async (req, res) => {
|
|||||||
rateLimitWindow: parseInt(keyData.rateLimitWindow) || 0,
|
rateLimitWindow: parseInt(keyData.rateLimitWindow) || 0,
|
||||||
rateLimitRequests: parseInt(keyData.rateLimitRequests) || 0,
|
rateLimitRequests: parseInt(keyData.rateLimitRequests) || 0,
|
||||||
dailyCostLimit: parseFloat(keyData.dailyCostLimit) || 0,
|
dailyCostLimit: parseFloat(keyData.dailyCostLimit) || 0,
|
||||||
|
totalUsageLimit: parseFloat(keyData.totalUsageLimit) || 0,
|
||||||
|
totalCostLimit: parseFloat(keyData.totalCostLimit) || 0,
|
||||||
dailyCost: dailyCost || 0,
|
dailyCost: dailyCost || 0,
|
||||||
|
totalCost: costStats.total || 0,
|
||||||
enableModelRestriction: keyData.enableModelRestriction === 'true',
|
enableModelRestriction: keyData.enableModelRestriction === 'true',
|
||||||
restrictedModels,
|
restrictedModels,
|
||||||
enableClientRestriction: keyData.enableClientRestriction === 'true',
|
enableClientRestriction: keyData.enableClientRestriction === 'true',
|
||||||
@@ -372,11 +376,14 @@ router.post('/api/user-stats', async (req, res) => {
|
|||||||
rateLimitRequests: fullKeyData.rateLimitRequests || 0,
|
rateLimitRequests: fullKeyData.rateLimitRequests || 0,
|
||||||
rateLimitCost: parseFloat(fullKeyData.rateLimitCost) || 0, // 新增:费用限制
|
rateLimitCost: parseFloat(fullKeyData.rateLimitCost) || 0, // 新增:费用限制
|
||||||
dailyCostLimit: fullKeyData.dailyCostLimit || 0,
|
dailyCostLimit: fullKeyData.dailyCostLimit || 0,
|
||||||
|
totalUsageLimit: fullKeyData.totalUsageLimit || 0,
|
||||||
|
totalCostLimit: fullKeyData.totalCostLimit || 0,
|
||||||
// 当前使用量
|
// 当前使用量
|
||||||
currentWindowRequests,
|
currentWindowRequests,
|
||||||
currentWindowTokens,
|
currentWindowTokens,
|
||||||
currentWindowCost, // 新增:当前窗口费用
|
currentWindowCost, // 新增:当前窗口费用
|
||||||
currentDailyCost,
|
currentDailyCost,
|
||||||
|
currentTotalCost: totalCost,
|
||||||
// 时间窗口信息
|
// 时间窗口信息
|
||||||
windowStartTime,
|
windowStartTime,
|
||||||
windowEndTime,
|
windowEndTime,
|
||||||
|
|||||||
@@ -258,6 +258,9 @@ router.get('/api-keys', authenticateUser, async (req, res) => {
|
|||||||
usage: flatUsage,
|
usage: flatUsage,
|
||||||
dailyCost: key.dailyCost,
|
dailyCost: key.dailyCost,
|
||||||
dailyCostLimit: key.dailyCostLimit,
|
dailyCostLimit: key.dailyCostLimit,
|
||||||
|
totalUsageLimit: key.totalUsageLimit,
|
||||||
|
totalCost: key.totalCost,
|
||||||
|
totalCostLimit: key.totalCostLimit,
|
||||||
// 不返回实际的key值,只返回前缀和后几位
|
// 不返回实际的key值,只返回前缀和后几位
|
||||||
keyPreview: key.key
|
keyPreview: key.key
|
||||||
? `${key.key.substring(0, 8)}...${key.key.substring(key.key.length - 4)}`
|
? `${key.key.substring(0, 8)}...${key.key.substring(key.key.length - 4)}`
|
||||||
@@ -287,7 +290,15 @@ router.get('/api-keys', authenticateUser, async (req, res) => {
|
|||||||
// 🔑 创建新的API Key
|
// 🔑 创建新的API Key
|
||||||
router.post('/api-keys', authenticateUser, async (req, res) => {
|
router.post('/api-keys', authenticateUser, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { name, description, tokenLimit, expiresAt, dailyCostLimit } = req.body
|
const {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
tokenLimit,
|
||||||
|
expiresAt,
|
||||||
|
dailyCostLimit,
|
||||||
|
totalUsageLimit,
|
||||||
|
totalCostLimit
|
||||||
|
} = req.body
|
||||||
|
|
||||||
if (!name || !name.trim()) {
|
if (!name || !name.trim()) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -296,6 +307,28 @@ router.post('/api-keys', authenticateUser, async (req, res) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
totalCostLimit !== undefined &&
|
||||||
|
totalCostLimit !== null &&
|
||||||
|
totalCostLimit !== '' &&
|
||||||
|
(Number.isNaN(Number(totalCostLimit)) || Number(totalCostLimit) < 0)
|
||||||
|
) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid total cost limit',
|
||||||
|
message: 'Total cost limit must be a non-negative number'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalUsageLimit !== undefined && totalUsageLimit !== null && totalUsageLimit !== '') {
|
||||||
|
const usageLimit = Number(totalUsageLimit)
|
||||||
|
if (Number.isNaN(usageLimit) || usageLimit < 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid total usage limit',
|
||||||
|
message: 'Total usage limit must be a non-negative number'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 检查用户API Key数量限制
|
// 检查用户API Key数量限制
|
||||||
const userApiKeys = await apiKeyService.getUserApiKeys(req.user.id)
|
const userApiKeys = await apiKeyService.getUserApiKeys(req.user.id)
|
||||||
if (userApiKeys.length >= config.userManagement.maxApiKeysPerUser) {
|
if (userApiKeys.length >= config.userManagement.maxApiKeysPerUser) {
|
||||||
@@ -314,6 +347,8 @@ router.post('/api-keys', authenticateUser, async (req, res) => {
|
|||||||
tokenLimit: tokenLimit || null,
|
tokenLimit: tokenLimit || null,
|
||||||
expiresAt: expiresAt || null,
|
expiresAt: expiresAt || null,
|
||||||
dailyCostLimit: dailyCostLimit || null,
|
dailyCostLimit: dailyCostLimit || null,
|
||||||
|
totalUsageLimit: totalUsageLimit || null,
|
||||||
|
totalCostLimit: totalCostLimit || null,
|
||||||
createdBy: 'user',
|
createdBy: 'user',
|
||||||
// 设置服务权限为全部服务,确保前端显示“服务权限”为“全部服务”且具备完整访问权限
|
// 设置服务权限为全部服务,确保前端显示“服务权限”为“全部服务”且具备完整访问权限
|
||||||
permissions: 'all'
|
permissions: 'all'
|
||||||
@@ -337,6 +372,8 @@ router.post('/api-keys', authenticateUser, async (req, res) => {
|
|||||||
tokenLimit: newApiKey.tokenLimit,
|
tokenLimit: newApiKey.tokenLimit,
|
||||||
expiresAt: newApiKey.expiresAt,
|
expiresAt: newApiKey.expiresAt,
|
||||||
dailyCostLimit: newApiKey.dailyCostLimit,
|
dailyCostLimit: newApiKey.dailyCostLimit,
|
||||||
|
totalUsageLimit: newApiKey.totalUsageLimit,
|
||||||
|
totalCostLimit: newApiKey.totalCostLimit,
|
||||||
createdAt: newApiKey.createdAt
|
createdAt: newApiKey.createdAt
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ class ApiKeyService {
|
|||||||
enableClientRestriction = false,
|
enableClientRestriction = false,
|
||||||
allowedClients = [],
|
allowedClients = [],
|
||||||
dailyCostLimit = 0,
|
dailyCostLimit = 0,
|
||||||
|
totalUsageLimit = 0,
|
||||||
|
totalCostLimit = 0,
|
||||||
weeklyOpusCostLimit = 0,
|
weeklyOpusCostLimit = 0,
|
||||||
tags = [],
|
tags = [],
|
||||||
activationDays = 0, // 新增:激活后有效天数(0表示不使用此功能)
|
activationDays = 0, // 新增:激活后有效天数(0表示不使用此功能)
|
||||||
@@ -68,6 +70,8 @@ class ApiKeyService {
|
|||||||
enableClientRestriction: String(enableClientRestriction || false),
|
enableClientRestriction: String(enableClientRestriction || false),
|
||||||
allowedClients: JSON.stringify(allowedClients || []),
|
allowedClients: JSON.stringify(allowedClients || []),
|
||||||
dailyCostLimit: String(dailyCostLimit || 0),
|
dailyCostLimit: String(dailyCostLimit || 0),
|
||||||
|
totalUsageLimit: String(totalUsageLimit || 0),
|
||||||
|
totalCostLimit: String(totalCostLimit || 0),
|
||||||
weeklyOpusCostLimit: String(weeklyOpusCostLimit || 0),
|
weeklyOpusCostLimit: String(weeklyOpusCostLimit || 0),
|
||||||
tags: JSON.stringify(tags || []),
|
tags: JSON.stringify(tags || []),
|
||||||
activationDays: String(activationDays || 0), // 新增:激活后有效天数
|
activationDays: String(activationDays || 0), // 新增:激活后有效天数
|
||||||
@@ -111,6 +115,8 @@ class ApiKeyService {
|
|||||||
enableClientRestriction: keyData.enableClientRestriction === 'true',
|
enableClientRestriction: keyData.enableClientRestriction === 'true',
|
||||||
allowedClients: JSON.parse(keyData.allowedClients || '[]'),
|
allowedClients: JSON.parse(keyData.allowedClients || '[]'),
|
||||||
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
|
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
|
||||||
|
totalUsageLimit: parseFloat(keyData.totalUsageLimit || 0),
|
||||||
|
totalCostLimit: parseFloat(keyData.totalCostLimit || 0),
|
||||||
weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0),
|
weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0),
|
||||||
tags: JSON.parse(keyData.tags || '[]'),
|
tags: JSON.parse(keyData.tags || '[]'),
|
||||||
activationDays: parseInt(keyData.activationDays || 0),
|
activationDays: parseInt(keyData.activationDays || 0),
|
||||||
@@ -162,7 +168,9 @@ class ApiKeyService {
|
|||||||
await redis.setApiKey(keyData.id, keyData)
|
await redis.setApiKey(keyData.id, keyData)
|
||||||
|
|
||||||
logger.success(
|
logger.success(
|
||||||
`🔓 API key activated: ${keyData.id} (${keyData.name}), will expire in ${activationDays} days at ${expiresAt.toISOString()}`
|
`🔓 API key activated: ${keyData.id} (${
|
||||||
|
keyData.name
|
||||||
|
}), will expire in ${activationDays} days at ${expiresAt.toISOString()}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,8 +196,12 @@ class ApiKeyService {
|
|||||||
// 获取使用统计(供返回数据使用)
|
// 获取使用统计(供返回数据使用)
|
||||||
const usage = await redis.getUsageStats(keyData.id)
|
const usage = await redis.getUsageStats(keyData.id)
|
||||||
|
|
||||||
// 获取当日费用统计
|
// 获取费用统计
|
||||||
const dailyCost = await redis.getDailyCost(keyData.id)
|
const [dailyCost, costStats] = await Promise.all([
|
||||||
|
redis.getDailyCost(keyData.id),
|
||||||
|
redis.getCostStats(keyData.id)
|
||||||
|
])
|
||||||
|
const totalCost = costStats?.total || 0
|
||||||
|
|
||||||
// 更新最后使用时间(优化:只在实际API调用时更新,而不是验证时)
|
// 更新最后使用时间(优化:只在实际API调用时更新,而不是验证时)
|
||||||
// 注意:lastUsedAt的更新已移至recordUsage方法中
|
// 注意:lastUsedAt的更新已移至recordUsage方法中
|
||||||
@@ -245,8 +257,11 @@ class ApiKeyService {
|
|||||||
enableClientRestriction: keyData.enableClientRestriction === 'true',
|
enableClientRestriction: keyData.enableClientRestriction === 'true',
|
||||||
allowedClients,
|
allowedClients,
|
||||||
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
|
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
|
||||||
|
totalUsageLimit: parseFloat(keyData.totalUsageLimit || 0),
|
||||||
|
totalCostLimit: parseFloat(keyData.totalCostLimit || 0),
|
||||||
weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0),
|
weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0),
|
||||||
dailyCost: dailyCost || 0,
|
dailyCost: dailyCost || 0,
|
||||||
|
totalCost,
|
||||||
weeklyOpusCost: (await redis.getWeeklyOpusCost(keyData.id)) || 0,
|
weeklyOpusCost: (await redis.getWeeklyOpusCost(keyData.id)) || 0,
|
||||||
tags,
|
tags,
|
||||||
usage
|
usage
|
||||||
@@ -306,7 +321,10 @@ class ApiKeyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取当日费用
|
// 获取当日费用
|
||||||
const dailyCost = (await redis.getDailyCost(keyData.id)) || 0
|
const [dailyCost, costStats] = await Promise.all([
|
||||||
|
redis.getDailyCost(keyData.id),
|
||||||
|
redis.getCostStats(keyData.id)
|
||||||
|
])
|
||||||
|
|
||||||
// 获取使用统计
|
// 获取使用统计
|
||||||
const usage = await redis.getUsageStats(keyData.id)
|
const usage = await redis.getUsageStats(keyData.id)
|
||||||
@@ -365,8 +383,11 @@ class ApiKeyService {
|
|||||||
enableClientRestriction: keyData.enableClientRestriction === 'true',
|
enableClientRestriction: keyData.enableClientRestriction === 'true',
|
||||||
allowedClients,
|
allowedClients,
|
||||||
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
|
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
|
||||||
|
totalUsageLimit: parseFloat(keyData.totalUsageLimit || 0),
|
||||||
|
totalCostLimit: parseFloat(keyData.totalCostLimit || 0),
|
||||||
weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0),
|
weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0),
|
||||||
dailyCost: dailyCost || 0,
|
dailyCost: dailyCost || 0,
|
||||||
|
totalCost: costStats?.total || 0,
|
||||||
weeklyOpusCost: (await redis.getWeeklyOpusCost(keyData.id)) || 0,
|
weeklyOpusCost: (await redis.getWeeklyOpusCost(keyData.id)) || 0,
|
||||||
tags,
|
tags,
|
||||||
usage
|
usage
|
||||||
@@ -405,12 +426,17 @@ class ApiKeyService {
|
|||||||
key.rateLimitWindow = parseInt(key.rateLimitWindow || 0)
|
key.rateLimitWindow = parseInt(key.rateLimitWindow || 0)
|
||||||
key.rateLimitRequests = parseInt(key.rateLimitRequests || 0)
|
key.rateLimitRequests = parseInt(key.rateLimitRequests || 0)
|
||||||
key.rateLimitCost = parseFloat(key.rateLimitCost || 0) // 新增:速率限制费用字段
|
key.rateLimitCost = parseFloat(key.rateLimitCost || 0) // 新增:速率限制费用字段
|
||||||
|
key.totalUsageLimit = parseFloat(key.totalUsageLimit || 0)
|
||||||
|
if (Number.isNaN(key.totalUsageLimit)) {
|
||||||
|
key.totalUsageLimit = 0
|
||||||
|
}
|
||||||
key.currentConcurrency = await redis.getConcurrency(key.id)
|
key.currentConcurrency = await redis.getConcurrency(key.id)
|
||||||
key.isActive = key.isActive === 'true'
|
key.isActive = key.isActive === 'true'
|
||||||
key.enableModelRestriction = key.enableModelRestriction === 'true'
|
key.enableModelRestriction = key.enableModelRestriction === 'true'
|
||||||
key.enableClientRestriction = key.enableClientRestriction === 'true'
|
key.enableClientRestriction = key.enableClientRestriction === 'true'
|
||||||
key.permissions = key.permissions || 'all' // 兼容旧数据
|
key.permissions = key.permissions || 'all' // 兼容旧数据
|
||||||
key.dailyCostLimit = parseFloat(key.dailyCostLimit || 0)
|
key.dailyCostLimit = parseFloat(key.dailyCostLimit || 0)
|
||||||
|
key.totalCostLimit = parseFloat(key.totalCostLimit || 0)
|
||||||
key.weeklyOpusCostLimit = parseFloat(key.weeklyOpusCostLimit || 0)
|
key.weeklyOpusCostLimit = parseFloat(key.weeklyOpusCostLimit || 0)
|
||||||
key.dailyCost = (await redis.getDailyCost(key.id)) || 0
|
key.dailyCost = (await redis.getDailyCost(key.id)) || 0
|
||||||
key.weeklyOpusCost = (await redis.getWeeklyOpusCost(key.id)) || 0
|
key.weeklyOpusCost = (await redis.getWeeklyOpusCost(key.id)) || 0
|
||||||
@@ -532,6 +558,8 @@ class ApiKeyService {
|
|||||||
'enableClientRestriction',
|
'enableClientRestriction',
|
||||||
'allowedClients',
|
'allowedClients',
|
||||||
'dailyCostLimit',
|
'dailyCostLimit',
|
||||||
|
'totalUsageLimit',
|
||||||
|
'totalCostLimit',
|
||||||
'weeklyOpusCostLimit',
|
'weeklyOpusCostLimit',
|
||||||
'tags',
|
'tags',
|
||||||
'userId', // 新增:用户ID(所有者变更)
|
'userId', // 新增:用户ID(所有者变更)
|
||||||
@@ -827,6 +855,21 @@ class ApiKeyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 记录单次请求的使用详情
|
||||||
|
const usageCost = costInfo && costInfo.costs ? costInfo.costs.total || 0 : 0
|
||||||
|
await redis.addUsageRecord(keyId, {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
model,
|
||||||
|
accountId: accountId || null,
|
||||||
|
inputTokens,
|
||||||
|
outputTokens,
|
||||||
|
cacheCreateTokens,
|
||||||
|
cacheReadTokens,
|
||||||
|
totalTokens,
|
||||||
|
cost: Number(usageCost.toFixed(6)),
|
||||||
|
costBreakdown: costInfo && costInfo.costs ? costInfo.costs : undefined
|
||||||
|
})
|
||||||
|
|
||||||
const logParts = [`Model: ${model}`, `Input: ${inputTokens}`, `Output: ${outputTokens}`]
|
const logParts = [`Model: ${model}`, `Input: ${inputTokens}`, `Output: ${outputTokens}`]
|
||||||
if (cacheCreateTokens > 0) {
|
if (cacheCreateTokens > 0) {
|
||||||
logParts.push(`Cache Create: ${cacheCreateTokens}`)
|
logParts.push(`Cache Create: ${cacheCreateTokens}`)
|
||||||
@@ -862,7 +905,9 @@ class ApiKeyService {
|
|||||||
// 记录 Opus 周费用
|
// 记录 Opus 周费用
|
||||||
await redis.incrementWeeklyOpusCost(keyId, cost)
|
await redis.incrementWeeklyOpusCost(keyId, cost)
|
||||||
logger.database(
|
logger.database(
|
||||||
`💰 Recorded Opus weekly cost for ${keyId}: $${cost.toFixed(6)}, model: ${model}, account type: ${accountType}`
|
`💰 Recorded Opus weekly cost for ${keyId}: $${cost.toFixed(
|
||||||
|
6
|
||||||
|
)}, model: ${model}, account type: ${accountType}`
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to record Opus cost:', error)
|
logger.error('❌ Failed to record Opus cost:', error)
|
||||||
@@ -937,7 +982,9 @@ class ApiKeyService {
|
|||||||
// 记录详细的缓存费用(如果有)
|
// 记录详细的缓存费用(如果有)
|
||||||
if (costInfo.ephemeral5mCost > 0 || costInfo.ephemeral1hCost > 0) {
|
if (costInfo.ephemeral5mCost > 0 || costInfo.ephemeral1hCost > 0) {
|
||||||
logger.database(
|
logger.database(
|
||||||
`💰 Cache costs - 5m: $${costInfo.ephemeral5mCost.toFixed(6)}, 1h: $${costInfo.ephemeral1hCost.toFixed(6)}`
|
`💰 Cache costs - 5m: $${costInfo.ephemeral5mCost.toFixed(
|
||||||
|
6
|
||||||
|
)}, 1h: $${costInfo.ephemeral1hCost.toFixed(6)}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -973,6 +1020,32 @@ class ApiKeyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const usageRecord = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
model,
|
||||||
|
accountId: accountId || null,
|
||||||
|
accountType: accountType || null,
|
||||||
|
inputTokens,
|
||||||
|
outputTokens,
|
||||||
|
cacheCreateTokens,
|
||||||
|
cacheReadTokens,
|
||||||
|
ephemeral5mTokens,
|
||||||
|
ephemeral1hTokens,
|
||||||
|
totalTokens,
|
||||||
|
cost: Number((costInfo.totalCost || 0).toFixed(6)),
|
||||||
|
costBreakdown: {
|
||||||
|
input: costInfo.inputCost || 0,
|
||||||
|
output: costInfo.outputCost || 0,
|
||||||
|
cacheCreate: costInfo.cacheCreateCost || 0,
|
||||||
|
cacheRead: costInfo.cacheReadCost || 0,
|
||||||
|
ephemeral5m: costInfo.ephemeral5mCost || 0,
|
||||||
|
ephemeral1h: costInfo.ephemeral1hCost || 0
|
||||||
|
},
|
||||||
|
isLongContext: costInfo.isLongContextRequest || false
|
||||||
|
}
|
||||||
|
|
||||||
|
await redis.addUsageRecord(keyId, usageRecord)
|
||||||
|
|
||||||
const logParts = [`Model: ${model}`, `Input: ${inputTokens}`, `Output: ${outputTokens}`]
|
const logParts = [`Model: ${model}`, `Input: ${inputTokens}`, `Output: ${outputTokens}`]
|
||||||
if (cacheCreateTokens > 0) {
|
if (cacheCreateTokens > 0) {
|
||||||
logParts.push(`Cache Create: ${cacheCreateTokens}`)
|
logParts.push(`Cache Create: ${cacheCreateTokens}`)
|
||||||
@@ -1014,8 +1087,24 @@ class ApiKeyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 📈 获取使用统计
|
// 📈 获取使用统计
|
||||||
async getUsageStats(keyId) {
|
async getUsageStats(keyId, options = {}) {
|
||||||
return await redis.getUsageStats(keyId)
|
const usageStats = await redis.getUsageStats(keyId)
|
||||||
|
|
||||||
|
// options 可能是字符串(兼容旧接口),仅当为对象时才解析
|
||||||
|
const optionObject =
|
||||||
|
options && typeof options === 'object' && !Array.isArray(options) ? options : {}
|
||||||
|
|
||||||
|
if (optionObject.includeRecords === false) {
|
||||||
|
return usageStats
|
||||||
|
}
|
||||||
|
|
||||||
|
const recordLimit = optionObject.recordLimit || 20
|
||||||
|
const recentRecords = await redis.getUsageRecords(keyId, recordLimit)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...usageStats,
|
||||||
|
recentRecords
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 📊 获取账户使用统计
|
// 📊 获取账户使用统计
|
||||||
@@ -1067,6 +1156,8 @@ class ApiKeyService {
|
|||||||
dailyCost,
|
dailyCost,
|
||||||
totalCost: costStats.total,
|
totalCost: costStats.total,
|
||||||
dailyCostLimit: parseFloat(key.dailyCostLimit || 0),
|
dailyCostLimit: parseFloat(key.dailyCostLimit || 0),
|
||||||
|
totalUsageLimit: parseFloat(key.totalUsageLimit || 0),
|
||||||
|
totalCostLimit: parseFloat(key.totalCostLimit || 0),
|
||||||
userId: key.userId,
|
userId: key.userId,
|
||||||
userUsername: key.userUsername,
|
userUsername: key.userUsername,
|
||||||
createdBy: key.createdBy,
|
createdBy: key.createdBy,
|
||||||
@@ -1112,7 +1203,9 @@ class ApiKeyService {
|
|||||||
userUsername: keyData.userUsername,
|
userUsername: keyData.userUsername,
|
||||||
createdBy: keyData.createdBy,
|
createdBy: keyData.createdBy,
|
||||||
permissions: keyData.permissions,
|
permissions: keyData.permissions,
|
||||||
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0)
|
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
|
||||||
|
totalUsageLimit: parseFloat(keyData.totalUsageLimit || 0),
|
||||||
|
totalCostLimit: parseFloat(keyData.totalCostLimit || 0)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to get API key by ID:', error)
|
logger.error('❌ Failed to get API key by ID:', error)
|
||||||
|
|||||||
@@ -218,6 +218,20 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
总费用限制 (美元)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="form.totalUsageLimit"
|
||||||
|
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||||
|
min="0"
|
||||||
|
placeholder="不修改 (0 表示无限制)"
|
||||||
|
step="0.01"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Opus 模型周费用限制 -->
|
<!-- Opus 模型周费用限制 -->
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
@@ -521,6 +535,7 @@ const form = reactive({
|
|||||||
rateLimitRequests: '',
|
rateLimitRequests: '',
|
||||||
concurrencyLimit: '',
|
concurrencyLimit: '',
|
||||||
dailyCostLimit: '',
|
dailyCostLimit: '',
|
||||||
|
totalUsageLimit: '',
|
||||||
weeklyOpusCostLimit: '', // 新增Opus周费用限制
|
weeklyOpusCostLimit: '', // 新增Opus周费用限制
|
||||||
permissions: '', // 空字符串表示不修改
|
permissions: '', // 空字符串表示不修改
|
||||||
claudeAccountId: '',
|
claudeAccountId: '',
|
||||||
@@ -652,6 +667,9 @@ const batchUpdateApiKeys = async () => {
|
|||||||
if (form.dailyCostLimit !== '' && form.dailyCostLimit !== null) {
|
if (form.dailyCostLimit !== '' && form.dailyCostLimit !== null) {
|
||||||
updates.dailyCostLimit = parseFloat(form.dailyCostLimit)
|
updates.dailyCostLimit = parseFloat(form.dailyCostLimit)
|
||||||
}
|
}
|
||||||
|
if (form.totalUsageLimit !== '' && form.totalUsageLimit !== null) {
|
||||||
|
updates.totalUsageLimit = parseFloat(form.totalUsageLimit)
|
||||||
|
}
|
||||||
if (form.weeklyOpusCostLimit !== '' && form.weeklyOpusCostLimit !== null) {
|
if (form.weeklyOpusCostLimit !== '' && form.weeklyOpusCostLimit !== null) {
|
||||||
updates.weeklyOpusCostLimit = parseFloat(form.weeklyOpusCostLimit)
|
updates.weeklyOpusCostLimit = parseFloat(form.weeklyOpusCostLimit)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -329,12 +329,61 @@
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
type="number"
|
type="number"
|
||||||
/>
|
/>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
<p class="dark:text灰-400 text-xs text-gray-500">
|
||||||
设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制
|
设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||||
|
>总费用限制 (美元)</label
|
||||||
|
>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
|
type="button"
|
||||||
|
@click="form.totalUsageLimit = '100'"
|
||||||
|
>
|
||||||
|
$100
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
|
type="button"
|
||||||
|
@click="form.totalUsageLimit = '500'"
|
||||||
|
>
|
||||||
|
$500
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
|
type="button"
|
||||||
|
@click="form.totalUsageLimit = '1000'"
|
||||||
|
>
|
||||||
|
$1000
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
|
type="button"
|
||||||
|
@click="form.totalUsageLimit = ''"
|
||||||
|
>
|
||||||
|
自定义
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="form.totalUsageLimit"
|
||||||
|
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||||
|
min="0"
|
||||||
|
placeholder="0 表示无限制"
|
||||||
|
step="0.01"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
设置此 API Key 的累计总费用限制,达到限制后将拒绝所有后续请求,0 或留空表示无限制
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||||
>Opus 模型周费用限制 (美元)</label
|
>Opus 模型周费用限制 (美元)</label
|
||||||
@@ -861,6 +910,7 @@ const form = reactive({
|
|||||||
rateLimitCost: '', // 新增:费用限制
|
rateLimitCost: '', // 新增:费用限制
|
||||||
concurrencyLimit: '',
|
concurrencyLimit: '',
|
||||||
dailyCostLimit: '',
|
dailyCostLimit: '',
|
||||||
|
totalUsageLimit: '',
|
||||||
weeklyOpusCostLimit: '',
|
weeklyOpusCostLimit: '',
|
||||||
expireDuration: '',
|
expireDuration: '',
|
||||||
customExpireDate: '',
|
customExpireDate: '',
|
||||||
@@ -1199,6 +1249,10 @@ const createApiKey = async () => {
|
|||||||
form.dailyCostLimit !== '' && form.dailyCostLimit !== null
|
form.dailyCostLimit !== '' && form.dailyCostLimit !== null
|
||||||
? parseFloat(form.dailyCostLimit)
|
? parseFloat(form.dailyCostLimit)
|
||||||
: 0,
|
: 0,
|
||||||
|
totalUsageLimit:
|
||||||
|
form.totalUsageLimit !== '' && form.totalUsageLimit !== null
|
||||||
|
? parseFloat(form.totalUsageLimit)
|
||||||
|
: 0,
|
||||||
weeklyOpusCostLimit:
|
weeklyOpusCostLimit:
|
||||||
form.weeklyOpusCostLimit !== '' && form.weeklyOpusCostLimit !== null
|
form.weeklyOpusCostLimit !== '' && form.weeklyOpusCostLimit !== null
|
||||||
? parseFloat(form.weeklyOpusCostLimit)
|
? parseFloat(form.weeklyOpusCostLimit)
|
||||||
|
|||||||
@@ -273,6 +273,55 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||||
|
>总费用限制 (美元)</label
|
||||||
|
>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
class="rounded-lg bg-gray-100 px-3 py-1 text-sm font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
|
type="button"
|
||||||
|
@click="form.totalUsageLimit = '100'"
|
||||||
|
>
|
||||||
|
$100
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="rounded-lg bg-gray-100 px-3 py-1 text-sm font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
|
type="button"
|
||||||
|
@click="form.totalUsageLimit = '500'"
|
||||||
|
>
|
||||||
|
$500
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="rounded-lg bg-gray-100 px-3 py-1 text-sm font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
|
type="button"
|
||||||
|
@click="form.totalUsageLimit = '1000'"
|
||||||
|
>
|
||||||
|
$1000
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="rounded-lg bg-gray-100 px-3 py-1 text-sm font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
|
type="button"
|
||||||
|
@click="form.totalUsageLimit = ''"
|
||||||
|
>
|
||||||
|
自定义
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="form.totalUsageLimit"
|
||||||
|
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||||
|
min="0"
|
||||||
|
placeholder="0 表示无限制"
|
||||||
|
step="0.01"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
设置此 API Key 的累计总费用限制,达到限制后将拒绝所有后续请求,0 或留空表示无限制
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||||
>Opus 模型周费用限制 (美元)</label
|
>Opus 模型周费用限制 (美元)</label
|
||||||
@@ -713,6 +762,7 @@ const form = reactive({
|
|||||||
rateLimitCost: '', // 新增:费用限制
|
rateLimitCost: '', // 新增:费用限制
|
||||||
concurrencyLimit: '',
|
concurrencyLimit: '',
|
||||||
dailyCostLimit: '',
|
dailyCostLimit: '',
|
||||||
|
totalUsageLimit: '',
|
||||||
weeklyOpusCostLimit: '',
|
weeklyOpusCostLimit: '',
|
||||||
permissions: 'all',
|
permissions: 'all',
|
||||||
claudeAccountId: '',
|
claudeAccountId: '',
|
||||||
@@ -826,6 +876,10 @@ const updateApiKey = async () => {
|
|||||||
form.dailyCostLimit !== '' && form.dailyCostLimit !== null
|
form.dailyCostLimit !== '' && form.dailyCostLimit !== null
|
||||||
? parseFloat(form.dailyCostLimit)
|
? parseFloat(form.dailyCostLimit)
|
||||||
: 0,
|
: 0,
|
||||||
|
totalUsageLimit:
|
||||||
|
form.totalUsageLimit !== '' && form.totalUsageLimit !== null
|
||||||
|
? parseFloat(form.totalUsageLimit)
|
||||||
|
: 0,
|
||||||
weeklyOpusCostLimit:
|
weeklyOpusCostLimit:
|
||||||
form.weeklyOpusCostLimit !== '' && form.weeklyOpusCostLimit !== null
|
form.weeklyOpusCostLimit !== '' && form.weeklyOpusCostLimit !== null
|
||||||
? parseFloat(form.weeklyOpusCostLimit)
|
? parseFloat(form.weeklyOpusCostLimit)
|
||||||
@@ -1100,6 +1154,7 @@ onMounted(async () => {
|
|||||||
form.rateLimitRequests = props.apiKey.rateLimitRequests || ''
|
form.rateLimitRequests = props.apiKey.rateLimitRequests || ''
|
||||||
form.concurrencyLimit = props.apiKey.concurrencyLimit || ''
|
form.concurrencyLimit = props.apiKey.concurrencyLimit || ''
|
||||||
form.dailyCostLimit = props.apiKey.dailyCostLimit || ''
|
form.dailyCostLimit = props.apiKey.dailyCostLimit || ''
|
||||||
|
form.totalUsageLimit = props.apiKey.totalUsageLimit || ''
|
||||||
form.weeklyOpusCostLimit = props.apiKey.weeklyOpusCostLimit || ''
|
form.weeklyOpusCostLimit = props.apiKey.weeklyOpusCostLimit || ''
|
||||||
form.permissions = props.apiKey.permissions || 'all'
|
form.permissions = props.apiKey.permissions || 'all'
|
||||||
// 处理 Claude 账号(区分 OAuth 和 Console)
|
// 处理 Claude 账号(区分 OAuth 和 Console)
|
||||||
|
|||||||
@@ -190,6 +190,31 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="apiKey.totalUsageLimit > 0" class="space-y-2">
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">总费用限制</span>
|
||||||
|
<span class="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
${{ apiKey.totalUsageLimit.toFixed(2) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-2 w-full rounded-full bg-gray-200 dark:bg-gray-600">
|
||||||
|
<div
|
||||||
|
class="h-2 rounded-full transition-all duration-300"
|
||||||
|
:class="
|
||||||
|
totalUsagePercentage >= 100
|
||||||
|
? 'bg-red-500'
|
||||||
|
: totalUsagePercentage >= 80
|
||||||
|
? 'bg-yellow-500'
|
||||||
|
: 'bg-indigo-500'
|
||||||
|
"
|
||||||
|
:style="{ width: Math.min(totalUsagePercentage, 100) + '%' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-right text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
已使用 {{ totalUsagePercentage.toFixed(1) }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="apiKey.rateLimitWindow > 0" class="space-y-2">
|
<div v-if="apiKey.rateLimitWindow > 0" class="space-y-2">
|
||||||
<h5 class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<h5 class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
<i class="fas fa-clock mr-1 text-blue-500" />
|
<i class="fas fa-clock mr-1 text-blue-500" />
|
||||||
@@ -250,6 +275,7 @@ const totalTokens = computed(() => props.apiKey.usage?.total?.tokens || 0)
|
|||||||
const dailyTokens = computed(() => props.apiKey.usage?.daily?.tokens || 0)
|
const dailyTokens = computed(() => props.apiKey.usage?.daily?.tokens || 0)
|
||||||
const totalCost = computed(() => props.apiKey.usage?.total?.cost || 0)
|
const totalCost = computed(() => props.apiKey.usage?.total?.cost || 0)
|
||||||
const dailyCost = computed(() => props.apiKey.dailyCost || 0)
|
const dailyCost = computed(() => props.apiKey.dailyCost || 0)
|
||||||
|
const totalUsageLimit = computed(() => props.apiKey.totalUsageLimit || 0)
|
||||||
const inputTokens = computed(() => props.apiKey.usage?.total?.inputTokens || 0)
|
const inputTokens = computed(() => props.apiKey.usage?.total?.inputTokens || 0)
|
||||||
const outputTokens = computed(() => props.apiKey.usage?.total?.outputTokens || 0)
|
const outputTokens = computed(() => props.apiKey.usage?.total?.outputTokens || 0)
|
||||||
const cacheCreateTokens = computed(() => props.apiKey.usage?.total?.cacheCreateTokens || 0)
|
const cacheCreateTokens = computed(() => props.apiKey.usage?.total?.cacheCreateTokens || 0)
|
||||||
@@ -260,6 +286,7 @@ const tpm = computed(() => props.apiKey.usage?.averages?.tpm || 0)
|
|||||||
const hasLimits = computed(() => {
|
const hasLimits = computed(() => {
|
||||||
return (
|
return (
|
||||||
props.apiKey.dailyCostLimit > 0 ||
|
props.apiKey.dailyCostLimit > 0 ||
|
||||||
|
props.apiKey.totalUsageLimit > 0 ||
|
||||||
props.apiKey.concurrencyLimit > 0 ||
|
props.apiKey.concurrencyLimit > 0 ||
|
||||||
props.apiKey.rateLimitWindow > 0 ||
|
props.apiKey.rateLimitWindow > 0 ||
|
||||||
props.apiKey.tokenLimit > 0
|
props.apiKey.tokenLimit > 0
|
||||||
@@ -271,6 +298,11 @@ const dailyCostPercentage = computed(() => {
|
|||||||
return (dailyCost.value / props.apiKey.dailyCostLimit) * 100
|
return (dailyCost.value / props.apiKey.dailyCostLimit) * 100
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const totalUsagePercentage = computed(() => {
|
||||||
|
if (!totalUsageLimit.value || totalUsageLimit.value === 0) return 0
|
||||||
|
return (totalCost.value / totalUsageLimit.value) * 100
|
||||||
|
})
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
const formatNumber = (num) => {
|
const formatNumber = (num) => {
|
||||||
if (!num && num !== 0) return '0'
|
if (!num && num !== 0) return '0'
|
||||||
|
|||||||
Reference in New Issue
Block a user