feat: 新增Claude Console账户临时封禁处理和错误消息清理

- 新增 CLAUDE_CONSOLE_BLOCKED_HANDLING_MINUTES 配置项,自动处理账户临时禁用的 400 错误(如 "organization has been disabled"、"too many active sessions" 等)。
  - 添加 errorSanitizer 工具模块,自动清理上游错误响应中的供应商特定信息(URL、供应商名称等),避免泄露中转服务商信息。
  - 统一调度器现在会主动检查并恢复已过期的封禁账户,确保账户在临时封禁时长结束后可以立即重新使用。
This commit is contained in:
sususu
2025-10-17 15:27:47 +08:00
parent f6eb077d82
commit b0917b75a4
6 changed files with 548 additions and 59 deletions

View File

@@ -36,6 +36,20 @@ class ClaudeConsoleAccountService {
)
}
_getBlockedHandlingMinutes() {
const raw = process.env.CLAUDE_CONSOLE_BLOCKED_HANDLING_MINUTES
if (raw === undefined || raw === null || raw === '') {
return 0
}
const parsed = Number.parseInt(raw, 10)
if (!Number.isFinite(parsed) || parsed <= 0) {
return 0
}
return parsed
}
// 🏢 创建Claude Console账户
async createAccount(options = {}) {
const {
@@ -690,6 +704,183 @@ class ClaudeConsoleAccountService {
}
}
// 🚫 标记账号为临时封禁状态400错误 - 账户临时禁用)
async markConsoleAccountBlocked(accountId, errorDetails = '') {
try {
const client = redis.getClientSafe()
const account = await this.getAccount(accountId)
if (!account) {
throw new Error('Account not found')
}
const blockedMinutes = this._getBlockedHandlingMinutes()
if (blockedMinutes <= 0) {
logger.info(
` CLAUDE_CONSOLE_BLOCKED_HANDLING_MINUTES 未设置或为0跳过账户封禁${account.name} (${accountId})`
)
if (account.blockedStatus === 'blocked') {
try {
await this.removeAccountBlocked(accountId)
} catch (cleanupError) {
logger.warn(`⚠️ 尝试移除账户封禁状态失败:${accountId}`, cleanupError)
}
}
return { success: false, skipped: true }
}
const updates = {
blockedAt: new Date().toISOString(),
blockedStatus: 'blocked',
isActive: 'false', // 禁用账户与429保持一致
schedulable: 'false', // 停止调度与429保持一致
status: 'account_blocked', // 设置状态与429保持一致
errorMessage: '账户临时被禁用400错误',
// 使用独立的封禁自动停止标记
blockedAutoStopped: 'true'
}
await client.hset(`${this.ACCOUNT_KEY_PREFIX}${accountId}`, updates)
// 发送Webhook通知包含完整错误详情
try {
const webhookNotifier = require('../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: account.name || 'Claude Console Account',
platform: 'claude-console',
status: 'error',
errorCode: 'CLAUDE_CONSOLE_BLOCKED',
reason: `账户临时被禁用400错误。账户将在 ${blockedMinutes} 分钟后自动恢复。`,
errorDetails: errorDetails || '无错误详情',
timestamp: new Date().toISOString()
})
} catch (webhookError) {
logger.error('Failed to send blocked webhook notification:', webhookError)
}
logger.warn(`🚫 Claude Console account temporarily blocked: ${account.name} (${accountId})`)
return { success: true }
} catch (error) {
logger.error(`❌ Failed to mark Claude Console account as blocked: ${accountId}`, error)
throw error
}
}
// ✅ 移除账号的临时封禁状态
async removeAccountBlocked(accountId) {
try {
const client = redis.getClientSafe()
const accountKey = `${this.ACCOUNT_KEY_PREFIX}${accountId}`
// 获取账户当前状态和额度信息
const [currentStatus, quotaStoppedAt] = await client.hmget(
accountKey,
'status',
'quotaStoppedAt'
)
// 删除封禁相关字段
await client.hdel(accountKey, 'blockedAt', 'blockedStatus')
// 根据不同情况决定是否恢复账户
if (currentStatus === 'account_blocked') {
if (quotaStoppedAt) {
// 还有额度限制改为quota_exceeded状态
await client.hset(accountKey, {
status: 'quota_exceeded'
// isActive保持false
})
logger.info(
`⚠️ Blocked status removed but quota exceeded remains for account: ${accountId}`
)
} else {
// 没有额度限制,完全恢复
const accountData = await client.hgetall(accountKey)
const updateData = {
isActive: 'true',
status: 'active',
errorMessage: ''
}
const hadAutoStop = accountData.blockedAutoStopped === 'true'
// 只恢复因封禁而自动停止的账户
if (hadAutoStop && accountData.schedulable === 'false') {
updateData.schedulable = 'true' // 恢复调度
logger.info(
`✅ Auto-resuming scheduling for Claude Console account ${accountId} after blocked status cleared`
)
}
if (hadAutoStop) {
await client.hdel(accountKey, 'blockedAutoStopped')
}
await client.hset(accountKey, updateData)
logger.success(`✅ Blocked status removed and account re-enabled: ${accountId}`)
}
} else {
if (await client.hdel(accountKey, 'blockedAutoStopped')) {
logger.info(
` Removed stale auto-stop flag for Claude Console account ${accountId} during blocked status recovery`
)
}
logger.success(`✅ Blocked status removed for Claude Console account: ${accountId}`)
}
return { success: true }
} catch (error) {
logger.error(
`❌ Failed to remove blocked status for Claude Console account: ${accountId}`,
error
)
throw error
}
}
// 🔍 检查账号是否处于临时封禁状态
async isAccountBlocked(accountId) {
try {
const account = await this.getAccount(accountId)
if (!account) {
return false
}
if (account.blockedStatus === 'blocked' && account.blockedAt) {
const blockedDuration = this._getBlockedHandlingMinutes()
if (blockedDuration <= 0) {
await this.removeAccountBlocked(accountId)
return false
}
const blockedAt = new Date(account.blockedAt)
const now = new Date()
const minutesSinceBlocked = (now - blockedAt) / (1000 * 60)
// 禁用时长过后自动恢复
if (minutesSinceBlocked >= blockedDuration) {
await this.removeAccountBlocked(accountId)
return false
}
return true
}
return false
} catch (error) {
logger.error(
`❌ Failed to check blocked status for Claude Console account: ${accountId}`,
error
)
return false
}
}
// 🚫 标记账号为过载状态529错误
async markAccountOverloaded(accountId) {
try {