mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
fix: 修复PR #458中的totalCostLimit功能问题
主要修复: - 移除重复的totalUsageLimit字段,统一使用totalCostLimit - 删除auth.js中重复的总费用限制检查逻辑 - 删除admin.js中重复的totalCostLimit验证代码 - 更新所有前端组件,移除totalUsageLimit引用 功能改进: - 确保totalCostLimit作为永久累计费用限制正常工作 - 与dailyCostLimit(每日重置)功能互补 - 适用于预付费、一次性API Key场景 测试: - 删除有逻辑错误的test-total-usage-limit.js - 创建新的test-total-cost-limit.js验证功能正确性 - 所有测试通过,功能正常工作 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -21,13 +21,13 @@ function createMockReq(apiKey) {
|
|||||||
return {
|
return {
|
||||||
headers: {
|
headers: {
|
||||||
'x-api-key': apiKey,
|
'x-api-key': apiKey,
|
||||||
'user-agent': 'total-usage-limit-test'
|
'user-agent': 'total-cost-limit-test'
|
||||||
},
|
},
|
||||||
query: {},
|
query: {},
|
||||||
body: {},
|
body: {},
|
||||||
ip: '127.0.0.1',
|
ip: '127.0.0.1',
|
||||||
connection: {},
|
connection: {},
|
||||||
originalUrl: '/test-total-usage-limit',
|
originalUrl: '/test-total-cost-limit',
|
||||||
once: () => {},
|
once: () => {},
|
||||||
on: () => {},
|
on: () => {},
|
||||||
get(header) {
|
get(header) {
|
||||||
@@ -102,18 +102,18 @@ async function cleanupKey(keyId) {
|
|||||||
async function main() {
|
async function main() {
|
||||||
await redis.connect()
|
await redis.connect()
|
||||||
|
|
||||||
const testName = `TotalUsageLimitTest-${Date.now()}`
|
const testName = `TotalCostLimitTest-${Date.now()}`
|
||||||
const totalLimit = 1.0
|
const totalCostLimit = 1.0
|
||||||
const newKey = await apiKeyService.generateApiKey({
|
const newKey = await apiKeyService.generateApiKey({
|
||||||
name: testName,
|
name: testName,
|
||||||
permissions: 'all',
|
permissions: 'all',
|
||||||
totalUsageLimit: totalLimit
|
totalCostLimit: totalCostLimit
|
||||||
})
|
})
|
||||||
|
|
||||||
const keyId = newKey.id
|
const keyId = newKey.id
|
||||||
const { apiKey } = newKey
|
const { apiKey } = newKey
|
||||||
|
|
||||||
console.log(`➕ Created test API key ${keyId} with total usage limit $${totalLimit}`)
|
console.log(`➕ Created test API key ${keyId} with total cost limit $${totalCostLimit}`)
|
||||||
|
|
||||||
let authResult = await runAuth(apiKey)
|
let authResult = await runAuth(apiKey)
|
||||||
if (authResult.status !== 200) {
|
if (authResult.status !== 200) {
|
||||||
@@ -121,32 +121,37 @@ async function main() {
|
|||||||
}
|
}
|
||||||
console.log('✅ Authentication succeeds before consuming quota')
|
console.log('✅ Authentication succeeds before consuming quota')
|
||||||
|
|
||||||
await redis.incrementDailyCost(keyId, 0.6)
|
// 增加总费用
|
||||||
|
const client = redis.getClient()
|
||||||
|
await client.set(`usage:cost:total:${keyId}`, '0.6')
|
||||||
|
|
||||||
authResult = await runAuth(apiKey)
|
authResult = await runAuth(apiKey)
|
||||||
if (authResult.status !== 200) {
|
if (authResult.status !== 200) {
|
||||||
throw new Error(`Expected success under quota, got status ${authResult.status}`)
|
throw new Error(`Expected success under quota, got status ${authResult.status}`)
|
||||||
}
|
}
|
||||||
console.log('✅ Authentication succeeds while still under quota ($0.60)')
|
console.log('✅ Authentication succeeds while still under quota ($0.60)')
|
||||||
|
|
||||||
await redis.incrementDailyCost(keyId, 0.5)
|
// 继续增加总费用超过限制
|
||||||
|
await client.set(`usage:cost:total:${keyId}`, '1.1')
|
||||||
|
|
||||||
authResult = await runAuth(apiKey)
|
authResult = await runAuth(apiKey)
|
||||||
if (authResult.status !== 429) {
|
if (authResult.status !== 429) {
|
||||||
throw new Error(`Expected 429 after exceeding quota, got status ${authResult.status}`)
|
throw new Error(`Expected 429 after exceeding quota, got status ${authResult.status}`)
|
||||||
}
|
}
|
||||||
console.log('✅ Authentication returns 429 after exceeding total usage limit ($1.10)')
|
console.log('✅ Authentication returns 429 after exceeding total cost limit ($1.10)')
|
||||||
|
|
||||||
await cleanupKey(keyId)
|
await cleanupKey(keyId)
|
||||||
await redis.disconnect()
|
await redis.disconnect()
|
||||||
|
|
||||||
console.log('🎉 Total usage limit test completed successfully')
|
console.log('🎉 Total cost limit test completed successfully')
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(async (error) => {
|
main().catch(async (error) => {
|
||||||
console.error('❌ Total usage limit test failed:', error)
|
console.error('❌ Total cost limit test failed:', error)
|
||||||
try {
|
try {
|
||||||
await redis.disconnect()
|
await redis.disconnect()
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Ignore disconnect errors during cleanup
|
// Ignore disconnect errors during cleanup
|
||||||
}
|
}
|
||||||
process.exitCode = 1
|
process.exitCode = 1
|
||||||
})
|
})
|
||||||
@@ -312,33 +312,6 @@ 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) {
|
||||||
@@ -460,7 +433,6 @@ 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,
|
totalCostLimit: validation.keyData.totalCostLimit,
|
||||||
totalCost: validation.keyData.totalCost,
|
totalCost: validation.keyData.totalCost,
|
||||||
usage: validation.keyData.usage
|
usage: validation.keyData.usage
|
||||||
|
|||||||
@@ -550,7 +550,6 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
enableClientRestriction,
|
enableClientRestriction,
|
||||||
allowedClients,
|
allowedClients,
|
||||||
dailyCostLimit,
|
dailyCostLimit,
|
||||||
totalUsageLimit,
|
|
||||||
totalCostLimit,
|
totalCostLimit,
|
||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
@@ -634,22 +633,6 @@ 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 (
|
if (
|
||||||
totalCostLimit !== undefined &&
|
totalCostLimit !== undefined &&
|
||||||
totalCostLimit !== null &&
|
totalCostLimit !== null &&
|
||||||
@@ -704,7 +687,6 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
enableClientRestriction,
|
enableClientRestriction,
|
||||||
allowedClients,
|
allowedClients,
|
||||||
dailyCostLimit,
|
dailyCostLimit,
|
||||||
totalUsageLimit,
|
|
||||||
totalCostLimit,
|
totalCostLimit,
|
||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
@@ -745,7 +727,6 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
|||||||
enableClientRestriction,
|
enableClientRestriction,
|
||||||
allowedClients,
|
allowedClients,
|
||||||
dailyCostLimit,
|
dailyCostLimit,
|
||||||
totalUsageLimit,
|
|
||||||
totalCostLimit,
|
totalCostLimit,
|
||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
@@ -796,7 +777,6 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
|||||||
enableClientRestriction,
|
enableClientRestriction,
|
||||||
allowedClients,
|
allowedClients,
|
||||||
dailyCostLimit,
|
dailyCostLimit,
|
||||||
totalUsageLimit,
|
|
||||||
totalCostLimit,
|
totalCostLimit,
|
||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
@@ -915,9 +895,6 @@ 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) {
|
if (updates.totalCostLimit !== undefined) {
|
||||||
finalUpdates.totalCostLimit = updates.totalCostLimit
|
finalUpdates.totalCostLimit = updates.totalCostLimit
|
||||||
}
|
}
|
||||||
@@ -1049,7 +1026,6 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
allowedClients,
|
allowedClients,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
dailyCostLimit,
|
dailyCostLimit,
|
||||||
totalUsageLimit,
|
|
||||||
totalCostLimit,
|
totalCostLimit,
|
||||||
weeklyOpusCostLimit,
|
weeklyOpusCostLimit,
|
||||||
tags,
|
tags,
|
||||||
@@ -1208,14 +1184,6 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
updates.totalCostLimit = costLimit
|
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 &&
|
||||||
|
|||||||
@@ -141,7 +141,6 @@ 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,
|
totalCostLimit: parseFloat(keyData.totalCostLimit) || 0,
|
||||||
dailyCost: dailyCost || 0,
|
dailyCost: dailyCost || 0,
|
||||||
totalCost: costStats.total || 0,
|
totalCost: costStats.total || 0,
|
||||||
@@ -376,7 +375,6 @@ 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,
|
totalCostLimit: fullKeyData.totalCostLimit || 0,
|
||||||
// 当前使用量
|
// 当前使用量
|
||||||
currentWindowRequests,
|
currentWindowRequests,
|
||||||
|
|||||||
@@ -258,7 +258,6 @@ 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,
|
totalCost: key.totalCost,
|
||||||
totalCostLimit: key.totalCostLimit,
|
totalCostLimit: key.totalCostLimit,
|
||||||
// 不返回实际的key值,只返回前缀和后几位
|
// 不返回实际的key值,只返回前缀和后几位
|
||||||
@@ -290,15 +289,7 @@ 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 {
|
const { name, description, tokenLimit, expiresAt, dailyCostLimit, totalCostLimit } = req.body
|
||||||
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({
|
||||||
@@ -319,16 +310,6 @@ router.post('/api-keys', authenticateUser, async (req, res) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -347,7 +328,6 @@ 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,
|
totalCostLimit: totalCostLimit || null,
|
||||||
createdBy: 'user',
|
createdBy: 'user',
|
||||||
// 设置服务权限为全部服务,确保前端显示“服务权限”为“全部服务”且具备完整访问权限
|
// 设置服务权限为全部服务,确保前端显示“服务权限”为“全部服务”且具备完整访问权限
|
||||||
@@ -372,7 +352,6 @@ 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,
|
totalCostLimit: newApiKey.totalCostLimit,
|
||||||
createdAt: newApiKey.createdAt
|
createdAt: newApiKey.createdAt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ class ApiKeyService {
|
|||||||
enableClientRestriction = false,
|
enableClientRestriction = false,
|
||||||
allowedClients = [],
|
allowedClients = [],
|
||||||
dailyCostLimit = 0,
|
dailyCostLimit = 0,
|
||||||
totalUsageLimit = 0,
|
|
||||||
totalCostLimit = 0,
|
totalCostLimit = 0,
|
||||||
weeklyOpusCostLimit = 0,
|
weeklyOpusCostLimit = 0,
|
||||||
tags = [],
|
tags = [],
|
||||||
@@ -70,7 +69,6 @@ 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),
|
totalCostLimit: String(totalCostLimit || 0),
|
||||||
weeklyOpusCostLimit: String(weeklyOpusCostLimit || 0),
|
weeklyOpusCostLimit: String(weeklyOpusCostLimit || 0),
|
||||||
tags: JSON.stringify(tags || []),
|
tags: JSON.stringify(tags || []),
|
||||||
@@ -115,7 +113,6 @@ 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),
|
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 || '[]'),
|
||||||
@@ -257,7 +254,6 @@ 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),
|
totalCostLimit: parseFloat(keyData.totalCostLimit || 0),
|
||||||
weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0),
|
weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0),
|
||||||
dailyCost: dailyCost || 0,
|
dailyCost: dailyCost || 0,
|
||||||
@@ -383,7 +379,6 @@ 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),
|
totalCostLimit: parseFloat(keyData.totalCostLimit || 0),
|
||||||
weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0),
|
weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0),
|
||||||
dailyCost: dailyCost || 0,
|
dailyCost: dailyCost || 0,
|
||||||
@@ -426,10 +421,6 @@ 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'
|
||||||
@@ -558,7 +549,6 @@ class ApiKeyService {
|
|||||||
'enableClientRestriction',
|
'enableClientRestriction',
|
||||||
'allowedClients',
|
'allowedClients',
|
||||||
'dailyCostLimit',
|
'dailyCostLimit',
|
||||||
'totalUsageLimit',
|
|
||||||
'totalCostLimit',
|
'totalCostLimit',
|
||||||
'weeklyOpusCostLimit',
|
'weeklyOpusCostLimit',
|
||||||
'tags',
|
'tags',
|
||||||
@@ -1156,7 +1146,6 @@ 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),
|
totalCostLimit: parseFloat(key.totalCostLimit || 0),
|
||||||
userId: key.userId,
|
userId: key.userId,
|
||||||
userUsername: key.userUsername,
|
userUsername: key.userUsername,
|
||||||
@@ -1204,7 +1193,6 @@ class ApiKeyService {
|
|||||||
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)
|
totalCostLimit: parseFloat(keyData.totalCostLimit || 0)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -223,7 +223,7 @@
|
|||||||
总费用限制 (美元)
|
总费用限制 (美元)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
v-model="form.totalUsageLimit"
|
v-model="form.totalCostLimit"
|
||||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||||
min="0"
|
min="0"
|
||||||
placeholder="不修改 (0 表示无限制)"
|
placeholder="不修改 (0 表示无限制)"
|
||||||
@@ -535,7 +535,7 @@ const form = reactive({
|
|||||||
rateLimitRequests: '',
|
rateLimitRequests: '',
|
||||||
concurrencyLimit: '',
|
concurrencyLimit: '',
|
||||||
dailyCostLimit: '',
|
dailyCostLimit: '',
|
||||||
totalUsageLimit: '',
|
totalCostLimit: '',
|
||||||
weeklyOpusCostLimit: '', // 新增Opus周费用限制
|
weeklyOpusCostLimit: '', // 新增Opus周费用限制
|
||||||
permissions: '', // 空字符串表示不修改
|
permissions: '', // 空字符串表示不修改
|
||||||
claudeAccountId: '',
|
claudeAccountId: '',
|
||||||
@@ -667,8 +667,8 @@ 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) {
|
if (form.totalCostLimit !== '' && form.totalCostLimit !== null) {
|
||||||
updates.totalUsageLimit = parseFloat(form.totalUsageLimit)
|
updates.totalCostLimit = parseFloat(form.totalCostLimit)
|
||||||
}
|
}
|
||||||
if (form.weeklyOpusCostLimit !== '' && form.weeklyOpusCostLimit !== null) {
|
if (form.weeklyOpusCostLimit !== '' && form.weeklyOpusCostLimit !== null) {
|
||||||
updates.weeklyOpusCostLimit = parseFloat(form.weeklyOpusCostLimit)
|
updates.weeklyOpusCostLimit = parseFloat(form.weeklyOpusCostLimit)
|
||||||
|
|||||||
@@ -344,34 +344,34 @@
|
|||||||
<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"
|
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"
|
type="button"
|
||||||
@click="form.totalUsageLimit = '100'"
|
@click="form.totalCostLimit = '100'"
|
||||||
>
|
>
|
||||||
$100
|
$100
|
||||||
</button>
|
</button>
|
||||||
<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"
|
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"
|
type="button"
|
||||||
@click="form.totalUsageLimit = '500'"
|
@click="form.totalCostLimit = '500'"
|
||||||
>
|
>
|
||||||
$500
|
$500
|
||||||
</button>
|
</button>
|
||||||
<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"
|
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"
|
type="button"
|
||||||
@click="form.totalUsageLimit = '1000'"
|
@click="form.totalCostLimit = '1000'"
|
||||||
>
|
>
|
||||||
$1000
|
$1000
|
||||||
</button>
|
</button>
|
||||||
<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"
|
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"
|
type="button"
|
||||||
@click="form.totalUsageLimit = ''"
|
@click="form.totalCostLimit = ''"
|
||||||
>
|
>
|
||||||
自定义
|
自定义
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
v-model="form.totalUsageLimit"
|
v-model="form.totalCostLimit"
|
||||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
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"
|
min="0"
|
||||||
placeholder="0 表示无限制"
|
placeholder="0 表示无限制"
|
||||||
@@ -910,7 +910,7 @@ const form = reactive({
|
|||||||
rateLimitCost: '', // 新增:费用限制
|
rateLimitCost: '', // 新增:费用限制
|
||||||
concurrencyLimit: '',
|
concurrencyLimit: '',
|
||||||
dailyCostLimit: '',
|
dailyCostLimit: '',
|
||||||
totalUsageLimit: '',
|
totalCostLimit: '',
|
||||||
weeklyOpusCostLimit: '',
|
weeklyOpusCostLimit: '',
|
||||||
expireDuration: '',
|
expireDuration: '',
|
||||||
customExpireDate: '',
|
customExpireDate: '',
|
||||||
@@ -1249,9 +1249,9 @@ const createApiKey = async () => {
|
|||||||
form.dailyCostLimit !== '' && form.dailyCostLimit !== null
|
form.dailyCostLimit !== '' && form.dailyCostLimit !== null
|
||||||
? parseFloat(form.dailyCostLimit)
|
? parseFloat(form.dailyCostLimit)
|
||||||
: 0,
|
: 0,
|
||||||
totalUsageLimit:
|
totalCostLimit:
|
||||||
form.totalUsageLimit !== '' && form.totalUsageLimit !== null
|
form.totalCostLimit !== '' && form.totalCostLimit !== null
|
||||||
? parseFloat(form.totalUsageLimit)
|
? parseFloat(form.totalCostLimit)
|
||||||
: 0,
|
: 0,
|
||||||
weeklyOpusCostLimit:
|
weeklyOpusCostLimit:
|
||||||
form.weeklyOpusCostLimit !== '' && form.weeklyOpusCostLimit !== null
|
form.weeklyOpusCostLimit !== '' && form.weeklyOpusCostLimit !== null
|
||||||
|
|||||||
@@ -282,34 +282,34 @@
|
|||||||
<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"
|
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"
|
type="button"
|
||||||
@click="form.totalUsageLimit = '100'"
|
@click="form.totalCostLimit = '100'"
|
||||||
>
|
>
|
||||||
$100
|
$100
|
||||||
</button>
|
</button>
|
||||||
<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"
|
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"
|
type="button"
|
||||||
@click="form.totalUsageLimit = '500'"
|
@click="form.totalCostLimit = '500'"
|
||||||
>
|
>
|
||||||
$500
|
$500
|
||||||
</button>
|
</button>
|
||||||
<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"
|
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"
|
type="button"
|
||||||
@click="form.totalUsageLimit = '1000'"
|
@click="form.totalCostLimit = '1000'"
|
||||||
>
|
>
|
||||||
$1000
|
$1000
|
||||||
</button>
|
</button>
|
||||||
<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"
|
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"
|
type="button"
|
||||||
@click="form.totalUsageLimit = ''"
|
@click="form.totalCostLimit = ''"
|
||||||
>
|
>
|
||||||
自定义
|
自定义
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
v-model="form.totalUsageLimit"
|
v-model="form.totalCostLimit"
|
||||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
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"
|
min="0"
|
||||||
placeholder="0 表示无限制"
|
placeholder="0 表示无限制"
|
||||||
@@ -762,7 +762,7 @@ const form = reactive({
|
|||||||
rateLimitCost: '', // 新增:费用限制
|
rateLimitCost: '', // 新增:费用限制
|
||||||
concurrencyLimit: '',
|
concurrencyLimit: '',
|
||||||
dailyCostLimit: '',
|
dailyCostLimit: '',
|
||||||
totalUsageLimit: '',
|
totalCostLimit: '',
|
||||||
weeklyOpusCostLimit: '',
|
weeklyOpusCostLimit: '',
|
||||||
permissions: 'all',
|
permissions: 'all',
|
||||||
claudeAccountId: '',
|
claudeAccountId: '',
|
||||||
@@ -876,9 +876,9 @@ const updateApiKey = async () => {
|
|||||||
form.dailyCostLimit !== '' && form.dailyCostLimit !== null
|
form.dailyCostLimit !== '' && form.dailyCostLimit !== null
|
||||||
? parseFloat(form.dailyCostLimit)
|
? parseFloat(form.dailyCostLimit)
|
||||||
: 0,
|
: 0,
|
||||||
totalUsageLimit:
|
totalCostLimit:
|
||||||
form.totalUsageLimit !== '' && form.totalUsageLimit !== null
|
form.totalCostLimit !== '' && form.totalCostLimit !== null
|
||||||
? parseFloat(form.totalUsageLimit)
|
? parseFloat(form.totalCostLimit)
|
||||||
: 0,
|
: 0,
|
||||||
weeklyOpusCostLimit:
|
weeklyOpusCostLimit:
|
||||||
form.weeklyOpusCostLimit !== '' && form.weeklyOpusCostLimit !== null
|
form.weeklyOpusCostLimit !== '' && form.weeklyOpusCostLimit !== null
|
||||||
@@ -1154,7 +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.totalCostLimit = props.apiKey.totalCostLimit || ''
|
||||||
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,11 +190,11 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="apiKey.totalUsageLimit > 0" class="space-y-2">
|
<div v-if="apiKey.totalCostLimit > 0" class="space-y-2">
|
||||||
<div class="flex items-center justify-between text-sm">
|
<div class="flex items-center justify-between text-sm">
|
||||||
<span class="text-gray-600 dark:text-gray-400">总费用限制</span>
|
<span class="text-gray-600 dark:text-gray-400">总费用限制</span>
|
||||||
<span class="font-semibold text-gray-900 dark:text-gray-100">
|
<span class="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
${{ apiKey.totalUsageLimit.toFixed(2) }}
|
${{ apiKey.totalCostLimit.toFixed(2) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-2 w-full rounded-full bg-gray-200 dark:bg-gray-600">
|
<div class="h-2 w-full rounded-full bg-gray-200 dark:bg-gray-600">
|
||||||
@@ -275,7 +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 totalCostLimit = computed(() => props.apiKey.totalCostLimit || 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)
|
||||||
@@ -286,7 +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.totalCostLimit > 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
|
||||||
@@ -299,8 +299,8 @@ const dailyCostPercentage = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const totalUsagePercentage = computed(() => {
|
const totalUsagePercentage = computed(() => {
|
||||||
if (!totalUsageLimit.value || totalUsageLimit.value === 0) return 0
|
if (!totalCostLimit.value || totalCostLimit.value === 0) return 0
|
||||||
return (totalCost.value / totalUsageLimit.value) * 100
|
return (totalCost.value / totalCostLimit.value) * 100
|
||||||
})
|
})
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
|
|||||||
Reference in New Issue
Block a user