diff --git a/VERSION b/VERSION index c3c6aefa..48cd0cc3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.127 +1.1.128 diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index 1c37262b..86e595e5 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -1813,6 +1813,20 @@ class ClaudeAccountService { // 保存更新后的账户数据 await redis.setClaudeAccount(accountId, updatedAccountData) + // 显式从 Redis 中删除这些字段(因为 HSET 不会删除现有字段) + const fieldsToDelete = [ + 'errorMessage', + 'unauthorizedAt', + 'blockedAt', + 'rateLimitedAt', + 'rateLimitStatus', + 'rateLimitEndAt', + 'tempErrorAt', + 'sessionWindowStart', + 'sessionWindowEnd' + ] + await redis.client.hdel(`claude:account:${accountId}`, ...fieldsToDelete) + // 清除401错误计数 const errorKey = `claude_account:${accountId}:401_errors` await redis.client.del(errorKey) @@ -1864,6 +1878,10 @@ class ClaudeAccountService { delete account.errorMessage delete account.tempErrorAt await redis.setClaudeAccount(account.id, account) + + // 显式从 Redis 中删除这些字段(因为 HSET 不会删除现有字段) + await redis.client.hdel(`claude:account:${account.id}`, 'errorMessage', 'tempErrorAt') + // 同时清除500错误计数 await this.clearInternalErrors(account.id) cleanedCount++ @@ -1951,6 +1969,52 @@ class ClaudeAccountService { // 保存更新后的账户数据 await redis.setClaudeAccount(accountId, updatedAccountData) + // 设置 5 分钟后自动恢复(一次性定时器) + setTimeout( + async () => { + try { + const account = await redis.getClaudeAccount(accountId) + if (account && account.status === 'temp_error' && account.tempErrorAt) { + // 验证是否确实过了 5 分钟(防止重复定时器) + const tempErrorAt = new Date(account.tempErrorAt) + const now = new Date() + const minutesSince = (now - tempErrorAt) / (1000 * 60) + + if (minutesSince >= 5) { + // 恢复账户 + account.status = 'active' + account.schedulable = 'true' + delete account.errorMessage + delete account.tempErrorAt + + await redis.setClaudeAccount(accountId, account) + + // 显式删除 Redis 字段 + await redis.client.hdel( + `claude:account:${accountId}`, + 'errorMessage', + 'tempErrorAt' + ) + + // 清除 500 错误计数 + await this.clearInternalErrors(accountId) + + logger.success( + `✅ Auto-recovered temp_error after 5 minutes: ${account.name} (${accountId})` + ) + } else { + logger.debug( + `⏰ Temp error timer triggered but only ${minutesSince.toFixed(1)} minutes passed for ${account.name} (${accountId})` + ) + } + } + } catch (error) { + logger.error(`❌ Failed to auto-recover temp_error account ${accountId}:`, error) + } + }, + 6 * 60 * 1000 + ) // 6 分钟后执行,确保已过 5 分钟 + // 如果有sessionHash,删除粘性会话映射 if (sessionHash) { await redis.client.del(`sticky_session:${sessionHash}`) diff --git a/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue index 8fe44e0a..01fae1c5 100644 --- a/web/admin-spa/src/views/AccountsView.vue +++ b/web/admin-spa/src/views/AccountsView.vue @@ -376,9 +376,11 @@ ? 'bg-orange-100 text-orange-800' : account.status === 'unauthorized' ? 'bg-red-100 text-red-800' - : account.isActive - ? 'bg-green-100 text-green-800' - : 'bg-red-100 text-red-800' + : account.status === 'temp_error' + ? 'bg-orange-100 text-orange-800' + : account.isActive + ? 'bg-green-100 text-green-800' + : 'bg-red-100 text-red-800' ]" >
{{ @@ -398,9 +402,11 @@ ? '已封锁' : account.status === 'unauthorized' ? '异常' - : account.isActive - ? '正常' - : '异常' + : account.status === 'temp_error' + ? '临时异常' + : account.isActive + ? '正常' + : '异常' }} { if (account.status === 'unauthorized') { return '认证失败(401错误)' } + if (account.status === 'temp_error' && account.errorMessage) { + return account.errorMessage + } if (account.status === 'error' && account.errorMessage) { return account.errorMessage } @@ -1668,6 +1677,8 @@ const getAccountStatusText = (account) => { account.rateLimitStatus === 'limited' ) return '限流中' + // 检查是否临时错误 + if (account.status === 'temp_error') return '临时异常' // 检查是否错误 if (account.status === 'error' || !account.isActive) return '错误' // 检查是否可调度 @@ -1692,6 +1703,9 @@ const getAccountStatusClass = (account) => { ) { return 'bg-orange-100 text-orange-800' } + if (account.status === 'temp_error') { + return 'bg-orange-100 text-orange-800' + } if (account.status === 'error' || !account.isActive) { return 'bg-red-100 text-red-800' } @@ -1717,6 +1731,9 @@ const getAccountStatusDotClass = (account) => { ) { return 'bg-orange-500' } + if (account.status === 'temp_error') { + return 'bg-orange-500' + } if (account.status === 'error' || !account.isActive) { return 'bg-red-500' }