mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
feat: 添加Claude账户403错误处理和封禁状态支持
- 新增Claude账户403错误自动检测和处理机制 - 区分Claude账户401未授权和403封禁两种错误状态 - 支持非流式和流式请求中的401/403错误处理 - 优化Claude账户错误处理代码,减少重复逻辑 - 支持前端显示不同的Claude账户错误状态和颜色 - 完善Claude账户异常Webhook通知错误码区分
This commit is contained in:
@@ -1695,9 +1695,31 @@ class ClaudeAccountService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🚫 标记账户为未授权状态(401错误)
|
// 🚫 通用的账户错误标记方法
|
||||||
async markAccountUnauthorized(accountId, sessionHash = null) {
|
async markAccountError(accountId, errorType, sessionHash = null) {
|
||||||
|
const ERROR_CONFIG = {
|
||||||
|
unauthorized: {
|
||||||
|
status: 'unauthorized',
|
||||||
|
errorMessage: 'Account unauthorized (401 errors detected)',
|
||||||
|
timestampField: 'unauthorizedAt',
|
||||||
|
errorCode: 'CLAUDE_OAUTH_UNAUTHORIZED',
|
||||||
|
logMessage: 'unauthorized'
|
||||||
|
},
|
||||||
|
blocked: {
|
||||||
|
status: 'blocked',
|
||||||
|
errorMessage: 'Account blocked (403 error detected - account may be suspended by Claude)',
|
||||||
|
timestampField: 'blockedAt',
|
||||||
|
errorCode: 'CLAUDE_OAUTH_BLOCKED',
|
||||||
|
logMessage: 'blocked'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const errorConfig = ERROR_CONFIG[errorType]
|
||||||
|
if (!errorConfig) {
|
||||||
|
throw new Error(`Unsupported error type: ${errorType}`)
|
||||||
|
}
|
||||||
|
|
||||||
const accountData = await redis.getClaudeAccount(accountId)
|
const accountData = await redis.getClaudeAccount(accountId)
|
||||||
if (!accountData || Object.keys(accountData).length === 0) {
|
if (!accountData || Object.keys(accountData).length === 0) {
|
||||||
throw new Error('Account not found')
|
throw new Error('Account not found')
|
||||||
@@ -1705,10 +1727,10 @@ class ClaudeAccountService {
|
|||||||
|
|
||||||
// 更新账户状态
|
// 更新账户状态
|
||||||
const updatedAccountData = { ...accountData }
|
const updatedAccountData = { ...accountData }
|
||||||
updatedAccountData.status = 'unauthorized'
|
updatedAccountData.status = errorConfig.status
|
||||||
updatedAccountData.schedulable = 'false' // 设置为不可调度
|
updatedAccountData.schedulable = 'false' // 设置为不可调度
|
||||||
updatedAccountData.errorMessage = 'Account unauthorized (401 errors detected)'
|
updatedAccountData.errorMessage = errorConfig.errorMessage
|
||||||
updatedAccountData.unauthorizedAt = new Date().toISOString()
|
updatedAccountData[errorConfig.timestampField] = new Date().toISOString()
|
||||||
|
|
||||||
// 保存更新后的账户数据
|
// 保存更新后的账户数据
|
||||||
await redis.setClaudeAccount(accountId, updatedAccountData)
|
await redis.setClaudeAccount(accountId, updatedAccountData)
|
||||||
@@ -1720,7 +1742,7 @@ class ClaudeAccountService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`⚠️ Account ${accountData.name} (${accountId}) marked as unauthorized and disabled for scheduling`
|
`⚠️ Account ${accountData.name} (${accountId}) marked as ${errorConfig.logMessage} and disabled for scheduling`
|
||||||
)
|
)
|
||||||
|
|
||||||
// 发送Webhook通知
|
// 发送Webhook通知
|
||||||
@@ -1730,9 +1752,10 @@ class ClaudeAccountService {
|
|||||||
accountId,
|
accountId,
|
||||||
accountName: accountData.name,
|
accountName: accountData.name,
|
||||||
platform: 'claude-oauth',
|
platform: 'claude-oauth',
|
||||||
status: 'unauthorized',
|
status: errorConfig.status,
|
||||||
errorCode: 'CLAUDE_OAUTH_UNAUTHORIZED',
|
errorCode: errorConfig.errorCode,
|
||||||
reason: 'Account unauthorized (401 errors detected)'
|
reason: errorConfig.errorMessage,
|
||||||
|
timestamp: getISOStringWithTimezone(new Date())
|
||||||
})
|
})
|
||||||
} catch (webhookError) {
|
} catch (webhookError) {
|
||||||
logger.error('Failed to send webhook notification:', webhookError)
|
logger.error('Failed to send webhook notification:', webhookError)
|
||||||
@@ -1740,11 +1763,21 @@ class ClaudeAccountService {
|
|||||||
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`❌ Failed to mark account ${accountId} as unauthorized:`, error)
|
logger.error(`❌ Failed to mark account ${accountId} as ${errorType}:`, error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🚫 标记账户为未授权状态(401错误)
|
||||||
|
async markAccountUnauthorized(accountId, sessionHash = null) {
|
||||||
|
return this.markAccountError(accountId, 'unauthorized', sessionHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🚫 标记账户为被封锁状态(403错误)
|
||||||
|
async markAccountBlocked(accountId, sessionHash = null) {
|
||||||
|
return this.markAccountError(accountId, 'blocked', sessionHash)
|
||||||
|
}
|
||||||
|
|
||||||
// 🔄 重置账户所有异常状态
|
// 🔄 重置账户所有异常状态
|
||||||
async resetAccountStatus(accountId) {
|
async resetAccountStatus(accountId) {
|
||||||
try {
|
try {
|
||||||
@@ -1769,6 +1802,7 @@ class ClaudeAccountService {
|
|||||||
// 清除错误相关字段
|
// 清除错误相关字段
|
||||||
delete updatedAccountData.errorMessage
|
delete updatedAccountData.errorMessage
|
||||||
delete updatedAccountData.unauthorizedAt
|
delete updatedAccountData.unauthorizedAt
|
||||||
|
delete updatedAccountData.blockedAt
|
||||||
delete updatedAccountData.rateLimitedAt
|
delete updatedAccountData.rateLimitedAt
|
||||||
delete updatedAccountData.rateLimitStatus
|
delete updatedAccountData.rateLimitStatus
|
||||||
delete updatedAccountData.rateLimitEndAt
|
delete updatedAccountData.rateLimitEndAt
|
||||||
|
|||||||
@@ -198,6 +198,13 @@ class ClaudeRelayService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 检查是否为403状态码(禁止访问)
|
||||||
|
else if (response.statusCode === 403) {
|
||||||
|
logger.error(
|
||||||
|
`🚫 Forbidden error (403) detected for account ${accountId}, marking as blocked`
|
||||||
|
)
|
||||||
|
await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash)
|
||||||
|
}
|
||||||
// 检查是否为5xx状态码
|
// 检查是否为5xx状态码
|
||||||
else if (response.statusCode >= 500 && response.statusCode < 600) {
|
else if (response.statusCode >= 500 && response.statusCode < 600) {
|
||||||
logger.warn(`🔥 Server error (${response.statusCode}) detected for account ${accountId}`)
|
logger.warn(`🔥 Server error (${response.statusCode}) detected for account ${accountId}`)
|
||||||
@@ -953,8 +960,32 @@ class ClaudeRelayService {
|
|||||||
if (res.statusCode !== 200) {
|
if (res.statusCode !== 200) {
|
||||||
// 将错误处理逻辑封装在一个异步函数中
|
// 将错误处理逻辑封装在一个异步函数中
|
||||||
const handleErrorResponse = async () => {
|
const handleErrorResponse = async () => {
|
||||||
// 增加对5xx错误的处理
|
if (res.statusCode === 401) {
|
||||||
if (res.statusCode >= 500 && res.statusCode < 600) {
|
logger.warn(`🔐 [Stream] Unauthorized error (401) detected for account ${accountId}`)
|
||||||
|
|
||||||
|
await this.recordUnauthorizedError(accountId)
|
||||||
|
|
||||||
|
const errorCount = await this.getUnauthorizedErrorCount(accountId)
|
||||||
|
logger.info(
|
||||||
|
`🔐 [Stream] Account ${accountId} has ${errorCount} consecutive 401 errors in the last 5 minutes`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (errorCount >= 1) {
|
||||||
|
logger.error(
|
||||||
|
`❌ [Stream] Account ${accountId} encountered 401 error (${errorCount} errors), marking as unauthorized`
|
||||||
|
)
|
||||||
|
await unifiedClaudeScheduler.markAccountUnauthorized(
|
||||||
|
accountId,
|
||||||
|
accountType,
|
||||||
|
sessionHash
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (res.statusCode === 403) {
|
||||||
|
logger.error(
|
||||||
|
`🚫 [Stream] Forbidden error (403) detected for account ${accountId}, marking as blocked`
|
||||||
|
)
|
||||||
|
await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash)
|
||||||
|
} else if (res.statusCode >= 500 && res.statusCode < 600) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`🔥 [Stream] Server error (${res.statusCode}) detected for account ${accountId}`
|
`🔥 [Stream] Server error (${res.statusCode}) detected for account ${accountId}`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -636,6 +636,32 @@ class UnifiedClaudeScheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🚫 标记账户为被封锁状态(403错误)
|
||||||
|
async markAccountBlocked(accountId, accountType, sessionHash = null) {
|
||||||
|
try {
|
||||||
|
// 只处理claude-official类型的账户,不处理claude-console和gemini
|
||||||
|
if (accountType === 'claude-official') {
|
||||||
|
await claudeAccountService.markAccountBlocked(accountId, sessionHash)
|
||||||
|
|
||||||
|
// 删除会话映射
|
||||||
|
if (sessionHash) {
|
||||||
|
await this._deleteSessionMapping(sessionHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn(`🚫 Account ${accountId} marked as blocked due to 403 error`)
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
`ℹ️ Skipping blocked marking for non-Claude OAuth account: ${accountId} (${accountType})`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Failed to mark account as blocked: ${accountId} (${accountType})`, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🚫 标记Claude Console账户为封锁状态(模型不支持)
|
// 🚫 标记Claude Console账户为封锁状态(模型不支持)
|
||||||
async blockConsoleAccount(accountId, reason) {
|
async blockConsoleAccount(accountId, reason) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class WebhookNotifier {
|
|||||||
const errorCodes = {
|
const errorCodes = {
|
||||||
'claude-oauth': {
|
'claude-oauth': {
|
||||||
unauthorized: 'CLAUDE_OAUTH_UNAUTHORIZED',
|
unauthorized: 'CLAUDE_OAUTH_UNAUTHORIZED',
|
||||||
|
blocked: 'CLAUDE_OAUTH_BLOCKED',
|
||||||
error: 'CLAUDE_OAUTH_ERROR',
|
error: 'CLAUDE_OAUTH_ERROR',
|
||||||
disabled: 'CLAUDE_OAUTH_MANUALLY_DISABLED'
|
disabled: 'CLAUDE_OAUTH_MANUALLY_DISABLED'
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user