This commit is contained in:
sczheng189
2025-09-06 23:40:10 +08:00
parent 8f08d7843f
commit 9d1906c0b1
48 changed files with 4687 additions and 815 deletions

View File

@@ -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 {
const errorConfig = ERROR_CONFIG[errorType]
if (!errorConfig) {
throw new Error(`Unsupported error type: ${errorType}`)
}
const accountData = await redis.getClaudeAccount(accountId)
if (!accountData || Object.keys(accountData).length === 0) {
throw new Error('Account not found')
@@ -1705,10 +1727,10 @@ class ClaudeAccountService {
// 更新账户状态
const updatedAccountData = { ...accountData }
updatedAccountData.status = 'unauthorized'
updatedAccountData.status = errorConfig.status
updatedAccountData.schedulable = 'false' // 设置为不可调度
updatedAccountData.errorMessage = 'Account unauthorized (401 errors detected)'
updatedAccountData.unauthorizedAt = new Date().toISOString()
updatedAccountData.errorMessage = errorConfig.errorMessage
updatedAccountData[errorConfig.timestampField] = new Date().toISOString()
// 保存更新后的账户数据
await redis.setClaudeAccount(accountId, updatedAccountData)
@@ -1720,7 +1742,7 @@ class ClaudeAccountService {
}
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通知
@@ -1730,9 +1752,10 @@ class ClaudeAccountService {
accountId,
accountName: accountData.name,
platform: 'claude-oauth',
status: 'unauthorized',
errorCode: 'CLAUDE_OAUTH_UNAUTHORIZED',
reason: 'Account unauthorized (401 errors detected)'
status: errorConfig.status,
errorCode: errorConfig.errorCode,
reason: errorConfig.errorMessage,
timestamp: getISOStringWithTimezone(new Date())
})
} catch (webhookError) {
logger.error('Failed to send webhook notification:', webhookError)
@@ -1740,11 +1763,21 @@ class ClaudeAccountService {
return { success: true }
} catch (error) {
logger.error(`❌ Failed to mark account ${accountId} as unauthorized:`, error)
logger.error(`❌ Failed to mark account ${accountId} as ${errorType}:`, 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) {
try {
@@ -1769,6 +1802,7 @@ class ClaudeAccountService {
// 清除错误相关字段
delete updatedAccountData.errorMessage
delete updatedAccountData.unauthorizedAt
delete updatedAccountData.blockedAt
delete updatedAccountData.rateLimitedAt
delete updatedAccountData.rateLimitStatus
delete updatedAccountData.rateLimitEndAt
@@ -1779,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)
@@ -1830,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++
@@ -1917,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}`)