mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat(account): 新增账户自动防护禁用开关
支持 disableAutoProtection 配置项,启用后上游 401/400/429/529 错误不再自动禁用账户
This commit is contained in:
6357
pnpm-lock.yaml
generated
Normal file
6357
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -131,7 +131,8 @@ router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
||||
groupId,
|
||||
dailyQuota,
|
||||
quotaResetTime,
|
||||
maxConcurrentTasks
|
||||
maxConcurrentTasks,
|
||||
disableAutoProtection
|
||||
} = req.body
|
||||
|
||||
if (!name || !apiUrl || !apiKey) {
|
||||
@@ -151,6 +152,10 @@ router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 校验上游错误自动防护开关
|
||||
const normalizedDisableAutoProtection =
|
||||
disableAutoProtection === true || disableAutoProtection === 'true'
|
||||
|
||||
// 验证accountType的有效性
|
||||
if (accountType && !['shared', 'dedicated', 'group'].includes(accountType)) {
|
||||
return res
|
||||
@@ -180,7 +185,8 @@ router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
||||
maxConcurrentTasks:
|
||||
maxConcurrentTasks !== undefined && maxConcurrentTasks !== null
|
||||
? Number(maxConcurrentTasks)
|
||||
: 0
|
||||
: 0,
|
||||
disableAutoProtection: normalizedDisableAutoProtection
|
||||
})
|
||||
|
||||
// 如果是分组类型,将账户添加到分组(CCR 归属 Claude 平台分组)
|
||||
@@ -250,6 +256,13 @@ router.put('/claude-console-accounts/:accountId', authenticateAdmin, async (req,
|
||||
return res.status(404).json({ error: 'Account not found' })
|
||||
}
|
||||
|
||||
// 规范化上游错误自动防护开关
|
||||
if (mappedUpdates.disableAutoProtection !== undefined) {
|
||||
mappedUpdates.disableAutoProtection =
|
||||
mappedUpdates.disableAutoProtection === true ||
|
||||
mappedUpdates.disableAutoProtection === 'true'
|
||||
}
|
||||
|
||||
// 处理分组的变更
|
||||
if (mappedUpdates.accountType !== undefined) {
|
||||
// 如果之前是分组类型,需要从所有分组中移除
|
||||
|
||||
@@ -67,7 +67,8 @@ class ClaudeConsoleAccountService {
|
||||
schedulable = true, // 是否可被调度
|
||||
dailyQuota = 0, // 每日额度限制(美元),0表示不限制
|
||||
quotaResetTime = '00:00', // 额度重置时间(HH:mm格式)
|
||||
maxConcurrentTasks = 0 // 最大并发任务数,0表示无限制
|
||||
maxConcurrentTasks = 0, // 最大并发任务数,0表示无限制
|
||||
disableAutoProtection = false // 是否关闭自动防护(429/401/400/529 不自动禁用)
|
||||
} = options
|
||||
|
||||
// 验证必填字段
|
||||
@@ -115,7 +116,8 @@ class ClaudeConsoleAccountService {
|
||||
lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区)
|
||||
quotaResetTime, // 额度重置时间
|
||||
quotaStoppedAt: '', // 因额度停用的时间
|
||||
maxConcurrentTasks: maxConcurrentTasks.toString() // 最大并发任务数,0表示无限制
|
||||
maxConcurrentTasks: maxConcurrentTasks.toString(), // 最大并发任务数,0表示无限制
|
||||
disableAutoProtection: disableAutoProtection.toString() // 关闭自动防护
|
||||
}
|
||||
|
||||
const client = redis.getClientSafe()
|
||||
@@ -153,6 +155,7 @@ class ClaudeConsoleAccountService {
|
||||
quotaResetTime,
|
||||
quotaStoppedAt: null,
|
||||
maxConcurrentTasks, // 新增:返回并发限制配置
|
||||
disableAutoProtection, // 新增:返回自动防护开关
|
||||
activeTaskCount: 0 // 新增:新建账户当前并发数为0
|
||||
}
|
||||
}
|
||||
@@ -213,7 +216,8 @@ class ClaudeConsoleAccountService {
|
||||
|
||||
// 并发控制相关
|
||||
maxConcurrentTasks: parseInt(accountData.maxConcurrentTasks) || 0,
|
||||
activeTaskCount
|
||||
activeTaskCount,
|
||||
disableAutoProtection: accountData.disableAutoProtection === 'true'
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -259,6 +263,7 @@ class ClaudeConsoleAccountService {
|
||||
}
|
||||
accountData.isActive = accountData.isActive === 'true'
|
||||
accountData.schedulable = accountData.schedulable !== 'false' // 默认为true
|
||||
accountData.disableAutoProtection = accountData.disableAutoProtection === 'true'
|
||||
|
||||
if (accountData.proxy) {
|
||||
accountData.proxy = JSON.parse(accountData.proxy)
|
||||
@@ -367,6 +372,9 @@ class ClaudeConsoleAccountService {
|
||||
if (updates.maxConcurrentTasks !== undefined) {
|
||||
updatedData.maxConcurrentTasks = updates.maxConcurrentTasks.toString()
|
||||
}
|
||||
if (updates.disableAutoProtection !== undefined) {
|
||||
updatedData.disableAutoProtection = updates.disableAutoProtection.toString()
|
||||
}
|
||||
|
||||
// ✅ 直接保存 subscriptionExpiresAt(如果提供)
|
||||
// Claude Console 没有 token 刷新逻辑,不会覆盖此字段
|
||||
|
||||
@@ -37,6 +37,8 @@ class ClaudeConsoleRelayService {
|
||||
throw new Error('Claude Console Claude account not found')
|
||||
}
|
||||
|
||||
const autoProtectionDisabled = account.disableAutoProtection === true
|
||||
|
||||
logger.info(
|
||||
`📤 Processing Claude Console API request for key: ${apiKeyData.name || apiKeyData.id}, account: ${account.name} (${accountId}), request: ${requestId}`
|
||||
)
|
||||
@@ -248,27 +250,41 @@ class ClaudeConsoleRelayService {
|
||||
|
||||
// 检查错误状态并相应处理
|
||||
if (response.status === 401) {
|
||||
logger.warn(`🚫 Unauthorized error detected for Claude Console account ${accountId}`)
|
||||
logger.warn(
|
||||
`🚫 Unauthorized error detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
|
||||
)
|
||||
if (!autoProtectionDisabled) {
|
||||
await claudeConsoleAccountService.markAccountUnauthorized(accountId)
|
||||
}
|
||||
} else if (accountDisabledError) {
|
||||
logger.error(
|
||||
`🚫 Account disabled error (400) detected for Claude Console account ${accountId}, marking as blocked`
|
||||
`🚫 Account disabled error (400) detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
|
||||
)
|
||||
// 传入完整的错误详情到 webhook
|
||||
const errorDetails =
|
||||
typeof response.data === 'string' ? response.data : JSON.stringify(response.data)
|
||||
if (!autoProtectionDisabled) {
|
||||
await claudeConsoleAccountService.markConsoleAccountBlocked(accountId, errorDetails)
|
||||
}
|
||||
} else if (response.status === 429) {
|
||||
logger.warn(`🚫 Rate limit detected for Claude Console account ${accountId}`)
|
||||
logger.warn(
|
||||
`🚫 Rate limit detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
|
||||
)
|
||||
// 收到429先检查是否因为超过了手动配置的每日额度
|
||||
await claudeConsoleAccountService.checkQuotaUsage(accountId).catch((err) => {
|
||||
logger.error('❌ Failed to check quota after 429 error:', err)
|
||||
})
|
||||
|
||||
if (!autoProtectionDisabled) {
|
||||
await claudeConsoleAccountService.markAccountRateLimited(accountId)
|
||||
}
|
||||
} else if (response.status === 529) {
|
||||
logger.warn(`🚫 Overload error detected for Claude Console account ${accountId}`)
|
||||
logger.warn(
|
||||
`🚫 Overload error detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
|
||||
)
|
||||
if (!autoProtectionDisabled) {
|
||||
await claudeConsoleAccountService.markAccountOverloaded(accountId)
|
||||
}
|
||||
} else if (response.status === 200 || response.status === 201) {
|
||||
// 如果请求成功,检查并移除错误状态
|
||||
const isRateLimited = await claudeConsoleAccountService.isAccountRateLimited(accountId)
|
||||
@@ -597,6 +613,7 @@ class ClaudeConsoleRelayService {
|
||||
})
|
||||
|
||||
response.data.on('end', async () => {
|
||||
const autoProtectionDisabled = account.disableAutoProtection === true
|
||||
// 记录原始错误消息到日志(方便调试,包含供应商信息)
|
||||
logger.error(
|
||||
`📝 [Stream] Upstream error response from ${account?.name || accountId}: ${errorDataForCheck.substring(0, 500)}`
|
||||
@@ -609,25 +626,42 @@ class ClaudeConsoleRelayService {
|
||||
)
|
||||
|
||||
if (response.status === 401) {
|
||||
logger.warn(
|
||||
`🚫 [Stream] Unauthorized error detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
|
||||
)
|
||||
if (!autoProtectionDisabled) {
|
||||
await claudeConsoleAccountService.markAccountUnauthorized(accountId)
|
||||
}
|
||||
} else if (accountDisabledError) {
|
||||
logger.error(
|
||||
`🚫 [Stream] Account disabled error (400) detected for Claude Console account ${accountId}, marking as blocked`
|
||||
`🚫 [Stream] Account disabled error (400) detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
|
||||
)
|
||||
// 传入完整的错误详情到 webhook
|
||||
if (!autoProtectionDisabled) {
|
||||
await claudeConsoleAccountService.markConsoleAccountBlocked(
|
||||
accountId,
|
||||
errorDataForCheck
|
||||
)
|
||||
}
|
||||
} else if (response.status === 429) {
|
||||
await claudeConsoleAccountService.markAccountRateLimited(accountId)
|
||||
logger.warn(
|
||||
`🚫 [Stream] Rate limit detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
|
||||
)
|
||||
// 检查是否因为超过每日额度
|
||||
claudeConsoleAccountService.checkQuotaUsage(accountId).catch((err) => {
|
||||
logger.error('❌ Failed to check quota after 429 error:', err)
|
||||
})
|
||||
if (!autoProtectionDisabled) {
|
||||
await claudeConsoleAccountService.markAccountRateLimited(accountId)
|
||||
}
|
||||
} else if (response.status === 529) {
|
||||
logger.warn(
|
||||
`🚫 [Stream] Overload error detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
|
||||
)
|
||||
if (!autoProtectionDisabled) {
|
||||
await claudeConsoleAccountService.markAccountOverloaded(accountId)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
if (!responseStream.headersSent) {
|
||||
|
||||
@@ -1451,6 +1451,26 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上游错误处理 -->
|
||||
<div v-if="form.platform === 'claude-console'">
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>上游错误处理</label
|
||||
>
|
||||
<label class="inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="form.disableAutoProtection"
|
||||
class="mr-2 rounded border-gray-300 text-blue-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||
上游错误不自动暂停调度
|
||||
</span>
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
勾选后遇到 401/400/429/529 等上游错误仅记录日志并透传,不自动禁用或限流
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI-Responses 特定字段 -->
|
||||
@@ -3070,6 +3090,26 @@
|
||||
<p class="mt-1 text-xs text-gray-500">账号被限流后暂停调度的时间(分钟)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上游错误处理(编辑模式)-->
|
||||
<div v-if="form.platform === 'claude-console'">
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
上游错误处理
|
||||
</label>
|
||||
<label class="inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="form.disableAutoProtection"
|
||||
class="mr-2 rounded border-gray-300 text-blue-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||
上游错误不自动暂停调度
|
||||
</span>
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
勾选后遇到 401/400/429/529 等上游错误仅记录日志并透传,不自动禁用或限流
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI-Responses 特定字段(编辑模式)-->
|
||||
@@ -3912,6 +3952,7 @@ const form = ref({
|
||||
})(),
|
||||
userAgent: props.account?.userAgent || '',
|
||||
enableRateLimit: props.account ? props.account.rateLimitDuration > 0 : true,
|
||||
disableAutoProtection: props.account?.disableAutoProtection === true,
|
||||
// 额度管理字段
|
||||
dailyQuota: props.account?.dailyQuota || 0,
|
||||
dailyUsage: props.account?.dailyUsage || 0,
|
||||
@@ -5015,6 +5056,10 @@ const createAccount = async () => {
|
||||
data.userAgent = form.value.userAgent || null
|
||||
// 如果不启用限流,传递 0 表示不限流
|
||||
data.rateLimitDuration = form.value.enableRateLimit ? form.value.rateLimitDuration || 60 : 0
|
||||
// 上游错误处理(仅 Claude Console)
|
||||
if (form.value.platform === 'claude-console') {
|
||||
data.disableAutoProtection = !!form.value.disableAutoProtection
|
||||
}
|
||||
// 额度管理字段
|
||||
data.dailyQuota = form.value.dailyQuota || 0
|
||||
data.quotaResetTime = form.value.quotaResetTime || '00:00'
|
||||
@@ -5343,6 +5388,8 @@ const updateAccount = async () => {
|
||||
data.userAgent = form.value.userAgent || null
|
||||
// 如果不启用限流,传递 0 表示不限流
|
||||
data.rateLimitDuration = form.value.enableRateLimit ? form.value.rateLimitDuration || 60 : 0
|
||||
// 上游错误处理
|
||||
data.disableAutoProtection = !!form.value.disableAutoProtection
|
||||
// 额度管理字段
|
||||
data.dailyQuota = form.value.dailyQuota || 0
|
||||
data.quotaResetTime = form.value.quotaResetTime || '00:00'
|
||||
@@ -5964,7 +6011,9 @@ watch(
|
||||
dailyUsage: newAccount.dailyUsage || 0,
|
||||
quotaResetTime: newAccount.quotaResetTime || '00:00',
|
||||
// 并发控制字段
|
||||
maxConcurrentTasks: newAccount.maxConcurrentTasks || 0
|
||||
maxConcurrentTasks: newAccount.maxConcurrentTasks || 0,
|
||||
// 上游错误处理
|
||||
disableAutoProtection: newAccount.disableAutoProtection === true
|
||||
}
|
||||
|
||||
// 如果是Claude Console账户,加载实时使用情况
|
||||
|
||||
Reference in New Issue
Block a user