mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
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:
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user