mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 08:59:16 +00:00
feat: 添加 CCR (Claude Code Router) 账户类型支持
实现通过供应商前缀语法进行 CCR 后端路由的完整支持。 用户现在可以在 Claude Code 中使用 `/model ccr,model_name` 将请求路由到 CCR 后端。 暂时没有实现`/v1/messages/count_tokens`,因为这需要在CCR后端支持。 CCR类型的账户也暂时没有考虑模型的支持情况 ## 核心实现 ### 供应商前缀路由 - 添加 modelHelper 工具用于解析模型名称中的 `ccr,` 供应商前缀 - 检测到前缀时自动路由到 CCR 账户池 - 转发到 CCR 后端前移除供应商前缀 ### 账户管理 - 创建 ccrAccountService 实现 CCR 账户的完整 CRUD 操作 - 支持账户属性:名称、API URL、API Key、代理、优先级、配额 - 实现账户状态:active、rate_limited、unauthorized、overloaded - 支持模型映射和支持模型配置 ### 请求转发 - 实现 ccrRelayService 处理 CCR 后端通信 - 支持流式和非流式请求 - 从 SSE 流中解析和捕获使用数据 - 支持 Bearer 和 x-api-key 两种认证格式 ### 统一调度 - 将 CCR 账户集成到 unifiedClaudeScheduler - 添加 \_selectCcrAccount 方法用于 CCR 特定账户选择 - 支持 CCR 账户的会话粘性 - 防止跨类型会话映射(CCR 会话仅用于 CCR 请求) ### 错误处理 - 实现全面的错误状态管理 - 处理 401(未授权)、429(速率限制)、529(过载)错误 - 成功请求后自动从错误状态恢复 - 支持可配置的速率限制持续时间 ### Web 管理界面 - 添加 CcrAccountForm 组件用于创建/编辑 CCR 账户 - 将 CCR 账户集成到 AccountsView 中,提供完整管理功能 - 支持账户切换、重置和使用统计 - 在界面中显示账户状态和错误信息 ### API 端点 - POST /admin/ccr-accounts - 创建 CCR 账户 - GET /admin/ccr-accounts - 列出所有 CCR 账户 - PUT /admin/ccr-accounts/:id - 更新 CCR 账户 - DELETE /admin/ccr-accounts/:id - 删除 CCR 账户 - PUT /admin/ccr-accounts/:id/toggle - 切换账户启用状态 - PUT /admin/ccr-accounts/:id/toggle-schedulable - 切换可调度状态 - POST /admin/ccr-accounts/:id/reset-usage - 重置每日使用量 - POST /admin/ccr-accounts/:id/reset-status - 重置错误状态 ## 技术细节 - CCR 账户使用 'ccr' 作为 accountType 标识符 - 带有 `ccr,` 前缀的请求绕过普通账户池 - 转发到 CCR 后端前清理模型名称内的`ccr,` - 从流式和非流式响应中捕获使用数据 - 支持缓存令牌跟踪(创建和读取)
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
const claudeAccountService = require('./claudeAccountService')
|
||||
const claudeConsoleAccountService = require('./claudeConsoleAccountService')
|
||||
const bedrockAccountService = require('./bedrockAccountService')
|
||||
const ccrAccountService = require('./ccrAccountService')
|
||||
const accountGroupService = require('./accountGroupService')
|
||||
const redis = require('../models/redis')
|
||||
const logger = require('../utils/logger')
|
||||
const { parseVendorPrefixedModel } = require('../utils/modelHelper')
|
||||
|
||||
class UnifiedClaudeScheduler {
|
||||
constructor() {
|
||||
@@ -88,12 +90,53 @@ class UnifiedClaudeScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
// CCR 账户的模型支持检查
|
||||
if (accountType === 'ccr' && account.supportedModels) {
|
||||
// 兼容旧格式(数组)和新格式(对象)
|
||||
if (Array.isArray(account.supportedModels)) {
|
||||
// 旧格式:数组
|
||||
if (
|
||||
account.supportedModels.length > 0 &&
|
||||
!account.supportedModels.includes(requestedModel)
|
||||
) {
|
||||
logger.info(
|
||||
`🚫 CCR account ${account.name} does not support model ${requestedModel}${context ? ` ${context}` : ''}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
} else if (typeof account.supportedModels === 'object') {
|
||||
// 新格式:映射表
|
||||
if (
|
||||
Object.keys(account.supportedModels).length > 0 &&
|
||||
!ccrAccountService.isModelSupported(account.supportedModels, requestedModel)
|
||||
) {
|
||||
logger.info(
|
||||
`🚫 CCR account ${account.name} does not support model ${requestedModel}${context ? ` ${context}` : ''}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 🎯 统一调度Claude账号(官方和Console)
|
||||
async selectAccountForApiKey(apiKeyData, sessionHash = null, requestedModel = null) {
|
||||
try {
|
||||
// 解析供应商前缀
|
||||
const { vendor, baseModel } = parseVendorPrefixedModel(requestedModel)
|
||||
const effectiveModel = vendor === 'ccr' ? baseModel : requestedModel
|
||||
|
||||
logger.debug(
|
||||
`🔍 Model parsing - Original: ${requestedModel}, Vendor: ${vendor}, Effective: ${effectiveModel}`
|
||||
)
|
||||
|
||||
// 如果是 CCR 前缀,只在 CCR 账户池中选择
|
||||
if (vendor === 'ccr') {
|
||||
logger.info(`🎯 CCR vendor prefix detected, routing to CCR accounts only`)
|
||||
return await this._selectCcrAccount(apiKeyData, sessionHash, effectiveModel)
|
||||
}
|
||||
// 如果API Key绑定了专属账户或分组,优先使用
|
||||
if (apiKeyData.claudeAccountId) {
|
||||
// 检查是否是分组
|
||||
@@ -102,7 +145,12 @@ class UnifiedClaudeScheduler {
|
||||
logger.info(
|
||||
`🎯 API key ${apiKeyData.name} is bound to group ${groupId}, selecting from group`
|
||||
)
|
||||
return await this.selectAccountFromGroup(groupId, sessionHash, requestedModel)
|
||||
return await this.selectAccountFromGroup(
|
||||
groupId,
|
||||
sessionHash,
|
||||
effectiveModel,
|
||||
vendor === 'ccr'
|
||||
)
|
||||
}
|
||||
|
||||
// 普通专属账户
|
||||
@@ -176,15 +224,24 @@ class UnifiedClaudeScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
// CCR 账户不支持绑定(仅通过 ccr, 前缀进行 CCR 路由)
|
||||
|
||||
// 如果有会话哈希,检查是否有已映射的账户
|
||||
if (sessionHash) {
|
||||
const mappedAccount = await this._getSessionMapping(sessionHash)
|
||||
if (mappedAccount) {
|
||||
// 当本次请求不是 CCR 前缀时,不允许使用指向 CCR 的粘性会话映射
|
||||
if (vendor !== 'ccr' && mappedAccount.accountType === 'ccr') {
|
||||
logger.info(
|
||||
`ℹ️ Skipping CCR sticky session mapping for non-CCR request; removing mapping for session ${sessionHash}`
|
||||
)
|
||||
await this._deleteSessionMapping(sessionHash)
|
||||
} else {
|
||||
// 验证映射的账户是否仍然可用
|
||||
const isAvailable = await this._isAccountAvailable(
|
||||
mappedAccount.accountId,
|
||||
mappedAccount.accountType,
|
||||
requestedModel
|
||||
effectiveModel
|
||||
)
|
||||
if (isAvailable) {
|
||||
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
||||
@@ -199,17 +256,22 @@ class UnifiedClaudeScheduler {
|
||||
)
|
||||
await this._deleteSessionMapping(sessionHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有可用账户(传递请求的模型进行过滤)
|
||||
const availableAccounts = await this._getAllAvailableAccounts(apiKeyData, requestedModel)
|
||||
const availableAccounts = await this._getAllAvailableAccounts(
|
||||
apiKeyData,
|
||||
effectiveModel,
|
||||
false // 仅前缀才走 CCR:默认池不包含 CCR 账户
|
||||
)
|
||||
|
||||
if (availableAccounts.length === 0) {
|
||||
// 提供更详细的错误信息
|
||||
if (requestedModel) {
|
||||
if (effectiveModel) {
|
||||
throw new Error(
|
||||
`No available Claude accounts support the requested model: ${requestedModel}`
|
||||
`No available Claude accounts support the requested model: ${effectiveModel}`
|
||||
)
|
||||
} else {
|
||||
throw new Error('No available Claude accounts (neither official nor console)')
|
||||
@@ -249,7 +311,7 @@ class UnifiedClaudeScheduler {
|
||||
}
|
||||
|
||||
// 📋 获取所有可用账户(合并官方和Console)
|
||||
async _getAllAvailableAccounts(apiKeyData, requestedModel = null) {
|
||||
async _getAllAvailableAccounts(apiKeyData, requestedModel = null, includeCcr = false) {
|
||||
const availableAccounts = []
|
||||
|
||||
// 如果API Key绑定了专属账户,优先返回
|
||||
@@ -496,8 +558,60 @@ class UnifiedClaudeScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取CCR账户(共享池)- 仅当明确要求包含时
|
||||
if (includeCcr) {
|
||||
const ccrAccounts = await ccrAccountService.getAllAccounts()
|
||||
logger.info(`📋 Found ${ccrAccounts.length} total CCR accounts`)
|
||||
|
||||
for (const account of ccrAccounts) {
|
||||
logger.info(
|
||||
`🔍 Checking CCR account: ${account.name} - isActive: ${account.isActive}, status: ${account.status}, accountType: ${account.accountType}, schedulable: ${account.schedulable}`
|
||||
)
|
||||
|
||||
if (
|
||||
account.isActive === true &&
|
||||
account.status === 'active' &&
|
||||
account.accountType === 'shared' &&
|
||||
this._isSchedulable(account.schedulable)
|
||||
) {
|
||||
// 检查模型支持
|
||||
if (!this._isModelSupportedByAccount(account, 'ccr', requestedModel)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查是否被限流
|
||||
const isRateLimited = await ccrAccountService.isAccountRateLimited(account.id)
|
||||
const isQuotaExceeded = await ccrAccountService.isAccountQuotaExceeded(account.id)
|
||||
|
||||
if (!isRateLimited && !isQuotaExceeded) {
|
||||
availableAccounts.push({
|
||||
...account,
|
||||
accountId: account.id,
|
||||
accountType: 'ccr',
|
||||
priority: parseInt(account.priority) || 50,
|
||||
lastUsedAt: account.lastUsedAt || '0'
|
||||
})
|
||||
logger.info(
|
||||
`✅ Added CCR account to available pool: ${account.name} (priority: ${account.priority})`
|
||||
)
|
||||
} else {
|
||||
if (isRateLimited) {
|
||||
logger.warn(`⚠️ CCR account ${account.name} is rate limited`)
|
||||
}
|
||||
if (isQuotaExceeded) {
|
||||
logger.warn(`💰 CCR account ${account.name} quota exceeded`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.info(
|
||||
`❌ CCR account ${account.name} not eligible - isActive: ${account.isActive}, status: ${account.status}, accountType: ${account.accountType}, schedulable: ${account.schedulable}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`📊 Total available accounts: ${availableAccounts.length} (Claude: ${availableAccounts.filter((a) => a.accountType === 'claude-official').length}, Console: ${availableAccounts.filter((a) => a.accountType === 'claude-console').length}, Bedrock: ${availableAccounts.filter((a) => a.accountType === 'bedrock').length})`
|
||||
`📊 Total available accounts: ${availableAccounts.length} (Claude: ${availableAccounts.filter((a) => a.accountType === 'claude-official').length}, Console: ${availableAccounts.filter((a) => a.accountType === 'claude-console').length}, Bedrock: ${availableAccounts.filter((a) => a.accountType === 'bedrock').length}, CCR: ${availableAccounts.filter((a) => a.accountType === 'ccr').length})`
|
||||
)
|
||||
return availableAccounts
|
||||
}
|
||||
@@ -617,6 +731,52 @@ class UnifiedClaudeScheduler {
|
||||
}
|
||||
// Bedrock账户暂不需要限流检查,因为AWS管理限流
|
||||
return true
|
||||
} else if (accountType === 'ccr') {
|
||||
const account = await ccrAccountService.getAccount(accountId)
|
||||
if (!account || !account.isActive) {
|
||||
return false
|
||||
}
|
||||
// 检查账户状态
|
||||
if (
|
||||
account.status !== 'active' &&
|
||||
account.status !== 'unauthorized' &&
|
||||
account.status !== 'overloaded'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
// 检查是否可调度
|
||||
if (!this._isSchedulable(account.schedulable)) {
|
||||
logger.info(`🚫 CCR account ${accountId} is not schedulable`)
|
||||
return false
|
||||
}
|
||||
// 检查模型支持
|
||||
if (!this._isModelSupportedByAccount(account, 'ccr', requestedModel, 'in session check')) {
|
||||
return false
|
||||
}
|
||||
// 检查是否超额
|
||||
try {
|
||||
await ccrAccountService.checkQuotaUsage(accountId)
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to check quota for CCR account ${accountId}: ${e.message}`)
|
||||
// 继续处理
|
||||
}
|
||||
|
||||
// 检查是否被限流
|
||||
if (await ccrAccountService.isAccountRateLimited(accountId)) {
|
||||
return false
|
||||
}
|
||||
if (await ccrAccountService.isAccountQuotaExceeded(accountId)) {
|
||||
return false
|
||||
}
|
||||
// 检查是否未授权(401错误)
|
||||
if (account.status === 'unauthorized') {
|
||||
return false
|
||||
}
|
||||
// 检查是否过载(529错误)
|
||||
if (await ccrAccountService.isAccountOverloaded(accountId)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
@@ -673,6 +833,8 @@ class UnifiedClaudeScheduler {
|
||||
)
|
||||
} else if (accountType === 'claude-console') {
|
||||
await claudeConsoleAccountService.markAccountRateLimited(accountId)
|
||||
} else if (accountType === 'ccr') {
|
||||
await ccrAccountService.markAccountRateLimited(accountId)
|
||||
}
|
||||
|
||||
// 删除会话映射
|
||||
@@ -697,6 +859,8 @@ class UnifiedClaudeScheduler {
|
||||
await claudeAccountService.removeAccountRateLimit(accountId)
|
||||
} else if (accountType === 'claude-console') {
|
||||
await claudeConsoleAccountService.removeAccountRateLimit(accountId)
|
||||
} else if (accountType === 'ccr') {
|
||||
await ccrAccountService.removeAccountRateLimit(accountId)
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
@@ -716,6 +880,8 @@ class UnifiedClaudeScheduler {
|
||||
return await claudeAccountService.isAccountRateLimited(accountId)
|
||||
} else if (accountType === 'claude-console') {
|
||||
return await claudeConsoleAccountService.isAccountRateLimited(accountId)
|
||||
} else if (accountType === 'ccr') {
|
||||
return await ccrAccountService.isAccountRateLimited(accountId)
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
@@ -791,7 +957,12 @@ class UnifiedClaudeScheduler {
|
||||
}
|
||||
|
||||
// 👥 从分组中选择账户
|
||||
async selectAccountFromGroup(groupId, sessionHash = null, requestedModel = null) {
|
||||
async selectAccountFromGroup(
|
||||
groupId,
|
||||
sessionHash = null,
|
||||
requestedModel = null,
|
||||
allowCcr = false
|
||||
) {
|
||||
try {
|
||||
// 获取分组信息
|
||||
const group = await accountGroupService.getGroup(groupId)
|
||||
@@ -808,18 +979,23 @@ class UnifiedClaudeScheduler {
|
||||
// 验证映射的账户是否属于这个分组
|
||||
const memberIds = await accountGroupService.getGroupMembers(groupId)
|
||||
if (memberIds.includes(mappedAccount.accountId)) {
|
||||
const isAvailable = await this._isAccountAvailable(
|
||||
mappedAccount.accountId,
|
||||
mappedAccount.accountType,
|
||||
requestedModel
|
||||
)
|
||||
if (isAvailable) {
|
||||
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
||||
await redis.extendSessionAccountMappingTTL(sessionHash)
|
||||
logger.info(
|
||||
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
||||
// 非 CCR 请求时不允许 CCR 粘性映射
|
||||
if (!allowCcr && mappedAccount.accountType === 'ccr') {
|
||||
await this._deleteSessionMapping(sessionHash)
|
||||
} else {
|
||||
const isAvailable = await this._isAccountAvailable(
|
||||
mappedAccount.accountId,
|
||||
mappedAccount.accountType,
|
||||
requestedModel
|
||||
)
|
||||
return mappedAccount
|
||||
if (isAvailable) {
|
||||
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
||||
await redis.extendSessionAccountMappingTTL(sessionHash)
|
||||
logger.info(
|
||||
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
||||
)
|
||||
return mappedAccount
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果映射的账户不可用或不在分组中,删除映射
|
||||
@@ -851,6 +1027,14 @@ class UnifiedClaudeScheduler {
|
||||
account = await claudeConsoleAccountService.getAccount(memberId)
|
||||
if (account) {
|
||||
accountType = 'claude-console'
|
||||
} else {
|
||||
// 尝试CCR账户(仅允许在 allowCcr 为 true 时)
|
||||
if (allowCcr) {
|
||||
account = await ccrAccountService.getAccount(memberId)
|
||||
if (account) {
|
||||
accountType = 'ccr'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (group.platform === 'gemini') {
|
||||
@@ -873,7 +1057,9 @@ class UnifiedClaudeScheduler {
|
||||
const status =
|
||||
accountType === 'claude-official'
|
||||
? account.status !== 'error' && account.status !== 'blocked'
|
||||
: account.status === 'active'
|
||||
: accountType === 'ccr'
|
||||
? account.status === 'active'
|
||||
: account.status === 'active'
|
||||
|
||||
if (isActive && status && this._isSchedulable(account.schedulable)) {
|
||||
// 检查模型支持
|
||||
@@ -930,6 +1116,133 @@ class UnifiedClaudeScheduler {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 🎯 专门选择CCR账户(仅限CCR前缀路由使用)
|
||||
async _selectCcrAccount(apiKeyData, sessionHash = null, effectiveModel = null) {
|
||||
try {
|
||||
// 1. 检查会话粘性
|
||||
if (sessionHash) {
|
||||
const mappedAccount = await this._getSessionMapping(sessionHash)
|
||||
if (mappedAccount && mappedAccount.accountType === 'ccr') {
|
||||
// 验证映射的CCR账户是否仍然可用
|
||||
const isAvailable = await this._isAccountAvailable(
|
||||
mappedAccount.accountId,
|
||||
mappedAccount.accountType,
|
||||
effectiveModel
|
||||
)
|
||||
if (isAvailable) {
|
||||
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
||||
await redis.extendSessionAccountMappingTTL(sessionHash)
|
||||
logger.info(
|
||||
`🎯 Using sticky CCR session account: ${mappedAccount.accountId} for session ${sessionHash}`
|
||||
)
|
||||
return mappedAccount
|
||||
} else {
|
||||
logger.warn(
|
||||
`⚠️ Mapped CCR account ${mappedAccount.accountId} is no longer available, selecting new account`
|
||||
)
|
||||
await this._deleteSessionMapping(sessionHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 获取所有可用的CCR账户
|
||||
const availableCcrAccounts = await this._getAvailableCcrAccounts(effectiveModel)
|
||||
|
||||
if (availableCcrAccounts.length === 0) {
|
||||
throw new Error(
|
||||
`No available CCR accounts support the requested model: ${effectiveModel || 'unspecified'}`
|
||||
)
|
||||
}
|
||||
|
||||
// 3. 按优先级和最后使用时间排序
|
||||
const sortedAccounts = this._sortAccountsByPriority(availableCcrAccounts)
|
||||
const selectedAccount = sortedAccounts[0]
|
||||
|
||||
// 4. 建立会话映射
|
||||
if (sessionHash) {
|
||||
await this._setSessionMapping(
|
||||
sessionHash,
|
||||
selectedAccount.accountId,
|
||||
selectedAccount.accountType
|
||||
)
|
||||
logger.info(
|
||||
`🎯 Created new sticky CCR session mapping: ${selectedAccount.name} (${selectedAccount.accountId}) for session ${sessionHash}`
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`🎯 Selected CCR account: ${selectedAccount.name} (${selectedAccount.accountId}) with priority ${selectedAccount.priority} for API key ${apiKeyData.name}`
|
||||
)
|
||||
|
||||
return {
|
||||
accountId: selectedAccount.accountId,
|
||||
accountType: selectedAccount.accountType
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to select CCR account:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 📋 获取所有可用的CCR账户
|
||||
async _getAvailableCcrAccounts(requestedModel = null) {
|
||||
const availableAccounts = []
|
||||
|
||||
try {
|
||||
const ccrAccounts = await ccrAccountService.getAllAccounts()
|
||||
logger.info(`📋 Found ${ccrAccounts.length} total CCR accounts for CCR-only selection`)
|
||||
|
||||
for (const account of ccrAccounts) {
|
||||
logger.debug(
|
||||
`🔍 Checking CCR account: ${account.name} - isActive: ${account.isActive}, status: ${account.status}, accountType: ${account.accountType}, schedulable: ${account.schedulable}`
|
||||
)
|
||||
|
||||
if (
|
||||
account.isActive === true &&
|
||||
account.status === 'active' &&
|
||||
account.accountType === 'shared' &&
|
||||
this._isSchedulable(account.schedulable)
|
||||
) {
|
||||
// 检查模型支持
|
||||
if (!this._isModelSupportedByAccount(account, 'ccr', requestedModel)) {
|
||||
logger.debug(`CCR account ${account.name} does not support model ${requestedModel}`)
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查是否被限流或超额
|
||||
const isRateLimited = await ccrAccountService.isAccountRateLimited(account.id)
|
||||
const isQuotaExceeded = await ccrAccountService.isAccountQuotaExceeded(account.id)
|
||||
const isOverloaded = await ccrAccountService.isAccountOverloaded(account.id)
|
||||
|
||||
if (!isRateLimited && !isQuotaExceeded && !isOverloaded) {
|
||||
availableAccounts.push({
|
||||
...account,
|
||||
accountId: account.id,
|
||||
accountType: 'ccr',
|
||||
priority: parseInt(account.priority) || 50,
|
||||
lastUsedAt: account.lastUsedAt || '0'
|
||||
})
|
||||
logger.debug(`✅ Added CCR account to available pool: ${account.name}`)
|
||||
} else {
|
||||
logger.debug(
|
||||
`❌ CCR account ${account.name} not available - rateLimited: ${isRateLimited}, quotaExceeded: ${isQuotaExceeded}, overloaded: ${isOverloaded}`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
logger.debug(
|
||||
`❌ CCR account ${account.name} not eligible - isActive: ${account.isActive}, status: ${account.status}, accountType: ${account.accountType}, schedulable: ${account.schedulable}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`📊 Total available CCR accounts: ${availableAccounts.length}`)
|
||||
return availableAccounts
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to get available CCR accounts:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new UnifiedClaudeScheduler()
|
||||
|
||||
Reference in New Issue
Block a user