From cc27b377d8f71bc0fbe13d5490056c52cf4455c5 Mon Sep 17 00:00:00 2001 From: Edric Li Date: Mon, 8 Sep 2025 01:17:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=9F=BA=E4=BA=8E?= =?UTF-8?q?=E6=97=A5=E8=B4=B9=E7=94=A8=E7=9A=84=E6=99=BA=E8=83=BD=E8=B4=9F?= =?UTF-8?q?=E8=BD=BD=E5=9D=87=E8=A1=A1=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 sortAccountsByCost() 方法,支持按日费用排序账号 - 修改账号选择逻辑从时间排序改为费用排序 - 添加多层容错机制:单账号失败、全局失败、方法异常 - 费用获取失败的账号设为最低优先级,避免故障传播 - 费用相同时仍按时间排序,保持负载均衡 - 增强日志输出,显示账号费用排名和选中账号费用 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/services/claudeAccountService.js | 84 +++++++++++++++++++++++----- 1 file changed, 71 insertions(+), 13 deletions(-) diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index 29dee2bd..deaaf87e 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -712,12 +712,8 @@ class ClaudeAccountService { } // 如果没有映射或映射无效,选择新账户 - // 优先选择最久未使用的账户(负载均衡) - const sortedAccounts = activeAccounts.sort((a, b) => { - const aLastUsed = new Date(a.lastUsedAt || 0).getTime() - const bLastUsed = new Date(b.lastUsedAt || 0).getTime() - return aLastUsed - bLastUsed // 最久未使用的优先 - }) + // 基于日费用选择账户(费用最少的优先) + const sortedAccounts = await this.sortAccountsByCost(activeAccounts) const selectedAccountId = sortedAccounts[0].id @@ -861,12 +857,8 @@ class ClaudeAccountService { return aRateLimitedAt - bRateLimitedAt // 最早限流的优先 }) } else { - // 非限流账户按最后使用时间排序(最久未使用的优先) - candidateAccounts = candidateAccounts.sort((a, b) => { - const aLastUsed = new Date(a.lastUsedAt || 0).getTime() - const bLastUsed = new Date(b.lastUsedAt || 0).getTime() - return aLastUsed - bLastUsed // 最久未使用的优先 - }) + // 非限流账户按日费用排序(费用最少的优先) + candidateAccounts = await this.sortAccountsByCost(candidateAccounts) } if (candidateAccounts.length === 0) { @@ -883,8 +875,10 @@ class ClaudeAccountService { ) } + // 显示选择的账号和其日费用 + const selectedCost = candidateAccounts[0]._dailyCost || 0 logger.info( - `🎯 Selected shared account: ${candidateAccounts[0].name} (${selectedAccountId}) for API key ${apiKeyData.name}` + `🎯 Selected shared account: ${candidateAccounts[0].name} (${selectedAccountId}) for API key ${apiKeyData.name} - Daily cost: $${selectedCost.toFixed(4)}` ) return selectedAccountId } catch (error) { @@ -2114,6 +2108,70 @@ class ClaudeAccountService { logger.error(`❌ Failed to update session window status for account ${accountId}:`, error) } } + + // 💰 基于日费用排序账号(费用最少的优先) + async sortAccountsByCost(accounts) { + try { + // 并行获取所有账号的日费用 + const accountsWithCost = await Promise.all( + accounts.map(async (account) => { + try { + const dailyCost = await redis.getAccountDailyCost(account.id) + return { + ...account, + _dailyCost: dailyCost + } + } catch (error) { + logger.warn(`Failed to get daily cost for account ${account.id}: ${error.message}`) + return { + ...account, + _dailyCost: Number.MAX_SAFE_INTEGER, // 获取费用失败时,设为最高值(最低优先级) + _costError: true + } + } + }) + ) + + // 按日费用排序(费用最少的优先) + const sortedAccounts = accountsWithCost.sort((a, b) => { + // 如果费用相同,按最后使用时间排序(最久未使用的优先) + if (Math.abs(a._dailyCost - b._dailyCost) < 0.000001) { + const aLastUsed = new Date(a.lastUsedAt || 0).getTime() + const bLastUsed = new Date(b.lastUsedAt || 0).getTime() + return aLastUsed - bLastUsed + } + return a._dailyCost - b._dailyCost + }) + + // 检查是否所有账号的费用获取都失败了 + const allAccountsHaveErrors = sortedAccounts.every((account) => account._costError) + + if (allAccountsHaveErrors) { + logger.warn('⚠️ All accounts failed to get daily cost, falling back to time-based sorting') + return accounts.sort((a, b) => { + const aLastUsed = new Date(a.lastUsedAt || 0).getTime() + const bLastUsed = new Date(b.lastUsedAt || 0).getTime() + return aLastUsed - bLastUsed + }) + } + + logger.debug('💰 Account cost ranking:') + sortedAccounts.forEach((account, index) => { + const costDisplay = account._costError ? 'ERROR' : `$${account._dailyCost.toFixed(4)}` + logger.debug(` ${index + 1}. ${account.name} (${account.id}): ${costDisplay}`) + }) + + return sortedAccounts + } catch (error) { + logger.error('❌ Failed to sort accounts by cost:', error) + // 回退到原有的按时间排序策略 + return accounts.sort((a, b) => { + const aLastUsed = new Date(a.lastUsedAt || 0).getTime() + const bLastUsed = new Date(b.lastUsedAt || 0).getTime() + return aLastUsed - bLastUsed + }) + } + } } module.exports = new ClaudeAccountService()