合并 main 分支到 dev 分支

This commit is contained in:
shaw
2025-07-30 10:18:35 +08:00
17 changed files with 2174 additions and 38 deletions

View File

@@ -1,6 +1,7 @@
const express = require('express');
const apiKeyService = require('../services/apiKeyService');
const claudeAccountService = require('../services/claudeAccountService');
const claudeConsoleAccountService = require('../services/claudeConsoleAccountService');
const geminiAccountService = require('../services/geminiAccountService');
const redis = require('../models/redis');
const { authenticateAdmin } = require('../middleware/auth');
@@ -703,7 +704,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
refreshToken,
claudeAiOauth,
proxy,
accountType
accountType,
priority
} = req.body;
if (!name) {
@@ -715,6 +717,11 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
return res.status(400).json({ error: 'Invalid account type. Must be "shared" or "dedicated"' });
}
// 验证priority的有效性
if (priority !== undefined && (typeof priority !== 'number' || priority < 1 || priority > 100)) {
return res.status(400).json({ error: 'Priority must be a number between 1 and 100' });
}
const newAccount = await claudeAccountService.createAccount({
name,
description,
@@ -723,7 +730,8 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
refreshToken,
claudeAiOauth,
proxy,
accountType: accountType || 'shared' // 默认为共享类型
accountType: accountType || 'shared', // 默认为共享类型
priority: priority || 50 // 默认优先级为50
});
logger.success(`🏢 Admin created new Claude account: ${name} (${accountType || 'shared'})`);
@@ -740,6 +748,11 @@ router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) =>
const { accountId } = req.params;
const updates = req.body;
// 验证priority的有效性
if (updates.priority !== undefined && (typeof updates.priority !== 'number' || updates.priority < 1 || updates.priority > 100)) {
return res.status(400).json({ error: 'Priority must be a number between 1 and 100' });
}
await claudeAccountService.updateAccount(accountId, updates);
logger.success(`📝 Admin updated Claude account: ${accountId}`);
@@ -780,6 +793,198 @@ router.post('/claude-accounts/:accountId/refresh', authenticateAdmin, async (req
}
});
// 切换Claude账户调度状态
router.put('/claude-accounts/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params;
const accounts = await claudeAccountService.getAllAccounts();
const account = accounts.find(acc => acc.id === accountId);
if (!account) {
return res.status(404).json({ error: 'Account not found' });
}
const newSchedulable = !account.schedulable;
await claudeAccountService.updateAccount(accountId, { schedulable: newSchedulable });
logger.success(`🔄 Admin toggled Claude account schedulable status: ${accountId} -> ${newSchedulable ? 'schedulable' : 'not schedulable'}`);
res.json({ success: true, schedulable: newSchedulable });
} catch (error) {
logger.error('❌ Failed to toggle Claude account schedulable status:', error);
res.status(500).json({ error: 'Failed to toggle schedulable status', message: error.message });
}
});
// 🎮 Claude Console 账户管理
// 获取所有Claude Console账户
router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
try {
const accounts = await claudeConsoleAccountService.getAllAccounts();
// 为每个账户添加使用统计信息
const accountsWithStats = await Promise.all(accounts.map(async (account) => {
try {
const usageStats = await redis.getAccountUsageStats(account.id);
return {
...account,
usage: {
daily: usageStats.daily,
total: usageStats.total,
averages: usageStats.averages
}
};
} catch (statsError) {
logger.warn(`⚠️ Failed to get usage stats for Claude Console account ${account.id}:`, statsError.message);
return {
...account,
usage: {
daily: { tokens: 0, requests: 0, allTokens: 0 },
total: { tokens: 0, requests: 0, allTokens: 0 },
averages: { rpm: 0, tpm: 0 }
}
};
}
}));
res.json({ success: true, data: accountsWithStats });
} catch (error) {
logger.error('❌ Failed to get Claude Console accounts:', error);
res.status(500).json({ error: 'Failed to get Claude Console accounts', message: error.message });
}
});
// 创建新的Claude Console账户
router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
try {
const {
name,
description,
apiUrl,
apiKey,
priority,
supportedModels,
userAgent,
rateLimitDuration,
proxy,
accountType
} = req.body;
if (!name || !apiUrl || !apiKey) {
return res.status(400).json({ error: 'Name, API URL and API Key are required' });
}
// 验证priority的有效性1-100
if (priority !== undefined && (priority < 1 || priority > 100)) {
return res.status(400).json({ error: 'Priority must be between 1 and 100' });
}
// 验证accountType的有效性
if (accountType && !['shared', 'dedicated'].includes(accountType)) {
return res.status(400).json({ error: 'Invalid account type. Must be "shared" or "dedicated"' });
}
const newAccount = await claudeConsoleAccountService.createAccount({
name,
description,
apiUrl,
apiKey,
priority: priority || 50,
supportedModels: supportedModels || [],
userAgent,
rateLimitDuration: rateLimitDuration || 60,
proxy,
accountType: accountType || 'shared'
});
logger.success(`🎮 Admin created Claude Console account: ${name}`);
res.json({ success: true, data: newAccount });
} catch (error) {
logger.error('❌ Failed to create Claude Console account:', error);
res.status(500).json({ error: 'Failed to create Claude Console account', message: error.message });
}
});
// 更新Claude Console账户
router.put('/claude-console-accounts/:accountId', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params;
const updates = req.body;
// 验证priority的有效性1-100
if (updates.priority !== undefined && (updates.priority < 1 || updates.priority > 100)) {
return res.status(400).json({ error: 'Priority must be between 1 and 100' });
}
await claudeConsoleAccountService.updateAccount(accountId, updates);
logger.success(`📝 Admin updated Claude Console account: ${accountId}`);
res.json({ success: true, message: 'Claude Console account updated successfully' });
} catch (error) {
logger.error('❌ Failed to update Claude Console account:', error);
res.status(500).json({ error: 'Failed to update Claude Console account', message: error.message });
}
});
// 删除Claude Console账户
router.delete('/claude-console-accounts/:accountId', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params;
await claudeConsoleAccountService.deleteAccount(accountId);
logger.success(`🗑️ Admin deleted Claude Console account: ${accountId}`);
res.json({ success: true, message: 'Claude Console account deleted successfully' });
} catch (error) {
logger.error('❌ Failed to delete Claude Console account:', error);
res.status(500).json({ error: 'Failed to delete Claude Console account', message: error.message });
}
});
// 切换Claude Console账户状态
router.put('/claude-console-accounts/:accountId/toggle', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params;
const account = await claudeConsoleAccountService.getAccount(accountId);
if (!account) {
return res.status(404).json({ error: 'Account not found' });
}
const newStatus = !account.isActive;
await claudeConsoleAccountService.updateAccount(accountId, { isActive: newStatus });
logger.success(`🔄 Admin toggled Claude Console account status: ${accountId} -> ${newStatus ? 'active' : 'inactive'}`);
res.json({ success: true, isActive: newStatus });
} catch (error) {
logger.error('❌ Failed to toggle Claude Console account status:', error);
res.status(500).json({ error: 'Failed to toggle account status', message: error.message });
}
});
// 切换Claude Console账户调度状态
router.put('/claude-console-accounts/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params;
const account = await claudeConsoleAccountService.getAccount(accountId);
if (!account) {
return res.status(404).json({ error: 'Account not found' });
}
const newSchedulable = !account.schedulable;
await claudeConsoleAccountService.updateAccount(accountId, { schedulable: newSchedulable });
logger.success(`🔄 Admin toggled Claude Console account schedulable status: ${accountId} -> ${newSchedulable ? 'schedulable' : 'not schedulable'}`);
res.json({ success: true, schedulable: newSchedulable });
} catch (error) {
logger.error('❌ Failed to toggle Claude Console account schedulable status:', error);
res.status(500).json({ error: 'Failed to toggle schedulable status', message: error.message });
}
});
// 🤖 Gemini 账户管理
// 生成 Gemini OAuth 授权 URL

