feat: add comprehensive 401 error handling and account status management

- Add 401 error detection and automatic account suspension after 3 consecutive failures
- Implement account status reset functionality for clearing all error states
- Enhance admin interface with status reset controls and improved status display
- Upgrade service management script with backup protection and retry mechanisms
- Add mandatory code formatting requirements using Prettier
- Improve account selector with detailed status information and color coding

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-08-08 00:35:26 +08:00
parent d8dd2faf72
commit 31bdb4aa8c
9 changed files with 456 additions and 31 deletions

View File

@@ -1341,6 +1341,21 @@ router.post('/claude-accounts/:accountId/refresh', authenticateAdmin, async (req
}
})
// 重置Claude账户状态清除所有异常状态
router.post('/claude-accounts/:accountId/reset-status', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const result = await claudeAccountService.resetAccountStatus(accountId)
logger.success(`✅ Admin reset status for Claude account: ${accountId}`)
return res.json({ success: true, data: result })
} catch (error) {
logger.error('❌ Failed to reset Claude account status:', error)
return res.status(500).json({ error: 'Failed to reset status', message: error.message })
}
})
// 切换Claude账户调度状态
router.put(
'/claude-accounts/:accountId/toggle-schedulable',

View File

@@ -1194,6 +1194,99 @@ class ClaudeAccountService {
}
}
}
// 🚫 标记账户为未授权状态401错误
async markAccountUnauthorized(accountId, sessionHash = null) {
try {
const accountData = await redis.getClaudeAccount(accountId)
if (!accountData || Object.keys(accountData).length === 0) {
throw new Error('Account not found')
}
// 更新账户状态
const updatedAccountData = { ...accountData }
updatedAccountData.status = 'unauthorized'
updatedAccountData.schedulable = 'false' // 设置为不可调度
updatedAccountData.errorMessage = 'Account unauthorized (401 errors detected)'
updatedAccountData.unauthorizedAt = new Date().toISOString()
// 保存更新后的账户数据
await redis.setClaudeAccount(accountId, updatedAccountData)
// 如果有sessionHash删除粘性会话映射
if (sessionHash) {
await redis.client.del(`sticky_session:${sessionHash}`)
logger.info(`🗑️ Deleted sticky session mapping for hash: ${sessionHash}`)
}
logger.warn(
`⚠️ Account ${accountData.name} (${accountId}) marked as unauthorized and disabled for scheduling`
)
return { success: true }
} catch (error) {
logger.error(`❌ Failed to mark account ${accountId} as unauthorized:`, error)
throw error
}
}
// 🔄 重置账户所有异常状态
async resetAccountStatus(accountId) {
try {
const accountData = await redis.getClaudeAccount(accountId)
if (!accountData || Object.keys(accountData).length === 0) {
throw new Error('Account not found')
}
// 重置账户状态
const updatedAccountData = { ...accountData }
// 根据是否有有效的accessToken来设置status
if (updatedAccountData.accessToken) {
updatedAccountData.status = 'active'
} else {
updatedAccountData.status = 'created'
}
// 恢复可调度状态
updatedAccountData.schedulable = 'true'
// 清除错误相关字段
delete updatedAccountData.errorMessage
delete updatedAccountData.unauthorizedAt
delete updatedAccountData.rateLimitedAt
delete updatedAccountData.rateLimitStatus
delete updatedAccountData.rateLimitEndAt
// 保存更新后的账户数据
await redis.setClaudeAccount(accountId, updatedAccountData)
// 清除401错误计数
const errorKey = `claude_account:${accountId}:401_errors`
await redis.client.del(errorKey)
// 清除限流状态(如果存在)
const rateLimitKey = `ratelimit:${accountId}`
await redis.client.del(rateLimitKey)
logger.info(
`✅ Successfully reset all error states for account ${accountData.name} (${accountId})`
)
return {
success: true,
account: {
id: accountId,
name: accountData.name,
status: updatedAccountData.status,
schedulable: updatedAccountData.schedulable === 'true'
}
}
} catch (error) {
logger.error(`❌ Failed to reset account status for ${accountId}:`, error)
throw error
}
}
}
module.exports = new ClaudeAccountService()

