mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
feat: 适配claude新opus周限规则
This commit is contained in:
@@ -1286,6 +1286,121 @@ class ClaudeAccountService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🚫 标记账号的 Opus 限流状态(不影响其他模型调度)
|
||||||
|
async markAccountOpusRateLimited(accountId, rateLimitResetTimestamp = null) {
|
||||||
|
try {
|
||||||
|
const accountData = await redis.getClaudeAccount(accountId)
|
||||||
|
if (!accountData || Object.keys(accountData).length === 0) {
|
||||||
|
throw new Error('Account not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedAccountData = { ...accountData }
|
||||||
|
const now = new Date()
|
||||||
|
updatedAccountData.opusRateLimitedAt = now.toISOString()
|
||||||
|
|
||||||
|
if (rateLimitResetTimestamp) {
|
||||||
|
const resetTime = new Date(rateLimitResetTimestamp * 1000)
|
||||||
|
updatedAccountData.opusRateLimitEndAt = resetTime.toISOString()
|
||||||
|
logger.warn(
|
||||||
|
`🚫 Account ${accountData.name} (${accountId}) reached Opus weekly cap, resets at ${resetTime.toISOString()}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 如果缺少准确时间戳,保留现有值但记录警告,便于后续人工干预
|
||||||
|
logger.warn(
|
||||||
|
`⚠️ Account ${accountData.name} (${accountId}) reported Opus limit without reset timestamp`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await redis.setClaudeAccount(accountId, updatedAccountData)
|
||||||
|
return { success: true }
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Failed to mark Opus rate limit for account: ${accountId}`, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 清除账号的 Opus 限流状态
|
||||||
|
async clearAccountOpusRateLimit(accountId) {
|
||||||
|
try {
|
||||||
|
const accountData = await redis.getClaudeAccount(accountId)
|
||||||
|
if (!accountData || Object.keys(accountData).length === 0) {
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedAccountData = { ...accountData }
|
||||||
|
delete updatedAccountData.opusRateLimitedAt
|
||||||
|
delete updatedAccountData.opusRateLimitEndAt
|
||||||
|
|
||||||
|
await redis.setClaudeAccount(accountId, updatedAccountData)
|
||||||
|
|
||||||
|
const redisKey = `claude:account:${accountId}`
|
||||||
|
if (redis.client && typeof redis.client.hdel === 'function') {
|
||||||
|
await redis.client.hdel(redisKey, 'opusRateLimitedAt', 'opusRateLimitEndAt')
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`✅ Cleared Opus rate limit state for account ${accountId}`)
|
||||||
|
return { success: true }
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Failed to clear Opus rate limit for account: ${accountId}`, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔍 检查账号是否处于 Opus 限流状态(自动清理过期标记)
|
||||||
|
async isAccountOpusRateLimited(accountId) {
|
||||||
|
try {
|
||||||
|
const accountData = await redis.getClaudeAccount(accountId)
|
||||||
|
if (!accountData || Object.keys(accountData).length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountData.opusRateLimitEndAt) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetTime = new Date(accountData.opusRateLimitEndAt)
|
||||||
|
if (Number.isNaN(resetTime.getTime())) {
|
||||||
|
await this.clearAccountOpusRateLimit(accountId)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
if (now >= resetTime) {
|
||||||
|
await this.clearAccountOpusRateLimit(accountId)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Failed to check Opus rate limit status for account: ${accountId}`, error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ♻️ 检查并清理已过期的 Opus 限流标记
|
||||||
|
async clearExpiredOpusRateLimit(accountId) {
|
||||||
|
try {
|
||||||
|
const accountData = await redis.getClaudeAccount(accountId)
|
||||||
|
if (!accountData || Object.keys(accountData).length === 0) {
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountData.opusRateLimitEndAt) {
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetTime = new Date(accountData.opusRateLimitEndAt)
|
||||||
|
if (Number.isNaN(resetTime.getTime()) || new Date() >= resetTime) {
|
||||||
|
await this.clearAccountOpusRateLimit(accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Failed to clear expired Opus rate limit for account: ${accountId}`, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ✅ 移除账号的限流状态
|
// ✅ 移除账号的限流状态
|
||||||
async removeAccountRateLimit(accountId) {
|
async removeAccountRateLimit(accountId) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ class ClaudeRelayService {
|
|||||||
requestedModel: requestBody.model
|
requestedModel: requestBody.model
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isOpusModelRequest =
|
||||||
|
typeof requestBody?.model === 'string' && requestBody.model.toLowerCase().includes('opus')
|
||||||
|
|
||||||
// 生成会话哈希用于sticky会话
|
// 生成会话哈希用于sticky会话
|
||||||
const sessionHash = sessionHelper.generateSessionHash(requestBody)
|
const sessionHash = sessionHelper.generateSessionHash(requestBody)
|
||||||
|
|
||||||
@@ -71,12 +74,44 @@ class ClaudeRelayService {
|
|||||||
`📤 Processing API request for key: ${apiKeyData.name || apiKeyData.id}, account: ${accountId} (${accountType})${sessionHash ? `, session: ${sessionHash}` : ''}`
|
`📤 Processing API request for key: ${apiKeyData.name || apiKeyData.id}, account: ${accountId} (${accountType})${sessionHash ? `, session: ${sessionHash}` : ''}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 获取账户信息
|
||||||
|
let account = await claudeAccountService.getAccount(accountId)
|
||||||
|
|
||||||
|
if (isOpusModelRequest) {
|
||||||
|
await claudeAccountService.clearExpiredOpusRateLimit(accountId)
|
||||||
|
account = await claudeAccountService.getAccount(accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDedicatedOfficialAccount =
|
||||||
|
apiKeyData.claudeAccountId &&
|
||||||
|
!apiKeyData.claudeAccountId.startsWith('group:') &&
|
||||||
|
apiKeyData.claudeAccountId === accountId
|
||||||
|
|
||||||
|
let opusRateLimitActive = false
|
||||||
|
let opusRateLimitEndAt = null
|
||||||
|
if (isOpusModelRequest) {
|
||||||
|
opusRateLimitActive = await claudeAccountService.isAccountOpusRateLimited(accountId)
|
||||||
|
opusRateLimitEndAt = account?.opusRateLimitEndAt || null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOpusModelRequest && isDedicatedOfficialAccount && opusRateLimitActive) {
|
||||||
|
logger.warn(
|
||||||
|
`🚫 Dedicated account ${account?.name || accountId} is under Opus weekly limit until ${opusRateLimitEndAt}`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
statusCode: 403,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
error: 'opus_weekly_limit',
|
||||||
|
message: '此专属账号的Opus模型已达到本周使用限制,请尝试切换其他模型后再试。'
|
||||||
|
}),
|
||||||
|
accountId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取有效的访问token
|
// 获取有效的访问token
|
||||||
const accessToken = await claudeAccountService.getValidAccessToken(accountId)
|
const accessToken = await claudeAccountService.getValidAccessToken(accountId)
|
||||||
|
|
||||||
// 获取账户信息
|
|
||||||
const account = await claudeAccountService.getAccount(accountId)
|
|
||||||
|
|
||||||
// 处理请求体(传递 clientHeaders 以判断是否需要设置 Claude Code 系统提示词)
|
// 处理请求体(传递 clientHeaders 以判断是否需要设置 Claude Code 系统提示词)
|
||||||
const processedBody = this._processRequestBody(requestBody, clientHeaders, account)
|
const processedBody = this._processRequestBody(requestBody, clientHeaders, account)
|
||||||
|
|
||||||
@@ -181,17 +216,25 @@ class ClaudeRelayService {
|
|||||||
}
|
}
|
||||||
// 检查是否为429状态码
|
// 检查是否为429状态码
|
||||||
else if (response.statusCode === 429) {
|
else if (response.statusCode === 429) {
|
||||||
isRateLimited = true
|
const resetHeader = response.headers
|
||||||
|
? response.headers['anthropic-ratelimit-unified-reset']
|
||||||
|
: null
|
||||||
|
const parsedResetTimestamp = resetHeader ? parseInt(resetHeader, 10) : NaN
|
||||||
|
|
||||||
// 提取限流重置时间戳
|
if (isOpusModelRequest && !Number.isNaN(parsedResetTimestamp)) {
|
||||||
if (response.headers && response.headers['anthropic-ratelimit-unified-reset']) {
|
await claudeAccountService.markAccountOpusRateLimited(accountId, parsedResetTimestamp)
|
||||||
rateLimitResetTimestamp = parseInt(
|
logger.warn(
|
||||||
response.headers['anthropic-ratelimit-unified-reset']
|
`🚫 Account ${accountId} hit Opus limit, resets at ${new Date(parsedResetTimestamp * 1000).toISOString()}`
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
isRateLimited = true
|
||||||
|
if (!Number.isNaN(parsedResetTimestamp)) {
|
||||||
|
rateLimitResetTimestamp = parsedResetTimestamp
|
||||||
logger.info(
|
logger.info(
|
||||||
`🕐 Extracted rate limit reset timestamp: ${rateLimitResetTimestamp} (${new Date(rateLimitResetTimestamp * 1000).toISOString()})`
|
`🕐 Extracted rate limit reset timestamp: ${rateLimitResetTimestamp} (${new Date(rateLimitResetTimestamp * 1000).toISOString()})`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 检查响应体中的错误信息
|
// 检查响应体中的错误信息
|
||||||
try {
|
try {
|
||||||
@@ -812,6 +855,9 @@ class ClaudeRelayService {
|
|||||||
requestedModel: requestBody.model
|
requestedModel: requestBody.model
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isOpusModelRequest =
|
||||||
|
typeof requestBody?.model === 'string' && requestBody.model.toLowerCase().includes('opus')
|
||||||
|
|
||||||
// 生成会话哈希用于sticky会话
|
// 生成会话哈希用于sticky会话
|
||||||
const sessionHash = sessionHelper.generateSessionHash(requestBody)
|
const sessionHash = sessionHelper.generateSessionHash(requestBody)
|
||||||
|
|
||||||
@@ -828,12 +874,42 @@ class ClaudeRelayService {
|
|||||||
`📡 Processing streaming API request with usage capture for key: ${apiKeyData.name || apiKeyData.id}, account: ${accountId} (${accountType})${sessionHash ? `, session: ${sessionHash}` : ''}`
|
`📡 Processing streaming API request with usage capture for key: ${apiKeyData.name || apiKeyData.id}, account: ${accountId} (${accountType})${sessionHash ? `, session: ${sessionHash}` : ''}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 获取账户信息
|
||||||
|
let account = await claudeAccountService.getAccount(accountId)
|
||||||
|
|
||||||
|
if (isOpusModelRequest) {
|
||||||
|
await claudeAccountService.clearExpiredOpusRateLimit(accountId)
|
||||||
|
account = await claudeAccountService.getAccount(accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDedicatedOfficialAccount =
|
||||||
|
apiKeyData.claudeAccountId &&
|
||||||
|
!apiKeyData.claudeAccountId.startsWith('group:') &&
|
||||||
|
apiKeyData.claudeAccountId === accountId
|
||||||
|
|
||||||
|
let opusRateLimitActive = false
|
||||||
|
if (isOpusModelRequest) {
|
||||||
|
opusRateLimitActive = await claudeAccountService.isAccountOpusRateLimited(accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOpusModelRequest && isDedicatedOfficialAccount && opusRateLimitActive) {
|
||||||
|
if (!responseStream.headersSent) {
|
||||||
|
responseStream.status(403)
|
||||||
|
responseStream.setHeader('Content-Type', 'application/json')
|
||||||
|
}
|
||||||
|
responseStream.write(
|
||||||
|
JSON.stringify({
|
||||||
|
error: 'opus_weekly_limit',
|
||||||
|
message: '此专属账号的Opus模型已达到本周使用限制,请尝试切换其他模型后再试。'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
responseStream.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 获取有效的访问token
|
// 获取有效的访问token
|
||||||
const accessToken = await claudeAccountService.getValidAccessToken(accountId)
|
const accessToken = await claudeAccountService.getValidAccessToken(accountId)
|
||||||
|
|
||||||
// 获取账户信息
|
|
||||||
const account = await claudeAccountService.getAccount(accountId)
|
|
||||||
|
|
||||||
// 处理请求体(传递 clientHeaders 以判断是否需要设置 Claude Code 系统提示词)
|
// 处理请求体(传递 clientHeaders 以判断是否需要设置 Claude Code 系统提示词)
|
||||||
const processedBody = this._processRequestBody(requestBody, clientHeaders, account)
|
const processedBody = this._processRequestBody(requestBody, clientHeaders, account)
|
||||||
|
|
||||||
@@ -880,6 +956,9 @@ class ClaudeRelayService {
|
|||||||
// 获取账户信息用于统一 User-Agent
|
// 获取账户信息用于统一 User-Agent
|
||||||
const account = await claudeAccountService.getAccount(accountId)
|
const account = await claudeAccountService.getAccount(accountId)
|
||||||
|
|
||||||
|
const isOpusModelRequest =
|
||||||
|
typeof body?.model === 'string' && body.model.toLowerCase().includes('opus')
|
||||||
|
|
||||||
// 获取统一的 User-Agent
|
// 获取统一的 User-Agent
|
||||||
const unifiedUA = await this.captureAndGetUnifiedUserAgent(clientHeaders, account)
|
const unifiedUA = await this.captureAndGetUnifiedUserAgent(clientHeaders, account)
|
||||||
|
|
||||||
@@ -1291,22 +1370,34 @@ class ClaudeRelayService {
|
|||||||
|
|
||||||
// 处理限流状态
|
// 处理限流状态
|
||||||
if (rateLimitDetected || res.statusCode === 429) {
|
if (rateLimitDetected || res.statusCode === 429) {
|
||||||
// 提取限流重置时间戳
|
const resetHeader = res.headers
|
||||||
let rateLimitResetTimestamp = null
|
? res.headers['anthropic-ratelimit-unified-reset']
|
||||||
if (res.headers && res.headers['anthropic-ratelimit-unified-reset']) {
|
: null
|
||||||
rateLimitResetTimestamp = parseInt(res.headers['anthropic-ratelimit-unified-reset'])
|
const parsedResetTimestamp = resetHeader ? parseInt(resetHeader, 10) : NaN
|
||||||
|
|
||||||
|
if (isOpusModelRequest && !Number.isNaN(parsedResetTimestamp)) {
|
||||||
|
await claudeAccountService.markAccountOpusRateLimited(accountId, parsedResetTimestamp)
|
||||||
|
logger.warn(
|
||||||
|
`🚫 [Stream] Account ${accountId} hit Opus limit, resets at ${new Date(parsedResetTimestamp * 1000).toISOString()}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const rateLimitResetTimestamp = Number.isNaN(parsedResetTimestamp)
|
||||||
|
? null
|
||||||
|
: parsedResetTimestamp
|
||||||
|
|
||||||
|
if (!Number.isNaN(parsedResetTimestamp)) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`🕐 Extracted rate limit reset timestamp from stream: ${rateLimitResetTimestamp} (${new Date(rateLimitResetTimestamp * 1000).toISOString()})`
|
`🕐 Extracted rate limit reset timestamp from stream: ${parsedResetTimestamp} (${new Date(parsedResetTimestamp * 1000).toISOString()})`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标记账号为限流状态并删除粘性会话映射
|
|
||||||
await unifiedClaudeScheduler.markAccountRateLimited(
|
await unifiedClaudeScheduler.markAccountRateLimited(
|
||||||
accountId,
|
accountId,
|
||||||
accountType,
|
accountType,
|
||||||
sessionHash,
|
sessionHash,
|
||||||
rateLimitResetTimestamp
|
rateLimitResetTimestamp
|
||||||
)
|
)
|
||||||
|
}
|
||||||
} else if (res.statusCode === 200) {
|
} else if (res.statusCode === 200) {
|
||||||
// 请求成功,清除401和500错误计数
|
// 请求成功,清除401和500错误计数
|
||||||
await this.clearUnauthorizedErrors(accountId)
|
await this.clearUnauthorizedErrors(accountId)
|
||||||
|
|||||||
@@ -131,6 +131,10 @@ class UnifiedClaudeScheduler {
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
`🔍 Model parsing - Original: ${requestedModel}, Vendor: ${vendor}, Effective: ${effectiveModel}`
|
`🔍 Model parsing - Original: ${requestedModel}, Vendor: ${vendor}, Effective: ${effectiveModel}`
|
||||||
)
|
)
|
||||||
|
const isOpusRequest =
|
||||||
|
effectiveModel && typeof effectiveModel === 'string'
|
||||||
|
? effectiveModel.toLowerCase().includes('opus')
|
||||||
|
: false
|
||||||
|
|
||||||
// 如果是 CCR 前缀,只在 CCR 账户池中选择
|
// 如果是 CCR 前缀,只在 CCR 账户池中选择
|
||||||
if (vendor === 'ccr') {
|
if (vendor === 'ccr') {
|
||||||
@@ -161,6 +165,9 @@ class UnifiedClaudeScheduler {
|
|||||||
boundAccount.status !== 'error' &&
|
boundAccount.status !== 'error' &&
|
||||||
this._isSchedulable(boundAccount.schedulable)
|
this._isSchedulable(boundAccount.schedulable)
|
||||||
) {
|
) {
|
||||||
|
if (isOpusRequest) {
|
||||||
|
await claudeAccountService.clearExpiredOpusRateLimit(boundAccount.id)
|
||||||
|
}
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using bound dedicated Claude OAuth account: ${boundAccount.name} (${apiKeyData.claudeAccountId}) for API key ${apiKeyData.name}`
|
`🎯 Using bound dedicated Claude OAuth account: ${boundAccount.name} (${apiKeyData.claudeAccountId}) for API key ${apiKeyData.name}`
|
||||||
)
|
)
|
||||||
@@ -313,6 +320,10 @@ class UnifiedClaudeScheduler {
|
|||||||
// 📋 获取所有可用账户(合并官方和Console)
|
// 📋 获取所有可用账户(合并官方和Console)
|
||||||
async _getAllAvailableAccounts(apiKeyData, requestedModel = null, includeCcr = false) {
|
async _getAllAvailableAccounts(apiKeyData, requestedModel = null, includeCcr = false) {
|
||||||
const availableAccounts = []
|
const availableAccounts = []
|
||||||
|
const isOpusRequest =
|
||||||
|
requestedModel && typeof requestedModel === 'string'
|
||||||
|
? requestedModel.toLowerCase().includes('opus')
|
||||||
|
: false
|
||||||
|
|
||||||
// 如果API Key绑定了专属账户,优先返回
|
// 如果API Key绑定了专属账户,优先返回
|
||||||
// 1. 检查Claude OAuth账户绑定
|
// 1. 检查Claude OAuth账户绑定
|
||||||
@@ -447,7 +458,20 @@ class UnifiedClaudeScheduler {
|
|||||||
|
|
||||||
// 检查是否被限流
|
// 检查是否被限流
|
||||||
const isRateLimited = await claudeAccountService.isAccountRateLimited(account.id)
|
const isRateLimited = await claudeAccountService.isAccountRateLimited(account.id)
|
||||||
if (!isRateLimited) {
|
if (isRateLimited) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOpusRequest) {
|
||||||
|
const isOpusRateLimited = await claudeAccountService.isAccountOpusRateLimited(account.id)
|
||||||
|
if (isOpusRateLimited) {
|
||||||
|
logger.info(
|
||||||
|
`🚫 Skipping account ${account.name} (${account.id}) due to active Opus limit`
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
availableAccounts.push({
|
availableAccounts.push({
|
||||||
...account,
|
...account,
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
@@ -457,7 +481,6 @@ class UnifiedClaudeScheduler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 获取Claude Console账户
|
// 获取Claude Console账户
|
||||||
const consoleAccounts = await claudeConsoleAccountService.getAllAccounts()
|
const consoleAccounts = await claudeConsoleAccountService.getAllAccounts()
|
||||||
@@ -665,7 +688,23 @@ class UnifiedClaudeScheduler {
|
|||||||
// 检查是否限流或过载
|
// 检查是否限流或过载
|
||||||
const isRateLimited = await claudeAccountService.isAccountRateLimited(accountId)
|
const isRateLimited = await claudeAccountService.isAccountRateLimited(accountId)
|
||||||
const isOverloaded = await claudeAccountService.isAccountOverloaded(accountId)
|
const isOverloaded = await claudeAccountService.isAccountOverloaded(accountId)
|
||||||
return !isRateLimited && !isOverloaded
|
if (isRateLimited || isOverloaded) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
requestedModel &&
|
||||||
|
typeof requestedModel === 'string' &&
|
||||||
|
requestedModel.toLowerCase().includes('opus')
|
||||||
|
) {
|
||||||
|
const isOpusRateLimited = await claudeAccountService.isAccountOpusRateLimited(accountId)
|
||||||
|
if (isOpusRateLimited) {
|
||||||
|
logger.info(`🚫 Account ${accountId} skipped due to active Opus limit (session check)`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
} else if (accountType === 'claude-console') {
|
} else if (accountType === 'claude-console') {
|
||||||
const account = await claudeConsoleAccountService.getAccount(accountId)
|
const account = await claudeConsoleAccountService.getAccount(accountId)
|
||||||
if (!account || !account.isActive) {
|
if (!account || !account.isActive) {
|
||||||
@@ -1056,6 +1095,10 @@ class UnifiedClaudeScheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const availableAccounts = []
|
const availableAccounts = []
|
||||||
|
const isOpusRequest =
|
||||||
|
requestedModel && typeof requestedModel === 'string'
|
||||||
|
? requestedModel.toLowerCase().includes('opus')
|
||||||
|
: false
|
||||||
|
|
||||||
// 获取所有成员账户的详细信息
|
// 获取所有成员账户的详细信息
|
||||||
for (const memberId of memberIds) {
|
for (const memberId of memberIds) {
|
||||||
@@ -1115,7 +1158,22 @@ class UnifiedClaudeScheduler {
|
|||||||
|
|
||||||
// 检查是否被限流
|
// 检查是否被限流
|
||||||
const isRateLimited = await this.isAccountRateLimited(account.id, accountType)
|
const isRateLimited = await this.isAccountRateLimited(account.id, accountType)
|
||||||
if (!isRateLimited) {
|
if (isRateLimited) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accountType === 'claude-official' && isOpusRequest) {
|
||||||
|
const isOpusRateLimited = await claudeAccountService.isAccountOpusRateLimited(
|
||||||
|
account.id
|
||||||
|
)
|
||||||
|
if (isOpusRateLimited) {
|
||||||
|
logger.info(
|
||||||
|
`🚫 Skipping group member ${account.name} (${account.id}) due to active Opus limit`
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
availableAccounts.push({
|
availableAccounts.push({
|
||||||
...account,
|
...account,
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
@@ -1125,7 +1183,6 @@ class UnifiedClaudeScheduler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (availableAccounts.length === 0) {
|
if (availableAccounts.length === 0) {
|
||||||
throw new Error(`No available accounts in group ${group.name}`)
|
throw new Error(`No available accounts in group ${group.name}`)
|
||||||
|
|||||||
@@ -466,15 +466,6 @@
|
|||||||
>添加方式</label
|
>添加方式</label
|
||||||
>
|
>
|
||||||
<div class="flex flex-wrap gap-4">
|
<div class="flex flex-wrap gap-4">
|
||||||
<label v-if="form.platform === 'claude'" class="flex cursor-pointer items-center">
|
|
||||||
<input
|
|
||||||
v-model="form.addType"
|
|
||||||
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
|
||||||
type="radio"
|
|
||||||
value="setup-token"
|
|
||||||
/>
|
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300">Setup Token (推荐)</span>
|
|
||||||
</label>
|
|
||||||
<label class="flex cursor-pointer items-center">
|
<label class="flex cursor-pointer items-center">
|
||||||
<input
|
<input
|
||||||
v-model="form.addType"
|
v-model="form.addType"
|
||||||
@@ -482,7 +473,18 @@
|
|||||||
type="radio"
|
type="radio"
|
||||||
value="oauth"
|
value="oauth"
|
||||||
/>
|
/>
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300">OAuth 授权</span>
|
<span class="text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>OAuth 授权 (用量可视化)</span
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<label v-if="form.platform === 'claude'" class="flex cursor-pointer items-center">
|
||||||
|
<input
|
||||||
|
v-model="form.addType"
|
||||||
|
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
type="radio"
|
||||||
|
value="setup-token"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300">Setup Token (效期长)</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex cursor-pointer items-center">
|
<label class="flex cursor-pointer items-center">
|
||||||
<input
|
<input
|
||||||
@@ -2799,7 +2801,7 @@ const form = ref({
|
|||||||
addType: (() => {
|
addType: (() => {
|
||||||
const platform = props.account?.platform || 'claude'
|
const platform = props.account?.platform || 'claude'
|
||||||
if (platform === 'gemini' || platform === 'openai') return 'oauth'
|
if (platform === 'gemini' || platform === 'openai') return 'oauth'
|
||||||
if (platform === 'claude') return 'setup-token'
|
if (platform === 'claude') return 'oauth'
|
||||||
return 'manual'
|
return 'manual'
|
||||||
})(),
|
})(),
|
||||||
name: props.account?.name || '',
|
name: props.account?.name || '',
|
||||||
@@ -3924,8 +3926,8 @@ watch(
|
|||||||
) {
|
) {
|
||||||
form.value.addType = 'manual' // Claude Console、CCR、Bedrock 和 OpenAI-Responses 只支持手动模式
|
form.value.addType = 'manual' // Claude Console、CCR、Bedrock 和 OpenAI-Responses 只支持手动模式
|
||||||
} else if (newPlatform === 'claude') {
|
} else if (newPlatform === 'claude') {
|
||||||
// 切换到 Claude 时,使用 Setup Token 作为默认方式
|
// 切换到 Claude 时,使用 oauth 作为默认方式
|
||||||
form.value.addType = 'setup-token'
|
form.value.addType = 'oauth'
|
||||||
} else if (newPlatform === 'gemini') {
|
} else if (newPlatform === 'gemini') {
|
||||||
// 切换到 Gemini 时,使用 OAuth 作为默认方式
|
// 切换到 Gemini 时,使用 OAuth 作为默认方式
|
||||||
form.value.addType = 'oauth'
|
form.value.addType = 'oauth'
|
||||||
|
|||||||
Reference in New Issue
Block a user