View File

@@ -1,9 +1,12 @@
const express = require('express');
const claudeRelayService = require('../services/claudeRelayService');
const claudeConsoleRelayService = require('../services/claudeConsoleRelayService');
const unifiedClaudeScheduler = require('../services/unifiedClaudeScheduler');
const apiKeyService = require('../services/apiKeyService');
const { authenticateApiKey } = require('../middleware/auth');
const logger = require('../utils/logger');
const redis = require('../models/redis');
const sessionHelper = require('../utils/sessionHelper');
const router = express.Router();
@@ -56,8 +59,17 @@ async function handleMessagesRequest(req, res) {
let usageDataCaptured = false;
// 使用自定义流处理器来捕获usage数据
await claudeRelayService.relayStreamRequestWithUsageCapture(req.body, req.apiKey, res, req.headers, (usageData) => {
// 生成会话哈希用于sticky会话
const sessionHash = sessionHelper.generateSessionHash(req.body);
// 使用统一调度选择账号(传递请求的模型)
const requestedModel = req.body.model;
const { accountId, accountType } = await unifiedClaudeScheduler.selectAccountForApiKey(req.apiKey, sessionHash, requestedModel);
// 根据账号类型选择对应的转发服务并调用
if (accountType === 'claude-official') {
// 官方Claude账号使用原有的转发服务会自己选择账号
await claudeRelayService.relayStreamRequestWithUsageCapture(req.body, req.apiKey, res, req.headers, (usageData) => {
// 回调函数当检测到完整usage数据时记录真实token使用量
logger.info('🎯 Usage callback triggered with complete data:', JSON.stringify(usageData, null, 2));
@@ -88,7 +100,42 @@ async function handleMessagesRequest(req, res) {
} else {
logger.warn('⚠️ Usage callback triggered but data is incomplete:', JSON.stringify(usageData));
}
});
});
} else {
// Claude Console账号使用Console转发服务需要传递accountId
await claudeConsoleRelayService.relayStreamRequestWithUsageCapture(req.body, req.apiKey, res, req.headers, (usageData) => {
// 回调函数当检测到完整usage数据时记录真实token使用量
logger.info('🎯 Usage callback triggered with complete data:', JSON.stringify(usageData, null, 2));
if (usageData && usageData.input_tokens !== undefined && usageData.output_tokens !== undefined) {
const inputTokens = usageData.input_tokens || 0;
const outputTokens = usageData.output_tokens || 0;
const cacheCreateTokens = usageData.cache_creation_input_tokens || 0;
const cacheReadTokens = usageData.cache_read_input_tokens || 0;
const model = usageData.model || 'unknown';
// 记录真实的token使用量包含模型信息和所有4种token以及账户ID
const usageAccountId = usageData.accountId;
apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model, usageAccountId).catch(error => {
logger.error('❌ Failed to record stream usage:', error);
});
// 更新时间窗口内的token计数
if (req.rateLimitInfo) {
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
redis.getClient().incrby(req.rateLimitInfo.tokenCountKey, totalTokens).catch(error => {
logger.error('❌ Failed to update rate limit token count:', error);
});
logger.api(`📊 Updated rate limit token count: +${totalTokens} tokens`);
}
usageDataCaptured = true;
logger.api(`📊 Stream usage recorded (real) - Model: ${model}, Input: ${inputTokens}, Output: ${outputTokens}, Cache Create: ${cacheCreateTokens}, Cache Read: ${cacheReadTokens}, Total: ${inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens} tokens`);
} else {
logger.warn('⚠️ Usage callback triggered but data is incomplete:', JSON.stringify(usageData));
}
}, accountId);
}
// 流式请求完成后 - 如果没有捕获到usage数据记录警告但不进行估算
setTimeout(() => {
@@ -103,7 +150,27 @@ async function handleMessagesRequest(req, res) {
apiKeyName: req.apiKey.name
});
const response = await claudeRelayService.relayRequest(req.body, req.apiKey, req, res, req.headers);
// 生成会话哈希用于sticky会话
const sessionHash = sessionHelper.generateSessionHash(req.body);
// 使用统一调度选择账号(传递请求的模型)
const requestedModel = req.body.model;
const { accountId, accountType } = await unifiedClaudeScheduler.selectAccountForApiKey(req.apiKey, sessionHash, requestedModel);
// 根据账号类型选择对应的转发服务
let response;
logger.debug(`[DEBUG] Request query params: ${JSON.stringify(req.query)}`);
logger.debug(`[DEBUG] Request URL: ${req.url}`);
logger.debug(`[DEBUG] Request path: ${req.path}`);
if (accountType === 'claude-official') {
// 官方Claude账号使用原有的转发服务
response = await claudeRelayService.relayRequest(req.body, req.apiKey, req, res, req.headers);
} else {
// Claude Console账号使用Console转发服务
logger.debug(`[DEBUG] Calling claudeConsoleRelayService.relayRequest with accountId: ${accountId}`);
response = await claudeConsoleRelayService.relayRequest(req.body, req.apiKey, req, res, req.headers, accountId);
}
logger.info('📡 Claude API response received', {
statusCode: response.statusCode,

View File

@@ -37,7 +37,9 @@ class ClaudeAccountService {
claudeAiOauth = null, // Claude标准格式的OAuth数据
proxy = null, // { type: 'socks5', host: 'localhost', port: 1080, username: '', password: '' }
isActive = true,
accountType = 'shared' // 'dedicated' or 'shared'
accountType = 'shared', // 'dedicated' or 'shared'
priority = 50, // 调度优先级 (1-100数字越小优先级越高)
schedulable = true // 是否可被调度
} = options;
const accountId = uuidv4();
@@ -60,11 +62,13 @@ class ClaudeAccountService {
proxy: proxy ? JSON.stringify(proxy) : '',
isActive: isActive.toString(),
accountType: accountType, // 账号类型:'dedicated' 或 'shared'
priority: priority.toString(), // 调度优先级
createdAt: new Date().toISOString(),
lastUsedAt: '',
lastRefreshAt: '',
status: 'active', // 有OAuth数据的账户直接设为active
errorMessage: ''
errorMessage: '',
schedulable: schedulable.toString() // 是否可被调度
};
} else {
// 兼容旧格式
@@ -81,11 +85,13 @@ class ClaudeAccountService {
proxy: proxy ? JSON.stringify(proxy) : '',
isActive: isActive.toString(),
accountType: accountType, // 账号类型:'dedicated' 或 'shared'
priority: priority.toString(), // 调度优先级
createdAt: new Date().toISOString(),
lastUsedAt: '',
lastRefreshAt: '',
status: 'created', // created, active, expired, error
errorMessage: ''
errorMessage: '',
schedulable: schedulable.toString() // 是否可被调度
};
}
@@ -101,6 +107,7 @@ class ClaudeAccountService {
isActive,
proxy,
accountType,
priority,
status: accountData.status,
createdAt: accountData.createdAt,
expiresAt: accountData.expiresAt,
@@ -305,6 +312,7 @@ class ClaudeAccountService {
status: account.status,
errorMessage: account.errorMessage,
accountType: account.accountType || 'shared', // 兼容旧数据,默认为共享
priority: parseInt(account.priority) || 50, // 兼容旧数据默认优先级50
createdAt: account.createdAt,
lastUsedAt: account.lastUsedAt,
lastRefreshAt: account.lastRefreshAt,
@@ -323,7 +331,9 @@ class ClaudeAccountService {
progress: 0,
remainingTime: null,
lastRequestTime: null
}
},
// 添加调度状态
schedulable: account.schedulable !== 'false' // 默认为true兼容历史数据
};
}));
@@ -343,7 +353,7 @@ class ClaudeAccountService {
throw new Error('Account not found');
}
const allowedUpdates = ['name', 'description', 'email', 'password', 'refreshToken', 'proxy', 'isActive', 'claudeAiOauth', 'accountType'];
const allowedUpdates = ['name', 'description', 'email', 'password', 'refreshToken', 'proxy', 'isActive', 'claudeAiOauth', 'accountType', 'priority', 'schedulable'];
const updatedData = { ...accountData };
// 检查是否新增了 refresh token
@@ -355,6 +365,8 @@ class ClaudeAccountService {
updatedData[field] = this._encryptSensitiveData(value);
} else if (field === 'proxy') {
updatedData[field] = value ? JSON.stringify(value) : '';
} else if (field === 'priority') {
updatedData[field] = value.toString();
} else if (field === 'claudeAiOauth') {
// 更新 Claude AI OAuth 数据
if (value) {
@@ -1008,7 +1020,7 @@ class ClaudeAccountService {
}
}
logger.success(`✅ Session window initialization completed:`);
logger.success('✅ Session window initialization completed:');
logger.success(` 📊 Total accounts: ${accounts.length}`);
logger.success(` ✅ Initialized: ${initializedCount}`);
logger.success(` ⏭️ Skipped (existing): ${skippedCount}`);

View File

@@ -0,0 +1,493 @@
const { v4: uuidv4 } = require('uuid');
const crypto = require('crypto');
const { SocksProxyAgent } = require('socks-proxy-agent');
const { HttpsProxyAgent } = require('https-proxy-agent');
const redis = require('../models/redis');
const logger = require('../utils/logger');
const config = require('../../config/config');
class ClaudeConsoleAccountService {
constructor() {
// 加密相关常量
this.ENCRYPTION_ALGORITHM = 'aes-256-cbc';
this.ENCRYPTION_SALT = 'claude-console-salt';
// Redis键前缀
this.ACCOUNT_KEY_PREFIX = 'claude_console_account:';
this.SHARED_ACCOUNTS_KEY = 'shared_claude_console_accounts';
}
// 🏢 创建Claude Console账户
async createAccount(options = {}) {
const {
name = 'Claude Console Account',
description = '',
apiUrl = '',
apiKey = '',
priority = 50, // 默认优先级501-100
supportedModels = [], // 支持的模型列表,空数组表示支持所有
userAgent = 'claude-cli/1.0.61 (console, cli)',
rateLimitDuration = 60, // 限流时间(分钟)
proxy = null,
isActive = true,
accountType = 'shared', // 'dedicated' or 'shared'
schedulable = true // 是否可被调度
} = options;
// 验证必填字段
if (!apiUrl || !apiKey) {
throw new Error('API URL and API Key are required for Claude Console account');
}
const accountId = uuidv4();
const accountData = {
id: accountId,
platform: 'claude-console',
name,
description,
apiUrl: apiUrl,
apiKey: this._encryptSensitiveData(apiKey),
priority: priority.toString(),
supportedModels: JSON.stringify(supportedModels),
userAgent,
rateLimitDuration: rateLimitDuration.toString(),
proxy: proxy ? JSON.stringify(proxy) : '',
isActive: isActive.toString(),
accountType,
createdAt: new Date().toISOString(),
lastUsedAt: '',
status: 'active',
errorMessage: '',
// 限流相关
rateLimitedAt: '',
rateLimitStatus: '',
// 调度控制
schedulable: schedulable.toString()
};
const client = redis.getClientSafe();
logger.debug(`[DEBUG] Saving account data to Redis with key: ${this.ACCOUNT_KEY_PREFIX}${accountId}`);
logger.debug(`[DEBUG] Account data to save: ${JSON.stringify(accountData, null, 2)}`);
await client.hset(
`${this.ACCOUNT_KEY_PREFIX}${accountId}`,
accountData
);
// 如果是共享账户,添加到共享账户集合
if (accountType === 'shared') {
await client.sadd(this.SHARED_ACCOUNTS_KEY, accountId);
}
logger.success(`🏢 Created Claude Console account: ${name} (${accountId})`);
return {
id: accountId,
name,
description,
apiUrl,
priority,
supportedModels,
userAgent,
rateLimitDuration,
isActive,
proxy,
accountType,
status: 'active',
createdAt: accountData.createdAt
};
}
// 📋 获取所有Claude Console账户
async getAllAccounts() {
try {
const client = redis.getClientSafe();
const keys = await client.keys(`${this.ACCOUNT_KEY_PREFIX}*`);
const accounts = [];
for (const key of keys) {
const accountData = await client.hgetall(key);
if (accountData && Object.keys(accountData).length > 0) {
// 获取限流状态信息
const rateLimitInfo = this._getRateLimitInfo(accountData);
accounts.push({
id: accountData.id,
platform: accountData.platform,
name: accountData.name,
description: accountData.description,
apiUrl: accountData.apiUrl,
priority: parseInt(accountData.priority) || 50,
supportedModels: JSON.parse(accountData.supportedModels || '[]'),
userAgent: accountData.userAgent,
rateLimitDuration: parseInt(accountData.rateLimitDuration) || 60,
isActive: accountData.isActive === 'true',
proxy: accountData.proxy ? JSON.parse(accountData.proxy) : null,
accountType: accountData.accountType || 'shared',
status: accountData.status,
errorMessage: accountData.errorMessage,
createdAt: accountData.createdAt,
lastUsedAt: accountData.lastUsedAt,
rateLimitStatus: rateLimitInfo,
schedulable: accountData.schedulable !== 'false' // 默认为true只有明确设置为false才不可调度
});
}
}
return accounts;
} catch (error) {
logger.error('❌ Failed to get Claude Console accounts:', error);
throw error;
}
}
// 🔍 获取单个账户(内部使用,包含敏感信息)
async getAccount(accountId) {
const client = redis.getClientSafe();
logger.debug(`[DEBUG] Getting account data for ID: ${accountId}`);
const accountData = await client.hgetall(`${this.ACCOUNT_KEY_PREFIX}${accountId}`);
if (!accountData || Object.keys(accountData).length === 0) {
logger.debug(`[DEBUG] No account data found for ID: ${accountId}`);
return null;
}
logger.debug(`[DEBUG] Raw account data keys: ${Object.keys(accountData).join(', ')}`);
logger.debug(`[DEBUG] Raw supportedModels value: ${accountData.supportedModels}`);
// 解密敏感字段只解密apiKeyapiUrl不加密
const decryptedKey = this._decryptSensitiveData(accountData.apiKey);
logger.debug(`[DEBUG] URL exists: ${!!accountData.apiUrl}, Decrypted key exists: ${!!decryptedKey}`);
accountData.apiKey = decryptedKey;
// 解析JSON字段
const parsedModels = JSON.parse(accountData.supportedModels || '[]');
logger.debug(`[DEBUG] Parsed supportedModels: ${JSON.stringify(parsedModels)}`);
accountData.supportedModels = parsedModels;
accountData.priority = parseInt(accountData.priority) || 50;
accountData.rateLimitDuration = parseInt(accountData.rateLimitDuration) || 60;
accountData.isActive = accountData.isActive === 'true';
accountData.schedulable = accountData.schedulable !== 'false'; // 默认为true
if (accountData.proxy) {
accountData.proxy = JSON.parse(accountData.proxy);
}
logger.debug(`[DEBUG] Final account data - name: ${accountData.name}, hasApiUrl: ${!!accountData.apiUrl}, hasApiKey: ${!!accountData.apiKey}, supportedModels: ${JSON.stringify(accountData.supportedModels)}`);
return accountData;
}
// 📝 更新账户
async updateAccount(accountId, updates) {
try {
const existingAccount = await this.getAccount(accountId);
if (!existingAccount) {
throw new Error('Account not found');
}
const client = redis.getClientSafe();
const updatedData = {};
// 处理各个字段的更新
logger.debug(`[DEBUG] Update request received with fields: ${Object.keys(updates).join(', ')}`);
logger.debug(`[DEBUG] Updates content: ${JSON.stringify(updates, null, 2)}`);
if (updates.name !== undefined) updatedData.name = updates.name;
if (updates.description !== undefined) updatedData.description = updates.description;
if (updates.apiUrl !== undefined) {
logger.debug(`[DEBUG] Updating apiUrl from frontend: ${updates.apiUrl}`);
updatedData.apiUrl = updates.apiUrl;
}
if (updates.apiKey !== undefined) {
logger.debug(`[DEBUG] Updating apiKey (length: ${updates.apiKey?.length})`);
updatedData.apiKey = this._encryptSensitiveData(updates.apiKey);
}
if (updates.priority !== undefined) updatedData.priority = updates.priority.toString();
if (updates.supportedModels !== undefined) {
logger.debug(`[DEBUG] Updating supportedModels: ${JSON.stringify(updates.supportedModels)}`);
updatedData.supportedModels = JSON.stringify(updates.supportedModels);
}
if (updates.userAgent !== undefined) updatedData.userAgent = updates.userAgent;
if (updates.rateLimitDuration !== undefined) updatedData.rateLimitDuration = updates.rateLimitDuration.toString();
if (updates.proxy !== undefined) updatedData.proxy = updates.proxy ? JSON.stringify(updates.proxy) : '';
if (updates.isActive !== undefined) updatedData.isActive = updates.isActive.toString();
if (updates.schedulable !== undefined) updatedData.schedulable = updates.schedulable.toString();
// 处理账户类型变更
if (updates.accountType && updates.accountType !== existingAccount.accountType) {
updatedData.accountType = updates.accountType;
if (updates.accountType === 'shared') {
await client.sadd(this.SHARED_ACCOUNTS_KEY, accountId);
} else {
await client.srem(this.SHARED_ACCOUNTS_KEY, accountId);
}
}
updatedData.updatedAt = new Date().toISOString();
logger.debug(`[DEBUG] Final updatedData to save: ${JSON.stringify(updatedData, null, 2)}`);
logger.debug(`[DEBUG] Updating Redis key: ${this.ACCOUNT_KEY_PREFIX}${accountId}`);
await client.hset(
`${this.ACCOUNT_KEY_PREFIX}${accountId}`,
updatedData
);
logger.success(`📝 Updated Claude Console account: ${accountId}`);
return { success: true };
} catch (error) {
logger.error('❌ Failed to update Claude Console account:', error);
throw error;
}
}
// 🗑️ 删除账户
async deleteAccount(accountId) {
try {
const client = redis.getClientSafe();
const account = await this.getAccount(accountId);
if (!account) {
throw new Error('Account not found');
}
// 从Redis删除
await client.del(`${this.ACCOUNT_KEY_PREFIX}${accountId}`);
// 从共享账户集合中移除
if (account.accountType === 'shared') {
await client.srem(this.SHARED_ACCOUNTS_KEY, accountId);
}
logger.success(`🗑️ Deleted Claude Console account: ${accountId}`);
return { success: true };
} catch (error) {
logger.error('❌ Failed to delete Claude Console account:', error);
throw error;
}
}
// 🚫 标记账号为限流状态
async markAccountRateLimited(accountId) {
try {
const client = redis.getClientSafe();
const account = await this.getAccount(accountId);
if (!account) {
throw new Error('Account not found');
}
const updates = {
rateLimitedAt: new Date().toISOString(),
rateLimitStatus: 'limited'
};
await client.hset(
`${this.ACCOUNT_KEY_PREFIX}${accountId}`,
updates
);
logger.warn(`🚫 Claude Console account marked as rate limited: ${account.name} (${accountId})`);
return { success: true };
} catch (error) {
logger.error(`❌ Failed to mark Claude Console account as rate limited: ${accountId}`, error);
throw error;
}
}
// ✅ 移除账号的限流状态
async removeAccountRateLimit(accountId) {
try {
const client = redis.getClientSafe();
await client.hdel(
`${this.ACCOUNT_KEY_PREFIX}${accountId}`,
'rateLimitedAt',
'rateLimitStatus'
);
logger.success(`✅ Rate limit removed for Claude Console account: ${accountId}`);
return { success: true };
} catch (error) {
logger.error(`❌ Failed to remove rate limit for Claude Console account: ${accountId}`, error);
throw error;
}
}
// 🔍 检查账号是否处于限流状态
async isAccountRateLimited(accountId) {
try {
const account = await this.getAccount(accountId);
if (!account) {
return false;
}
if (account.rateLimitStatus === 'limited' && account.rateLimitedAt) {
const rateLimitedAt = new Date(account.rateLimitedAt);
const now = new Date();
const minutesSinceRateLimit = (now - rateLimitedAt) / (1000 * 60);
// 使用账户配置的限流时间
const rateLimitDuration = account.rateLimitDuration || 60;
if (minutesSinceRateLimit >= rateLimitDuration) {
await this.removeAccountRateLimit(accountId);
return false;
}
return true;
}
return false;
} catch (error) {
logger.error(`❌ Failed to check rate limit status for Claude Console account: ${accountId}`, error);
return false;
}
}
// 🚫 标记账号为封锁状态(模型不支持等原因)
async blockAccount(accountId, reason) {
try {
const client = redis.getClientSafe();
const updates = {
status: 'blocked',
errorMessage: reason,
blockedAt: new Date().toISOString()
};
await client.hset(
`${this.ACCOUNT_KEY_PREFIX}${accountId}`,
updates
);
logger.warn(`🚫 Claude Console account blocked: ${accountId} - ${reason}`);
return { success: true };
} catch (error) {
logger.error(`❌ Failed to block Claude Console account: ${accountId}`, error);
throw error;
}
}
// 🌐 创建代理agent
_createProxyAgent(proxyConfig) {
if (!proxyConfig) {
return null;
}
try {
const proxy = typeof proxyConfig === 'string' ? JSON.parse(proxyConfig) : proxyConfig;
if (proxy.type === 'socks5') {
const auth = proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '';
const socksUrl = `socks5://${auth}${proxy.host}:${proxy.port}`;
return new SocksProxyAgent(socksUrl);
} else if (proxy.type === 'http' || proxy.type === 'https') {
const auth = proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '';
const httpUrl = `${proxy.type}://${auth}${proxy.host}:${proxy.port}`;
return new HttpsProxyAgent(httpUrl);
}
} catch (error) {
logger.warn('⚠️ Invalid proxy configuration:', error);
}
return null;
}
// 🔐 加密敏感数据
_encryptSensitiveData(data) {
if (!data) return '';
try {
const key = this._generateEncryptionKey();
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.ENCRYPTION_ALGORITHM, key, iv);
let encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
} catch (error) {
logger.error('❌ Encryption error:', error);
return data;
}
}
// 🔓 解密敏感数据
_decryptSensitiveData(encryptedData) {
if (!encryptedData) return '';
try {
if (encryptedData.includes(':')) {
const parts = encryptedData.split(':');
if (parts.length === 2) {
const key = this._generateEncryptionKey();
const iv = Buffer.from(parts[0], 'hex');
const encrypted = parts[1];
const decipher = crypto.createDecipheriv(this.ENCRYPTION_ALGORITHM, key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
return encryptedData;
} catch (error) {
logger.error('❌ Decryption error:', error);
return encryptedData;
}
}
// 🔑 生成加密密钥
_generateEncryptionKey() {
return crypto.scryptSync(config.security.encryptionKey, this.ENCRYPTION_SALT, 32);
}
// 🎭 掩码API URL
_maskApiUrl(apiUrl) {
if (!apiUrl) return '';
try {
const url = new URL(apiUrl);
return `${url.protocol}//${url.hostname}/***`;
} catch {
return '***';
}
}
// 📊 获取限流信息
_getRateLimitInfo(accountData) {
if (accountData.rateLimitStatus === 'limited' && accountData.rateLimitedAt) {
const rateLimitedAt = new Date(accountData.rateLimitedAt);
const now = new Date();
const minutesSinceRateLimit = Math.floor((now - rateLimitedAt) / (1000 * 60));
const rateLimitDuration = parseInt(accountData.rateLimitDuration) || 60;
const minutesRemaining = Math.max(0, rateLimitDuration - minutesSinceRateLimit);
return {
isRateLimited: minutesRemaining > 0,
rateLimitedAt: accountData.rateLimitedAt,
minutesSinceRateLimit,
minutesRemaining
};
}
return {
isRateLimited: false,
rateLimitedAt: null,
minutesSinceRateLimit: 0,
minutesRemaining: 0
};
}
}
module.exports = new ClaudeConsoleAccountService();

View File

@@ -0,0 +1,496 @@
const axios = require('axios');
const claudeConsoleAccountService = require('./claudeConsoleAccountService');
const logger = require('../utils/logger');
const config = require('../../config/config');
class ClaudeConsoleRelayService {
constructor() {
this.defaultUserAgent = 'claude-cli/1.0.61 (console, cli)';
}
// 🚀 转发请求到Claude Console API
async relayRequest(requestBody, apiKeyData, clientRequest, clientResponse, clientHeaders, accountId, options = {}) {
let abortController = null;
try {
// 获取账户信息
const account = await claudeConsoleAccountService.getAccount(accountId);
if (!account) {
throw new Error('Claude Console Claude account not found');
}
logger.info(`📤 Processing Claude Console API request for key: ${apiKeyData.name || apiKeyData.id}, account: ${account.name} (${accountId})`);
logger.debug(`🌐 Account API URL: ${account.apiUrl}`);
logger.debug(`🔍 Account supportedModels: ${JSON.stringify(account.supportedModels)}`);
logger.debug(`🔑 Account has apiKey: ${!!account.apiKey}`);
logger.debug(`📝 Request model: ${requestBody.model}`);
// 模型兼容性检查已经在调度器中完成,这里不需要再检查
// 创建代理agent
const proxyAgent = claudeConsoleAccountService._createProxyAgent(account.proxy);
// 创建AbortController用于取消请求
abortController = new AbortController();
// 设置客户端断开监听器
const handleClientDisconnect = () => {
logger.info('🔌 Client disconnected, aborting Claude Console Claude request');
if (abortController && !abortController.signal.aborted) {
abortController.abort();
}
};
// 监听客户端断开事件
if (clientRequest) {
clientRequest.once('close', handleClientDisconnect);
}
if (clientResponse) {
clientResponse.once('close', handleClientDisconnect);
}
// 构建完整的API URL
const cleanUrl = account.apiUrl.replace(/\/$/, ''); // 移除末尾斜杠
const apiEndpoint = cleanUrl.endsWith('/v1/messages')
? cleanUrl
: `${cleanUrl}/v1/messages`;
logger.debug(`🎯 Final API endpoint: ${apiEndpoint}`);
logger.debug(`[DEBUG] Options passed to relayRequest: ${JSON.stringify(options)}`);
logger.debug(`[DEBUG] Client headers received: ${JSON.stringify(clientHeaders)}`);
// 过滤客户端请求头
const filteredHeaders = this._filterClientHeaders(clientHeaders);
logger.debug(`[DEBUG] Filtered client headers: ${JSON.stringify(filteredHeaders)}`);
// 准备请求配置
const requestConfig = {
method: 'POST',
url: apiEndpoint,
data: requestBody,
headers: {
'Content-Type': 'application/json',
'x-api-key': account.apiKey,
'anthropic-version': '2023-06-01',
'User-Agent': account.userAgent || this.defaultUserAgent,
...filteredHeaders
},
httpsAgent: proxyAgent,
timeout: config.proxy.timeout || 60000,
signal: abortController.signal,
validateStatus: () => true // 接受所有状态码
};
logger.debug(`[DEBUG] Initial headers before beta: ${JSON.stringify(requestConfig.headers, null, 2)}`);
// 添加beta header如果需要
if (options.betaHeader) {
logger.debug(`[DEBUG] Adding beta header: ${options.betaHeader}`);
requestConfig.headers['anthropic-beta'] = options.betaHeader;
} else {
logger.debug(`[DEBUG] No beta header to add`);
}
// 发送请求
logger.debug(`📤 Sending request to Claude Console API with headers:`, JSON.stringify(requestConfig.headers, null, 2));
const response = await axios(requestConfig);
// 移除监听器(请求成功完成)
if (clientRequest) {
clientRequest.removeListener('close', handleClientDisconnect);
}
if (clientResponse) {
clientResponse.removeListener('close', handleClientDisconnect);
}
logger.debug(`🔗 Claude Console API response: ${response.status}`);
logger.debug(`[DEBUG] Response headers: ${JSON.stringify(response.headers)}`);
logger.debug(`[DEBUG] Response data type: ${typeof response.data}`);
logger.debug(`[DEBUG] Response data length: ${response.data ? (typeof response.data === 'string' ? response.data.length : JSON.stringify(response.data).length) : 0}`);
logger.debug(`[DEBUG] Response data preview: ${typeof response.data === 'string' ? response.data.substring(0, 200) : JSON.stringify(response.data).substring(0, 200)}`);
// 检查是否为限流错误
if (response.status === 429) {
logger.warn(`🚫 Rate limit detected for Claude Console account ${accountId}`);
await claudeConsoleAccountService.markAccountRateLimited(accountId);
} else if (response.status === 200 || response.status === 201) {
// 如果请求成功,检查并移除限流状态
const isRateLimited = await claudeConsoleAccountService.isAccountRateLimited(accountId);
if (isRateLimited) {
await claudeConsoleAccountService.removeAccountRateLimit(accountId);
}
}
// 更新最后使用时间
await this._updateLastUsedTime(accountId);
const responseBody = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
logger.debug(`[DEBUG] Final response body to return: ${responseBody}`);
return {
statusCode: response.status,
headers: response.headers,
body: responseBody,
accountId
};
} catch (error) {
// 处理特定错误
if (error.name === 'AbortError' || error.code === 'ECONNABORTED') {
logger.info('Request aborted due to client disconnect');
throw new Error('Client disconnected');
}
logger.error('❌ Claude Console Claude relay request failed:', error.message);
// 不再因为模型不支持而block账号
throw error;
}
}
// 🌊 处理流式响应
async relayStreamRequestWithUsageCapture(requestBody, apiKeyData, responseStream, clientHeaders, usageCallback, accountId, streamTransformer = null, options = {}) {
try {
// 获取账户信息
const account = await claudeConsoleAccountService.getAccount(accountId);
if (!account) {
throw new Error('Claude Console Claude account not found');
}
logger.info(`📡 Processing streaming Claude Console API request for key: ${apiKeyData.name || apiKeyData.id}, account: ${account.name} (${accountId})`);
logger.debug(`🌐 Account API URL: ${account.apiUrl}`);
// 模型兼容性检查已经在调度器中完成,这里不需要再检查
// 创建代理agent
const proxyAgent = claudeConsoleAccountService._createProxyAgent(account.proxy);
// 发送流式请求
await this._makeClaudeConsoleStreamRequest(
requestBody,
account,
proxyAgent,
clientHeaders,
responseStream,
accountId,
usageCallback,
streamTransformer,
options
);
// 更新最后使用时间
await this._updateLastUsedTime(accountId);
} catch (error) {
logger.error('❌ Claude Console Claude stream relay failed:', error);
throw error;
}
}
// 🌊 发送流式请求到Claude Console API
async _makeClaudeConsoleStreamRequest(body, account, proxyAgent, clientHeaders, responseStream, accountId, usageCallback, streamTransformer = null, requestOptions = {}) {
return new Promise((resolve, reject) => {
let aborted = false;
// 构建完整的API URL
const cleanUrl = account.apiUrl.replace(/\/$/, ''); // 移除末尾斜杠
const apiEndpoint = cleanUrl.endsWith('/v1/messages')
? cleanUrl
: `${cleanUrl}/v1/messages`;
logger.debug(`🎯 Final API endpoint for stream: ${apiEndpoint}`);
// 准备请求配置
const requestConfig = {
method: 'POST',
url: apiEndpoint,
data: body,
headers: {
'Content-Type': 'application/json',
'x-api-key': account.apiKey,
'anthropic-version': '2023-06-01',
'User-Agent': account.userAgent || this.defaultUserAgent,
...this._filterClientHeaders(clientHeaders)
},
httpsAgent: proxyAgent,
timeout: config.proxy.timeout || 60000,
responseType: 'stream'
};
// 添加beta header如果需要
if (requestOptions.betaHeader) {
requestConfig.headers['anthropic-beta'] = requestOptions.betaHeader;
}
// 发送请求
const request = axios(requestConfig);
request.then(response => {
logger.debug(`🌊 Claude Console Claude stream response status: ${response.status}`);
// 错误响应处理
if (response.status !== 200) {
logger.error(`❌ Claude Console API returned error status: ${response.status}`);
if (response.status === 429) {
claudeConsoleAccountService.markAccountRateLimited(accountId);
}
// 收集错误数据
let errorData = '';
response.data.on('data', chunk => {
errorData += chunk.toString();
});
response.data.on('end', () => {
if (!responseStream.destroyed) {
responseStream.write('event: error\n');
responseStream.write(`data: ${JSON.stringify({
error: 'Claude Console API error',
status: response.status,
details: errorData,
timestamp: new Date().toISOString()
})}\n\n`);
responseStream.end();
}
reject(new Error(`Claude Console API error: ${response.status}`));
});
return;
}
// 成功响应,检查并移除限流状态
claudeConsoleAccountService.isAccountRateLimited(accountId).then(isRateLimited => {
if (isRateLimited) {
claudeConsoleAccountService.removeAccountRateLimit(accountId);
}
});
// 设置响应头
if (!responseStream.headersSent) {
responseStream.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no'
});
}
let buffer = '';
let finalUsageReported = false;
let collectedUsageData = {};
// 处理流数据
response.data.on('data', chunk => {
try {
if (aborted) return;
const chunkStr = chunk.toString();
buffer += chunkStr;
// 处理完整的SSE行
const lines = buffer.split('\n');
buffer = lines.pop() || '';
// 转发数据并解析usage
if (lines.length > 0 && !responseStream.destroyed) {
const linesToForward = lines.join('\n') + (lines.length > 0 ? '\n' : '');
// 应用流转换器如果有
if (streamTransformer) {
const transformed = streamTransformer(linesToForward);
if (transformed) {
responseStream.write(transformed);
}
} else {
responseStream.write(linesToForward);
}
// 解析SSE数据寻找usage信息
for (const line of lines) {
if (line.startsWith('data: ') && line.length > 6) {
try {
const jsonStr = line.slice(6);
const data = JSON.parse(jsonStr);
// 收集usage数据
if (data.type === 'message_start' && data.message && data.message.usage) {
collectedUsageData.input_tokens = data.message.usage.input_tokens || 0;
collectedUsageData.cache_creation_input_tokens = data.message.usage.cache_creation_input_tokens || 0;
collectedUsageData.cache_read_input_tokens = data.message.usage.cache_read_input_tokens || 0;
collectedUsageData.model = data.message.model;
}
if (data.type === 'message_delta' && data.usage && data.usage.output_tokens !== undefined) {
collectedUsageData.output_tokens = data.usage.output_tokens || 0;
if (collectedUsageData.input_tokens !== undefined && !finalUsageReported) {
usageCallback({ ...collectedUsageData, accountId });
finalUsageReported = true;
}
}
// 不再因为模型不支持而block账号
} catch (e) {
// 忽略解析错误
}
}
}
}
} catch (error) {
logger.error('❌ Error processing Claude Console stream data:', error);
if (!responseStream.destroyed) {
responseStream.write('event: error\n');
responseStream.write(`data: ${JSON.stringify({
error: 'Stream processing error',
message: error.message,
timestamp: new Date().toISOString()
})}\n\n`);
}
}
});
response.data.on('end', () => {
try {
// 处理缓冲区中剩余的数据
if (buffer.trim() && !responseStream.destroyed) {
if (streamTransformer) {
const transformed = streamTransformer(buffer);
if (transformed) {
responseStream.write(transformed);
}
} else {
responseStream.write(buffer);
}
}
// 确保流正确结束
if (!responseStream.destroyed) {
responseStream.end();
}
logger.debug('🌊 Claude Console Claude stream response completed');
resolve();
} catch (error) {
logger.error('❌ Error processing stream end:', error);
reject(error);
}
});
response.data.on('error', error => {
logger.error('❌ Claude Console stream error:', error);
if (!responseStream.destroyed) {
responseStream.write('event: error\n');
responseStream.write(`data: ${JSON.stringify({
error: 'Stream error',
message: error.message,
timestamp: new Date().toISOString()
})}\n\n`);
responseStream.end();
}
reject(error);
});
}).catch(error => {
if (aborted) return;
logger.error('❌ Claude Console Claude stream request error:', error.message);
// 检查是否是429错误
if (error.response && error.response.status === 429) {
claudeConsoleAccountService.markAccountRateLimited(accountId);
}
// 发送错误响应
if (!responseStream.headersSent) {
responseStream.writeHead(error.response?.status || 500, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
}
if (!responseStream.destroyed) {
responseStream.write('event: error\n');
responseStream.write(`data: ${JSON.stringify({
error: error.message,
code: error.code,
timestamp: new Date().toISOString()
})}\n\n`);
responseStream.end();
}
reject(error);
});
// 处理客户端断开连接
responseStream.on('close', () => {
logger.debug('🔌 Client disconnected, cleaning up Claude Console stream');
aborted = true;
});
});
}
// 🔧 过滤客户端请求头
_filterClientHeaders(clientHeaders) {
const sensitiveHeaders = [
'x-api-key',
'authorization',
'host',
'content-length',
'connection',
'proxy-authorization',
'content-encoding',
'transfer-encoding'
];
const filteredHeaders = {};
Object.keys(clientHeaders || {}).forEach(key => {
const lowerKey = key.toLowerCase();
if (!sensitiveHeaders.includes(lowerKey)) {
filteredHeaders[key] = clientHeaders[key];
}
});
return filteredHeaders;
}
// 🕐 更新最后使用时间
async _updateLastUsedTime(accountId) {
try {
const client = require('../models/redis').getClientSafe();
await client.hset(
`claude_console_account:${accountId}`,
'lastUsedAt',
new Date().toISOString()
);
} catch (error) {
logger.warn(`⚠️ Failed to update last used time for Claude Console account ${accountId}:`, error.message);
}
}
// 🎯 健康检查
async healthCheck() {
try {
const accounts = await claudeConsoleAccountService.getAllAccounts();
const activeAccounts = accounts.filter(acc => acc.isActive && acc.status === 'active');
return {
healthy: activeAccounts.length > 0,
activeAccounts: activeAccounts.length,
totalAccounts: accounts.length,
timestamp: new Date().toISOString()
};
} catch (error) {
logger.error('❌ Claude Console Claude health check failed:', error);
return {
healthy: false,
error: error.message,
timestamp: new Date().toISOString()
};
}
}
}
module.exports = new ClaudeConsoleRelayService();

View File

@@ -0,0 +1,319 @@
const claudeAccountService = require('./claudeAccountService');
const claudeConsoleAccountService = require('./claudeConsoleAccountService');
const redis = require('../models/redis');
const logger = require('../utils/logger');
class UnifiedClaudeScheduler {
constructor() {
this.SESSION_MAPPING_PREFIX = 'unified_claude_session_mapping:';
}
// 🎯 统一调度Claude账号官方和Console
async selectAccountForApiKey(apiKeyData, sessionHash = null, requestedModel = null) {
try {
// 如果API Key绑定了专属账户优先使用
if (apiKeyData.claudeAccountId) {
const boundAccount = await redis.getClaudeAccount(apiKeyData.claudeAccountId);
if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') {
logger.info(`🎯 Using bound dedicated Claude account: ${boundAccount.name} (${apiKeyData.claudeAccountId}) for API key ${apiKeyData.name}`);
return {
accountId: apiKeyData.claudeAccountId,
accountType: 'claude-official'
};
} else {
logger.warn(`⚠️ Bound Claude account ${apiKeyData.claudeAccountId} is not available, falling back to pool`);
}
}
// 如果有会话哈希,检查是否有已映射的账户
if (sessionHash) {
const mappedAccount = await this._getSessionMapping(sessionHash);
if (mappedAccount) {
// 验证映射的账户是否仍然可用
const isAvailable = await this._isAccountAvailable(mappedAccount.accountId, mappedAccount.accountType);
if (isAvailable) {
logger.info(`🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`);
return mappedAccount;
} else {
logger.warn(`⚠️ Mapped account ${mappedAccount.accountId} is no longer available, selecting new account`);
await this._deleteSessionMapping(sessionHash);
}
}
}
// 获取所有可用账户(传递请求的模型进行过滤)
const availableAccounts = await this._getAllAvailableAccounts(apiKeyData, requestedModel);
if (availableAccounts.length === 0) {
// 提供更详细的错误信息
if (requestedModel) {
throw new Error(`No available Claude accounts support the requested model: ${requestedModel}`);
} else {
throw new Error('No available Claude accounts (neither official nor console)');
}
}
// 按优先级和最后使用时间排序
const sortedAccounts = this._sortAccountsByPriority(availableAccounts);
// 选择第一个账户
const selectedAccount = sortedAccounts[0];
// 如果有会话哈希,建立新的映射
if (sessionHash) {
await this._setSessionMapping(sessionHash, selectedAccount.accountId, selectedAccount.accountType);
logger.info(`🎯 Created new sticky session mapping: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) for session ${sessionHash}`);
}
logger.info(`🎯 Selected account: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) with priority ${selectedAccount.priority} for API key ${apiKeyData.name}`);
return {
accountId: selectedAccount.accountId,
accountType: selectedAccount.accountType
};
} catch (error) {
logger.error('❌ Failed to select account for API key:', error);
throw error;
}
}
// 📋 获取所有可用账户合并官方和Console
async _getAllAvailableAccounts(apiKeyData, requestedModel = null) {
const availableAccounts = [];
// 如果API Key绑定了专属Claude账户优先返回
if (apiKeyData.claudeAccountId) {
const boundAccount = await redis.getClaudeAccount(apiKeyData.claudeAccountId);
if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error' && boundAccount.status !== 'blocked') {
const isRateLimited = await claudeAccountService.isAccountRateLimited(boundAccount.id);
if (!isRateLimited) {
logger.info(`🎯 Using bound dedicated Claude account: ${boundAccount.name} (${apiKeyData.claudeAccountId})`);
return [{
...boundAccount,
accountId: boundAccount.id,
accountType: 'claude-official',
priority: parseInt(boundAccount.priority) || 50,
lastUsedAt: boundAccount.lastUsedAt || '0'
}];
}
} else {
logger.warn(`⚠️ Bound Claude account ${apiKeyData.claudeAccountId} is not available`);
}
}
// 获取官方Claude账户共享池
const claudeAccounts = await redis.getAllClaudeAccounts();
for (const account of claudeAccounts) {
if (account.isActive === 'true' &&
account.status !== 'error' &&
account.status !== 'blocked' &&
(account.accountType === 'shared' || !account.accountType) && // 兼容旧数据
account.schedulable !== 'false') { // 检查是否可调度
// 检查是否被限流
const isRateLimited = await claudeAccountService.isAccountRateLimited(account.id);
if (!isRateLimited) {
availableAccounts.push({
...account,
accountId: account.id,
accountType: 'claude-official',
priority: parseInt(account.priority) || 50, // 默认优先级50
lastUsedAt: account.lastUsedAt || '0'
});
}
}
}
// 获取Claude Console账户
const consoleAccounts = await claudeConsoleAccountService.getAllAccounts();
logger.info(`📋 Found ${consoleAccounts.length} total Claude Console accounts`);
for (const account of consoleAccounts) {
logger.info(`🔍 Checking Claude Console account: ${account.name} - isActive: ${account.isActive}, status: ${account.status}, accountType: ${account.accountType}, schedulable: ${account.schedulable}`);
// 注意getAllAccounts返回的isActive是布尔值
if (account.isActive === true &&
account.status === 'active' &&
account.accountType === 'shared' &&
account.schedulable !== false) { // 检查是否可调度
// 检查模型支持(如果有请求的模型)
if (requestedModel && account.supportedModels && account.supportedModels.length > 0) {
if (!account.supportedModels.includes(requestedModel)) {
logger.info(`🚫 Claude Console account ${account.name} does not support model ${requestedModel}`);
continue;
}
}
// 检查是否被限流
const isRateLimited = await claudeConsoleAccountService.isAccountRateLimited(account.id);
if (!isRateLimited) {
availableAccounts.push({
...account,
accountId: account.id,
accountType: 'claude-console',
priority: parseInt(account.priority) || 50,
lastUsedAt: account.lastUsedAt || '0'
});
logger.info(`✅ Added Claude Console account to available pool: ${account.name} (priority: ${account.priority})`);
} else {
logger.warn(`⚠️ Claude Console account ${account.name} is rate limited`);
}
} else {
logger.info(`❌ Claude Console 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})`);
return availableAccounts;
}
// 🔢 按优先级和最后使用时间排序账户
_sortAccountsByPriority(accounts) {
return accounts.sort((a, b) => {
// 首先按优先级排序(数字越小优先级越高)
if (a.priority !== b.priority) {
return a.priority - b.priority;
}
// 优先级相同时,按最后使用时间排序(最久未使用的优先)
const aLastUsed = new Date(a.lastUsedAt || 0).getTime();
const bLastUsed = new Date(b.lastUsedAt || 0).getTime();
return aLastUsed - bLastUsed;
});
}
// 🔍 检查账户是否可用
async _isAccountAvailable(accountId, accountType) {
try {
if (accountType === 'claude-official') {
const account = await redis.getClaudeAccount(accountId);
if (!account || account.isActive !== 'true' || account.status === 'error') {
return false;
}
// 检查是否可调度
if (account.schedulable === 'false') {
logger.info(`🚫 Account ${accountId} is not schedulable`);
return false;
}
return !(await claudeAccountService.isAccountRateLimited(accountId));
} else if (accountType === 'claude-console') {
const account = await claudeConsoleAccountService.getAccount(accountId);
if (!account || !account.isActive || account.status !== 'active') {
return false;
}
// 检查是否可调度
if (account.schedulable === false) {
logger.info(`🚫 Claude Console account ${accountId} is not schedulable`);
return false;
}
return !(await claudeConsoleAccountService.isAccountRateLimited(accountId));
}
return false;
} catch (error) {
logger.warn(`⚠️ Failed to check account availability: ${accountId}`, error);
return false;
}
}
// 🔗 获取会话映射
async _getSessionMapping(sessionHash) {
const client = redis.getClientSafe();
const mappingData = await client.get(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`);
if (mappingData) {
try {
return JSON.parse(mappingData);
} catch (error) {
logger.warn('⚠️ Failed to parse session mapping:', error);
return null;
}
}
return null;
}
// 💾 设置会话映射
async _setSessionMapping(sessionHash, accountId, accountType) {
const client = redis.getClientSafe();
const mappingData = JSON.stringify({ accountId, accountType });
// 设置1小时过期
await client.setex(
`${this.SESSION_MAPPING_PREFIX}${sessionHash}`,
3600,
mappingData
);
}
// 🗑️ 删除会话映射
async _deleteSessionMapping(sessionHash) {
const client = redis.getClientSafe();
await client.del(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`);
}
// 🚫 标记账户为限流状态
async markAccountRateLimited(accountId, accountType, sessionHash = null) {
try {
if (accountType === 'claude-official') {
await claudeAccountService.markAccountRateLimited(accountId, sessionHash);
} else if (accountType === 'claude-console') {
await claudeConsoleAccountService.markAccountRateLimited(accountId);
}
// 删除会话映射
if (sessionHash) {
await this._deleteSessionMapping(sessionHash);
}
return { success: true };
} catch (error) {
logger.error(`❌ Failed to mark account as rate limited: ${accountId} (${accountType})`, error);
throw error;
}
}
// ✅ 移除账户的限流状态
async removeAccountRateLimit(accountId, accountType) {
try {
if (accountType === 'claude-official') {
await claudeAccountService.removeAccountRateLimit(accountId);
} else if (accountType === 'claude-console') {
await claudeConsoleAccountService.removeAccountRateLimit(accountId);
}
return { success: true };
} catch (error) {
logger.error(`❌ Failed to remove rate limit for account: ${accountId} (${accountType})`, error);
throw error;
}
}
// 🔍 检查账户是否处于限流状态
async isAccountRateLimited(accountId, accountType) {
try {
if (accountType === 'claude-official') {
return await claudeAccountService.isAccountRateLimited(accountId);
} else if (accountType === 'claude-console') {
return await claudeConsoleAccountService.isAccountRateLimited(accountId);
}
return false;
} catch (error) {
logger.error(`❌ Failed to check rate limit status: ${accountId} (${accountType})`, error);
return false;
}
}
// 🚫 标记Claude Console账户为封锁状态模型不支持
async blockConsoleAccount(accountId, reason) {
try {
await claudeConsoleAccountService.blockAccount(accountId, reason);
return { success: true };
} catch (error) {
logger.error(`❌ Failed to block console account: ${accountId}`, error);
throw error;
}
}
}
module.exports = new UnifiedClaudeScheduler();