View File

@@ -169,13 +169,37 @@ class ClaudeRelayService {
clientResponse.removeListener('close', handleClientDisconnect)
}
// 检查响应是否为限流错误
// 检查响应是否为限流错误或认证错误
if (response.statusCode !== 200 && response.statusCode !== 201) {
let isRateLimited = false
let rateLimitResetTimestamp = null
// 检查是否为401状态码未授权
if (response.statusCode === 401) {
logger.warn(`🔐 Unauthorized error (401) detected for account ${accountId}`)
// 记录401错误
await this.recordUnauthorizedError(accountId)
// 检查是否需要标记为异常连续3次401
const errorCount = await this.getUnauthorizedErrorCount(accountId)
logger.info(
`🔐 Account ${accountId} has ${errorCount} consecutive 401 errors in the last 5 minutes`
)
if (errorCount >= 3) {
logger.error(
`❌ Account ${accountId} exceeded 401 error threshold (${errorCount} errors), marking as unauthorized`
)
await unifiedClaudeScheduler.markAccountUnauthorized(
accountId,
accountType,
sessionHash
)
}
}
// 检查是否为429状态码
if (response.statusCode === 429) {
else if (response.statusCode === 429) {
isRateLimited = true
// 提取限流重置时间戳
@@ -224,6 +248,8 @@ class ClaudeRelayService {
)
}
} else if (response.statusCode === 200 || response.statusCode === 201) {
// 请求成功清除401错误计数
await this.clearUnauthorizedErrors(accountId)
// 如果请求成功,检查并移除限流状态
const isRateLimited = await unifiedClaudeScheduler.isAccountRateLimited(
accountId,
@@ -1295,6 +1321,49 @@ class ClaudeRelayService {
throw lastError
}
// 🔐 记录401未授权错误
async recordUnauthorizedError(accountId) {
try {
const key = `claude_account:${accountId}:401_errors`
const redis = require('../models/redis')
// 增加错误计数设置5分钟过期时间
await redis.client.incr(key)
await redis.client.expire(key, 300) // 5分钟
logger.info(`📝 Recorded 401 error for account ${accountId}`)
} catch (error) {
logger.error(`❌ Failed to record 401 error for account ${accountId}:`, error)
}
}
// 🔍 获取401错误计数
async getUnauthorizedErrorCount(accountId) {
try {
const key = `claude_account:${accountId}:401_errors`
const redis = require('../models/redis')
const count = await redis.client.get(key)
return parseInt(count) || 0
} catch (error) {
logger.error(`❌ Failed to get 401 error count for account ${accountId}:`, error)
return 0
}
}
// 🧹 清除401错误计数
async clearUnauthorizedErrors(accountId) {
try {
const key = `claude_account:${accountId}:401_errors`
const redis = require('../models/redis')
await redis.client.del(key)
logger.info(`✅ Cleared 401 error count for account ${accountId}`)
} catch (error) {
logger.error(`❌ Failed to clear 401 errors for account ${accountId}:`, error)
}
}
// 🎯 健康检查
async healthCheck() {
try {

View File

@@ -551,6 +551,35 @@ class UnifiedClaudeScheduler {
}
}
// 🚫 标记账户为未授权状态401错误
async markAccountUnauthorized(accountId, accountType, sessionHash = null) {
try {
// 只处理claude-official类型的账户不处理claude-console和gemini
if (accountType === 'claude-official') {
await claudeAccountService.markAccountUnauthorized(accountId, sessionHash)
// 删除会话映射
if (sessionHash) {
await this._deleteSessionMapping(sessionHash)
}
logger.warn(`🚫 Account ${accountId} marked as unauthorized due to consecutive 401 errors`)
} else {
logger.info(
` Skipping unauthorized marking for non-Claude OAuth account: ${accountId} (${accountType})`
)
}
return { success: true }
} catch (error) {
logger.error(
`❌ Failed to mark account as unauthorized: ${accountId} (${accountType})`,
error
)
throw error
}
}
// 🚫 标记Claude Console账户为封锁状态模型不支持
async blockConsoleAccount(accountId, reason) {
try {