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:
shaw
2025-09-20 17:37:20 +08:00
parent 398f00c4cb
commit 08c2b7a444
10 changed files with 47 additions and 137 deletions

View File

@@ -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,28 +121,33 @@ 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 (_) {

View File

@@ -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

View File

@@ -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 &&

View File

@@ -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,

View File

@@ -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
} }

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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
}) })
// 方法 // 方法