mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
feat(Claude Console): 添加Claude Console账号每日配额
1. 额度检查优先级更高:即使不启用限流机制,超额仍会禁用账户 2. 状态会被覆盖:quota_exceeded 会覆盖 rate_limited 3. 两种恢复时间: - 限流恢复:分钟级(如60分钟) - 额度恢复:天级(第二天重置) 4. 独立控制: - rateLimitDuration = 0:只管理额度,忽略429 - rateLimitDuration > 0:同时管理限流和额度
This commit is contained in:
@@ -2292,7 +2292,9 @@ router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
rateLimitDuration,
|
rateLimitDuration,
|
||||||
proxy,
|
proxy,
|
||||||
accountType,
|
accountType,
|
||||||
groupId
|
groupId,
|
||||||
|
dailyQuota,
|
||||||
|
quotaResetTime
|
||||||
} = req.body
|
} = req.body
|
||||||
|
|
||||||
if (!name || !apiUrl || !apiKey) {
|
if (!name || !apiUrl || !apiKey) {
|
||||||
@@ -2327,7 +2329,9 @@ router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
|||||||
rateLimitDuration:
|
rateLimitDuration:
|
||||||
rateLimitDuration !== undefined && rateLimitDuration !== null ? rateLimitDuration : 60,
|
rateLimitDuration !== undefined && rateLimitDuration !== null ? rateLimitDuration : 60,
|
||||||
proxy,
|
proxy,
|
||||||
accountType: accountType || 'shared'
|
accountType: accountType || 'shared',
|
||||||
|
dailyQuota: dailyQuota || 0,
|
||||||
|
quotaResetTime: quotaResetTime || '00:00'
|
||||||
})
|
})
|
||||||
|
|
||||||
// 如果是分组类型,将账户添加到分组
|
// 如果是分组类型,将账户添加到分组
|
||||||
@@ -2506,6 +2510,56 @@ router.put(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 获取Claude Console账户的使用统计
|
||||||
|
router.get('/claude-console-accounts/:accountId/usage', authenticateAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { accountId } = req.params
|
||||||
|
const usageStats = await claudeConsoleAccountService.getAccountUsageStats(accountId)
|
||||||
|
|
||||||
|
if (!usageStats) {
|
||||||
|
return res.status(404).json({ error: 'Account not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(usageStats)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Failed to get Claude Console account usage stats:', error)
|
||||||
|
return res.status(500).json({ error: 'Failed to get usage stats', message: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 手动重置Claude Console账户的每日使用量
|
||||||
|
router.post(
|
||||||
|
'/claude-console-accounts/:accountId/reset-usage',
|
||||||
|
authenticateAdmin,
|
||||||
|
async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { accountId } = req.params
|
||||||
|
await claudeConsoleAccountService.resetDailyUsage(accountId)
|
||||||
|
|
||||||
|
logger.success(`✅ Admin manually reset daily usage for Claude Console account: ${accountId}`)
|
||||||
|
return res.json({ success: true, message: 'Daily usage reset successfully' })
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Failed to reset Claude Console account daily usage:', error)
|
||||||
|
return res.status(500).json({ error: 'Failed to reset daily usage', message: error.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 手动重置所有Claude Console账户的每日使用量
|
||||||
|
router.post('/claude-console-accounts/reset-all-usage', authenticateAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
await claudeConsoleAccountService.resetAllDailyUsage()
|
||||||
|
|
||||||
|
logger.success('✅ Admin manually reset daily usage for all Claude Console accounts')
|
||||||
|
return res.json({ success: true, message: 'All daily usage reset successfully' })
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Failed to reset all Claude Console accounts daily usage:', error)
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.json({ error: 'Failed to reset all daily usage', message: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// ☁️ Bedrock 账户管理
|
// ☁️ Bedrock 账户管理
|
||||||
|
|
||||||
// 获取所有Bedrock账户
|
// 获取所有Bedrock账户
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ class ClaudeConsoleAccountService {
|
|||||||
proxy = null,
|
proxy = null,
|
||||||
isActive = true,
|
isActive = true,
|
||||||
accountType = 'shared', // 'dedicated' or 'shared'
|
accountType = 'shared', // 'dedicated' or 'shared'
|
||||||
schedulable = true // 是否可被调度
|
schedulable = true, // 是否可被调度
|
||||||
|
dailyQuota = 0, // 每日额度限制(美元),0表示不限制
|
||||||
|
quotaResetTime = '00:00' // 额度重置时间(HH:mm格式)
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
// 验证必填字段
|
// 验证必填字段
|
||||||
@@ -85,7 +87,14 @@ class ClaudeConsoleAccountService {
|
|||||||
rateLimitedAt: '',
|
rateLimitedAt: '',
|
||||||
rateLimitStatus: '',
|
rateLimitStatus: '',
|
||||||
// 调度控制
|
// 调度控制
|
||||||
schedulable: schedulable.toString()
|
schedulable: schedulable.toString(),
|
||||||
|
// 额度管理相关
|
||||||
|
dailyQuota: dailyQuota.toString(), // 每日额度限制(美元)
|
||||||
|
dailyUsage: '0', // 当日使用金额(美元)
|
||||||
|
// 使用与统计一致的时区日期,避免边界问题
|
||||||
|
lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区)
|
||||||
|
quotaResetTime, // 额度重置时间
|
||||||
|
quotaStoppedAt: '' // 因额度停用的时间
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
@@ -116,7 +125,12 @@ class ClaudeConsoleAccountService {
|
|||||||
proxy,
|
proxy,
|
||||||
accountType,
|
accountType,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
createdAt: accountData.createdAt
|
createdAt: accountData.createdAt,
|
||||||
|
dailyQuota,
|
||||||
|
dailyUsage: 0,
|
||||||
|
lastResetDate: accountData.lastResetDate,
|
||||||
|
quotaResetTime,
|
||||||
|
quotaStoppedAt: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,12 +162,18 @@ class ClaudeConsoleAccountService {
|
|||||||
isActive: accountData.isActive === 'true',
|
isActive: accountData.isActive === 'true',
|
||||||
proxy: accountData.proxy ? JSON.parse(accountData.proxy) : null,
|
proxy: accountData.proxy ? JSON.parse(accountData.proxy) : null,
|
||||||
accountType: accountData.accountType || 'shared',
|
accountType: accountData.accountType || 'shared',
|
||||||
status: accountData.status,
|
|
||||||
errorMessage: accountData.errorMessage,
|
|
||||||
createdAt: accountData.createdAt,
|
createdAt: accountData.createdAt,
|
||||||
lastUsedAt: accountData.lastUsedAt,
|
lastUsedAt: accountData.lastUsedAt,
|
||||||
rateLimitStatus: rateLimitInfo,
|
status: accountData.status || 'active',
|
||||||
schedulable: accountData.schedulable !== 'false' // 默认为true,只有明确设置为false才不可调度
|
errorMessage: accountData.errorMessage,
|
||||||
|
rateLimitInfo,
|
||||||
|
schedulable: accountData.schedulable !== 'false', // 默认为true,只有明确设置为false才不可调度
|
||||||
|
// 额度管理相关
|
||||||
|
dailyQuota: parseFloat(accountData.dailyQuota || '0'),
|
||||||
|
dailyUsage: parseFloat(accountData.dailyUsage || '0'),
|
||||||
|
lastResetDate: accountData.lastResetDate || '',
|
||||||
|
quotaResetTime: accountData.quotaResetTime || '00:00',
|
||||||
|
quotaStoppedAt: accountData.quotaStoppedAt || null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,6 +287,23 @@ class ClaudeConsoleAccountService {
|
|||||||
updatedData.schedulable = updates.schedulable.toString()
|
updatedData.schedulable = updates.schedulable.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 额度管理相关字段
|
||||||
|
if (updates.dailyQuota !== undefined) {
|
||||||
|
updatedData.dailyQuota = updates.dailyQuota.toString()
|
||||||
|
}
|
||||||
|
if (updates.quotaResetTime !== undefined) {
|
||||||
|
updatedData.quotaResetTime = updates.quotaResetTime
|
||||||
|
}
|
||||||
|
if (updates.dailyUsage !== undefined) {
|
||||||
|
updatedData.dailyUsage = updates.dailyUsage.toString()
|
||||||
|
}
|
||||||
|
if (updates.lastResetDate !== undefined) {
|
||||||
|
updatedData.lastResetDate = updates.lastResetDate
|
||||||
|
}
|
||||||
|
if (updates.quotaStoppedAt !== undefined) {
|
||||||
|
updatedData.quotaStoppedAt = updates.quotaStoppedAt
|
||||||
|
}
|
||||||
|
|
||||||
// 处理账户类型变更
|
// 处理账户类型变更
|
||||||
if (updates.accountType && updates.accountType !== existingAccount.accountType) {
|
if (updates.accountType && updates.accountType !== existingAccount.accountType) {
|
||||||
updatedData.accountType = updates.accountType
|
updatedData.accountType = updates.accountType
|
||||||
@@ -361,7 +398,16 @@ class ClaudeConsoleAccountService {
|
|||||||
|
|
||||||
const updates = {
|
const updates = {
|
||||||
rateLimitedAt: new Date().toISOString(),
|
rateLimitedAt: new Date().toISOString(),
|
||||||
rateLimitStatus: 'limited'
|
rateLimitStatus: 'limited',
|
||||||
|
isActive: 'false', // 禁用账户
|
||||||
|
errorMessage: `Rate limited at ${new Date().toISOString()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有当前状态不是quota_exceeded时才设置为rate_limited
|
||||||
|
// 避免覆盖更重要的配额超限状态
|
||||||
|
const currentStatus = await client.hget(`${this.ACCOUNT_KEY_PREFIX}${accountId}`, 'status')
|
||||||
|
if (currentStatus !== 'quota_exceeded') {
|
||||||
|
updates.status = 'rate_limited'
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.hset(`${this.ACCOUNT_KEY_PREFIX}${accountId}`, updates)
|
await client.hset(`${this.ACCOUNT_KEY_PREFIX}${accountId}`, updates)
|
||||||
@@ -376,7 +422,7 @@ class ClaudeConsoleAccountService {
|
|||||||
platform: 'claude-console',
|
platform: 'claude-console',
|
||||||
status: 'error',
|
status: 'error',
|
||||||
errorCode: 'CLAUDE_CONSOLE_RATE_LIMITED',
|
errorCode: 'CLAUDE_CONSOLE_RATE_LIMITED',
|
||||||
reason: `Account rate limited (429 error). ${account.rateLimitDuration ? `Will be blocked for ${account.rateLimitDuration} hours` : 'Temporary rate limit'}`,
|
reason: `Account rate limited (429 error) and has been disabled. ${account.rateLimitDuration ? `Will be automatically re-enabled after ${account.rateLimitDuration} minutes` : 'Manual intervention required to re-enable'}`,
|
||||||
timestamp: getISOStringWithTimezone(new Date())
|
timestamp: getISOStringWithTimezone(new Date())
|
||||||
})
|
})
|
||||||
} catch (webhookError) {
|
} catch (webhookError) {
|
||||||
@@ -397,14 +443,40 @@ class ClaudeConsoleAccountService {
|
|||||||
async removeAccountRateLimit(accountId) {
|
async removeAccountRateLimit(accountId) {
|
||||||
try {
|
try {
|
||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
|
const accountKey = `${this.ACCOUNT_KEY_PREFIX}${accountId}`
|
||||||
|
|
||||||
await client.hdel(
|
// 获取账户当前状态和额度信息
|
||||||
`${this.ACCOUNT_KEY_PREFIX}${accountId}`,
|
const [currentStatus, quotaStoppedAt] = await client.hmget(
|
||||||
'rateLimitedAt',
|
accountKey,
|
||||||
'rateLimitStatus'
|
'status',
|
||||||
|
'quotaStoppedAt'
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.success(`✅ Rate limit removed for Claude Console account: ${accountId}`)
|
// 删除限流相关字段
|
||||||
|
await client.hdel(accountKey, 'rateLimitedAt', 'rateLimitStatus')
|
||||||
|
|
||||||
|
// 根据不同情况决定是否恢复账户
|
||||||
|
if (currentStatus === 'rate_limited') {
|
||||||
|
if (quotaStoppedAt) {
|
||||||
|
// 还有额度限制,改为quota_exceeded状态
|
||||||
|
await client.hset(accountKey, {
|
||||||
|
status: 'quota_exceeded'
|
||||||
|
// isActive保持false
|
||||||
|
})
|
||||||
|
logger.info(`⚠️ Rate limit removed but quota exceeded remains for account: ${accountId}`)
|
||||||
|
} else {
|
||||||
|
// 没有额度限制,完全恢复
|
||||||
|
await client.hset(accountKey, {
|
||||||
|
isActive: 'true',
|
||||||
|
status: 'active',
|
||||||
|
errorMessage: ''
|
||||||
|
})
|
||||||
|
logger.success(`✅ Rate limit removed and account re-enabled: ${accountId}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.success(`✅ Rate limit removed for Claude Console account: ${accountId}`)
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`❌ Failed to remove rate limit for Claude Console account: ${accountId}`, error)
|
logger.error(`❌ Failed to remove rate limit for Claude Console account: ${accountId}`, error)
|
||||||
@@ -454,6 +526,64 @@ class ClaudeConsoleAccountService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔍 检查账号是否因额度超限而被停用(懒惰检查)
|
||||||
|
async isAccountQuotaExceeded(accountId) {
|
||||||
|
try {
|
||||||
|
const account = await this.getAccount(accountId)
|
||||||
|
if (!account) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有设置额度限制,不会超额
|
||||||
|
const dailyQuota = parseFloat(account.dailyQuota || '0')
|
||||||
|
if (isNaN(dailyQuota) || dailyQuota <= 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果账户没有被额度停用,检查当前使用情况
|
||||||
|
if (!account.quotaStoppedAt) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否应该重置额度(到了新的重置时间点)
|
||||||
|
if (this._shouldResetQuota(account)) {
|
||||||
|
await this.resetDailyUsage(accountId)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仍在额度超限状态
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`❌ Failed to check quota exceeded status for Claude Console account: ${accountId}`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔍 判断是否应该重置账户额度
|
||||||
|
_shouldResetQuota(account) {
|
||||||
|
// 与 Redis 统计一致:按配置时区判断“今天”与时间点
|
||||||
|
const tzNow = redis.getDateInTimezone(new Date())
|
||||||
|
const today = redis.getDateStringInTimezone(tzNow)
|
||||||
|
|
||||||
|
// 如果已经是今天重置过的,不需要重置
|
||||||
|
if (account.lastResetDate === today) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否到了重置时间点(按配置时区的小时/分钟)
|
||||||
|
const resetTime = account.quotaResetTime || '00:00'
|
||||||
|
const [resetHour, resetMinute] = resetTime.split(':').map((n) => parseInt(n))
|
||||||
|
|
||||||
|
const currentHour = tzNow.getUTCHours()
|
||||||
|
const currentMinute = tzNow.getUTCMinutes()
|
||||||
|
|
||||||
|
// 如果当前时间已过重置时间且不是同一天重置的,应该重置
|
||||||
|
return currentHour > resetHour || (currentHour === resetHour && currentMinute >= resetMinute)
|
||||||
|
}
|
||||||
|
|
||||||
// 🚫 标记账号为未授权状态(401错误)
|
// 🚫 标记账号为未授权状态(401错误)
|
||||||
async markAccountUnauthorized(accountId) {
|
async markAccountUnauthorized(accountId) {
|
||||||
try {
|
try {
|
||||||
@@ -820,6 +950,187 @@ class ClaudeConsoleAccountService {
|
|||||||
// 返回映射后的模型,如果不存在则返回原模型
|
// 返回映射后的模型,如果不存在则返回原模型
|
||||||
return modelMapping[requestedModel] || requestedModel
|
return modelMapping[requestedModel] || requestedModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 💰 检查账户使用额度(基于实时统计数据)
|
||||||
|
async checkQuotaUsage(accountId) {
|
||||||
|
try {
|
||||||
|
// 获取实时的使用统计(包含费用)
|
||||||
|
const usageStats = await redis.getAccountUsageStats(accountId)
|
||||||
|
const currentDailyCost = usageStats.daily.cost || 0
|
||||||
|
|
||||||
|
// 获取账户配置
|
||||||
|
const accountData = await this.getAccount(accountId)
|
||||||
|
if (!accountData) {
|
||||||
|
logger.warn(`Account not found: ${accountId}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析额度配置,确保数值有效
|
||||||
|
const dailyQuota = parseFloat(accountData.dailyQuota || '0')
|
||||||
|
if (isNaN(dailyQuota) || dailyQuota <= 0) {
|
||||||
|
// 没有设置有效额度,无需检查
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已经因额度停用(避免重复操作)
|
||||||
|
if (!accountData.isActive && accountData.quotaStoppedAt) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否超过额度限制
|
||||||
|
if (currentDailyCost >= dailyQuota) {
|
||||||
|
// 使用原子操作避免竞态条件 - 再次检查是否已设置quotaStoppedAt
|
||||||
|
const client = redis.getClientSafe()
|
||||||
|
const accountKey = `${this.ACCOUNT_KEY_PREFIX}${accountId}`
|
||||||
|
|
||||||
|
// double-check locking pattern - 检查quotaStoppedAt而不是status
|
||||||
|
const existingQuotaStop = await client.hget(accountKey, 'quotaStoppedAt')
|
||||||
|
if (existingQuotaStop) {
|
||||||
|
return // 已经被其他进程处理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超过额度,停用账户
|
||||||
|
const updates = {
|
||||||
|
isActive: false,
|
||||||
|
quotaStoppedAt: new Date().toISOString(),
|
||||||
|
errorMessage: `Daily quota exceeded: $${currentDailyCost.toFixed(2)} / $${dailyQuota.toFixed(2)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有当前状态是active时才改为quota_exceeded
|
||||||
|
// 如果是rate_limited等其他状态,保持原状态不变
|
||||||
|
const currentStatus = await client.hget(accountKey, 'status')
|
||||||
|
if (currentStatus === 'active') {
|
||||||
|
updates.status = 'quota_exceeded'
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.updateAccount(accountId, updates)
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
`💰 Account ${accountId} exceeded daily quota: $${currentDailyCost.toFixed(2)} / $${dailyQuota.toFixed(2)}`
|
||||||
|
)
|
||||||
|
|
||||||
|
// 发送webhook通知
|
||||||
|
try {
|
||||||
|
const webhookNotifier = require('../utils/webhookNotifier')
|
||||||
|
await webhookNotifier.sendAccountAnomalyNotification({
|
||||||
|
accountId,
|
||||||
|
accountName: accountData.name || 'Unknown Account',
|
||||||
|
platform: 'claude-console',
|
||||||
|
status: 'quota_exceeded',
|
||||||
|
errorCode: 'CLAUDE_CONSOLE_QUOTA_EXCEEDED',
|
||||||
|
reason: `Daily quota exceeded: $${currentDailyCost.toFixed(2)} / $${dailyQuota.toFixed(2)}`
|
||||||
|
})
|
||||||
|
} catch (webhookError) {
|
||||||
|
logger.error('Failed to send webhook notification for quota exceeded:', webhookError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`💰 Quota check for account ${accountId}: $${currentDailyCost.toFixed(4)} / $${dailyQuota.toFixed(2)}`
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to check quota usage:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔄 重置账户每日使用量(恢复因额度停用的账户)
|
||||||
|
async resetDailyUsage(accountId) {
|
||||||
|
try {
|
||||||
|
const accountData = await this.getAccount(accountId)
|
||||||
|
if (!accountData) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = redis.getDateStringInTimezone()
|
||||||
|
const updates = {
|
||||||
|
lastResetDate: today
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果账户是因为超额被停用的,恢复账户
|
||||||
|
// 注意:状态可能是 quota_exceeded 或 rate_limited(如果429错误时也超额了)
|
||||||
|
if (
|
||||||
|
accountData.quotaStoppedAt &&
|
||||||
|
accountData.isActive === false &&
|
||||||
|
(accountData.status === 'quota_exceeded' || accountData.status === 'rate_limited')
|
||||||
|
) {
|
||||||
|
updates.isActive = true
|
||||||
|
updates.status = 'active'
|
||||||
|
updates.errorMessage = ''
|
||||||
|
updates.quotaStoppedAt = ''
|
||||||
|
|
||||||
|
// 如果是rate_limited状态,也清除限流相关字段
|
||||||
|
if (accountData.status === 'rate_limited') {
|
||||||
|
const client = redis.getClientSafe()
|
||||||
|
const accountKey = `${this.ACCOUNT_KEY_PREFIX}${accountId}`
|
||||||
|
await client.hdel(accountKey, 'rateLimitedAt', 'rateLimitStatus')
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`✅ Restored account ${accountId} after daily reset (was ${accountData.status})`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.updateAccount(accountId, updates)
|
||||||
|
|
||||||
|
logger.debug(`🔄 Reset daily usage for account ${accountId}`)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to reset daily usage:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔄 重置所有账户的每日使用量
|
||||||
|
async resetAllDailyUsage() {
|
||||||
|
try {
|
||||||
|
const accounts = await this.getAllAccounts()
|
||||||
|
// 与统计一致使用配置时区日期
|
||||||
|
const today = redis.getDateStringInTimezone()
|
||||||
|
let resetCount = 0
|
||||||
|
|
||||||
|
for (const account of accounts) {
|
||||||
|
// 只重置需要重置的账户
|
||||||
|
if (account.lastResetDate !== today) {
|
||||||
|
await this.resetDailyUsage(account.id)
|
||||||
|
resetCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success(`✅ Reset daily usage for ${resetCount} Claude Console accounts`)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to reset all daily usage:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📊 获取账户使用统计(基于实时数据)
|
||||||
|
async getAccountUsageStats(accountId) {
|
||||||
|
try {
|
||||||
|
// 获取实时的使用统计(包含费用)
|
||||||
|
const usageStats = await redis.getAccountUsageStats(accountId)
|
||||||
|
const currentDailyCost = usageStats.daily.cost || 0
|
||||||
|
|
||||||
|
// 获取账户配置
|
||||||
|
const accountData = await this.getAccount(accountId)
|
||||||
|
if (!accountData) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const dailyQuota = parseFloat(accountData.dailyQuota || '0')
|
||||||
|
|
||||||
|
return {
|
||||||
|
dailyQuota,
|
||||||
|
dailyUsage: currentDailyCost, // 使用实时计算的费用
|
||||||
|
remainingQuota: dailyQuota > 0 ? Math.max(0, dailyQuota - currentDailyCost) : null,
|
||||||
|
usagePercentage: dailyQuota > 0 ? (currentDailyCost / dailyQuota) * 100 : 0,
|
||||||
|
lastResetDate: accountData.lastResetDate,
|
||||||
|
quotaStoppedAt: accountData.quotaStoppedAt,
|
||||||
|
isQuotaExceeded: dailyQuota > 0 && currentDailyCost >= dailyQuota,
|
||||||
|
// 额外返回完整的使用统计
|
||||||
|
fullUsageStats: usageStats
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get account usage stats:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new ClaudeConsoleAccountService()
|
module.exports = new ClaudeConsoleAccountService()
|
||||||
|
|||||||
@@ -181,6 +181,11 @@ class ClaudeConsoleRelayService {
|
|||||||
await claudeConsoleAccountService.markAccountUnauthorized(accountId)
|
await claudeConsoleAccountService.markAccountUnauthorized(accountId)
|
||||||
} else if (response.status === 429) {
|
} else if (response.status === 429) {
|
||||||
logger.warn(`🚫 Rate limit detected for Claude Console account ${accountId}`)
|
logger.warn(`🚫 Rate limit detected for Claude Console account ${accountId}`)
|
||||||
|
// 收到429先检查是否因为超过了手动配置的每日额度
|
||||||
|
await claudeConsoleAccountService.checkQuotaUsage(accountId).catch((err) => {
|
||||||
|
logger.error('❌ Failed to check quota after 429 error:', err)
|
||||||
|
})
|
||||||
|
|
||||||
await claudeConsoleAccountService.markAccountRateLimited(accountId)
|
await claudeConsoleAccountService.markAccountRateLimited(accountId)
|
||||||
} else if (response.status === 529) {
|
} else if (response.status === 529) {
|
||||||
logger.warn(`🚫 Overload error detected for Claude Console account ${accountId}`)
|
logger.warn(`🚫 Overload error detected for Claude Console account ${accountId}`)
|
||||||
@@ -377,6 +382,10 @@ class ClaudeConsoleRelayService {
|
|||||||
claudeConsoleAccountService.markAccountUnauthorized(accountId)
|
claudeConsoleAccountService.markAccountUnauthorized(accountId)
|
||||||
} else if (response.status === 429) {
|
} else if (response.status === 429) {
|
||||||
claudeConsoleAccountService.markAccountRateLimited(accountId)
|
claudeConsoleAccountService.markAccountRateLimited(accountId)
|
||||||
|
// 检查是否因为超过每日额度
|
||||||
|
claudeConsoleAccountService.checkQuotaUsage(accountId).catch((err) => {
|
||||||
|
logger.error('❌ Failed to check quota after 429 error:', err)
|
||||||
|
})
|
||||||
} else if (response.status === 529) {
|
} else if (response.status === 529) {
|
||||||
claudeConsoleAccountService.markAccountOverloaded(accountId)
|
claudeConsoleAccountService.markAccountOverloaded(accountId)
|
||||||
}
|
}
|
||||||
@@ -589,6 +598,10 @@ class ClaudeConsoleRelayService {
|
|||||||
claudeConsoleAccountService.markAccountUnauthorized(accountId)
|
claudeConsoleAccountService.markAccountUnauthorized(accountId)
|
||||||
} else if (error.response.status === 429) {
|
} else if (error.response.status === 429) {
|
||||||
claudeConsoleAccountService.markAccountRateLimited(accountId)
|
claudeConsoleAccountService.markAccountRateLimited(accountId)
|
||||||
|
// 检查是否因为超过每日额度
|
||||||
|
claudeConsoleAccountService.checkQuotaUsage(accountId).catch((err) => {
|
||||||
|
logger.error('❌ Failed to check quota after 429 error:', err)
|
||||||
|
})
|
||||||
} else if (error.response.status === 529) {
|
} else if (error.response.status === 529) {
|
||||||
claudeConsoleAccountService.markAccountOverloaded(accountId)
|
claudeConsoleAccountService.markAccountOverloaded(accountId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,10 +209,20 @@ class UnifiedClaudeScheduler {
|
|||||||
boundConsoleAccount.isActive === true &&
|
boundConsoleAccount.isActive === true &&
|
||||||
boundConsoleAccount.status === 'active'
|
boundConsoleAccount.status === 'active'
|
||||||
) {
|
) {
|
||||||
|
// 主动触发一次额度检查
|
||||||
|
try {
|
||||||
|
await claudeConsoleAccountService.checkQuotaUsage(boundConsoleAccount.id)
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// 检查限流状态和额度状态
|
||||||
const isRateLimited = await claudeConsoleAccountService.isAccountRateLimited(
|
const isRateLimited = await claudeConsoleAccountService.isAccountRateLimited(
|
||||||
boundConsoleAccount.id
|
boundConsoleAccount.id
|
||||||
)
|
)
|
||||||
if (!isRateLimited) {
|
const isQuotaExceeded = await claudeConsoleAccountService.isAccountQuotaExceeded(
|
||||||
|
boundConsoleAccount.id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isRateLimited && !isQuotaExceeded) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using bound dedicated Claude Console account: ${boundConsoleAccount.name} (${apiKeyData.claudeConsoleAccountId})`
|
`🎯 Using bound dedicated Claude Console account: ${boundConsoleAccount.name} (${apiKeyData.claudeConsoleAccountId})`
|
||||||
)
|
)
|
||||||
@@ -358,9 +368,16 @@ class UnifiedClaudeScheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否被限流
|
// 主动触发一次额度检查,确保状态即时生效
|
||||||
|
try {
|
||||||
|
await claudeConsoleAccountService.checkQuotaUsage(account.id)
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// 检查是否被限流或额度超限
|
||||||
const isRateLimited = await claudeConsoleAccountService.isAccountRateLimited(account.id)
|
const isRateLimited = await claudeConsoleAccountService.isAccountRateLimited(account.id)
|
||||||
if (!isRateLimited) {
|
const isQuotaExceeded = await claudeConsoleAccountService.isAccountQuotaExceeded(account.id)
|
||||||
|
|
||||||
|
if (!isRateLimited && !isQuotaExceeded) {
|
||||||
availableAccounts.push({
|
availableAccounts.push({
|
||||||
...account,
|
...account,
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
@@ -372,7 +389,12 @@ class UnifiedClaudeScheduler {
|
|||||||
`✅ Added Claude Console account to available pool: ${account.name} (priority: ${account.priority})`
|
`✅ Added Claude Console account to available pool: ${account.name} (priority: ${account.priority})`
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`⚠️ Claude Console account ${account.name} is rate limited`)
|
if (isRateLimited) {
|
||||||
|
logger.warn(`⚠️ Claude Console account ${account.name} is rate limited`)
|
||||||
|
}
|
||||||
|
if (isQuotaExceeded) {
|
||||||
|
logger.warn(`💰 Claude Console account ${account.name} quota exceeded`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -475,10 +497,17 @@ class UnifiedClaudeScheduler {
|
|||||||
logger.info(`🚫 Claude Console account ${accountId} is not schedulable`)
|
logger.info(`🚫 Claude Console account ${accountId} is not schedulable`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// 检查是否被限流
|
// 主动触发一次额度检查
|
||||||
|
try {
|
||||||
|
await claudeConsoleAccountService.checkQuotaUsage(accountId)
|
||||||
|
} catch (e) {}
|
||||||
|
// 检查是否被限流或额度超限
|
||||||
if (await claudeConsoleAccountService.isAccountRateLimited(accountId)) {
|
if (await claudeConsoleAccountService.isAccountRateLimited(accountId)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if (await claudeConsoleAccountService.isAccountQuotaExceeded(accountId)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
// 检查是否未授权(401错误)
|
// 检查是否未授权(401错误)
|
||||||
if (account.status === 'unauthorized') {
|
if (account.status === 'unauthorized') {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -658,6 +658,41 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 额度管理字段 -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
每日额度限制 ($)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model.number="form.dailyQuota"
|
||||||
|
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||||
|
min="0"
|
||||||
|
placeholder="0 表示不限制"
|
||||||
|
step="0.01"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
设置每日使用额度,0 表示不限制
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
额度重置时间
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="form.quotaResetTime"
|
||||||
|
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||||
|
placeholder="00:00"
|
||||||
|
type="time"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
每日自动重置额度的时间
|
||||||
|
</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"
|
||||||
>模型映射表 (可选)</label
|
>模型映射表 (可选)</label
|
||||||
@@ -1544,6 +1579,75 @@
|
|||||||
<p class="mt-1 text-xs text-gray-500">留空表示不更新 API Key</p>
|
<p class="mt-1 text-xs text-gray-500">留空表示不更新 API Key</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 额度管理字段 -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
每日额度限制 ($)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model.number="form.dailyQuota"
|
||||||
|
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||||
|
min="0"
|
||||||
|
placeholder="0 表示不限制"
|
||||||
|
step="0.01"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
设置每日使用额度,0 表示不限制
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
额度重置时间
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="form.quotaResetTime"
|
||||||
|
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||||
|
placeholder="00:00"
|
||||||
|
type="time"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">每日自动重置额度的时间</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 当前使用情况(仅编辑模式显示) -->
|
||||||
|
<div
|
||||||
|
v-if="isEdit && form.dailyQuota > 0"
|
||||||
|
class="rounded-lg bg-gray-50 p-4 dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<div class="mb-2 flex items-center justify-between">
|
||||||
|
<span class="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
今日使用情况
|
||||||
|
</span>
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
${{ calculateCurrentUsage().toFixed(4) }} / ${{ form.dailyQuota.toFixed(2) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="relative h-2 w-full rounded-full bg-gray-200 dark:bg-gray-700">
|
||||||
|
<div
|
||||||
|
class="absolute left-0 top-0 h-full rounded-full transition-all"
|
||||||
|
:class="
|
||||||
|
usagePercentage >= 90
|
||||||
|
? 'bg-red-500'
|
||||||
|
: usagePercentage >= 70
|
||||||
|
? 'bg-yellow-500'
|
||||||
|
: 'bg-green-500'
|
||||||
|
"
|
||||||
|
:style="{ width: `${Math.min(usagePercentage, 100)}%` }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 flex items-center justify-between text-xs">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">
|
||||||
|
剩余: ${{ Math.max(0, form.dailyQuota - calculateCurrentUsage()).toFixed(2) }}
|
||||||
|
</span>
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">
|
||||||
|
{{ usagePercentage.toFixed(1) }}% 已使用
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-3 block text-sm font-semibold text-gray-700"
|
<label class="mb-3 block text-sm font-semibold text-gray-700"
|
||||||
>模型映射表 (可选)</label
|
>模型映射表 (可选)</label
|
||||||
@@ -2100,6 +2204,10 @@ const form = ref({
|
|||||||
userAgent: props.account?.userAgent || '',
|
userAgent: props.account?.userAgent || '',
|
||||||
enableRateLimit: props.account ? props.account.rateLimitDuration > 0 : true,
|
enableRateLimit: props.account ? props.account.rateLimitDuration > 0 : true,
|
||||||
rateLimitDuration: props.account?.rateLimitDuration || 60,
|
rateLimitDuration: props.account?.rateLimitDuration || 60,
|
||||||
|
// 额度管理字段
|
||||||
|
dailyQuota: props.account?.dailyQuota || 0,
|
||||||
|
dailyUsage: props.account?.dailyUsage || 0,
|
||||||
|
quotaResetTime: props.account?.quotaResetTime || '00:00',
|
||||||
// Bedrock 特定字段
|
// Bedrock 特定字段
|
||||||
accessKeyId: props.account?.accessKeyId || '',
|
accessKeyId: props.account?.accessKeyId || '',
|
||||||
secretAccessKey: props.account?.secretAccessKey || '',
|
secretAccessKey: props.account?.secretAccessKey || '',
|
||||||
@@ -2162,6 +2270,45 @@ const canExchangeSetupToken = computed(() => {
|
|||||||
return setupTokenAuthUrl.value && setupTokenAuthCode.value.trim()
|
return setupTokenAuthUrl.value && setupTokenAuthCode.value.trim()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 获取当前使用量(实时)
|
||||||
|
const calculateCurrentUsage = () => {
|
||||||
|
// 如果不是编辑模式或没有账户ID,返回0
|
||||||
|
if (!isEdit.value || !props.account?.id) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已经加载了今日使用数据,直接使用
|
||||||
|
if (typeof form.value.dailyUsage === 'number') {
|
||||||
|
return form.value.dailyUsage
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算额度使用百分比
|
||||||
|
const usagePercentage = computed(() => {
|
||||||
|
if (!form.value.dailyQuota || form.value.dailyQuota <= 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const currentUsage = calculateCurrentUsage()
|
||||||
|
return (currentUsage / form.value.dailyQuota) * 100
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载账户今日使用情况
|
||||||
|
const loadAccountUsage = async () => {
|
||||||
|
if (!isEdit.value || !props.account?.id) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(`/admin/claude-console-accounts/${props.account.id}/usage`)
|
||||||
|
if (response) {
|
||||||
|
// 更新表单中的使用量数据
|
||||||
|
form.value.dailyUsage = response.dailyUsage || 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load account usage:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// // 计算是否可以创建
|
// // 计算是否可以创建
|
||||||
// const canCreate = computed(() => {
|
// const canCreate = computed(() => {
|
||||||
// if (form.value.addType === 'manual') {
|
// if (form.value.addType === 'manual') {
|
||||||
@@ -2601,6 +2748,9 @@ const createAccount = async () => {
|
|||||||
data.userAgent = form.value.userAgent || null
|
data.userAgent = form.value.userAgent || null
|
||||||
// 如果不启用限流,传递 0 表示不限流
|
// 如果不启用限流,传递 0 表示不限流
|
||||||
data.rateLimitDuration = form.value.enableRateLimit ? form.value.rateLimitDuration || 60 : 0
|
data.rateLimitDuration = form.value.enableRateLimit ? form.value.rateLimitDuration || 60 : 0
|
||||||
|
// 额度管理字段
|
||||||
|
data.dailyQuota = form.value.dailyQuota || 0
|
||||||
|
data.quotaResetTime = form.value.quotaResetTime || '00:00'
|
||||||
} else if (form.value.platform === 'bedrock') {
|
} else if (form.value.platform === 'bedrock') {
|
||||||
// Bedrock 账户特定数据 - 构造 awsCredentials 对象
|
// Bedrock 账户特定数据 - 构造 awsCredentials 对象
|
||||||
data.awsCredentials = {
|
data.awsCredentials = {
|
||||||
@@ -2798,6 +2948,9 @@ const updateAccount = async () => {
|
|||||||
data.userAgent = form.value.userAgent || null
|
data.userAgent = form.value.userAgent || null
|
||||||
// 如果不启用限流,传递 0 表示不限流
|
// 如果不启用限流,传递 0 表示不限流
|
||||||
data.rateLimitDuration = form.value.enableRateLimit ? form.value.rateLimitDuration || 60 : 0
|
data.rateLimitDuration = form.value.enableRateLimit ? form.value.rateLimitDuration || 60 : 0
|
||||||
|
// 额度管理字段
|
||||||
|
data.dailyQuota = form.value.dailyQuota || 0
|
||||||
|
data.quotaResetTime = form.value.quotaResetTime || '00:00'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bedrock 特定更新
|
// Bedrock 特定更新
|
||||||
@@ -3207,7 +3360,16 @@ watch(
|
|||||||
// Azure OpenAI 特定字段
|
// Azure OpenAI 特定字段
|
||||||
azureEndpoint: newAccount.azureEndpoint || '',
|
azureEndpoint: newAccount.azureEndpoint || '',
|
||||||
apiVersion: newAccount.apiVersion || '',
|
apiVersion: newAccount.apiVersion || '',
|
||||||
deploymentName: newAccount.deploymentName || ''
|
deploymentName: newAccount.deploymentName || '',
|
||||||
|
// 额度管理字段
|
||||||
|
dailyQuota: newAccount.dailyQuota || 0,
|
||||||
|
dailyUsage: newAccount.dailyUsage || 0,
|
||||||
|
quotaResetTime: newAccount.quotaResetTime || '00:00'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是Claude Console账户,加载实时使用情况
|
||||||
|
if (newAccount.platform === 'claude-console') {
|
||||||
|
loadAccountUsage()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是分组类型,加载分组ID
|
// 如果是分组类型,加载分组ID
|
||||||
@@ -3287,6 +3449,10 @@ const clearUnifiedCache = async () => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 获取Claude Code统一User-Agent信息
|
// 获取Claude Code统一User-Agent信息
|
||||||
fetchUnifiedUserAgent()
|
fetchUnifiedUserAgent()
|
||||||
|
// 如果是编辑模式且是Claude Console账户,加载使用情况
|
||||||
|
if (isEdit.value && props.account?.platform === 'claude-console') {
|
||||||
|
loadAccountUsage()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听平台变化,当切换到Claude平台时获取统一User-Agent信息
|
// 监听平台变化,当切换到Claude平台时获取统一User-Agent信息
|
||||||
|
|||||||
@@ -584,6 +584,44 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Claude Console: 显示每日额度使用进度 -->
|
||||||
|
<div v-else-if="account.platform === 'claude-console'" class="space-y-2">
|
||||||
|
<div v-if="Number(account.dailyQuota) > 0">
|
||||||
|
<div class="flex items-center justify-between text-xs">
|
||||||
|
<span class="text-gray-600 dark:text-gray-300">额度进度</span>
|
||||||
|
<span class="font-medium text-gray-700 dark:text-gray-200">
|
||||||
|
{{ getQuotaUsagePercent(account).toFixed(1) }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="h-2 w-24 rounded-full bg-gray-200 dark:bg-gray-700">
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'h-2 rounded-full transition-all duration-300',
|
||||||
|
getQuotaBarClass(getQuotaUsagePercent(account))
|
||||||
|
]"
|
||||||
|
:style="{ width: Math.min(100, getQuotaUsagePercent(account)) + '%' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="min-w-[32px] text-xs font-medium text-gray-700 dark:text-gray-200"
|
||||||
|
>
|
||||||
|
${{ formatCost(account.usage?.daily?.cost || 0) }} / ${{
|
||||||
|
Number(account.dailyQuota).toFixed(2)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
剩余 ${{ formatRemainingQuota(account) }}
|
||||||
|
<span class="ml-2 text-gray-400"
|
||||||
|
>重置 {{ account.quotaResetTime || '00:00' }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-sm text-gray-400">
|
||||||
|
<i class="fas fa-minus" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-else-if="account.platform === 'claude'" class="text-sm text-gray-400">
|
<div v-else-if="account.platform === 'claude'" class="text-sm text-gray-400">
|
||||||
<i class="fas fa-minus" />
|
<i class="fas fa-minus" />
|
||||||
</div>
|
</div>
|
||||||
@@ -1788,6 +1826,29 @@ const formatCost = (cost) => {
|
|||||||
return cost.toFixed(2)
|
return cost.toFixed(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 额度使用百分比(Claude Console)
|
||||||
|
const getQuotaUsagePercent = (account) => {
|
||||||
|
const used = Number(account?.usage?.daily?.cost || 0)
|
||||||
|
const quota = Number(account?.dailyQuota || 0)
|
||||||
|
if (!quota || quota <= 0) return 0
|
||||||
|
return (used / quota) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// 额度进度条颜色(Claude Console)
|
||||||
|
const getQuotaBarClass = (percent) => {
|
||||||
|
if (percent >= 90) return 'bg-red-500'
|
||||||
|
if (percent >= 70) return 'bg-yellow-500'
|
||||||
|
return 'bg-green-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 剩余额度(Claude Console)
|
||||||
|
const formatRemainingQuota = (account) => {
|
||||||
|
const used = Number(account?.usage?.daily?.cost || 0)
|
||||||
|
const quota = Number(account?.dailyQuota || 0)
|
||||||
|
if (!quota || quota <= 0) return '0.00'
|
||||||
|
return Math.max(0, quota - used).toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
// 计算每日费用(使用后端返回的精确费用数据)
|
// 计算每日费用(使用后端返回的精确费用数据)
|
||||||
const calculateDailyCost = (account) => {
|
const calculateDailyCost = (account) => {
|
||||||
if (!account.usage || !account.usage.daily) return '0.0000'
|
if (!account.usage || !account.usage.daily) return '0.0000'
|
||||||
|
|||||||
Reference in New Issue
Block a user