refactor: standardize code formatting and linting configuration

- Replace .eslintrc.js with .eslintrc.cjs for better ES module compatibility
- Add .prettierrc configuration for consistent code formatting
- Update package.json with new lint and format scripts
- Add nodemon.json for development hot reloading configuration
- Standardize code formatting across all JavaScript and Vue files
- Update web admin SPA with improved linting rules and formatting
- Add prettier configuration to web admin SPA

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
千羽
2025-08-07 18:19:31 +09:00
parent 4a0eba117c
commit 8a74bf5afe
124 changed files with 20878 additions and 18757 deletions

View File

@@ -1,12 +1,12 @@
const { v4: uuidv4 } = require('uuid');
const logger = require('../utils/logger');
const redis = require('../models/redis');
const { v4: uuidv4 } = require('uuid')
const logger = require('../utils/logger')
const redis = require('../models/redis')
class AccountGroupService {
constructor() {
this.GROUPS_KEY = 'account_groups';
this.GROUP_PREFIX = 'account_group:';
this.GROUP_MEMBERS_PREFIX = 'account_group_members:';
this.GROUPS_KEY = 'account_groups'
this.GROUP_PREFIX = 'account_group:'
this.GROUP_MEMBERS_PREFIX = 'account_group_members:'
}
/**
@@ -19,22 +19,22 @@ class AccountGroupService {
*/
async createGroup(groupData) {
try {
const { name, platform, description = '' } = groupData;
const { name, platform, description = '' } = groupData
// 验证必填字段
if (!name || !platform) {
throw new Error('分组名称和平台类型为必填项');
throw new Error('分组名称和平台类型为必填项')
}
// 验证平台类型
if (!['claude', 'gemini'].includes(platform)) {
throw new Error('平台类型必须是 claude 或 gemini');
throw new Error('平台类型必须是 claude 或 gemini')
}
const client = redis.getClientSafe();
const groupId = uuidv4();
const now = new Date().toISOString();
const client = redis.getClientSafe()
const groupId = uuidv4()
const now = new Date().toISOString()
const group = {
id: groupId,
name,
@@ -42,20 +42,20 @@ class AccountGroupService {
description,
createdAt: now,
updatedAt: now
};
}
// 保存分组数据
await client.hmset(`${this.GROUP_PREFIX}${groupId}`, group);
await client.hmset(`${this.GROUP_PREFIX}${groupId}`, group)
// 添加到分组集合
await client.sadd(this.GROUPS_KEY, groupId);
logger.success(`✅ 创建账户分组成功: ${name} (${platform})`);
return group;
await client.sadd(this.GROUPS_KEY, groupId)
logger.success(`✅ 创建账户分组成功: ${name} (${platform})`)
return group
} catch (error) {
logger.error('❌ 创建账户分组失败:', error);
throw error;
logger.error('❌ 创建账户分组失败:', error)
throw error
}
}
@@ -67,46 +67,46 @@ class AccountGroupService {
*/
async updateGroup(groupId, updates) {
try {
const client = redis.getClientSafe();
const groupKey = `${this.GROUP_PREFIX}${groupId}`;
const client = redis.getClientSafe()
const groupKey = `${this.GROUP_PREFIX}${groupId}`
// 检查分组是否存在
const exists = await client.exists(groupKey);
const exists = await client.exists(groupKey)
if (!exists) {
throw new Error('分组不存在');
throw new Error('分组不存在')
}
// 获取现有分组数据
const existingGroup = await client.hgetall(groupKey);
const existingGroup = await client.hgetall(groupKey)
// 不允许修改平台类型
if (updates.platform && updates.platform !== existingGroup.platform) {
throw new Error('不能修改分组的平台类型');
throw new Error('不能修改分组的平台类型')
}
// 准备更新数据
const updateData = {
...updates,
updatedAt: new Date().toISOString()
};
}
// 移除不允许修改的字段
delete updateData.id;
delete updateData.platform;
delete updateData.createdAt;
delete updateData.id
delete updateData.platform
delete updateData.createdAt
// 更新分组
await client.hmset(groupKey, updateData);
await client.hmset(groupKey, updateData)
// 返回更新后的完整数据
const updatedGroup = await client.hgetall(groupKey);
logger.success(`✅ 更新账户分组成功: ${updatedGroup.name}`);
return updatedGroup;
const updatedGroup = await client.hgetall(groupKey)
logger.success(`✅ 更新账户分组成功: ${updatedGroup.name}`)
return updatedGroup
} catch (error) {
logger.error('❌ 更新账户分组失败:', error);
throw error;
logger.error('❌ 更新账户分组失败:', error)
throw error
}
}
@@ -116,37 +116,37 @@ class AccountGroupService {
*/
async deleteGroup(groupId) {
try {
const client = redis.getClientSafe();
const client = redis.getClientSafe()
// 检查分组是否存在
const group = await this.getGroup(groupId);
const group = await this.getGroup(groupId)
if (!group) {
throw new Error('分组不存在');
throw new Error('分组不存在')
}
// 检查分组是否为空
const members = await this.getGroupMembers(groupId);
const members = await this.getGroupMembers(groupId)
if (members.length > 0) {
throw new Error('分组内还有账户,无法删除');
throw new Error('分组内还有账户,无法删除')
}
// 检查是否有API Key绑定此分组
const boundApiKeys = await this.getApiKeysUsingGroup(groupId);
const boundApiKeys = await this.getApiKeysUsingGroup(groupId)
if (boundApiKeys.length > 0) {
throw new Error('还有API Key使用此分组无法删除');
throw new Error('还有API Key使用此分组无法删除')
}
// 删除分组数据
await client.del(`${this.GROUP_PREFIX}${groupId}`);
await client.del(`${this.GROUP_MEMBERS_PREFIX}${groupId}`);
await client.del(`${this.GROUP_PREFIX}${groupId}`)
await client.del(`${this.GROUP_MEMBERS_PREFIX}${groupId}`)
// 从分组集合中移除
await client.srem(this.GROUPS_KEY, groupId);
logger.success(`✅ 删除账户分组成功: ${group.name}`);
await client.srem(this.GROUPS_KEY, groupId)
logger.success(`✅ 删除账户分组成功: ${group.name}`)
} catch (error) {
logger.error('❌ 删除账户分组失败:', error);
throw error;
logger.error('❌ 删除账户分组失败:', error)
throw error
}
}
@@ -157,23 +157,23 @@ class AccountGroupService {
*/
async getGroup(groupId) {
try {
const client = redis.getClientSafe();
const groupData = await client.hgetall(`${this.GROUP_PREFIX}${groupId}`);
const client = redis.getClientSafe()
const groupData = await client.hgetall(`${this.GROUP_PREFIX}${groupId}`)
if (!groupData || Object.keys(groupData).length === 0) {
return null;
return null
}
// 获取成员数量
const memberCount = await client.scard(`${this.GROUP_MEMBERS_PREFIX}${groupId}`);
const memberCount = await client.scard(`${this.GROUP_MEMBERS_PREFIX}${groupId}`)
return {
...groupData,
memberCount: memberCount || 0
};
}
} catch (error) {
logger.error('❌ 获取分组详情失败:', error);
throw error;
logger.error('❌ 获取分组详情失败:', error)
throw error
}
}
@@ -184,27 +184,27 @@ class AccountGroupService {
*/
async getAllGroups(platform = null) {
try {
const client = redis.getClientSafe();
const groupIds = await client.smembers(this.GROUPS_KEY);
const groups = [];
const client = redis.getClientSafe()
const groupIds = await client.smembers(this.GROUPS_KEY)
const groups = []
for (const groupId of groupIds) {
const group = await this.getGroup(groupId);
const group = await this.getGroup(groupId)
if (group) {
// 如果指定了平台,进行筛选
if (!platform || group.platform === platform) {
groups.push(group);
groups.push(group)
}
}
}
// 按创建时间倒序排序
groups.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
return groups;
groups.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
return groups
} catch (error) {
logger.error('❌ 获取分组列表失败:', error);
throw error;
logger.error('❌ 获取分组列表失败:', error)
throw error
}
}
@@ -216,27 +216,28 @@ class AccountGroupService {
*/
async addAccountToGroup(accountId, groupId, accountPlatform) {
try {
const client = redis.getClientSafe();
const client = redis.getClientSafe()
// 获取分组信息
const group = await this.getGroup(groupId);
const group = await this.getGroup(groupId)
if (!group) {
throw new Error('分组不存在');
throw new Error('分组不存在')
}
// 验证平台一致性 (Claude和Claude Console视为同一平台)
const normalizedAccountPlatform = accountPlatform === 'claude-console' ? 'claude' : accountPlatform;
const normalizedAccountPlatform =
accountPlatform === 'claude-console' ? 'claude' : accountPlatform
if (normalizedAccountPlatform !== group.platform) {
throw new Error('账户平台与分组平台不匹配');
throw new Error('账户平台与分组平台不匹配')
}
// 添加到分组成员集合
await client.sadd(`${this.GROUP_MEMBERS_PREFIX}${groupId}`, accountId);
logger.success(`✅ 添加账户到分组成功: ${accountId} -> ${group.name}`);
await client.sadd(`${this.GROUP_MEMBERS_PREFIX}${groupId}`, accountId)
logger.success(`✅ 添加账户到分组成功: ${accountId} -> ${group.name}`)
} catch (error) {
logger.error('❌ 添加账户到分组失败:', error);
throw error;
logger.error('❌ 添加账户到分组失败:', error)
throw error
}
}
@@ -247,15 +248,15 @@ class AccountGroupService {
*/
async removeAccountFromGroup(accountId, groupId) {
try {
const client = redis.getClientSafe();
const client = redis.getClientSafe()
// 从分组成员集合中移除
await client.srem(`${this.GROUP_MEMBERS_PREFIX}${groupId}`, accountId);
logger.success(`✅ 从分组移除账户成功: ${accountId}`);
await client.srem(`${this.GROUP_MEMBERS_PREFIX}${groupId}`, accountId)
logger.success(`✅ 从分组移除账户成功: ${accountId}`)
} catch (error) {
logger.error('❌ 从分组移除账户失败:', error);
throw error;
logger.error('❌ 从分组移除账户失败:', error)
throw error
}
}
@@ -266,12 +267,12 @@ class AccountGroupService {
*/
async getGroupMembers(groupId) {
try {
const client = redis.getClientSafe();
const members = await client.smembers(`${this.GROUP_MEMBERS_PREFIX}${groupId}`);
return members || [];
const client = redis.getClientSafe()
const members = await client.smembers(`${this.GROUP_MEMBERS_PREFIX}${groupId}`)
return members || []
} catch (error) {
logger.error('❌ 获取分组成员失败:', error);
throw error;
logger.error('❌ 获取分组成员失败:', error)
throw error
}
}
@@ -282,11 +283,11 @@ class AccountGroupService {
*/
async isGroupEmpty(groupId) {
try {
const members = await this.getGroupMembers(groupId);
return members.length === 0;
const members = await this.getGroupMembers(groupId)
return members.length === 0
} catch (error) {
logger.error('❌ 检查分组是否为空失败:', error);
throw error;
logger.error('❌ 检查分组是否为空失败:', error)
throw error
}
}
@@ -297,29 +298,30 @@ class AccountGroupService {
*/
async getApiKeysUsingGroup(groupId) {
try {
const client = redis.getClientSafe();
const groupKey = `group:${groupId}`;
const client = redis.getClientSafe()
const groupKey = `group:${groupId}`
// 获取所有API Key
const apiKeyIds = await client.smembers('api_keys');
const boundApiKeys = [];
const apiKeyIds = await client.smembers('api_keys')
const boundApiKeys = []
for (const keyId of apiKeyIds) {
const keyData = await client.hgetall(`api_key:${keyId}`);
if (keyData &&
(keyData.claudeAccountId === groupKey ||
keyData.geminiAccountId === groupKey)) {
const keyData = await client.hgetall(`api_key:${keyId}`)
if (
keyData &&
(keyData.claudeAccountId === groupKey || keyData.geminiAccountId === groupKey)
) {
boundApiKeys.push({
id: keyId,
name: keyData.name
});
})
}
}
return boundApiKeys;
return boundApiKeys
} catch (error) {
logger.error('❌ 获取使用分组的API Key失败:', error);
throw error;
logger.error('❌ 获取使用分组的API Key失败:', error)
throw error
}
}
@@ -330,22 +332,22 @@ class AccountGroupService {
*/
async getAccountGroup(accountId) {
try {
const client = redis.getClientSafe();
const allGroupIds = await client.smembers(this.GROUPS_KEY);
const client = redis.getClientSafe()
const allGroupIds = await client.smembers(this.GROUPS_KEY)
for (const groupId of allGroupIds) {
const isMember = await client.sismember(`${this.GROUP_MEMBERS_PREFIX}${groupId}`, accountId);
const isMember = await client.sismember(`${this.GROUP_MEMBERS_PREFIX}${groupId}`, accountId)
if (isMember) {
return await this.getGroup(groupId);
return await this.getGroup(groupId)
}
}
return null;
return null
} catch (error) {
logger.error('❌ 获取账户所属分组失败:', error);
throw error;
logger.error('❌ 获取账户所属分组失败:', error)
throw error
}
}
}
module.exports = new AccountGroupService();
module.exports = new AccountGroupService()

View File

@@ -1,12 +1,12 @@
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
const config = require('../../config/config');
const redis = require('../models/redis');
const logger = require('../utils/logger');
const crypto = require('crypto')
const { v4: uuidv4 } = require('uuid')
const config = require('../../config/config')
const redis = require('../models/redis')
const logger = require('../utils/logger')
class ApiKeyService {
constructor() {
this.prefix = config.security.apiKeyPrefix;
this.prefix = config.security.apiKeyPrefix
}
// 🔑 生成新的API Key
@@ -30,13 +30,13 @@ class ApiKeyService {
allowedClients = [],
dailyCostLimit = 0,
tags = []
} = options;
} = options
// 生成简单的API Key (64字符十六进制)
const apiKey = `${this.prefix}${this._generateSecretKey()}`;
const keyId = uuidv4();
const hashedKey = this._hashApiKey(apiKey);
const apiKey = `${this.prefix}${this._generateSecretKey()}`
const keyId = uuidv4()
const hashedKey = this._hashApiKey(apiKey)
const keyData = {
id: keyId,
name,
@@ -61,13 +61,13 @@ class ApiKeyService {
lastUsedAt: '',
expiresAt: expiresAt || '',
createdBy: 'admin' // 可以根据需要扩展用户系统
};
}
// 保存API Key数据并建立哈希映射
await redis.setApiKey(keyId, keyData, hashedKey);
logger.success(`🔑 Generated new API key: ${name} (${keyId})`);
await redis.setApiKey(keyId, keyData, hashedKey)
logger.success(`🔑 Generated new API key: ${name} (${keyId})`)
return {
id: keyId,
apiKey, // 只在创建时返回完整的key
@@ -91,69 +91,69 @@ class ApiKeyService {
createdAt: keyData.createdAt,
expiresAt: keyData.expiresAt,
createdBy: keyData.createdBy
};
}
}
// 🔍 验证API Key
// 🔍 验证API Key
async validateApiKey(apiKey) {
try {
if (!apiKey || !apiKey.startsWith(this.prefix)) {
return { valid: false, error: 'Invalid API key format' };
return { valid: false, error: 'Invalid API key format' }
}
// 计算API Key的哈希值
const hashedKey = this._hashApiKey(apiKey);
const hashedKey = this._hashApiKey(apiKey)
// 通过哈希值直接查找API Key性能优化
const keyData = await redis.findApiKeyByHash(hashedKey);
const keyData = await redis.findApiKeyByHash(hashedKey)
if (!keyData) {
return { valid: false, error: 'API key not found' };
return { valid: false, error: 'API key not found' }
}
// 检查是否激活
if (keyData.isActive !== 'true') {
return { valid: false, error: 'API key is disabled' };
return { valid: false, error: 'API key is disabled' }
}
// 检查是否过期
if (keyData.expiresAt && new Date() > new Date(keyData.expiresAt)) {
return { valid: false, error: 'API key has expired' };
return { valid: false, error: 'API key has expired' }
}
// 获取使用统计(供返回数据使用)
const usage = await redis.getUsageStats(keyData.id);
const usage = await redis.getUsageStats(keyData.id)
// 获取当日费用统计
const dailyCost = await redis.getDailyCost(keyData.id);
const dailyCost = await redis.getDailyCost(keyData.id)
// 更新最后使用时间优化只在实际API调用时更新而不是验证时
// 注意lastUsedAt的更新已移至recordUsage方法中
logger.api(`🔓 API key validated successfully: ${keyData.id}`);
logger.api(`🔓 API key validated successfully: ${keyData.id}`)
// 解析限制模型数据
let restrictedModels = [];
let restrictedModels = []
try {
restrictedModels = keyData.restrictedModels ? JSON.parse(keyData.restrictedModels) : [];
restrictedModels = keyData.restrictedModels ? JSON.parse(keyData.restrictedModels) : []
} catch (e) {
restrictedModels = [];
restrictedModels = []
}
// 解析允许的客户端
let allowedClients = [];
let allowedClients = []
try {
allowedClients = keyData.allowedClients ? JSON.parse(keyData.allowedClients) : [];
allowedClients = keyData.allowedClients ? JSON.parse(keyData.allowedClients) : []
} catch (e) {
allowedClients = [];
allowedClients = []
}
// 解析标签
let tags = [];
let tags = []
try {
tags = keyData.tags ? JSON.parse(keyData.tags) : [];
tags = keyData.tags ? JSON.parse(keyData.tags) : []
} catch (e) {
tags = [];
tags = []
}
return {
@@ -173,248 +173,306 @@ class ApiKeyService {
rateLimitWindow: parseInt(keyData.rateLimitWindow || 0),
rateLimitRequests: parseInt(keyData.rateLimitRequests || 0),
enableModelRestriction: keyData.enableModelRestriction === 'true',
restrictedModels: restrictedModels,
restrictedModels,
enableClientRestriction: keyData.enableClientRestriction === 'true',
allowedClients: allowedClients,
allowedClients,
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
dailyCost: dailyCost || 0,
tags: tags,
tags,
usage
}
};
}
} catch (error) {
logger.error('❌ API key validation error:', error);
return { valid: false, error: 'Internal validation error' };
logger.error('❌ API key validation error:', error)
return { valid: false, error: 'Internal validation error' }
}
}
// 📋 获取所有API Keys
async getAllApiKeys() {
try {
const apiKeys = await redis.getAllApiKeys();
const client = redis.getClientSafe();
const apiKeys = await redis.getAllApiKeys()
const client = redis.getClientSafe()
// 为每个key添加使用统计和当前并发数
for (const key of apiKeys) {
key.usage = await redis.getUsageStats(key.id);
key.tokenLimit = parseInt(key.tokenLimit);
key.concurrencyLimit = parseInt(key.concurrencyLimit || 0);
key.rateLimitWindow = parseInt(key.rateLimitWindow || 0);
key.rateLimitRequests = parseInt(key.rateLimitRequests || 0);
key.currentConcurrency = await redis.getConcurrency(key.id);
key.isActive = key.isActive === 'true';
key.enableModelRestriction = key.enableModelRestriction === 'true';
key.enableClientRestriction = key.enableClientRestriction === 'true';
key.permissions = key.permissions || 'all'; // 兼容旧数据
key.dailyCostLimit = parseFloat(key.dailyCostLimit || 0);
key.dailyCost = await redis.getDailyCost(key.id) || 0;
key.usage = await redis.getUsageStats(key.id)
key.tokenLimit = parseInt(key.tokenLimit)
key.concurrencyLimit = parseInt(key.concurrencyLimit || 0)
key.rateLimitWindow = parseInt(key.rateLimitWindow || 0)
key.rateLimitRequests = parseInt(key.rateLimitRequests || 0)
key.currentConcurrency = await redis.getConcurrency(key.id)
key.isActive = key.isActive === 'true'
key.enableModelRestriction = key.enableModelRestriction === 'true'
key.enableClientRestriction = key.enableClientRestriction === 'true'
key.permissions = key.permissions || 'all' // 兼容旧数据
key.dailyCostLimit = parseFloat(key.dailyCostLimit || 0)
key.dailyCost = (await redis.getDailyCost(key.id)) || 0
// 获取当前时间窗口的请求次数和Token使用量
if (key.rateLimitWindow > 0) {
const requestCountKey = `rate_limit:requests:${key.id}`;
const tokenCountKey = `rate_limit:tokens:${key.id}`;
key.currentWindowRequests = parseInt(await client.get(requestCountKey) || '0');
key.currentWindowTokens = parseInt(await client.get(tokenCountKey) || '0');
const requestCountKey = `rate_limit:requests:${key.id}`
const tokenCountKey = `rate_limit:tokens:${key.id}`
key.currentWindowRequests = parseInt((await client.get(requestCountKey)) || '0')
key.currentWindowTokens = parseInt((await client.get(tokenCountKey)) || '0')
} else {
key.currentWindowRequests = 0;
key.currentWindowTokens = 0;
key.currentWindowRequests = 0
key.currentWindowTokens = 0
}
try {
key.restrictedModels = key.restrictedModels ? JSON.parse(key.restrictedModels) : [];
key.restrictedModels = key.restrictedModels ? JSON.parse(key.restrictedModels) : []
} catch (e) {
key.restrictedModels = [];
key.restrictedModels = []
}
try {
key.allowedClients = key.allowedClients ? JSON.parse(key.allowedClients) : [];
key.allowedClients = key.allowedClients ? JSON.parse(key.allowedClients) : []
} catch (e) {
key.allowedClients = [];
key.allowedClients = []
}
try {
key.tags = key.tags ? JSON.parse(key.tags) : [];
key.tags = key.tags ? JSON.parse(key.tags) : []
} catch (e) {
key.tags = [];
key.tags = []
}
delete key.apiKey; // 不返回哈希后的key
delete key.apiKey // 不返回哈希后的key
}
return apiKeys;
return apiKeys
} catch (error) {
logger.error('❌ Failed to get API keys:', error);
throw error;
logger.error('❌ Failed to get API keys:', error)
throw error
}
}
// 📝 更新API Key
async updateApiKey(keyId, updates) {
try {
const keyData = await redis.getApiKey(keyId);
const keyData = await redis.getApiKey(keyId)
if (!keyData || Object.keys(keyData).length === 0) {
throw new Error('API key not found');
throw new Error('API key not found')
}
// 允许更新的字段
const allowedUpdates = ['name', 'description', 'tokenLimit', 'concurrencyLimit', 'rateLimitWindow', 'rateLimitRequests', 'isActive', 'claudeAccountId', 'claudeConsoleAccountId', 'geminiAccountId', 'permissions', 'expiresAt', 'enableModelRestriction', 'restrictedModels', 'enableClientRestriction', 'allowedClients', 'dailyCostLimit', 'tags'];
const updatedData = { ...keyData };
const allowedUpdates = [
'name',
'description',
'tokenLimit',
'concurrencyLimit',
'rateLimitWindow',
'rateLimitRequests',
'isActive',
'claudeAccountId',
'claudeConsoleAccountId',
'geminiAccountId',
'permissions',
'expiresAt',
'enableModelRestriction',
'restrictedModels',
'enableClientRestriction',
'allowedClients',
'dailyCostLimit',
'tags'
]
const updatedData = { ...keyData }
for (const [field, value] of Object.entries(updates)) {
if (allowedUpdates.includes(field)) {
if (field === 'restrictedModels' || field === 'allowedClients' || field === 'tags') {
// 特殊处理数组字段
updatedData[field] = JSON.stringify(value || []);
updatedData[field] = JSON.stringify(value || [])
} else if (field === 'enableModelRestriction' || field === 'enableClientRestriction') {
// 布尔值转字符串
updatedData[field] = String(value);
updatedData[field] = String(value)
} else {
updatedData[field] = (value != null ? value : '').toString();
updatedData[field] = (value !== null && value !== undefined ? value : '').toString()
}
}
}
updatedData.updatedAt = new Date().toISOString();
updatedData.updatedAt = new Date().toISOString()
// 更新时不需要重新建立哈希映射因为API Key本身没有变化
await redis.setApiKey(keyId, updatedData);
logger.success(`📝 Updated API key: ${keyId}`);
return { success: true };
await redis.setApiKey(keyId, updatedData)
logger.success(`📝 Updated API key: ${keyId}`)
return { success: true }
} catch (error) {
logger.error('❌ Failed to update API key:', error);
throw error;
logger.error('❌ Failed to update API key:', error)
throw error
}
}
// 🗑️ 删除API Key
async deleteApiKey(keyId) {
try {
const result = await redis.deleteApiKey(keyId);
const result = await redis.deleteApiKey(keyId)
if (result === 0) {
throw new Error('API key not found');
throw new Error('API key not found')
}
logger.success(`🗑️ Deleted API key: ${keyId}`);
return { success: true };
logger.success(`🗑️ Deleted API key: ${keyId}`)
return { success: true }
} catch (error) {
logger.error('❌ Failed to delete API key:', error);
throw error;
logger.error('❌ Failed to delete API key:', error)
throw error
}
}
// 📊 记录使用情况支持缓存token和账户级别统计
async recordUsage(keyId, inputTokens = 0, outputTokens = 0, cacheCreateTokens = 0, cacheReadTokens = 0, model = 'unknown', accountId = null) {
async recordUsage(
keyId,
inputTokens = 0,
outputTokens = 0,
cacheCreateTokens = 0,
cacheReadTokens = 0,
model = 'unknown',
accountId = null
) {
try {
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
// 计算费用
const CostCalculator = require('../utils/costCalculator');
const costInfo = CostCalculator.calculateCost({
input_tokens: inputTokens,
output_tokens: outputTokens,
cache_creation_input_tokens: cacheCreateTokens,
cache_read_input_tokens: cacheReadTokens
}, model);
const CostCalculator = require('../utils/costCalculator')
const costInfo = CostCalculator.calculateCost(
{
input_tokens: inputTokens,
output_tokens: outputTokens,
cache_creation_input_tokens: cacheCreateTokens,
cache_read_input_tokens: cacheReadTokens
},
model
)
// 记录API Key级别的使用统计
await redis.incrementTokenUsage(keyId, totalTokens, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model);
await redis.incrementTokenUsage(
keyId,
totalTokens,
inputTokens,
outputTokens,
cacheCreateTokens,
cacheReadTokens,
model
)
// 记录费用统计
if (costInfo.costs.total > 0) {
await redis.incrementDailyCost(keyId, costInfo.costs.total);
logger.database(`💰 Recorded cost for ${keyId}: $${costInfo.costs.total.toFixed(6)}, model: ${model}`);
await redis.incrementDailyCost(keyId, costInfo.costs.total)
logger.database(
`💰 Recorded cost for ${keyId}: $${costInfo.costs.total.toFixed(6)}, model: ${model}`
)
} else {
logger.debug(`💰 No cost recorded for ${keyId} - zero cost for model: ${model}`);
logger.debug(`💰 No cost recorded for ${keyId} - zero cost for model: ${model}`)
}
// 获取API Key数据以确定关联的账户
const keyData = await redis.getApiKey(keyId);
const keyData = await redis.getApiKey(keyId)
if (keyData && Object.keys(keyData).length > 0) {
// 更新最后使用时间
keyData.lastUsedAt = new Date().toISOString();
await redis.setApiKey(keyId, keyData);
keyData.lastUsedAt = new Date().toISOString()
await redis.setApiKey(keyId, keyData)
// 记录账户级别的使用统计(只统计实际处理请求的账户)
if (accountId) {
await redis.incrementAccountUsage(accountId, totalTokens, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model);
logger.database(`📊 Recorded account usage: ${accountId} - ${totalTokens} tokens (API Key: ${keyId})`);
await redis.incrementAccountUsage(
accountId,
totalTokens,
inputTokens,
outputTokens,
cacheCreateTokens,
cacheReadTokens,
model
)
logger.database(
`📊 Recorded account usage: ${accountId} - ${totalTokens} tokens (API Key: ${keyId})`
)
} else {
logger.debug('⚠️ No accountId provided for usage recording, skipping account-level statistics');
logger.debug(
'⚠️ No accountId provided for usage recording, skipping account-level statistics'
)
}
}
const logParts = [`Model: ${model}`, `Input: ${inputTokens}`, `Output: ${outputTokens}`];
if (cacheCreateTokens > 0) logParts.push(`Cache Create: ${cacheCreateTokens}`);
if (cacheReadTokens > 0) logParts.push(`Cache Read: ${cacheReadTokens}`);
logParts.push(`Total: ${totalTokens} tokens`);
logger.database(`📊 Recorded usage: ${keyId} - ${logParts.join(', ')}`);
const logParts = [`Model: ${model}`, `Input: ${inputTokens}`, `Output: ${outputTokens}`]
if (cacheCreateTokens > 0) {
logParts.push(`Cache Create: ${cacheCreateTokens}`)
}
if (cacheReadTokens > 0) {
logParts.push(`Cache Read: ${cacheReadTokens}`)
}
logParts.push(`Total: ${totalTokens} tokens`)
logger.database(`📊 Recorded usage: ${keyId} - ${logParts.join(', ')}`)
} catch (error) {
logger.error('❌ Failed to record usage:', error);
logger.error('❌ Failed to record usage:', error)
}
}
// 🔐 生成密钥
_generateSecretKey() {
return crypto.randomBytes(32).toString('hex');
return crypto.randomBytes(32).toString('hex')
}
// 🔒 哈希API Key
_hashApiKey(apiKey) {
return crypto.createHash('sha256').update(apiKey + config.security.encryptionKey).digest('hex');
return crypto
.createHash('sha256')
.update(apiKey + config.security.encryptionKey)
.digest('hex')
}
// 📈 获取使用统计
async getUsageStats(keyId) {
return await redis.getUsageStats(keyId);
return await redis.getUsageStats(keyId)
}
// 📊 获取账户使用统计
async getAccountUsageStats(accountId) {
return await redis.getAccountUsageStats(accountId);
return await redis.getAccountUsageStats(accountId)
}
// 📈 获取所有账户使用统计
async getAllAccountsUsageStats() {
return await redis.getAllAccountsUsageStats();
return await redis.getAllAccountsUsageStats()
}
// 🧹 清理过期的API Keys
async cleanupExpiredKeys() {
try {
const apiKeys = await redis.getAllApiKeys();
const now = new Date();
let cleanedCount = 0;
const apiKeys = await redis.getAllApiKeys()
const now = new Date()
let cleanedCount = 0
for (const key of apiKeys) {
// 检查是否已过期且仍处于激活状态
if (key.expiresAt && new Date(key.expiresAt) < now && key.isActive === 'true') {
// 将过期的 API Key 标记为禁用状态,而不是直接删除
await this.updateApiKey(key.id, { isActive: false });
logger.info(`🔒 API Key ${key.id} (${key.name}) has expired and been disabled`);
cleanedCount++;
await this.updateApiKey(key.id, { isActive: false })
logger.info(`🔒 API Key ${key.id} (${key.name}) has expired and been disabled`)
cleanedCount++
}
}
if (cleanedCount > 0) {
logger.success(`🧹 Disabled ${cleanedCount} expired API keys`);
logger.success(`🧹 Disabled ${cleanedCount} expired API keys`)
}
return cleanedCount;
return cleanedCount
} catch (error) {
logger.error('❌ Failed to cleanup expired keys:', error);
return 0;
logger.error('❌ Failed to cleanup expired keys:', error)
return 0
}
}
}
// 导出实例和单独的方法
const apiKeyService = new ApiKeyService();
const apiKeyService = new ApiKeyService()
// 为了方便其他服务调用,导出 recordUsage 方法
apiKeyService.recordUsageMetrics = apiKeyService.recordUsage.bind(apiKeyService);
apiKeyService.recordUsageMetrics = apiKeyService.recordUsage.bind(apiKeyService)
module.exports = apiKeyService;
module.exports = apiKeyService

View File

@@ -1,15 +1,15 @@
const { v4: uuidv4 } = require('uuid');
const crypto = require('crypto');
const redis = require('../models/redis');
const logger = require('../utils/logger');
const config = require('../../config/config');
const bedrockRelayService = require('./bedrockRelayService');
const { v4: uuidv4 } = require('uuid')
const crypto = require('crypto')
const redis = require('../models/redis')
const logger = require('../utils/logger')
const config = require('../../config/config')
const bedrockRelayService = require('./bedrockRelayService')
class BedrockAccountService {
constructor() {
// 加密相关常量
this.ENCRYPTION_ALGORITHM = 'aes-256-cbc';
this.ENCRYPTION_SALT = 'salt';
this.ENCRYPTION_ALGORITHM = 'aes-256-cbc'
this.ENCRYPTION_SALT = 'salt'
}
// 🏢 创建Bedrock账户
@@ -25,11 +25,11 @@ class BedrockAccountService {
priority = 50, // 调度优先级 (1-100数字越小优先级越高)
schedulable = true, // 是否可被调度
credentialType = 'default' // 'default', 'access_key', 'bearer_token'
} = options;
} = options
const accountId = uuidv4();
const accountId = uuidv4()
let accountData = {
const accountData = {
id: accountId,
name,
description,
@@ -43,17 +43,17 @@ class BedrockAccountService {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
type: 'bedrock' // 标识这是Bedrock账户
};
}
// 加密存储AWS凭证
if (awsCredentials) {
accountData.awsCredentials = this._encryptAwsCredentials(awsCredentials);
accountData.awsCredentials = this._encryptAwsCredentials(awsCredentials)
}
const client = redis.getClientSafe();
await client.set(`bedrock_account:${accountId}`, JSON.stringify(accountData));
const client = redis.getClientSafe()
await client.set(`bedrock_account:${accountId}`, JSON.stringify(accountData))
logger.info(`✅ 创建Bedrock账户成功 - ID: ${accountId}, 名称: ${name}, 区域: ${region}`);
logger.info(`✅ 创建Bedrock账户成功 - ID: ${accountId}, 名称: ${name}, 区域: ${region}`)
return {
success: true,
@@ -71,48 +71,48 @@ class BedrockAccountService {
createdAt: accountData.createdAt,
type: 'bedrock'
}
};
}
}
// 🔍 获取账户信息
async getAccount(accountId) {
try {
const client = redis.getClientSafe();
const accountData = await client.get(`bedrock_account:${accountId}`);
const client = redis.getClientSafe()
const accountData = await client.get(`bedrock_account:${accountId}`)
if (!accountData) {
return { success: false, error: 'Account not found' };
return { success: false, error: 'Account not found' }
}
const account = JSON.parse(accountData);
const account = JSON.parse(accountData)
// 解密AWS凭证用于内部使用
if (account.awsCredentials) {
account.awsCredentials = this._decryptAwsCredentials(account.awsCredentials);
account.awsCredentials = this._decryptAwsCredentials(account.awsCredentials)
}
logger.debug(`🔍 获取Bedrock账户 - ID: ${accountId}, 名称: ${account.name}`);
logger.debug(`🔍 获取Bedrock账户 - ID: ${accountId}, 名称: ${account.name}`)
return {
success: true,
data: account
};
}
} catch (error) {
logger.error(`❌ 获取Bedrock账户失败 - ID: ${accountId}`, error);
return { success: false, error: error.message };
logger.error(`❌ 获取Bedrock账户失败 - ID: ${accountId}`, error)
return { success: false, error: error.message }
}
}
// 📋 获取所有账户列表
async getAllAccounts() {
try {
const client = redis.getClientSafe();
const keys = await client.keys('bedrock_account:*');
const accounts = [];
const client = redis.getClientSafe()
const keys = await client.keys('bedrock_account:*')
const accounts = []
for (const key of keys) {
const accountData = await client.get(key);
const accountData = await client.get(key)
if (accountData) {
const account = JSON.parse(accountData);
const account = JSON.parse(accountData)
// 返回给前端时,不包含敏感信息,只显示掩码
accounts.push({
@@ -130,25 +130,27 @@ class BedrockAccountService {
updatedAt: account.updatedAt,
type: 'bedrock',
hasCredentials: !!account.awsCredentials
});
})
}
}
// 按优先级和名称排序
accounts.sort((a, b) => {
if (a.priority !== b.priority) return a.priority - b.priority;
return a.name.localeCompare(b.name);
});
if (a.priority !== b.priority) {
return a.priority - b.priority
}
return a.name.localeCompare(b.name)
})
logger.debug(`📋 获取所有Bedrock账户 - 共 ${accounts.length}`);
logger.debug(`📋 获取所有Bedrock账户 - 共 ${accounts.length}`)
return {
success: true,
data: accounts
};
}
} catch (error) {
logger.error('❌ 获取Bedrock账户列表失败', error);
return { success: false, error: error.message };
logger.error('❌ 获取Bedrock账户列表失败', error)
return { success: false, error: error.message }
}
}
@@ -156,44 +158,62 @@ class BedrockAccountService {
async updateAccount(accountId, updates = {}) {
try {
// 获取原始账户数据(不解密凭证)
const client = redis.getClientSafe();
const accountData = await client.get(`bedrock_account:${accountId}`);
const client = redis.getClientSafe()
const accountData = await client.get(`bedrock_account:${accountId}`)
if (!accountData) {
return { success: false, error: 'Account not found' };
return { success: false, error: 'Account not found' }
}
const account = JSON.parse(accountData);
const account = JSON.parse(accountData)
// 更新字段
if (updates.name !== undefined) account.name = updates.name;
if (updates.description !== undefined) account.description = updates.description;
if (updates.region !== undefined) account.region = updates.region;
if (updates.defaultModel !== undefined) account.defaultModel = updates.defaultModel;
if (updates.isActive !== undefined) account.isActive = updates.isActive;
if (updates.accountType !== undefined) account.accountType = updates.accountType;
if (updates.priority !== undefined) account.priority = updates.priority;
if (updates.schedulable !== undefined) account.schedulable = updates.schedulable;
if (updates.credentialType !== undefined) account.credentialType = updates.credentialType;
if (updates.name !== undefined) {
account.name = updates.name
}
if (updates.description !== undefined) {
account.description = updates.description
}
if (updates.region !== undefined) {
account.region = updates.region
}
if (updates.defaultModel !== undefined) {
account.defaultModel = updates.defaultModel
}
if (updates.isActive !== undefined) {
account.isActive = updates.isActive
}
if (updates.accountType !== undefined) {
account.accountType = updates.accountType
}
if (updates.priority !== undefined) {
account.priority = updates.priority
}
if (updates.schedulable !== undefined) {
account.schedulable = updates.schedulable
}
if (updates.credentialType !== undefined) {
account.credentialType = updates.credentialType
}
// 更新AWS凭证
if (updates.awsCredentials !== undefined) {
if (updates.awsCredentials) {
account.awsCredentials = this._encryptAwsCredentials(updates.awsCredentials);
account.awsCredentials = this._encryptAwsCredentials(updates.awsCredentials)
} else {
delete account.awsCredentials;
delete account.awsCredentials
}
} else if (account.awsCredentials && account.awsCredentials.accessKeyId) {
// 如果没有提供新凭证但现有凭证是明文格式,重新加密
const plainCredentials = account.awsCredentials;
account.awsCredentials = this._encryptAwsCredentials(plainCredentials);
logger.info(`🔐 重新加密Bedrock账户凭证 - ID: ${accountId}`);
const plainCredentials = account.awsCredentials
account.awsCredentials = this._encryptAwsCredentials(plainCredentials)
logger.info(`🔐 重新加密Bedrock账户凭证 - ID: ${accountId}`)
}
account.updatedAt = new Date().toISOString();
account.updatedAt = new Date().toISOString()
await client.set(`bedrock_account:${accountId}`, JSON.stringify(account));
await client.set(`bedrock_account:${accountId}`, JSON.stringify(account))
logger.info(`✅ 更新Bedrock账户成功 - ID: ${accountId}, 名称: ${account.name}`);
logger.info(`✅ 更新Bedrock账户成功 - ID: ${accountId}, 名称: ${account.name}`)
return {
success: true,
@@ -211,87 +231,87 @@ class BedrockAccountService {
updatedAt: account.updatedAt,
type: 'bedrock'
}
};
}
} catch (error) {
logger.error(`❌ 更新Bedrock账户失败 - ID: ${accountId}`, error);
return { success: false, error: error.message };
logger.error(`❌ 更新Bedrock账户失败 - ID: ${accountId}`, error)
return { success: false, error: error.message }
}
}
// 🗑️ 删除账户
async deleteAccount(accountId) {
try {
const accountResult = await this.getAccount(accountId);
const accountResult = await this.getAccount(accountId)
if (!accountResult.success) {
return accountResult;
return accountResult
}
const client = redis.getClientSafe();
await client.del(`bedrock_account:${accountId}`);
const client = redis.getClientSafe()
await client.del(`bedrock_account:${accountId}`)
logger.info(`✅ 删除Bedrock账户成功 - ID: ${accountId}`);
logger.info(`✅ 删除Bedrock账户成功 - ID: ${accountId}`)
return { success: true };
return { success: true }
} catch (error) {
logger.error(`❌ 删除Bedrock账户失败 - ID: ${accountId}`, error);
return { success: false, error: error.message };
logger.error(`❌ 删除Bedrock账户失败 - ID: ${accountId}`, error)
return { success: false, error: error.message }
}
}
// 🎯 选择可用的Bedrock账户 (用于请求转发)
async selectAvailableAccount() {
try {
const accountsResult = await this.getAllAccounts();
const accountsResult = await this.getAllAccounts()
if (!accountsResult.success) {
return { success: false, error: 'Failed to get accounts' };
return { success: false, error: 'Failed to get accounts' }
}
const availableAccounts = accountsResult.data.filter(account =>
account.isActive && account.schedulable
);
const availableAccounts = accountsResult.data.filter(
(account) => account.isActive && account.schedulable
)
if (availableAccounts.length === 0) {
return { success: false, error: 'No available Bedrock accounts' };
return { success: false, error: 'No available Bedrock accounts' }
}
// 简单的轮询选择策略 - 选择优先级最高的账户
const selectedAccount = availableAccounts[0];
const selectedAccount = availableAccounts[0]
// 获取完整账户信息(包含解密的凭证)
const fullAccountResult = await this.getAccount(selectedAccount.id);
const fullAccountResult = await this.getAccount(selectedAccount.id)
if (!fullAccountResult.success) {
return { success: false, error: 'Failed to get selected account details' };
return { success: false, error: 'Failed to get selected account details' }
}
logger.debug(`🎯 选择Bedrock账户 - ID: ${selectedAccount.id}, 名称: ${selectedAccount.name}`);
logger.debug(`🎯 选择Bedrock账户 - ID: ${selectedAccount.id}, 名称: ${selectedAccount.name}`)
return {
success: true,
data: fullAccountResult.data
};
}
} catch (error) {
logger.error('❌ 选择Bedrock账户失败', error);
return { success: false, error: error.message };
logger.error('❌ 选择Bedrock账户失败', error)
return { success: false, error: error.message }
}
}
// 🧪 测试账户连接
async testAccount(accountId) {
try {
const accountResult = await this.getAccount(accountId);
const accountResult = await this.getAccount(accountId)
if (!accountResult.success) {
return accountResult;
return accountResult
}
const account = accountResult.data;
const account = accountResult.data
logger.info(`🧪 测试Bedrock账户连接 - ID: ${accountId}, 名称: ${account.name}`);
logger.info(`🧪 测试Bedrock账户连接 - ID: ${accountId}, 名称: ${account.name}`)
// 尝试获取模型列表来测试连接
const models = await bedrockRelayService.getAvailableModels(account);
const models = await bedrockRelayService.getAvailableModels(account)
if (models && models.length > 0) {
logger.info(`✅ Bedrock账户测试成功 - ID: ${accountId}, 发现 ${models.length} 个模型`);
logger.info(`✅ Bedrock账户测试成功 - ID: ${accountId}, 发现 ${models.length} 个模型`)
return {
success: true,
data: {
@@ -300,40 +320,40 @@ class BedrockAccountService {
region: account.region,
credentialType: account.credentialType
}
};
}
} else {
return {
success: false,
error: 'Unable to retrieve models from Bedrock'
};
}
}
} catch (error) {
logger.error(`❌ 测试Bedrock账户失败 - ID: ${accountId}`, error);
logger.error(`❌ 测试Bedrock账户失败 - ID: ${accountId}`, error)
return {
success: false,
error: error.message
};
}
}
}
// 🔐 加密AWS凭证
_encryptAwsCredentials(credentials) {
try {
const key = crypto.createHash('sha256').update(config.security.encryptionKey).digest();
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.ENCRYPTION_ALGORITHM, key, iv);
const key = crypto.createHash('sha256').update(config.security.encryptionKey).digest()
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv(this.ENCRYPTION_ALGORITHM, key, iv)
const credentialsString = JSON.stringify(credentials);
let encrypted = cipher.update(credentialsString, 'utf8', 'hex');
encrypted += cipher.final('hex');
const credentialsString = JSON.stringify(credentials)
let encrypted = cipher.update(credentialsString, 'utf8', 'hex')
encrypted += cipher.final('hex')
return {
encrypted: encrypted,
encrypted,
iv: iv.toString('hex')
};
}
} catch (error) {
logger.error('❌ AWS凭证加密失败', error);
throw new Error('Credentials encryption failed');
logger.error('❌ AWS凭证加密失败', error)
throw new Error('Credentials encryption failed')
}
}
@@ -342,70 +362,71 @@ class BedrockAccountService {
try {
// 检查数据格式
if (!encryptedData || typeof encryptedData !== 'object') {
logger.error('❌ 无效的加密数据格式:', encryptedData);
throw new Error('Invalid encrypted data format');
logger.error('❌ 无效的加密数据格式:', encryptedData)
throw new Error('Invalid encrypted data format')
}
// 检查是否为加密格式 (有 encrypted 和 iv 字段)
if (encryptedData.encrypted && encryptedData.iv) {
// 加密数据 - 进行解密
const key = crypto.createHash('sha256').update(config.security.encryptionKey).digest();
const iv = Buffer.from(encryptedData.iv, 'hex');
const decipher = crypto.createDecipheriv(this.ENCRYPTION_ALGORITHM, key, iv);
const key = crypto.createHash('sha256').update(config.security.encryptionKey).digest()
const iv = Buffer.from(encryptedData.iv, 'hex')
const decipher = crypto.createDecipheriv(this.ENCRYPTION_ALGORITHM, key, iv)
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8')
decrypted += decipher.final('utf8')
return JSON.parse(decrypted);
return JSON.parse(decrypted)
} else if (encryptedData.accessKeyId) {
// 纯文本数据 - 直接返回 (向后兼容)
logger.warn('⚠️ 发现未加密的AWS凭证建议更新账户以启用加密');
return encryptedData;
logger.warn('⚠️ 发现未加密的AWS凭证建议更新账户以启用加密')
return encryptedData
} else {
// 既不是加密格式也不是有效的凭证格式
logger.error('❌ 缺少加密数据字段:', {
hasEncrypted: !!encryptedData.encrypted,
hasIv: !!encryptedData.iv,
hasAccessKeyId: !!encryptedData.accessKeyId
});
throw new Error('Missing encrypted data fields or valid credentials');
})
throw new Error('Missing encrypted data fields or valid credentials')
}
} catch (error) {
logger.error('❌ AWS凭证解密失败', error);
throw new Error('Credentials decryption failed');
logger.error('❌ AWS凭证解密失败', error)
throw new Error('Credentials decryption failed')
}
}
// 🔍 获取账户统计信息
async getAccountStats() {
try {
const accountsResult = await this.getAllAccounts();
const accountsResult = await this.getAllAccounts()
if (!accountsResult.success) {
return { success: false, error: accountsResult.error };
return { success: false, error: accountsResult.error }
}
const accounts = accountsResult.data;
const accounts = accountsResult.data
const stats = {
total: accounts.length,
active: accounts.filter(acc => acc.isActive).length,
inactive: accounts.filter(acc => !acc.isActive).length,
schedulable: accounts.filter(acc => acc.schedulable).length,
active: accounts.filter((acc) => acc.isActive).length,
inactive: accounts.filter((acc) => !acc.isActive).length,
schedulable: accounts.filter((acc) => acc.schedulable).length,
byRegion: {},
byCredentialType: {}
};
}
// 按区域统计
accounts.forEach(acc => {
stats.byRegion[acc.region] = (stats.byRegion[acc.region] || 0) + 1;
stats.byCredentialType[acc.credentialType] = (stats.byCredentialType[acc.credentialType] || 0) + 1;
});
accounts.forEach((acc) => {
stats.byRegion[acc.region] = (stats.byRegion[acc.region] || 0) + 1
stats.byCredentialType[acc.credentialType] =
(stats.byCredentialType[acc.credentialType] || 0) + 1
})
return { success: true, data: stats };
return { success: true, data: stats }
} catch (error) {
logger.error('❌ 获取Bedrock账户统计失败', error);
return { success: false, error: error.message };
logger.error('❌ 获取Bedrock账户统计失败', error)
return { success: false, error: error.message }
}
}
}
module.exports = new BedrockAccountService();
module.exports = new BedrockAccountService()

View File

@@ -1,38 +1,44 @@
const { BedrockRuntimeClient, InvokeModelCommand, InvokeModelWithResponseStreamCommand } = require('@aws-sdk/client-bedrock-runtime');
const { fromEnv } = require('@aws-sdk/credential-providers');
const logger = require('../utils/logger');
const config = require('../../config/config');
const {
BedrockRuntimeClient,
InvokeModelCommand,
InvokeModelWithResponseStreamCommand
} = require('@aws-sdk/client-bedrock-runtime')
const { fromEnv } = require('@aws-sdk/credential-providers')
const logger = require('../utils/logger')
const config = require('../../config/config')
class BedrockRelayService {
constructor() {
this.defaultRegion = process.env.AWS_REGION || config.bedrock?.defaultRegion || 'us-east-1';
this.smallFastModelRegion = process.env.ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION || this.defaultRegion;
this.defaultRegion = process.env.AWS_REGION || config.bedrock?.defaultRegion || 'us-east-1'
this.smallFastModelRegion =
process.env.ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION || this.defaultRegion
// 默认模型配置
this.defaultModel = process.env.ANTHROPIC_MODEL || 'us.anthropic.claude-sonnet-4-20250514-v1:0';
this.defaultSmallModel = process.env.ANTHROPIC_SMALL_FAST_MODEL || 'us.anthropic.claude-3-5-haiku-20241022-v1:0';
this.defaultModel = process.env.ANTHROPIC_MODEL || 'us.anthropic.claude-sonnet-4-20250514-v1:0'
this.defaultSmallModel =
process.env.ANTHROPIC_SMALL_FAST_MODEL || 'us.anthropic.claude-3-5-haiku-20241022-v1:0'
// Token配置
this.maxOutputTokens = parseInt(process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS) || 4096;
this.maxThinkingTokens = parseInt(process.env.MAX_THINKING_TOKENS) || 1024;
this.enablePromptCaching = process.env.DISABLE_PROMPT_CACHING !== '1';
this.maxOutputTokens = parseInt(process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS) || 4096
this.maxThinkingTokens = parseInt(process.env.MAX_THINKING_TOKENS) || 1024
this.enablePromptCaching = process.env.DISABLE_PROMPT_CACHING !== '1'
// 创建Bedrock客户端
this.clients = new Map(); // 缓存不同区域的客户端
this.clients = new Map() // 缓存不同区域的客户端
}
// 获取或创建Bedrock客户端
_getBedrockClient(region = null, bedrockAccount = null) {
const targetRegion = region || this.defaultRegion;
const clientKey = `${targetRegion}-${bedrockAccount?.id || 'default'}`;
const targetRegion = region || this.defaultRegion
const clientKey = `${targetRegion}-${bedrockAccount?.id || 'default'}`
if (this.clients.has(clientKey)) {
return this.clients.get(clientKey);
return this.clients.get(clientKey)
}
const clientConfig = {
region: targetRegion
};
}
// 如果账户配置了特定的AWS凭证使用它们
if (bedrockAccount?.awsCredentials) {
@@ -40,51 +46,55 @@ class BedrockRelayService {
accessKeyId: bedrockAccount.awsCredentials.accessKeyId,
secretAccessKey: bedrockAccount.awsCredentials.secretAccessKey,
sessionToken: bedrockAccount.awsCredentials.sessionToken
};
}
} else {
// 检查是否有环境变量凭证
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
clientConfig.credentials = fromEnv();
clientConfig.credentials = fromEnv()
} else {
throw new Error('AWS凭证未配置。请在Bedrock账户中配置AWS访问密钥或设置环境变量AWS_ACCESS_KEY_ID和AWS_SECRET_ACCESS_KEY');
throw new Error(
'AWS凭证未配置。请在Bedrock账户中配置AWS访问密钥或设置环境变量AWS_ACCESS_KEY_ID和AWS_SECRET_ACCESS_KEY'
)
}
}
const client = new BedrockRuntimeClient(clientConfig);
this.clients.set(clientKey, client);
const client = new BedrockRuntimeClient(clientConfig)
this.clients.set(clientKey, client)
logger.debug(`🔧 Created Bedrock client for region: ${targetRegion}, account: ${bedrockAccount?.name || 'default'}`);
return client;
logger.debug(
`🔧 Created Bedrock client for region: ${targetRegion}, account: ${bedrockAccount?.name || 'default'}`
)
return client
}
// 处理非流式请求
async handleNonStreamRequest(requestBody, bedrockAccount = null) {
try {
const modelId = this._selectModel(requestBody, bedrockAccount);
const region = this._selectRegion(modelId, bedrockAccount);
const client = this._getBedrockClient(region, bedrockAccount);
const modelId = this._selectModel(requestBody, bedrockAccount)
const region = this._selectRegion(modelId, bedrockAccount)
const client = this._getBedrockClient(region, bedrockAccount)
// 转换请求格式为Bedrock格式
const bedrockPayload = this._convertToBedrockFormat(requestBody);
const bedrockPayload = this._convertToBedrockFormat(requestBody)
const command = new InvokeModelCommand({
modelId: modelId,
modelId,
body: JSON.stringify(bedrockPayload),
contentType: 'application/json',
accept: 'application/json'
});
})
logger.debug(`🚀 Bedrock非流式请求 - 模型: ${modelId}, 区域: ${region}`);
logger.debug(`🚀 Bedrock非流式请求 - 模型: ${modelId}, 区域: ${region}`)
const startTime = Date.now();
const response = await client.send(command);
const duration = Date.now() - startTime;
const startTime = Date.now()
const response = await client.send(command)
const duration = Date.now() - startTime
// 解析响应
const responseBody = JSON.parse(new TextDecoder().decode(response.body));
const claudeResponse = this._convertFromBedrockFormat(responseBody);
const responseBody = JSON.parse(new TextDecoder().decode(response.body))
const claudeResponse = this._convertFromBedrockFormat(responseBody)
logger.info(`✅ Bedrock请求完成 - 模型: ${modelId}, 耗时: ${duration}ms`);
logger.info(`✅ Bedrock请求完成 - 模型: ${modelId}, 耗时: ${duration}ms`)
return {
success: true,
@@ -92,127 +102,129 @@ class BedrockRelayService {
usage: claudeResponse.usage,
model: modelId,
duration
};
}
} catch (error) {
logger.error('❌ Bedrock非流式请求失败:', error);
throw this._handleBedrockError(error);
logger.error('❌ Bedrock非流式请求失败:', error)
throw this._handleBedrockError(error)
}
}
// 处理流式请求
async handleStreamRequest(requestBody, bedrockAccount = null, res) {
try {
const modelId = this._selectModel(requestBody, bedrockAccount);
const region = this._selectRegion(modelId, bedrockAccount);
const client = this._getBedrockClient(region, bedrockAccount);
const modelId = this._selectModel(requestBody, bedrockAccount)
const region = this._selectRegion(modelId, bedrockAccount)
const client = this._getBedrockClient(region, bedrockAccount)
// 转换请求格式为Bedrock格式
const bedrockPayload = this._convertToBedrockFormat(requestBody);
const bedrockPayload = this._convertToBedrockFormat(requestBody)
const command = new InvokeModelWithResponseStreamCommand({
modelId: modelId,
modelId,
body: JSON.stringify(bedrockPayload),
contentType: 'application/json',
accept: 'application/json'
});
})
logger.debug(`🌊 Bedrock流式请求 - 模型: ${modelId}, 区域: ${region}`);
logger.debug(`🌊 Bedrock流式请求 - 模型: ${modelId}, 区域: ${region}`)
const startTime = Date.now();
const response = await client.send(command);
const startTime = Date.now()
const response = await client.send(command)
// 设置SSE响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
Connection: 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
});
})
let totalUsage = null;
let isFirstChunk = true;
let totalUsage = null
let isFirstChunk = true
// 处理流式响应
for await (const chunk of response.body) {
if (chunk.chunk) {
const chunkData = JSON.parse(new TextDecoder().decode(chunk.chunk.bytes));
const claudeEvent = this._convertBedrockStreamToClaudeFormat(chunkData, isFirstChunk);
const chunkData = JSON.parse(new TextDecoder().decode(chunk.chunk.bytes))
const claudeEvent = this._convertBedrockStreamToClaudeFormat(chunkData, isFirstChunk)
if (claudeEvent) {
// 发送SSE事件
res.write(`event: ${claudeEvent.type}\n`);
res.write(`data: ${JSON.stringify(claudeEvent.data)}\n\n`);
res.write(`event: ${claudeEvent.type}\n`)
res.write(`data: ${JSON.stringify(claudeEvent.data)}\n\n`)
// 提取使用统计
if (claudeEvent.type === 'message_stop' && claudeEvent.data.usage) {
totalUsage = claudeEvent.data.usage;
totalUsage = claudeEvent.data.usage
}
isFirstChunk = false;
isFirstChunk = false
}
}
}
const duration = Date.now() - startTime;
logger.info(`✅ Bedrock流式请求完成 - 模型: ${modelId}, 耗时: ${duration}ms`);
const duration = Date.now() - startTime
logger.info(`✅ Bedrock流式请求完成 - 模型: ${modelId}, 耗时: ${duration}ms`)
// 发送结束事件
res.write('event: done\n');
res.write('data: [DONE]\n\n');
res.end();
res.write('event: done\n')
res.write('data: [DONE]\n\n')
res.end()
return {
success: true,
usage: totalUsage,
model: modelId,
duration
};
}
} catch (error) {
logger.error('❌ Bedrock流式请求失败:', error);
logger.error('❌ Bedrock流式请求失败:', error)
// 发送错误事件
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.writeHead(500, { 'Content-Type': 'application/json' })
}
res.write('event: error\n');
res.write(`data: ${JSON.stringify({ error: this._handleBedrockError(error).message })}\n\n`);
res.end();
res.write('event: error\n')
res.write(`data: ${JSON.stringify({ error: this._handleBedrockError(error).message })}\n\n`)
res.end()
throw this._handleBedrockError(error);
throw this._handleBedrockError(error)
}
}
// 选择使用的模型
_selectModel(requestBody, bedrockAccount) {
let selectedModel;
let selectedModel
// 优先使用账户配置的模型
if (bedrockAccount?.defaultModel) {
selectedModel = bedrockAccount.defaultModel;
logger.info(`🎯 使用账户配置的模型: ${selectedModel}`, { metadata: { source: 'account', accountId: bedrockAccount.id } });
selectedModel = bedrockAccount.defaultModel
logger.info(`🎯 使用账户配置的模型: ${selectedModel}`, {
metadata: { source: 'account', accountId: bedrockAccount.id }
})
}
// 检查请求中指定的模型
else if (requestBody.model) {
selectedModel = requestBody.model;
logger.info(`🎯 使用请求指定的模型: ${selectedModel}`, { metadata: { source: 'request' } });
selectedModel = requestBody.model
logger.info(`🎯 使用请求指定的模型: ${selectedModel}`, { metadata: { source: 'request' } })
}
// 使用默认模型
else {
selectedModel = this.defaultModel;
logger.info(`🎯 使用系统默认模型: ${selectedModel}`, { metadata: { source: 'default' } });
selectedModel = this.defaultModel
logger.info(`🎯 使用系统默认模型: ${selectedModel}`, { metadata: { source: 'default' } })
}
// 如果是标准Claude模型名需要映射为Bedrock格式
const bedrockModel = this._mapToBedrockModel(selectedModel);
const bedrockModel = this._mapToBedrockModel(selectedModel)
if (bedrockModel !== selectedModel) {
logger.info(`🔄 模型映射: ${selectedModel}${bedrockModel}`, { metadata: { originalModel: selectedModel, bedrockModel } });
logger.info(`🔄 模型映射: ${selectedModel}${bedrockModel}`, {
metadata: { originalModel: selectedModel, bedrockModel }
})
}
return bedrockModel;
return bedrockModel
}
// 将标准Claude模型名映射为Bedrock格式
@@ -222,63 +234,65 @@ class BedrockRelayService {
// Claude Sonnet 4
'claude-sonnet-4': 'us.anthropic.claude-sonnet-4-20250514-v1:0',
'claude-sonnet-4-20250514': 'us.anthropic.claude-sonnet-4-20250514-v1:0',
// Claude Opus 4.1
'claude-opus-4': 'us.anthropic.claude-opus-4-1-20250805-v1:0',
'claude-opus-4-1': 'us.anthropic.claude-opus-4-1-20250805-v1:0',
'claude-opus-4-1-20250805': 'us.anthropic.claude-opus-4-1-20250805-v1:0',
// Claude 3.7 Sonnet
'claude-3-7-sonnet': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
'claude-3-7-sonnet-20250219': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
// Claude 3.5 Sonnet v2
'claude-3-5-sonnet': 'us.anthropic.claude-3-5-sonnet-20241022-v2:0',
'claude-3-5-sonnet-20241022': 'us.anthropic.claude-3-5-sonnet-20241022-v2:0',
// Claude 3.5 Haiku
'claude-3-5-haiku': 'us.anthropic.claude-3-5-haiku-20241022-v1:0',
'claude-3-5-haiku-20241022': 'us.anthropic.claude-3-5-haiku-20241022-v1:0',
// Claude 3 Sonnet
'claude-3-sonnet': 'us.anthropic.claude-3-sonnet-20240229-v1:0',
'claude-3-sonnet-20240229': 'us.anthropic.claude-3-sonnet-20240229-v1:0',
// Claude 3 Haiku
'claude-3-haiku': 'us.anthropic.claude-3-haiku-20240307-v1:0',
'claude-3-haiku-20240307': 'us.anthropic.claude-3-haiku-20240307-v1:0'
};
}
// 如果已经是Bedrock格式直接返回
// Bedrock模型格式{region}.anthropic.{model-name} 或 anthropic.{model-name}
if (modelName.includes('.anthropic.') || modelName.startsWith('anthropic.')) {
return modelName;
return modelName
}
// 查找映射
const mappedModel = modelMapping[modelName];
const mappedModel = modelMapping[modelName]
if (mappedModel) {
return mappedModel;
return mappedModel
}
// 如果没有找到映射,返回原始模型名(可能会导致错误,但保持向后兼容)
logger.warn(`⚠️ 未找到模型映射: ${modelName},使用原始模型名`, { metadata: { originalModel: modelName } });
return modelName;
logger.warn(`⚠️ 未找到模型映射: ${modelName},使用原始模型名`, {
metadata: { originalModel: modelName }
})
return modelName
}
// 选择使用的区域
_selectRegion(modelId, bedrockAccount) {
// 优先使用账户配置的区域
if (bedrockAccount?.region) {
return bedrockAccount.region;
return bedrockAccount.region
}
// 对于小模型,使用专门的区域配置
if (modelId.includes('haiku')) {
return this.smallFastModelRegion;
return this.smallFastModelRegion
}
return this.defaultRegion;
return this.defaultRegion
}
// 转换Claude格式请求到Bedrock格式
@@ -287,40 +301,40 @@ class BedrockRelayService {
anthropic_version: 'bedrock-2023-05-31',
max_tokens: Math.min(requestBody.max_tokens || this.maxOutputTokens, this.maxOutputTokens),
messages: requestBody.messages || []
};
}
// 添加系统提示词
if (requestBody.system) {
bedrockPayload.system = requestBody.system;
bedrockPayload.system = requestBody.system
}
// 添加其他参数
if (requestBody.temperature !== undefined) {
bedrockPayload.temperature = requestBody.temperature;
bedrockPayload.temperature = requestBody.temperature
}
if (requestBody.top_p !== undefined) {
bedrockPayload.top_p = requestBody.top_p;
bedrockPayload.top_p = requestBody.top_p
}
if (requestBody.top_k !== undefined) {
bedrockPayload.top_k = requestBody.top_k;
bedrockPayload.top_k = requestBody.top_k
}
if (requestBody.stop_sequences) {
bedrockPayload.stop_sequences = requestBody.stop_sequences;
bedrockPayload.stop_sequences = requestBody.stop_sequences
}
// 工具调用支持
if (requestBody.tools) {
bedrockPayload.tools = requestBody.tools;
bedrockPayload.tools = requestBody.tools
}
if (requestBody.tool_choice) {
bedrockPayload.tool_choice = requestBody.tool_choice;
bedrockPayload.tool_choice = requestBody.tool_choice
}
return bedrockPayload;
return bedrockPayload
}
// 转换Bedrock响应到Claude格式
@@ -337,7 +351,7 @@ class BedrockRelayService {
input_tokens: 0,
output_tokens: 0
}
};
}
}
// 转换Bedrock流事件到Claude SSE格式
@@ -355,7 +369,7 @@ class BedrockRelayService {
stop_sequence: null,
usage: bedrockChunk.message?.usage || { input_tokens: 0, output_tokens: 0 }
}
};
}
}
if (bedrockChunk.type === 'content_block_delta') {
@@ -365,7 +379,7 @@ class BedrockRelayService {
index: bedrockChunk.index || 0,
delta: bedrockChunk.delta || {}
}
};
}
}
if (bedrockChunk.type === 'message_delta') {
@@ -375,7 +389,7 @@ class BedrockRelayService {
delta: bedrockChunk.delta || {},
usage: bedrockChunk.usage || {}
}
};
}
}
if (bedrockChunk.type === 'message_stop') {
@@ -384,39 +398,39 @@ class BedrockRelayService {
data: {
usage: bedrockChunk.usage || {}
}
};
}
}
return null;
return null
}
// 处理Bedrock错误
_handleBedrockError(error) {
const errorMessage = error.message || 'Unknown Bedrock error';
const errorMessage = error.message || 'Unknown Bedrock error'
if (error.name === 'ValidationException') {
return new Error(`Bedrock参数验证失败: ${errorMessage}`);
return new Error(`Bedrock参数验证失败: ${errorMessage}`)
}
if (error.name === 'ThrottlingException') {
return new Error('Bedrock请求限流请稍后重试');
return new Error('Bedrock请求限流请稍后重试')
}
if (error.name === 'AccessDeniedException') {
return new Error('Bedrock访问被拒绝请检查IAM权限');
return new Error('Bedrock访问被拒绝请检查IAM权限')
}
if (error.name === 'ModelNotReadyException') {
return new Error('Bedrock模型未就绪请稍后重试');
return new Error('Bedrock模型未就绪请稍后重试')
}
return new Error(`Bedrock服务错误: ${errorMessage}`);
return new Error(`Bedrock服务错误: ${errorMessage}`)
}
// 获取可用模型列表
async getAvailableModels(bedrockAccount = null) {
try {
const region = bedrockAccount?.region || this.defaultRegion;
const region = bedrockAccount?.region || this.defaultRegion
// Bedrock暂不支持列出推理配置文件的API返回预定义的模型列表
const models = [
@@ -450,16 +464,15 @@ class BedrockRelayService {
provider: 'anthropic',
type: 'bedrock'
}
];
logger.debug(`📋 返回Bedrock可用模型 ${models.length} 个, 区域: ${region}`);
return models;
]
logger.debug(`📋 返回Bedrock可用模型 ${models.length} 个, 区域: ${region}`)
return models
} catch (error) {
logger.error('❌ 获取Bedrock模型列表失败:', error);
return [];
logger.error('❌ 获取Bedrock模型列表失败:', error)
return []
}
}
}
module.exports = new BedrockRelayService();
module.exports = new BedrockRelayService()

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@
* 负责存储和管理不同账号使用的 Claude Code headers
*/
const redis = require('../models/redis');
const logger = require('../utils/logger');
const redis = require('../models/redis')
const logger = require('../utils/logger')
class ClaudeCodeHeadersService {
constructor() {
@@ -22,8 +22,8 @@ class ClaudeCodeHeadersService {
'user-agent': 'claude-cli/1.0.57 (external, cli)',
'accept-language': '*',
'sec-fetch-mode': 'cors'
};
}
// 需要捕获的 Claude Code 特定 headers
this.claudeCodeHeaderKeys = [
'x-stainless-retry-count',
@@ -40,16 +40,18 @@ class ClaudeCodeHeadersService {
'accept-language',
'sec-fetch-mode',
'accept-encoding'
];
]
}
/**
* 从 user-agent 中提取版本号
*/
extractVersionFromUserAgent(userAgent) {
if (!userAgent) return null;
const match = userAgent.match(/claude-cli\/(\d+\.\d+\.\d+)/);
return match ? match[1] : null;
if (!userAgent) {
return null
}
const match = userAgent.match(/claude-cli\/(\d+\.\d+\.\d+)/)
return match ? match[1] : null
}
/**
@@ -57,43 +59,49 @@ class ClaudeCodeHeadersService {
* @returns {number} 1 if v1 > v2, -1 if v1 < v2, 0 if equal
*/
compareVersions(v1, v2) {
if (!v1 || !v2) return 0;
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const p1 = parts1[i] || 0;
const p2 = parts2[i] || 0;
if (p1 > p2) return 1;
if (p1 < p2) return -1;
if (!v1 || !v2) {
return 0
}
return 0;
const parts1 = v1.split('.').map(Number)
const parts2 = v2.split('.').map(Number)
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const p1 = parts1[i] || 0
const p2 = parts2[i] || 0
if (p1 > p2) {
return 1
}
if (p1 < p2) {
return -1
}
}
return 0
}
/**
* 从客户端 headers 中提取 Claude Code 相关的 headers
*/
extractClaudeCodeHeaders(clientHeaders) {
const headers = {};
const headers = {}
// 转换所有 header keys 为小写进行比较
const lowerCaseHeaders = {};
Object.keys(clientHeaders || {}).forEach(key => {
lowerCaseHeaders[key.toLowerCase()] = clientHeaders[key];
});
const lowerCaseHeaders = {}
Object.keys(clientHeaders || {}).forEach((key) => {
lowerCaseHeaders[key.toLowerCase()] = clientHeaders[key]
})
// 提取需要的 headers
this.claudeCodeHeaderKeys.forEach(key => {
const lowerKey = key.toLowerCase();
this.claudeCodeHeaderKeys.forEach((key) => {
const lowerKey = key.toLowerCase()
if (lowerCaseHeaders[lowerKey]) {
headers[key] = lowerCaseHeaders[lowerKey];
headers[key] = lowerCaseHeaders[lowerKey]
}
});
return headers;
})
return headers
}
/**
@@ -101,48 +109,47 @@ class ClaudeCodeHeadersService {
*/
async storeAccountHeaders(accountId, clientHeaders) {
try {
const extractedHeaders = this.extractClaudeCodeHeaders(clientHeaders);
const extractedHeaders = this.extractClaudeCodeHeaders(clientHeaders)
// 检查是否有 user-agent
const userAgent = extractedHeaders['user-agent'];
const userAgent = extractedHeaders['user-agent']
if (!userAgent || !userAgent.includes('claude-cli')) {
// 不是 Claude Code 的请求,不存储
return;
return
}
const version = this.extractVersionFromUserAgent(userAgent);
const version = this.extractVersionFromUserAgent(userAgent)
if (!version) {
logger.warn(`⚠️ Failed to extract version from user-agent: ${userAgent}`);
return;
logger.warn(`⚠️ Failed to extract version from user-agent: ${userAgent}`)
return
}
// 获取当前存储的 headers
const key = `claude_code_headers:${accountId}`;
const currentData = await redis.getClient().get(key);
const key = `claude_code_headers:${accountId}`
const currentData = await redis.getClient().get(key)
if (currentData) {
const current = JSON.parse(currentData);
const currentVersion = this.extractVersionFromUserAgent(current.headers['user-agent']);
const current = JSON.parse(currentData)
const currentVersion = this.extractVersionFromUserAgent(current.headers['user-agent'])
// 只有新版本更高时才更新
if (this.compareVersions(version, currentVersion) <= 0) {
return;
return
}
}
// 存储新的 headers
const data = {
headers: extractedHeaders,
version: version,
version,
updatedAt: new Date().toISOString()
};
await redis.getClient().setex(key, 86400 * 7, JSON.stringify(data)); // 7天过期
logger.info(`✅ Stored Claude Code headers for account ${accountId}, version: ${version}`);
}
await redis.getClient().setex(key, 86400 * 7, JSON.stringify(data)) // 7天过期
logger.info(`✅ Stored Claude Code headers for account ${accountId}, version: ${version}`)
} catch (error) {
logger.error(`❌ Failed to store Claude Code headers for account ${accountId}:`, error);
logger.error(`❌ Failed to store Claude Code headers for account ${accountId}:`, error)
}
}
@@ -151,22 +158,23 @@ class ClaudeCodeHeadersService {
*/
async getAccountHeaders(accountId) {
try {
const key = `claude_code_headers:${accountId}`;
const data = await redis.getClient().get(key);
const key = `claude_code_headers:${accountId}`
const data = await redis.getClient().get(key)
if (data) {
const parsed = JSON.parse(data);
logger.debug(`📋 Retrieved Claude Code headers for account ${accountId}, version: ${parsed.version}`);
return parsed.headers;
const parsed = JSON.parse(data)
logger.debug(
`📋 Retrieved Claude Code headers for account ${accountId}, version: ${parsed.version}`
)
return parsed.headers
}
// 返回默认 headers
logger.debug(`📋 Using default Claude Code headers for account ${accountId}`);
return this.defaultHeaders;
logger.debug(`📋 Using default Claude Code headers for account ${accountId}`)
return this.defaultHeaders
} catch (error) {
logger.error(`❌ Failed to get Claude Code headers for account ${accountId}:`, error);
return this.defaultHeaders;
logger.error(`❌ Failed to get Claude Code headers for account ${accountId}:`, error)
return this.defaultHeaders
}
}
@@ -175,11 +183,11 @@ class ClaudeCodeHeadersService {
*/
async clearAccountHeaders(accountId) {
try {
const key = `claude_code_headers:${accountId}`;
await redis.getClient().del(key);
logger.info(`🗑️ Cleared Claude Code headers for account ${accountId}`);
const key = `claude_code_headers:${accountId}`
await redis.getClient().del(key)
logger.info(`🗑️ Cleared Claude Code headers for account ${accountId}`)
} catch (error) {
logger.error(`❌ Failed to clear Claude Code headers for account ${accountId}:`, error);
logger.error(`❌ Failed to clear Claude Code headers for account ${accountId}:`, error)
}
}
@@ -188,25 +196,24 @@ class ClaudeCodeHeadersService {
*/
async getAllAccountHeaders() {
try {
const pattern = 'claude_code_headers:*';
const keys = await redis.getClient().keys(pattern);
const results = {};
const pattern = 'claude_code_headers:*'
const keys = await redis.getClient().keys(pattern)
const results = {}
for (const key of keys) {
const accountId = key.replace('claude_code_headers:', '');
const data = await redis.getClient().get(key);
const accountId = key.replace('claude_code_headers:', '')
const data = await redis.getClient().get(key)
if (data) {
results[accountId] = JSON.parse(data);
results[accountId] = JSON.parse(data)
}
}
return results;
return results
} catch (error) {
logger.error('❌ Failed to get all account headers:', error);
return {};
logger.error('❌ Failed to get all account headers:', error)
return {}
}
}
}
module.exports = new ClaudeCodeHeadersService();
module.exports = new ClaudeCodeHeadersService()

View File

@@ -1,20 +1,20 @@
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');
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';
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';
this.ACCOUNT_KEY_PREFIX = 'claude_console_account:'
this.SHARED_ACCOUNTS_KEY = 'shared_claude_console_accounts'
}
// 🏢 创建Claude Console账户
@@ -32,24 +32,24 @@ class ClaudeConsoleAccountService {
isActive = true,
accountType = 'shared', // 'dedicated' or 'shared'
schedulable = true // 是否可被调度
} = options;
} = options
// 验证必填字段
if (!apiUrl || !apiKey) {
throw new Error('API URL and API Key are required for Claude Console account');
throw new Error('API URL and API Key are required for Claude Console account')
}
const accountId = uuidv4();
const accountId = uuidv4()
// 处理 supportedModels确保向后兼容
const processedModels = this._processModelMapping(supportedModels);
const processedModels = this._processModelMapping(supportedModels)
const accountData = {
id: accountId,
platform: 'claude-console',
name,
description,
apiUrl: apiUrl,
apiUrl,
apiKey: this._encryptSensitiveData(apiKey),
priority: priority.toString(),
supportedModels: JSON.stringify(processedModels),
@@ -67,24 +67,23 @@ class ClaudeConsoleAccountService {
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)
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);
await client.sadd(this.SHARED_ACCOUNTS_KEY, accountId)
}
logger.success(`🏢 Created Claude Console account: ${name} (${accountId})`);
logger.success(`🏢 Created Claude Console account: ${name} (${accountId})`)
return {
id: accountId,
name,
@@ -99,22 +98,22 @@ class ClaudeConsoleAccountService {
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 = [];
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);
const accountData = await client.hgetall(key)
if (accountData && Object.keys(accountData).length > 0) {
// 获取限流状态信息
const rateLimitInfo = this._getRateLimitInfo(accountData);
const rateLimitInfo = this._getRateLimitInfo(accountData)
accounts.push({
id: accountData.id,
platform: accountData.platform,
@@ -134,356 +133,379 @@ class ClaudeConsoleAccountService {
lastUsedAt: accountData.lastUsedAt,
rateLimitStatus: rateLimitInfo,
schedulable: accountData.schedulable !== 'false' // 默认为true只有明确设置为false才不可调度
});
})
}
}
return accounts;
return accounts
} catch (error) {
logger.error('❌ Failed to get Claude Console accounts:', error);
throw 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}`);
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] 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}`);
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;
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
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);
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;
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);
const existingAccount = await this.getAccount(accountId)
if (!existingAccount) {
throw new Error('Account not found');
throw new Error('Account not found')
}
const client = redis.getClientSafe();
const updatedData = {};
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;
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;
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);
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.priority !== undefined) updatedData.priority = updates.priority.toString();
if (updates.supportedModels !== undefined) {
logger.debug(`[DEBUG] Updating supportedModels: ${JSON.stringify(updates.supportedModels)}`);
logger.debug(`[DEBUG] Updating supportedModels: ${JSON.stringify(updates.supportedModels)}`)
// 处理 supportedModels确保向后兼容
const processedModels = this._processModelMapping(updates.supportedModels);
updatedData.supportedModels = JSON.stringify(processedModels);
const processedModels = this._processModelMapping(updates.supportedModels)
updatedData.supportedModels = JSON.stringify(processedModels)
}
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.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;
updatedData.accountType = updates.accountType
if (updates.accountType === 'shared') {
await client.sadd(this.SHARED_ACCOUNTS_KEY, accountId);
await client.sadd(this.SHARED_ACCOUNTS_KEY, accountId)
} else {
await client.srem(this.SHARED_ACCOUNTS_KEY, accountId);
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 };
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;
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);
const client = redis.getClientSafe()
const account = await this.getAccount(accountId)
if (!account) {
throw new Error('Account not found');
throw new Error('Account not found')
}
// 从Redis删除
await client.del(`${this.ACCOUNT_KEY_PREFIX}${accountId}`);
await client.del(`${this.ACCOUNT_KEY_PREFIX}${accountId}`)
// 从共享账户集合中移除
if (account.accountType === 'shared') {
await client.srem(this.SHARED_ACCOUNTS_KEY, accountId);
await client.srem(this.SHARED_ACCOUNTS_KEY, accountId)
}
logger.success(`🗑️ Deleted Claude Console account: ${accountId}`);
return { success: true };
logger.success(`🗑️ Deleted Claude Console account: ${accountId}`)
return { success: true }
} catch (error) {
logger.error('❌ Failed to delete Claude Console account:', error);
throw 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);
const client = redis.getClientSafe()
const account = await this.getAccount(accountId)
if (!account) {
throw new Error('Account not found');
throw new Error('Account not found')
}
const updates = {
rateLimitedAt: new Date().toISOString(),
rateLimitStatus: 'limited'
};
}
await client.hset(
`${this.ACCOUNT_KEY_PREFIX}${accountId}`,
updates
);
await client.hset(`${this.ACCOUNT_KEY_PREFIX}${accountId}`, updates)
logger.warn(`🚫 Claude Console account marked as rate limited: ${account.name} (${accountId})`);
return { success: true };
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;
logger.error(`❌ Failed to mark Claude Console account as rate limited: ${accountId}`, error)
throw error
}
}
// ✅ 移除账号的限流状态
async removeAccountRateLimit(accountId) {
try {
const client = redis.getClientSafe();
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 };
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;
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);
const account = await this.getAccount(accountId)
if (!account) {
return false;
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 rateLimitedAt = new Date(account.rateLimitedAt)
const now = new Date()
const minutesSinceRateLimit = (now - rateLimitedAt) / (1000 * 60)
// 使用账户配置的限流时间
const rateLimitDuration = account.rateLimitDuration || 60;
const rateLimitDuration = account.rateLimitDuration || 60
if (minutesSinceRateLimit >= rateLimitDuration) {
await this.removeAccountRateLimit(accountId);
return false;
await this.removeAccountRateLimit(accountId)
return false
}
return true;
return true
}
return false;
return false
} catch (error) {
logger.error(`❌ Failed to check rate limit status for Claude Console account: ${accountId}`, error);
return false;
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 client = redis.getClientSafe()
const updates = {
status: 'blocked',
errorMessage: reason,
blockedAt: new Date().toISOString()
};
}
await client.hset(
`${this.ACCOUNT_KEY_PREFIX}${accountId}`,
updates
);
await client.hset(`${this.ACCOUNT_KEY_PREFIX}${accountId}`, updates)
logger.warn(`🚫 Claude Console account blocked: ${accountId} - ${reason}`);
return { success: true };
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;
logger.error(`❌ Failed to block Claude Console account: ${accountId}`, error)
throw error
}
}
// 🌐 创建代理agent
_createProxyAgent(proxyConfig) {
if (!proxyConfig) {
return null;
return null
}
try {
const proxy = typeof proxyConfig === 'string' ? JSON.parse(proxyConfig) : proxyConfig;
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);
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);
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);
logger.warn('⚠️ Invalid proxy configuration:', error)
}
return null;
return null
}
// 🔐 加密敏感数据
_encryptSensitiveData(data) {
if (!data) return '';
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;
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;
logger.error('❌ Encryption error:', error)
return data
}
}
// 🔓 解密敏感数据
_decryptSensitiveData(encryptedData) {
if (!encryptedData) return '';
if (!encryptedData) {
return ''
}
try {
if (encryptedData.includes(':')) {
const parts = encryptedData.split(':');
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;
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;
return encryptedData
} catch (error) {
logger.error('❌ Decryption error:', error);
return encryptedData;
logger.error('❌ Decryption error:', error)
return encryptedData
}
}
// 🔑 生成加密密钥
_generateEncryptionKey() {
return crypto.scryptSync(config.security.encryptionKey, this.ENCRYPTION_SALT, 32);
return crypto.scryptSync(config.security.encryptionKey, this.ENCRYPTION_SALT, 32)
}
// 🎭 掩码API URL
_maskApiUrl(apiUrl) {
if (!apiUrl) return '';
if (!apiUrl) {
return ''
}
try {
const url = new URL(apiUrl);
return `${url.protocol}//${url.hostname}/***`;
const url = new URL(apiUrl)
return `${url.protocol}//${url.hostname}/***`
} catch {
return '***';
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);
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 {
@@ -491,57 +513,57 @@ class ClaudeConsoleAccountService {
rateLimitedAt: null,
minutesSinceRateLimit: 0,
minutesRemaining: 0
};
}
}
// 🔄 处理模型映射,确保向后兼容
_processModelMapping(supportedModels) {
// 如果是空值,返回空对象(支持所有模型)
if (!supportedModels || (Array.isArray(supportedModels) && supportedModels.length === 0)) {
return {};
return {}
}
// 如果已经是对象格式(新的映射表格式),直接返回
if (typeof supportedModels === 'object' && !Array.isArray(supportedModels)) {
return supportedModels;
return supportedModels
}
// 如果是数组格式(旧格式),转换为映射表
if (Array.isArray(supportedModels)) {
const mapping = {};
supportedModels.forEach(model => {
const mapping = {}
supportedModels.forEach((model) => {
if (model && typeof model === 'string') {
mapping[model] = model; // 映射到自身
mapping[model] = model // 映射到自身
}
});
return mapping;
})
return mapping
}
// 其他情况返回空对象
return {};
return {}
}
// 🔍 检查模型是否支持(用于调度)
isModelSupported(modelMapping, requestedModel) {
// 如果映射表为空,支持所有模型
if (!modelMapping || Object.keys(modelMapping).length === 0) {
return true;
return true
}
// 检查请求的模型是否在映射表的键中
return Object.prototype.hasOwnProperty.call(modelMapping, requestedModel);
return Object.prototype.hasOwnProperty.call(modelMapping, requestedModel)
}
// 🔄 获取映射后的模型名称
getMappedModel(modelMapping, requestedModel) {
// 如果映射表为空,返回原模型
if (!modelMapping || Object.keys(modelMapping).length === 0) {
return requestedModel;
return requestedModel
}
// 返回映射后的模型,如果不存在则返回原模型
return modelMapping[requestedModel] || requestedModel;
return modelMapping[requestedModel] || requestedModel
}
}
module.exports = new ClaudeConsoleAccountService();
module.exports = new ClaudeConsoleAccountService()

View File

@@ -1,37 +1,54 @@
const axios = require('axios');
const claudeConsoleAccountService = require('./claudeConsoleAccountService');
const logger = require('../utils/logger');
const config = require('../../config/config');
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.69 (external, cli)';
this.defaultUserAgent = 'claude-cli/1.0.69 (external, cli)'
}
// 🚀 转发请求到Claude Console API
async relayRequest(requestBody, apiKeyData, clientRequest, clientResponse, clientHeaders, accountId, options = {}) {
let abortController = null;
async relayRequest(
requestBody,
apiKeyData,
clientRequest,
clientResponse,
clientHeaders,
accountId,
options = {}
) {
let abortController = null
try {
// 获取账户信息
const account = await claudeConsoleAccountService.getAccount(accountId);
const account = await claudeConsoleAccountService.getAccount(accountId)
if (!account) {
throw new Error('Claude Console Claude account not found');
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}`);
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}`)
// 处理模型映射
let mappedModel = requestBody.model;
if (account.supportedModels && typeof account.supportedModels === 'object' && !Array.isArray(account.supportedModels)) {
const newModel = claudeConsoleAccountService.getMappedModel(account.supportedModels, requestBody.model);
let mappedModel = requestBody.model
if (
account.supportedModels &&
typeof account.supportedModels === 'object' &&
!Array.isArray(account.supportedModels)
) {
const newModel = claudeConsoleAccountService.getMappedModel(
account.supportedModels,
requestBody.model
)
if (newModel !== requestBody.model) {
logger.info(`🔄 Mapping model from ${requestBody.model} to ${newModel}`);
mappedModel = newModel;
logger.info(`🔄 Mapping model from ${requestBody.model} to ${newModel}`)
mappedModel = newModel
}
}
@@ -39,52 +56,51 @@ class ClaudeConsoleRelayService {
const modifiedRequestBody = {
...requestBody,
model: mappedModel
};
}
// 模型兼容性检查已经在调度器中完成,这里不需要再检查
// 创建代理agent
const proxyAgent = claudeConsoleAccountService._createProxyAgent(account.proxy);
const proxyAgent = claudeConsoleAccountService._createProxyAgent(account.proxy)
// 创建AbortController用于取消请求
abortController = new AbortController();
abortController = new AbortController()
// 设置客户端断开监听器
const handleClientDisconnect = () => {
logger.info('🔌 Client disconnected, aborting Claude Console Claude request');
logger.info('🔌 Client disconnected, aborting Claude Console Claude request')
if (abortController && !abortController.signal.aborted) {
abortController.abort();
abortController.abort()
}
};
}
// 监听客户端断开事件
if (clientRequest) {
clientRequest.once('close', handleClientDisconnect);
clientRequest.once('close', handleClientDisconnect)
}
if (clientResponse) {
clientResponse.once('close', handleClientDisconnect);
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 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 filteredHeaders = this._filterClientHeaders(clientHeaders)
logger.debug(`[DEBUG] Filtered client headers: ${JSON.stringify(filteredHeaders)}`)
// 决定使用的 User-Agent优先使用账户自定义的否则透传客户端的最后才使用默认值
const userAgent = account.userAgent ||
clientHeaders?.['user-agent'] ||
clientHeaders?.['User-Agent'] ||
this.defaultUserAgent;
const userAgent =
account.userAgent ||
clientHeaders?.['user-agent'] ||
clientHeaders?.['User-Agent'] ||
this.defaultUserAgent
// 准备请求配置
const requestConfig = {
method: 'POST',
@@ -100,103 +116,123 @@ class ClaudeConsoleRelayService {
timeout: config.proxy.timeout || 60000,
signal: abortController.signal,
validateStatus: () => true // 接受所有状态码
};
}
// 根据 API Key 格式选择认证方式
if (account.apiKey && account.apiKey.startsWith('sk-ant-')) {
// Anthropic 官方 API Key 使用 x-api-key
requestConfig.headers['x-api-key'] = account.apiKey;
logger.debug('[DEBUG] Using x-api-key authentication for sk-ant-* API key');
requestConfig.headers['x-api-key'] = account.apiKey
logger.debug('[DEBUG] Using x-api-key authentication for sk-ant-* API key')
} else {
// 其他 API Key 使用 Authorization Bearer
requestConfig.headers['Authorization'] = `Bearer ${account.apiKey}`;
logger.debug('[DEBUG] Using Authorization Bearer authentication');
requestConfig.headers['Authorization'] = `Bearer ${account.apiKey}`
logger.debug('[DEBUG] Using Authorization Bearer authentication')
}
logger.debug(`[DEBUG] Initial headers before beta: ${JSON.stringify(requestConfig.headers, null, 2)}`);
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;
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('[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);
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);
clientRequest.removeListener('close', handleClientDisconnect)
}
if (clientResponse) {
clientResponse.removeListener('close', handleClientDisconnect);
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)}`);
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);
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);
const isRateLimited = await claudeConsoleAccountService.isAccountRateLimited(accountId)
if (isRateLimited) {
await claudeConsoleAccountService.removeAccountRateLimit(accountId);
await claudeConsoleAccountService.removeAccountRateLimit(accountId)
}
}
// 更新最后使用时间
await this._updateLastUsedTime(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}`);
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.info('Request aborted due to client disconnect')
throw new Error('Client disconnected')
}
logger.error('❌ Claude Console Claude relay request failed:', error.message);
logger.error('❌ Claude Console Claude relay request failed:', error.message)
// 不再因为模型不支持而block账号
throw error;
throw error
}
}
// 🌊 处理流式响应
async relayStreamRequestWithUsageCapture(requestBody, apiKeyData, responseStream, clientHeaders, usageCallback, accountId, streamTransformer = null, options = {}) {
async relayStreamRequestWithUsageCapture(
requestBody,
apiKeyData,
responseStream,
clientHeaders,
usageCallback,
accountId,
streamTransformer = null,
options = {}
) {
try {
// 获取账户信息
const account = await claudeConsoleAccountService.getAccount(accountId);
const account = await claudeConsoleAccountService.getAccount(accountId)
if (!account) {
throw new Error('Claude Console Claude account not found');
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}`);
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);
const proxyAgent = claudeConsoleAccountService._createProxyAgent(account.proxy)
// 发送流式请求
await this._makeClaudeConsoleStreamRequest(
@@ -209,40 +245,48 @@ class ClaudeConsoleRelayService {
usageCallback,
streamTransformer,
options
);
)
// 更新最后使用时间
await this._updateLastUsedTime(accountId);
await this._updateLastUsedTime(accountId)
} catch (error) {
logger.error('❌ Claude Console Claude stream relay failed:', error);
throw 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 = {}) {
async _makeClaudeConsoleStreamRequest(
body,
account,
proxyAgent,
clientHeaders,
responseStream,
accountId,
usageCallback,
streamTransformer = null,
requestOptions = {}
) {
return new Promise((resolve, reject) => {
let aborted = false;
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 cleanUrl = account.apiUrl.replace(/\/$/, '') // 移除末尾斜杠
const apiEndpoint = cleanUrl.endsWith('/v1/messages') ? cleanUrl : `${cleanUrl}/v1/messages`
logger.debug(`🎯 Final API endpoint for stream: ${apiEndpoint}`)
// 过滤客户端请求头
const filteredHeaders = this._filterClientHeaders(clientHeaders);
logger.debug(`[DEBUG] Filtered client headers: ${JSON.stringify(filteredHeaders)}`);
const filteredHeaders = this._filterClientHeaders(clientHeaders)
logger.debug(`[DEBUG] Filtered client headers: ${JSON.stringify(filteredHeaders)}`)
// 决定使用的 User-Agent优先使用账户自定义的否则透传客户端的最后才使用默认值
const userAgent = account.userAgent ||
clientHeaders?.['user-agent'] ||
clientHeaders?.['User-Agent'] ||
this.defaultUserAgent;
const userAgent =
account.userAgent ||
clientHeaders?.['user-agent'] ||
clientHeaders?.['User-Agent'] ||
this.defaultUserAgent
// 准备请求配置
const requestConfig = {
method: 'POST',
@@ -258,237 +302,254 @@ class ClaudeConsoleRelayService {
timeout: config.proxy.timeout || 60000,
responseType: 'stream',
validateStatus: () => true // 接受所有状态码
};
}
// 根据 API Key 格式选择认证方式
if (account.apiKey && account.apiKey.startsWith('sk-ant-')) {
// Anthropic 官方 API Key 使用 x-api-key
requestConfig.headers['x-api-key'] = account.apiKey;
logger.debug('[DEBUG] Using x-api-key authentication for sk-ant-* API key');
requestConfig.headers['x-api-key'] = account.apiKey
logger.debug('[DEBUG] Using x-api-key authentication for sk-ant-* API key')
} else {
// 其他 API Key 使用 Authorization Bearer
requestConfig.headers['Authorization'] = `Bearer ${account.apiKey}`;
logger.debug('[DEBUG] Using Authorization Bearer authentication');
requestConfig.headers['Authorization'] = `Bearer ${account.apiKey}`
logger.debug('[DEBUG] Using Authorization Bearer authentication')
}
// 添加beta header如果需要
if (requestOptions.betaHeader) {
requestConfig.headers['anthropic-beta'] = requestOptions.betaHeader;
requestConfig.headers['anthropic-beta'] = requestOptions.betaHeader
}
// 发送请求
const request = axios(requestConfig);
const request = axios(requestConfig)
request.then(response => {
logger.debug(`🌊 Claude Console Claude stream response status: ${response.status}`);
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);
// 错误响应处理
if (response.status !== 200) {
logger.error(`❌ Claude Console API returned error status: ${response.status}`)
if (response.status === 429) {
claudeConsoleAccountService.markAccountRateLimited(accountId)
}
// 设置错误响应的状态码和响应头
if (!responseStream.headersSent) {
const errorHeaders = {
'Content-Type': response.headers['content-type'] || 'application/json',
'Cache-Control': 'no-cache',
Connection: 'keep-alive'
}
// 避免 Transfer-Encoding 冲突,让 Express 自动处理
delete errorHeaders['Transfer-Encoding']
delete errorHeaders['Content-Length']
responseStream.writeHead(response.status, errorHeaders)
}
// 直接透传错误数据,不进行包装
response.data.on('data', (chunk) => {
if (!responseStream.destroyed) {
responseStream.write(chunk)
}
})
response.data.on('end', () => {
if (!responseStream.destroyed) {
responseStream.end()
}
resolve() // 不抛出异常,正常完成流处理
})
return
}
// 设置错误响应的状态码和响应头
// 成功响应,检查并移除限流状态
claudeConsoleAccountService.isAccountRateLimited(accountId).then((isRateLimited) => {
if (isRateLimited) {
claudeConsoleAccountService.removeAccountRateLimit(accountId)
}
})
// 设置响应头
if (!responseStream.headersSent) {
const errorHeaders = {
'Content-Type': response.headers['content-type'] || 'application/json',
responseStream.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
};
// 避免 Transfer-Encoding 冲突,让 Express 自动处理
delete errorHeaders['Transfer-Encoding'];
delete errorHeaders['Content-Length'];
responseStream.writeHead(response.status, errorHeaders);
Connection: 'keep-alive',
'X-Accel-Buffering': 'no'
})
}
// 直接透传错误数据,不进行包装
response.data.on('data', chunk => {
if (!responseStream.destroyed) {
responseStream.write(chunk);
}
});
let buffer = ''
let finalUsageReported = false
const collectedUsageData = {}
response.data.on('end', () => {
if (!responseStream.destroyed) {
responseStream.end();
}
resolve(); // 不抛出异常,正常完成流处理
});
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);
// 处理流数据
response.data.on('data', (chunk) => {
try {
if (aborted) {
return
}
// 解析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;
}
}
const chunkStr = chunk.toString()
buffer += chunkStr
// 不再因为模型不支持而block账号
} catch (e) {
// 忽略解析错误
// 处理完整的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);
} 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`
)
}
}
// 确保流正确结束
if (!responseStream.destroyed) {
responseStream.end();
})
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)
}
})
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'
})
}
});
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();
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);
});
}).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);
});
reject(error)
})
// 处理客户端断开连接
responseStream.on('close', () => {
logger.debug('🔌 Client disconnected, cleaning up Claude Console stream');
aborted = true;
});
});
logger.debug('🔌 Client disconnected, cleaning up Claude Console stream')
aborted = true
})
})
}
// 🔧 过滤客户端请求头
@@ -505,55 +566,58 @@ class ClaudeConsoleRelayService {
'content-encoding',
'transfer-encoding',
'anthropic-version'
];
const filteredHeaders = {};
Object.keys(clientHeaders || {}).forEach(key => {
const lowerKey = key.toLowerCase();
]
const filteredHeaders = {}
Object.keys(clientHeaders || {}).forEach((key) => {
const lowerKey = key.toLowerCase()
if (!sensitiveHeaders.includes(lowerKey)) {
filteredHeaders[key] = clientHeaders[key];
filteredHeaders[key] = clientHeaders[key]
}
});
return filteredHeaders;
})
return filteredHeaders
}
// 🕐 更新最后使用时间
async _updateLastUsedTime(accountId) {
try {
const client = require('../models/redis').getClientSafe();
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);
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');
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);
logger.error('❌ Claude Console Claude health check failed:', error)
return {
healthy: false,
error: error.message,
timestamp: new Date().toISOString()
};
}
}
}
}
module.exports = new ClaudeConsoleRelayService();
module.exports = new ClaudeConsoleRelayService()

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
const redis = require('../models/redis');
const apiKeyService = require('./apiKeyService');
const CostCalculator = require('../utils/costCalculator');
const logger = require('../utils/logger');
const redis = require('../models/redis')
const apiKeyService = require('./apiKeyService')
const CostCalculator = require('../utils/costCalculator')
const logger = require('../utils/logger')
class CostInitService {
/**
@@ -10,173 +10,187 @@ class CostInitService {
*/
async initializeAllCosts() {
try {
logger.info('💰 Starting cost initialization for all API Keys...');
const apiKeys = await apiKeyService.getAllApiKeys();
const client = redis.getClientSafe();
let processedCount = 0;
let errorCount = 0;
logger.info('💰 Starting cost initialization for all API Keys...')
const apiKeys = await apiKeyService.getAllApiKeys()
const client = redis.getClientSafe()
let processedCount = 0
let errorCount = 0
for (const apiKey of apiKeys) {
try {
await this.initializeApiKeyCosts(apiKey.id, client);
processedCount++;
await this.initializeApiKeyCosts(apiKey.id, client)
processedCount++
if (processedCount % 10 === 0) {
logger.info(`💰 Processed ${processedCount} API Keys...`);
logger.info(`💰 Processed ${processedCount} API Keys...`)
}
} catch (error) {
errorCount++;
logger.error(`❌ Failed to initialize costs for API Key ${apiKey.id}:`, error);
errorCount++
logger.error(`❌ Failed to initialize costs for API Key ${apiKey.id}:`, error)
}
}
logger.success(`💰 Cost initialization completed! Processed: ${processedCount}, Errors: ${errorCount}`);
return { processed: processedCount, errors: errorCount };
logger.success(
`💰 Cost initialization completed! Processed: ${processedCount}, Errors: ${errorCount}`
)
return { processed: processedCount, errors: errorCount }
} catch (error) {
logger.error('❌ Failed to initialize costs:', error);
throw error;
logger.error('❌ Failed to initialize costs:', error)
throw error
}
}
/**
* 初始化单个API Key的费用数据
*/
async initializeApiKeyCosts(apiKeyId, client) {
// 获取所有时间的模型使用统计
const modelKeys = await client.keys(`usage:${apiKeyId}:model:*:*:*`);
const modelKeys = await client.keys(`usage:${apiKeyId}:model:*:*:*`)
// 按日期分组统计
const dailyCosts = new Map(); // date -> cost
const monthlyCosts = new Map(); // month -> cost
const hourlyCosts = new Map(); // hour -> cost
const dailyCosts = new Map() // date -> cost
const monthlyCosts = new Map() // month -> cost
const hourlyCosts = new Map() // hour -> cost
for (const key of modelKeys) {
// 解析key格式: usage:{keyId}:model:{period}:{model}:{date}
const match = key.match(/usage:(.+):model:(daily|monthly|hourly):(.+):(\d{4}-\d{2}(?:-\d{2})?(?::\d{2})?)$/);
if (!match) continue;
const [, , period, model, dateStr] = match;
const match = key.match(
/usage:(.+):model:(daily|monthly|hourly):(.+):(\d{4}-\d{2}(?:-\d{2})?(?::\d{2})?)$/
)
if (!match) {
continue
}
const [, , period, model, dateStr] = match
// 获取使用数据
const data = await client.hgetall(key);
if (!data || Object.keys(data).length === 0) continue;
const data = await client.hgetall(key)
if (!data || Object.keys(data).length === 0) {
continue
}
// 计算费用
const usage = {
input_tokens: parseInt(data.totalInputTokens) || parseInt(data.inputTokens) || 0,
output_tokens: parseInt(data.totalOutputTokens) || parseInt(data.outputTokens) || 0,
cache_creation_input_tokens: parseInt(data.totalCacheCreateTokens) || parseInt(data.cacheCreateTokens) || 0,
cache_read_input_tokens: parseInt(data.totalCacheReadTokens) || parseInt(data.cacheReadTokens) || 0
};
const costResult = CostCalculator.calculateCost(usage, model);
const cost = costResult.costs.total;
cache_creation_input_tokens:
parseInt(data.totalCacheCreateTokens) || parseInt(data.cacheCreateTokens) || 0,
cache_read_input_tokens:
parseInt(data.totalCacheReadTokens) || parseInt(data.cacheReadTokens) || 0
}
const costResult = CostCalculator.calculateCost(usage, model)
const cost = costResult.costs.total
// 根据period分组累加费用
if (period === 'daily') {
const currentCost = dailyCosts.get(dateStr) || 0;
dailyCosts.set(dateStr, currentCost + cost);
const currentCost = dailyCosts.get(dateStr) || 0
dailyCosts.set(dateStr, currentCost + cost)
} else if (period === 'monthly') {
const currentCost = monthlyCosts.get(dateStr) || 0;
monthlyCosts.set(dateStr, currentCost + cost);
const currentCost = monthlyCosts.get(dateStr) || 0
monthlyCosts.set(dateStr, currentCost + cost)
} else if (period === 'hourly') {
const currentCost = hourlyCosts.get(dateStr) || 0;
hourlyCosts.set(dateStr, currentCost + cost);
const currentCost = hourlyCosts.get(dateStr) || 0
hourlyCosts.set(dateStr, currentCost + cost)
}
}
// 将计算出的费用写入Redis
const promises = [];
const promises = []
// 写入每日费用
for (const [date, cost] of dailyCosts) {
const key = `usage:cost:daily:${apiKeyId}:${date}`;
const key = `usage:cost:daily:${apiKeyId}:${date}`
promises.push(
client.set(key, cost.toString()),
client.expire(key, 86400 * 30) // 30天过期
);
)
}
// 写入每月费用
for (const [month, cost] of monthlyCosts) {
const key = `usage:cost:monthly:${apiKeyId}:${month}`;
const key = `usage:cost:monthly:${apiKeyId}:${month}`
promises.push(
client.set(key, cost.toString()),
client.expire(key, 86400 * 90) // 90天过期
);
)
}
// 写入每小时费用
for (const [hour, cost] of hourlyCosts) {
const key = `usage:cost:hourly:${apiKeyId}:${hour}`;
const key = `usage:cost:hourly:${apiKeyId}:${hour}`
promises.push(
client.set(key, cost.toString()),
client.expire(key, 86400 * 7) // 7天过期
);
)
}
// 计算总费用
let totalCost = 0;
let totalCost = 0
for (const cost of dailyCosts.values()) {
totalCost += cost;
totalCost += cost
}
// 写入总费用
if (totalCost > 0) {
const totalKey = `usage:cost:total:${apiKeyId}`;
promises.push(client.set(totalKey, totalCost.toString()));
const totalKey = `usage:cost:total:${apiKeyId}`
promises.push(client.set(totalKey, totalCost.toString()))
}
await Promise.all(promises);
logger.debug(`💰 Initialized costs for API Key ${apiKeyId}: Daily entries: ${dailyCosts.size}, Total cost: $${totalCost.toFixed(2)}`);
await Promise.all(promises)
logger.debug(
`💰 Initialized costs for API Key ${apiKeyId}: Daily entries: ${dailyCosts.size}, Total cost: $${totalCost.toFixed(2)}`
)
}
/**
* 检查是否需要初始化费用数据
*/
async needsInitialization() {
try {
const client = redis.getClientSafe();
const client = redis.getClientSafe()
// 检查是否有任何费用数据
const costKeys = await client.keys('usage:cost:*');
const costKeys = await client.keys('usage:cost:*')
// 如果没有费用数据,需要初始化
if (costKeys.length === 0) {
logger.info('💰 No cost data found, initialization needed');
return true;
logger.info('💰 No cost data found, initialization needed')
return true
}
// 检查是否有使用数据但没有对应的费用数据
const sampleKeys = await client.keys('usage:*:model:daily:*:*');
const sampleKeys = await client.keys('usage:*:model:daily:*:*')
if (sampleKeys.length > 10) {
// 抽样检查
const sampleSize = Math.min(10, sampleKeys.length);
const sampleSize = Math.min(10, sampleKeys.length)
for (let i = 0; i < sampleSize; i++) {
const usageKey = sampleKeys[Math.floor(Math.random() * sampleKeys.length)];
const match = usageKey.match(/usage:(.+):model:daily:(.+):(\d{4}-\d{2}-\d{2})$/);
const usageKey = sampleKeys[Math.floor(Math.random() * sampleKeys.length)]
const match = usageKey.match(/usage:(.+):model:daily:(.+):(\d{4}-\d{2}-\d{2})$/)
if (match) {
const [, keyId, , date] = match;
const costKey = `usage:cost:daily:${keyId}:${date}`;
const hasCost = await client.exists(costKey);
const [, keyId, , date] = match
const costKey = `usage:cost:daily:${keyId}:${date}`
const hasCost = await client.exists(costKey)
if (!hasCost) {
logger.info(`💰 Found usage without cost data for key ${keyId} on ${date}, initialization needed`);
return true;
logger.info(
`💰 Found usage without cost data for key ${keyId} on ${date}, initialization needed`
)
return true
}
}
}
}
logger.info('💰 Cost data appears to be up to date');
return false;
logger.info('💰 Cost data appears to be up to date')
return false
} catch (error) {
logger.error('❌ Failed to check initialization status:', error);
return false;
logger.error('❌ Failed to check initialization status:', error)
return false
}
}
}
module.exports = new CostInitService();
module.exports = new CostInitService()

File diff suppressed because it is too large Load Diff

View File

@@ -1,228 +1,243 @@
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { SocksProxyAgent } = require('socks-proxy-agent');
const logger = require('../utils/logger');
const config = require('../../config/config');
const apiKeyService = require('./apiKeyService');
const axios = require('axios')
const { HttpsProxyAgent } = require('https-proxy-agent')
const { SocksProxyAgent } = require('socks-proxy-agent')
const logger = require('../utils/logger')
const config = require('../../config/config')
const apiKeyService = require('./apiKeyService')
// Gemini API 配置
const GEMINI_API_BASE = 'https://cloudcode.googleapis.com/v1';
const DEFAULT_MODEL = 'models/gemini-2.0-flash-exp';
const GEMINI_API_BASE = 'https://cloudcode.googleapis.com/v1'
const DEFAULT_MODEL = 'models/gemini-2.0-flash-exp'
// 创建代理 agent
function createProxyAgent(proxyConfig) {
if (!proxyConfig) return null;
if (!proxyConfig) {
return null
}
try {
const proxy = typeof proxyConfig === 'string' ? JSON.parse(proxyConfig) : proxyConfig;
const proxy = typeof proxyConfig === 'string' ? JSON.parse(proxyConfig) : proxyConfig
if (!proxy.type || !proxy.host || !proxy.port) {
return null;
return null
}
const proxyUrl = proxy.username && proxy.password
? `${proxy.type}://${proxy.username}:${proxy.password}@${proxy.host}:${proxy.port}`
: `${proxy.type}://${proxy.host}:${proxy.port}`;
const proxyUrl =
proxy.username && proxy.password
? `${proxy.type}://${proxy.username}:${proxy.password}@${proxy.host}:${proxy.port}`
: `${proxy.type}://${proxy.host}:${proxy.port}`
if (proxy.type === 'socks5') {
return new SocksProxyAgent(proxyUrl);
return new SocksProxyAgent(proxyUrl)
} else if (proxy.type === 'http' || proxy.type === 'https') {
return new HttpsProxyAgent(proxyUrl);
return new HttpsProxyAgent(proxyUrl)
}
} catch (error) {
logger.error('Error creating proxy agent:', error);
logger.error('Error creating proxy agent:', error)
}
return null;
return null
}
// 转换 OpenAI 消息格式到 Gemini 格式
function convertMessagesToGemini(messages) {
const contents = [];
let systemInstruction = '';
const contents = []
let systemInstruction = ''
for (const message of messages) {
if (message.role === 'system') {
systemInstruction += (systemInstruction ? '\n\n' : '') + message.content;
systemInstruction += (systemInstruction ? '\n\n' : '') + message.content
} else if (message.role === 'user') {
contents.push({
role: 'user',
parts: [{ text: message.content }]
});
})
} else if (message.role === 'assistant') {
contents.push({
role: 'model',
parts: [{ text: message.content }]
});
})
}
}
return { contents, systemInstruction };
return { contents, systemInstruction }
}
// 转换 Gemini 响应到 OpenAI 格式
function convertGeminiResponse(geminiResponse, model, stream = false) {
if (stream) {
// 流式响应
const candidate = geminiResponse.candidates?.[0];
if (!candidate) return null;
const content = candidate.content?.parts?.[0]?.text || '';
const finishReason = candidate.finishReason?.toLowerCase();
const candidate = geminiResponse.candidates?.[0]
if (!candidate) {
return null
}
const content = candidate.content?.parts?.[0]?.text || ''
const finishReason = candidate.finishReason?.toLowerCase()
return {
id: `chatcmpl-${Date.now()}`,
object: 'chat.completion.chunk',
created: Math.floor(Date.now() / 1000),
model: model,
choices: [{
index: 0,
delta: {
content: content
},
finish_reason: finishReason === 'stop' ? 'stop' : null
}]
};
model,
choices: [
{
index: 0,
delta: {
content
},
finish_reason: finishReason === 'stop' ? 'stop' : null
}
]
}
} else {
// 非流式响应
const candidate = geminiResponse.candidates?.[0];
const candidate = geminiResponse.candidates?.[0]
if (!candidate) {
throw new Error('No response from Gemini');
throw new Error('No response from Gemini')
}
const content = candidate.content?.parts?.[0]?.text || '';
const finishReason = candidate.finishReason?.toLowerCase() || 'stop';
const content = candidate.content?.parts?.[0]?.text || ''
const finishReason = candidate.finishReason?.toLowerCase() || 'stop'
// 计算 token 使用量
const usage = geminiResponse.usageMetadata || {
promptTokenCount: 0,
candidatesTokenCount: 0,
totalTokenCount: 0
};
}
return {
id: `chatcmpl-${Date.now()}`,
object: 'chat.completion',
created: Math.floor(Date.now() / 1000),
model: model,
choices: [{
index: 0,
message: {
role: 'assistant',
content: content
},
finish_reason: finishReason
}],
model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content
},
finish_reason: finishReason
}
],
usage: {
prompt_tokens: usage.promptTokenCount,
completion_tokens: usage.candidatesTokenCount,
total_tokens: usage.totalTokenCount
}
};
}
}
}
// 处理流式响应
async function* handleStreamResponse(response, model, apiKeyId, accountId = null) {
let buffer = '';
let buffer = ''
let totalUsage = {
promptTokenCount: 0,
candidatesTokenCount: 0,
totalTokenCount: 0
};
}
try {
for await (const chunk of response.data) {
buffer += chunk.toString();
buffer += chunk.toString()
// 处理 SSE 格式的数据
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // 保留最后一个不完整的行
const lines = buffer.split('\n')
buffer = lines.pop() || '' // 保留最后一个不完整的行
for (const line of lines) {
if (!line.trim()) continue;
// 处理 SSE 格式: "data: {...}"
let jsonData = line;
if (line.startsWith('data: ')) {
jsonData = line.substring(6).trim();
if (!line.trim()) {
continue
}
if (!jsonData || jsonData === '[DONE]') continue;
// 处理 SSE 格式: "data: {...}"
let jsonData = line
if (line.startsWith('data: ')) {
jsonData = line.substring(6).trim()
}
if (!jsonData || jsonData === '[DONE]') {
continue
}
try {
const data = JSON.parse(jsonData);
const data = JSON.parse(jsonData)
// 更新使用量统计
if (data.usageMetadata) {
totalUsage = data.usageMetadata;
totalUsage = data.usageMetadata
}
// 转换并发送响应
const openaiResponse = convertGeminiResponse(data, model, true);
const openaiResponse = convertGeminiResponse(data, model, true)
if (openaiResponse) {
yield `data: ${JSON.stringify(openaiResponse)}\n\n`;
yield `data: ${JSON.stringify(openaiResponse)}\n\n`
}
// 检查是否结束
if (data.candidates?.[0]?.finishReason === 'STOP') {
// 记录使用量
if (apiKeyId && totalUsage.totalTokenCount > 0) {
await apiKeyService.recordUsage(
apiKeyId,
totalUsage.promptTokenCount || 0, // inputTokens
totalUsage.candidatesTokenCount || 0, // outputTokens
0, // cacheCreateTokens (Gemini 没有这个概念)
0, // cacheReadTokens (Gemini 没有这个概念)
model,
accountId
).catch(error => {
logger.error('❌ Failed to record Gemini usage:', error);
});
await apiKeyService
.recordUsage(
apiKeyId,
totalUsage.promptTokenCount || 0, // inputTokens
totalUsage.candidatesTokenCount || 0, // outputTokens
0, // cacheCreateTokens (Gemini 没有这个概念)
0, // cacheReadTokens (Gemini 没有这个概念)
model,
accountId
)
.catch((error) => {
logger.error('❌ Failed to record Gemini usage:', error)
})
}
yield 'data: [DONE]\n\n';
return;
yield 'data: [DONE]\n\n'
return
}
} catch (e) {
logger.debug('Error parsing JSON line:', e.message, 'Line:', jsonData);
logger.debug('Error parsing JSON line:', e.message, 'Line:', jsonData)
}
}
}
// 处理剩余的 buffer
if (buffer.trim()) {
try {
let jsonData = buffer.trim();
let jsonData = buffer.trim()
if (jsonData.startsWith('data: ')) {
jsonData = jsonData.substring(6).trim();
jsonData = jsonData.substring(6).trim()
}
if (jsonData && jsonData !== '[DONE]') {
const data = JSON.parse(jsonData);
const openaiResponse = convertGeminiResponse(data, model, true);
const data = JSON.parse(jsonData)
const openaiResponse = convertGeminiResponse(data, model, true)
if (openaiResponse) {
yield `data: ${JSON.stringify(openaiResponse)}\n\n`;
yield `data: ${JSON.stringify(openaiResponse)}\n\n`
}
}
} catch (e) {
logger.debug('Error parsing final buffer:', e.message);
logger.debug('Error parsing final buffer:', e.message)
}
}
yield 'data: [DONE]\n\n';
yield 'data: [DONE]\n\n'
} catch (error) {
// 检查是否是请求被中止
if (error.name === 'CanceledError' || error.code === 'ECONNABORTED') {
logger.info('Stream request was aborted by client');
logger.info('Stream request was aborted by client')
} else {
logger.error('Stream processing error:', error);
logger.error('Stream processing error:', error)
yield `data: ${JSON.stringify({
error: {
message: error.message,
type: 'stream_error'
}
})}\n\n`;
})}\n\n`
}
}
}
@@ -244,12 +259,12 @@ async function sendGeminiRequest({
}) {
// 确保模型名称格式正确
if (!model.startsWith('models/')) {
model = `models/${model}`;
model = `models/${model}`
}
// 转换消息格式
const { contents, systemInstruction } = convertMessagesToGemini(messages);
const { contents, systemInstruction } = convertMessagesToGemini(messages)
// 构建请求体
const requestBody = {
contents,
@@ -258,160 +273,162 @@ async function sendGeminiRequest({
maxOutputTokens: maxTokens,
candidateCount: 1
}
};
if (systemInstruction) {
requestBody.systemInstruction = { parts: [{ text: systemInstruction }] };
}
if (systemInstruction) {
requestBody.systemInstruction = { parts: [{ text: systemInstruction }] }
}
// 配置请求选项
let apiUrl;
let apiUrl
if (projectId) {
// 使用项目特定的 URL 格式Google Cloud/Workspace 账号)
apiUrl = `${GEMINI_API_BASE}/projects/${projectId}/locations/${location}/${model}:${stream ? 'streamGenerateContent' : 'generateContent'}?alt=sse`;
logger.debug(`Using project-specific URL with projectId: ${projectId}, location: ${location}`);
apiUrl = `${GEMINI_API_BASE}/projects/${projectId}/locations/${location}/${model}:${stream ? 'streamGenerateContent' : 'generateContent'}?alt=sse`
logger.debug(`Using project-specific URL with projectId: ${projectId}, location: ${location}`)
} else {
// 使用标准 URL 格式(个人 Google 账号)
apiUrl = `${GEMINI_API_BASE}/${model}:${stream ? 'streamGenerateContent' : 'generateContent'}?alt=sse`;
logger.debug('Using standard URL without projectId');
apiUrl = `${GEMINI_API_BASE}/${model}:${stream ? 'streamGenerateContent' : 'generateContent'}?alt=sse`
logger.debug('Using standard URL without projectId')
}
const axiosConfig = {
method: 'POST',
url: apiUrl,
headers: {
'Authorization': `Bearer ${accessToken}`,
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
data: requestBody,
timeout: config.requestTimeout || 120000
};
// 添加代理配置
const proxyAgent = createProxyAgent(proxy);
if (proxyAgent) {
axiosConfig.httpsAgent = proxyAgent;
logger.debug('Using proxy for Gemini request');
}
// 添加代理配置
const proxyAgent = createProxyAgent(proxy)
if (proxyAgent) {
axiosConfig.httpsAgent = proxyAgent
logger.debug('Using proxy for Gemini request')
}
// 添加 AbortController 信号支持
if (signal) {
axiosConfig.signal = signal;
logger.debug('AbortController signal attached to request');
axiosConfig.signal = signal
logger.debug('AbortController signal attached to request')
}
if (stream) {
axiosConfig.responseType = 'stream';
axiosConfig.responseType = 'stream'
}
try {
logger.debug('Sending request to Gemini API');
const response = await axios(axiosConfig);
logger.debug('Sending request to Gemini API')
const response = await axios(axiosConfig)
if (stream) {
return handleStreamResponse(response, model, apiKeyId, accountId);
return handleStreamResponse(response, model, apiKeyId, accountId)
} else {
// 非流式响应
const openaiResponse = convertGeminiResponse(response.data, model, false);
const openaiResponse = convertGeminiResponse(response.data, model, false)
// 记录使用量
if (apiKeyId && openaiResponse.usage) {
await apiKeyService.recordUsage(
apiKeyId,
openaiResponse.usage.prompt_tokens || 0,
openaiResponse.usage.completion_tokens || 0,
0, // cacheCreateTokens
0, // cacheReadTokens
model,
accountId
).catch(error => {
logger.error('❌ Failed to record Gemini usage:', error);
});
await apiKeyService
.recordUsage(
apiKeyId,
openaiResponse.usage.prompt_tokens || 0,
openaiResponse.usage.completion_tokens || 0,
0, // cacheCreateTokens
0, // cacheReadTokens
model,
accountId
)
.catch((error) => {
logger.error('❌ Failed to record Gemini usage:', error)
})
}
return openaiResponse;
return openaiResponse
}
} catch (error) {
// 检查是否是请求被中止
if (error.name === 'CanceledError' || error.code === 'ECONNABORTED') {
logger.info('Gemini request was aborted by client');
throw {
status: 499,
error: {
message: 'Request canceled by client',
type: 'canceled',
code: 'request_canceled'
}
};
logger.info('Gemini request was aborted by client')
const err = new Error('Request canceled by client')
err.status = 499
err.error = {
message: 'Request canceled by client',
type: 'canceled',
code: 'request_canceled'
}
throw err
}
logger.error('Gemini API request failed:', error.response?.data || error.message);
logger.error('Gemini API request failed:', error.response?.data || error.message)
// 转换错误格式
if (error.response) {
const geminiError = error.response.data?.error;
throw {
status: error.response.status,
error: {
message: geminiError?.message || 'Gemini API request failed',
type: geminiError?.code || 'api_error',
code: geminiError?.code
}
};
}
throw {
status: 500,
error: {
message: error.message,
type: 'network_error'
const geminiError = error.response.data?.error
const err = new Error(geminiError?.message || 'Gemini API request failed')
err.status = error.response.status
err.error = {
message: geminiError?.message || 'Gemini API request failed',
type: geminiError?.code || 'api_error',
code: geminiError?.code
}
};
throw err
}
const err = new Error(error.message)
err.status = 500
err.error = {
message: error.message,
type: 'network_error'
}
throw err
}
}
// 获取可用模型列表
async function getAvailableModels(accessToken, proxy, projectId, location = 'us-central1') {
let apiUrl;
let apiUrl
if (projectId) {
// 使用项目特定的 URL 格式
apiUrl = `${GEMINI_API_BASE}/projects/${projectId}/locations/${location}/models`;
logger.debug(`Fetching models with projectId: ${projectId}, location: ${location}`);
apiUrl = `${GEMINI_API_BASE}/projects/${projectId}/locations/${location}/models`
logger.debug(`Fetching models with projectId: ${projectId}, location: ${location}`)
} else {
// 使用标准 URL 格式
apiUrl = `${GEMINI_API_BASE}/models`;
logger.debug('Fetching models without projectId');
apiUrl = `${GEMINI_API_BASE}/models`
logger.debug('Fetching models without projectId')
}
const axiosConfig = {
method: 'GET',
url: apiUrl,
headers: {
'Authorization': `Bearer ${accessToken}`
Authorization: `Bearer ${accessToken}`
},
timeout: 30000
};
const proxyAgent = createProxyAgent(proxy);
if (proxyAgent) {
axiosConfig.httpsAgent = proxyAgent;
}
const proxyAgent = createProxyAgent(proxy)
if (proxyAgent) {
axiosConfig.httpsAgent = proxyAgent
}
try {
const response = await axios(axiosConfig);
const models = response.data.models || [];
const response = await axios(axiosConfig)
const models = response.data.models || []
// 转换为 OpenAI 格式
return models
.filter(model => model.supportedGenerationMethods?.includes('generateContent'))
.map(model => ({
.filter((model) => model.supportedGenerationMethods?.includes('generateContent'))
.map((model) => ({
id: model.name.replace('models/', ''),
object: 'model',
created: Date.now() / 1000,
owned_by: 'google'
}));
}))
} catch (error) {
logger.error('Failed to get Gemini models:', error);
logger.error('Failed to get Gemini models:', error)
// 返回默认模型列表
return [
{
@@ -420,7 +437,7 @@ async function getAvailableModels(accessToken, proxy, projectId, location = 'us-
created: Date.now() / 1000,
owned_by: 'google'
}
];
]
}
}
@@ -429,4 +446,4 @@ module.exports = {
getAvailableModels,
convertMessagesToGemini,
convertGeminiResponse
};
}

View File

@@ -3,17 +3,17 @@
* 处理 OpenAI API 格式与 Claude API 格式之间的转换
*/
const logger = require('../utils/logger');
const logger = require('../utils/logger')
class OpenAIToClaudeConverter {
constructor() {
// 停止原因映射
this.stopReasonMapping = {
'end_turn': 'stop',
'max_tokens': 'length',
'stop_sequence': 'stop',
'tool_use': 'tool_calls'
};
end_turn: 'stop',
max_tokens: 'length',
stop_sequence: 'stop',
tool_use: 'tool_calls'
}
}
/**
@@ -29,39 +29,39 @@ class OpenAIToClaudeConverter {
temperature: openaiRequest.temperature,
top_p: openaiRequest.top_p,
stream: openaiRequest.stream || false
};
}
// Claude Code 必需的系统消息
const claudeCodeSystemMessage = 'You are Claude Code, Anthropic\'s official CLI for Claude.';
claudeRequest.system = claudeCodeSystemMessage;
const claudeCodeSystemMessage = "You are Claude Code, Anthropic's official CLI for Claude."
claudeRequest.system = claudeCodeSystemMessage
// 处理停止序列
if (openaiRequest.stop) {
claudeRequest.stop_sequences = Array.isArray(openaiRequest.stop)
? openaiRequest.stop
: [openaiRequest.stop];
claudeRequest.stop_sequences = Array.isArray(openaiRequest.stop)
? openaiRequest.stop
: [openaiRequest.stop]
}
// 处理工具调用
if (openaiRequest.tools) {
claudeRequest.tools = this._convertTools(openaiRequest.tools);
claudeRequest.tools = this._convertTools(openaiRequest.tools)
if (openaiRequest.tool_choice) {
claudeRequest.tool_choice = this._convertToolChoice(openaiRequest.tool_choice);
claudeRequest.tool_choice = this._convertToolChoice(openaiRequest.tool_choice)
}
}
// OpenAI 特有的参数已在转换过程中被忽略
// 包括: n, presence_penalty, frequency_penalty, logit_bias, user
logger.debug('📝 Converted OpenAI request to Claude format:', {
model: claudeRequest.model,
messageCount: claudeRequest.messages.length,
hasSystem: !!claudeRequest.system,
stream: claudeRequest.stream
});
})
return claudeRequest;
return claudeRequest
}
/**
@@ -71,28 +71,30 @@ class OpenAIToClaudeConverter {
* @returns {Object} OpenAI 格式的响应
*/
convertResponse(claudeResponse, requestModel) {
const timestamp = Math.floor(Date.now() / 1000);
const timestamp = Math.floor(Date.now() / 1000)
const openaiResponse = {
id: `chatcmpl-${this._generateId()}`,
object: 'chat.completion',
created: timestamp,
model: requestModel || 'gpt-4',
choices: [{
index: 0,
message: this._convertClaudeMessage(claudeResponse),
finish_reason: this._mapStopReason(claudeResponse.stop_reason)
}],
choices: [
{
index: 0,
message: this._convertClaudeMessage(claudeResponse),
finish_reason: this._mapStopReason(claudeResponse.stop_reason)
}
],
usage: this._convertUsage(claudeResponse.usage)
};
}
logger.debug('📝 Converted Claude response to OpenAI format:', {
responseId: openaiResponse.id,
finishReason: openaiResponse.choices[0].finish_reason,
usage: openaiResponse.usage
});
})
return openaiResponse;
return openaiResponse
}
/**
@@ -103,36 +105,38 @@ class OpenAIToClaudeConverter {
* @returns {String} OpenAI 格式的 SSE 数据块
*/
convertStreamChunk(chunk, requestModel, sessionId) {
if (!chunk || chunk.trim() === '') return '';
if (!chunk || chunk.trim() === '') {
return ''
}
// 解析 SSE 数据
const lines = chunk.split('\n');
let convertedChunks = [];
let hasMessageStop = false;
const lines = chunk.split('\n')
const convertedChunks = []
let hasMessageStop = false
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.substring(6);
const data = line.substring(6)
if (data === '[DONE]') {
convertedChunks.push('data: [DONE]\n\n');
continue;
convertedChunks.push('data: [DONE]\n\n')
continue
}
try {
const claudeEvent = JSON.parse(data);
const claudeEvent = JSON.parse(data)
// 检查是否是 message_stop 事件
if (claudeEvent.type === 'message_stop') {
hasMessageStop = true;
hasMessageStop = true
}
const openaiChunk = this._convertStreamEvent(claudeEvent, requestModel, sessionId);
const openaiChunk = this._convertStreamEvent(claudeEvent, requestModel, sessionId)
if (openaiChunk) {
convertedChunks.push(`data: ${JSON.stringify(openaiChunk)}\n\n`);
convertedChunks.push(`data: ${JSON.stringify(openaiChunk)}\n\n`)
}
} catch (e) {
// 跳过无法解析的数据不传递非JSON格式的行
continue;
continue
}
}
// 忽略 event: 行和空行OpenAI 格式不包含这些
@@ -140,95 +144,102 @@ class OpenAIToClaudeConverter {
// 如果收到 message_stop 事件,添加 [DONE] 标记
if (hasMessageStop) {
convertedChunks.push('data: [DONE]\n\n');
convertedChunks.push('data: [DONE]\n\n')
}
return convertedChunks.join('');
return convertedChunks.join('')
}
/**
* 提取系统消息
*/
_extractSystemMessage(messages) {
const systemMessages = messages.filter(msg => msg.role === 'system');
if (systemMessages.length === 0) return null;
const systemMessages = messages.filter((msg) => msg.role === 'system')
if (systemMessages.length === 0) {
return null
}
// 合并所有系统消息
return systemMessages.map(msg => msg.content).join('\n\n');
return systemMessages.map((msg) => msg.content).join('\n\n')
}
/**
* 转换消息格式
*/
_convertMessages(messages) {
const claudeMessages = [];
const claudeMessages = []
for (const msg of messages) {
// 跳过系统消息(已经在 system 字段处理)
if (msg.role === 'system') continue;
// 转换角色名称
const role = msg.role === 'user' ? 'user' : 'assistant';
// 转换消息内容
let content;
if (typeof msg.content === 'string') {
content = msg.content;
} else if (Array.isArray(msg.content)) {
// 处理多模态内容
content = this._convertMultimodalContent(msg.content);
} else {
content = JSON.stringify(msg.content);
if (msg.role === 'system') {
continue
}
// 转换角色名称
const role = msg.role === 'user' ? 'user' : 'assistant'
// 转换消息内容
const { content: rawContent } = msg
let content
if (typeof rawContent === 'string') {
content = rawContent
} else if (Array.isArray(rawContent)) {
// 处理多模态内容
content = this._convertMultimodalContent(rawContent)
} else {
content = JSON.stringify(rawContent)
}
const claudeMsg = {
role: role,
content: content
};
role,
content
}
// 处理工具调用
if (msg.tool_calls) {
claudeMsg.content = this._convertToolCalls(msg.tool_calls);
claudeMsg.content = this._convertToolCalls(msg.tool_calls)
}
// 处理工具响应
if (msg.role === 'tool') {
claudeMsg.role = 'user';
claudeMsg.content = [{
type: 'tool_result',
tool_use_id: msg.tool_call_id,
content: msg.content
}];
claudeMsg.role = 'user'
claudeMsg.content = [
{
type: 'tool_result',
tool_use_id: msg.tool_call_id,
content: msg.content
}
]
}
claudeMessages.push(claudeMsg);
claudeMessages.push(claudeMsg)
}
return claudeMessages;
return claudeMessages
}
/**
* 转换多模态内容
*/
_convertMultimodalContent(content) {
return content.map(item => {
return content.map((item) => {
if (item.type === 'text') {
return {
type: 'text',
text: item.text
};
}
} else if (item.type === 'image_url') {
const imageUrl = item.image_url.url;
const imageUrl = item.image_url.url
// 检查是否是 base64 格式的图片
if (imageUrl.startsWith('data:')) {
// 解析 data URL: data:image/jpeg;base64,/9j/4AAQ...
const matches = imageUrl.match(/^data:([^;]+);base64,(.+)$/);
const matches = imageUrl.match(/^data:([^;]+);base64,(.+)$/)
if (matches) {
const mediaType = matches[1]; // e.g., 'image/jpeg', 'image/png'
const base64Data = matches[2];
const mediaType = matches[1] // e.g., 'image/jpeg', 'image/png'
const base64Data = matches[2]
return {
type: 'image',
source: {
@@ -236,10 +247,10 @@ class OpenAIToClaudeConverter {
media_type: mediaType,
data: base64Data
}
};
}
} else {
// 如果格式不正确,尝试使用默认处理
logger.warn('⚠️ Invalid base64 image format, using default parsing');
logger.warn('⚠️ Invalid base64 image format, using default parsing')
return {
type: 'image',
source: {
@@ -247,60 +258,70 @@ class OpenAIToClaudeConverter {
media_type: 'image/jpeg',
data: imageUrl.split(',')[1] || ''
}
};
}
}
} else {
// 如果是 URL 格式的图片Claude 不支持直接 URL需要报错
logger.error('❌ URL images are not supported by Claude API, only base64 format is accepted');
throw new Error('Claude API only supports base64 encoded images, not URLs. Please convert the image to base64 format.');
logger.error(
'❌ URL images are not supported by Claude API, only base64 format is accepted'
)
throw new Error(
'Claude API only supports base64 encoded images, not URLs. Please convert the image to base64 format.'
)
}
}
return item;
});
return item
})
}
/**
* 转换工具定义
*/
_convertTools(tools) {
return tools.map(tool => {
return tools.map((tool) => {
if (tool.type === 'function') {
return {
name: tool.function.name,
description: tool.function.description,
input_schema: tool.function.parameters
};
}
}
return tool;
});
return tool
})
}
/**
* 转换工具选择
*/
_convertToolChoice(toolChoice) {
if (toolChoice === 'none') return { type: 'none' };
if (toolChoice === 'auto') return { type: 'auto' };
if (toolChoice === 'required') return { type: 'any' };
if (toolChoice === 'none') {
return { type: 'none' }
}
if (toolChoice === 'auto') {
return { type: 'auto' }
}
if (toolChoice === 'required') {
return { type: 'any' }
}
if (toolChoice.type === 'function') {
return {
type: 'tool',
name: toolChoice.function.name
};
}
}
return { type: 'auto' };
return { type: 'auto' }
}
/**
* 转换工具调用
*/
_convertToolCalls(toolCalls) {
return toolCalls.map(tc => ({
return toolCalls.map((tc) => ({
type: 'tool_use',
id: tc.id,
name: tc.function.name,
input: JSON.parse(tc.function.arguments)
}));
}))
}
/**
@@ -310,20 +331,20 @@ class OpenAIToClaudeConverter {
const message = {
role: 'assistant',
content: null
};
}
// 处理内容
if (claudeResponse.content) {
if (typeof claudeResponse.content === 'string') {
message.content = claudeResponse.content;
message.content = claudeResponse.content
} else if (Array.isArray(claudeResponse.content)) {
// 提取文本内容和工具调用
const textParts = [];
const toolCalls = [];
const textParts = []
const toolCalls = []
for (const item of claudeResponse.content) {
if (item.type === 'text') {
textParts.push(item.text);
textParts.push(item.text)
} else if (item.type === 'tool_use') {
toolCalls.push({
id: item.id,
@@ -332,114 +353,121 @@ class OpenAIToClaudeConverter {
name: item.name,
arguments: JSON.stringify(item.input)
}
});
})
}
}
message.content = textParts.join('') || null;
message.content = textParts.join('') || null
if (toolCalls.length > 0) {
message.tool_calls = toolCalls;
message.tool_calls = toolCalls
}
}
}
return message;
return message
}
/**
* 转换停止原因
*/
_mapStopReason(claudeReason) {
return this.stopReasonMapping[claudeReason] || 'stop';
return this.stopReasonMapping[claudeReason] || 'stop'
}
/**
* 转换使用统计
*/
_convertUsage(claudeUsage) {
if (!claudeUsage) return undefined;
if (!claudeUsage) {
return undefined
}
return {
prompt_tokens: claudeUsage.input_tokens || 0,
completion_tokens: claudeUsage.output_tokens || 0,
total_tokens: (claudeUsage.input_tokens || 0) + (claudeUsage.output_tokens || 0)
};
}
}
/**
* 转换流式事件
*/
_convertStreamEvent(event, requestModel, sessionId) {
const timestamp = Math.floor(Date.now() / 1000);
const timestamp = Math.floor(Date.now() / 1000)
const baseChunk = {
id: sessionId,
object: 'chat.completion.chunk',
created: timestamp,
model: requestModel || 'gpt-4',
choices: [{
index: 0,
delta: {},
finish_reason: null
}]
};
choices: [
{
index: 0,
delta: {},
finish_reason: null
}
]
}
// 根据事件类型处理
if (event.type === 'message_start') {
// 处理消息开始事件,发送角色信息
baseChunk.choices[0].delta.role = 'assistant';
return baseChunk;
baseChunk.choices[0].delta.role = 'assistant'
return baseChunk
} else if (event.type === 'content_block_start' && event.content_block) {
if (event.content_block.type === 'text') {
baseChunk.choices[0].delta.content = event.content_block.text || '';
baseChunk.choices[0].delta.content = event.content_block.text || ''
} else if (event.content_block.type === 'tool_use') {
// 开始工具调用
baseChunk.choices[0].delta.tool_calls = [{
index: event.index || 0,
id: event.content_block.id,
type: 'function',
function: {
name: event.content_block.name,
arguments: ''
baseChunk.choices[0].delta.tool_calls = [
{
index: event.index || 0,
id: event.content_block.id,
type: 'function',
function: {
name: event.content_block.name,
arguments: ''
}
}
}];
]
}
} else if (event.type === 'content_block_delta' && event.delta) {
if (event.delta.type === 'text_delta') {
baseChunk.choices[0].delta.content = event.delta.text || '';
baseChunk.choices[0].delta.content = event.delta.text || ''
} else if (event.delta.type === 'input_json_delta') {
// 工具调用参数的增量更新
baseChunk.choices[0].delta.tool_calls = [{
index: event.index || 0,
function: {
arguments: event.delta.partial_json || ''
baseChunk.choices[0].delta.tool_calls = [
{
index: event.index || 0,
function: {
arguments: event.delta.partial_json || ''
}
}
}];
]
}
} else if (event.type === 'message_delta' && event.delta) {
if (event.delta.stop_reason) {
baseChunk.choices[0].finish_reason = this._mapStopReason(event.delta.stop_reason);
baseChunk.choices[0].finish_reason = this._mapStopReason(event.delta.stop_reason)
}
if (event.usage) {
baseChunk.usage = this._convertUsage(event.usage);
baseChunk.usage = this._convertUsage(event.usage)
}
} else if (event.type === 'message_stop') {
// message_stop 事件不需要返回 chunk[DONE] 标记会在 convertStreamChunk 中添加
return null;
return null
} else {
// 忽略其他类型的事件
return null;
return null
}
return baseChunk;
return baseChunk
}
/**
* 生成随机 ID
*/
_generateId() {
return Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
}
}
module.exports = new OpenAIToClaudeConverter();
module.exports = new OpenAIToClaudeConverter()

View File

@@ -1,19 +1,25 @@
const fs = require('fs');
const path = require('path');
const https = require('https');
const logger = require('../utils/logger');
const fs = require('fs')
const path = require('path')
const https = require('https')
const logger = require('../utils/logger')
class PricingService {
constructor() {
this.dataDir = path.join(process.cwd(), 'data');
this.pricingFile = path.join(this.dataDir, 'model_pricing.json');
this.pricingUrl = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
this.fallbackFile = path.join(process.cwd(), 'resources', 'model-pricing', 'model_prices_and_context_window.json');
this.pricingData = null;
this.lastUpdated = null;
this.updateInterval = 24 * 60 * 60 * 1000; // 24小时
this.fileWatcher = null; // 文件监听器
this.reloadDebounceTimer = null; // 防抖定时器
this.dataDir = path.join(process.cwd(), 'data')
this.pricingFile = path.join(this.dataDir, 'model_pricing.json')
this.pricingUrl =
'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json'
this.fallbackFile = path.join(
process.cwd(),
'resources',
'model-pricing',
'model_prices_and_context_window.json'
)
this.pricingData = null
this.lastUpdated = null
this.updateInterval = 24 * 60 * 60 * 1000 // 24小时
this.fileWatcher = null // 文件监听器
this.reloadDebounceTimer = null // 防抖定时器
}
// 初始化价格服务
@@ -21,72 +27,74 @@ class PricingService {
try {
// 确保data目录存在
if (!fs.existsSync(this.dataDir)) {
fs.mkdirSync(this.dataDir, { recursive: true });
logger.info('📁 Created data directory');
fs.mkdirSync(this.dataDir, { recursive: true })
logger.info('📁 Created data directory')
}
// 检查是否需要下载或更新价格数据
await this.checkAndUpdatePricing();
await this.checkAndUpdatePricing()
// 设置定时更新
setInterval(() => {
this.checkAndUpdatePricing();
}, this.updateInterval);
this.checkAndUpdatePricing()
}, this.updateInterval)
// 设置文件监听器
this.setupFileWatcher();
this.setupFileWatcher()
logger.success('💰 Pricing service initialized successfully');
logger.success('💰 Pricing service initialized successfully')
} catch (error) {
logger.error('❌ Failed to initialize pricing service:', error);
logger.error('❌ Failed to initialize pricing service:', error)
}
}
// 检查并更新价格数据
async checkAndUpdatePricing() {
try {
const needsUpdate = this.needsUpdate();
const needsUpdate = this.needsUpdate()
if (needsUpdate) {
logger.info('🔄 Updating model pricing data...');
await this.downloadPricingData();
logger.info('🔄 Updating model pricing data...')
await this.downloadPricingData()
} else {
// 如果不需要更新,加载现有数据
await this.loadPricingData();
await this.loadPricingData()
}
} catch (error) {
logger.error('❌ Failed to check/update pricing:', error);
logger.error('❌ Failed to check/update pricing:', error)
// 如果更新失败尝试使用fallback
await this.useFallbackPricing();
await this.useFallbackPricing()
}
}
// 检查是否需要更新
needsUpdate() {
if (!fs.existsSync(this.pricingFile)) {
logger.info('📋 Pricing file not found, will download');
return true;
logger.info('📋 Pricing file not found, will download')
return true
}
const stats = fs.statSync(this.pricingFile);
const fileAge = Date.now() - stats.mtime.getTime();
const stats = fs.statSync(this.pricingFile)
const fileAge = Date.now() - stats.mtime.getTime()
if (fileAge > this.updateInterval) {
logger.info(`📋 Pricing file is ${Math.round(fileAge / (60 * 60 * 1000))} hours old, will update`);
return true;
logger.info(
`📋 Pricing file is ${Math.round(fileAge / (60 * 60 * 1000))} hours old, will update`
)
return true
}
return false;
return false
}
// 下载价格数据
async downloadPricingData() {
try {
await this._downloadFromRemote();
await this._downloadFromRemote()
} catch (downloadError) {
logger.warn(`⚠️ Failed to download pricing data: ${downloadError.message}`);
logger.info('📋 Using local fallback pricing data...');
await this.useFallbackPricing();
logger.warn(`⚠️ Failed to download pricing data: ${downloadError.message}`)
logger.info('📋 Using local fallback pricing data...')
await this.useFallbackPricing()
}
}
@@ -95,67 +103,69 @@ class PricingService {
return new Promise((resolve, reject) => {
const request = https.get(this.pricingUrl, (response) => {
if (response.statusCode !== 200) {
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
return;
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`))
return
}
let data = '';
let data = ''
response.on('data', (chunk) => {
data += chunk;
});
data += chunk
})
response.on('end', () => {
try {
const jsonData = JSON.parse(data);
const jsonData = JSON.parse(data)
// 保存到文件
fs.writeFileSync(this.pricingFile, JSON.stringify(jsonData, null, 2));
fs.writeFileSync(this.pricingFile, JSON.stringify(jsonData, null, 2))
// 更新内存中的数据
this.pricingData = jsonData;
this.lastUpdated = new Date();
logger.success(`💰 Downloaded pricing data for ${Object.keys(jsonData).length} models`);
this.pricingData = jsonData
this.lastUpdated = new Date()
logger.success(`💰 Downloaded pricing data for ${Object.keys(jsonData).length} models`)
// 设置或重新设置文件监听器
this.setupFileWatcher();
resolve();
this.setupFileWatcher()
resolve()
} catch (error) {
reject(new Error(`Failed to parse pricing data: ${error.message}`));
reject(new Error(`Failed to parse pricing data: ${error.message}`))
}
});
});
})
})
request.on('error', (error) => {
reject(new Error(`Network error: ${error.message}`));
});
reject(new Error(`Network error: ${error.message}`))
})
request.setTimeout(30000, () => {
request.destroy();
reject(new Error('Download timeout after 30 seconds'));
});
});
request.destroy()
reject(new Error('Download timeout after 30 seconds'))
})
})
}
// 加载本地价格数据
async loadPricingData() {
try {
if (fs.existsSync(this.pricingFile)) {
const data = fs.readFileSync(this.pricingFile, 'utf8');
this.pricingData = JSON.parse(data);
const stats = fs.statSync(this.pricingFile);
this.lastUpdated = stats.mtime;
logger.info(`💰 Loaded pricing data for ${Object.keys(this.pricingData).length} models from cache`);
const data = fs.readFileSync(this.pricingFile, 'utf8')
this.pricingData = JSON.parse(data)
const stats = fs.statSync(this.pricingFile)
this.lastUpdated = stats.mtime
logger.info(
`💰 Loaded pricing data for ${Object.keys(this.pricingData).length} models from cache`
)
} else {
logger.warn('💰 No pricing data file found, will use fallback');
await this.useFallbackPricing();
logger.warn('💰 No pricing data file found, will use fallback')
await this.useFallbackPricing()
}
} catch (error) {
logger.error('❌ Failed to load pricing data:', error);
await this.useFallbackPricing();
logger.error('❌ Failed to load pricing data:', error)
await this.useFallbackPricing()
}
}
@@ -163,89 +173,95 @@ class PricingService {
async useFallbackPricing() {
try {
if (fs.existsSync(this.fallbackFile)) {
logger.info('📋 Copying fallback pricing data to data directory...');
logger.info('📋 Copying fallback pricing data to data directory...')
// 读取fallback文件
const fallbackData = fs.readFileSync(this.fallbackFile, 'utf8');
const jsonData = JSON.parse(fallbackData);
const fallbackData = fs.readFileSync(this.fallbackFile, 'utf8')
const jsonData = JSON.parse(fallbackData)
// 保存到data目录
fs.writeFileSync(this.pricingFile, JSON.stringify(jsonData, null, 2));
fs.writeFileSync(this.pricingFile, JSON.stringify(jsonData, null, 2))
// 更新内存中的数据
this.pricingData = jsonData;
this.lastUpdated = new Date();
this.pricingData = jsonData
this.lastUpdated = new Date()
// 设置或重新设置文件监听器
this.setupFileWatcher();
logger.warn(`⚠️ Using fallback pricing data for ${Object.keys(jsonData).length} models`);
logger.info('💡 Note: This fallback data may be outdated. The system will try to update from the remote source on next check.');
this.setupFileWatcher()
logger.warn(`⚠️ Using fallback pricing data for ${Object.keys(jsonData).length} models`)
logger.info(
'💡 Note: This fallback data may be outdated. The system will try to update from the remote source on next check.'
)
} else {
logger.error('❌ Fallback pricing file not found at:', this.fallbackFile);
logger.error('❌ Please ensure the resources/model-pricing directory exists with the pricing file');
this.pricingData = {};
logger.error('❌ Fallback pricing file not found at:', this.fallbackFile)
logger.error(
'❌ Please ensure the resources/model-pricing directory exists with the pricing file'
)
this.pricingData = {}
}
} catch (error) {
logger.error('❌ Failed to use fallback pricing data:', error);
this.pricingData = {};
logger.error('❌ Failed to use fallback pricing data:', error)
this.pricingData = {}
}
}
// 获取模型价格信息
getModelPricing(modelName) {
if (!this.pricingData || !modelName) {
return null;
return null
}
// 尝试直接匹配
if (this.pricingData[modelName]) {
return this.pricingData[modelName];
return this.pricingData[modelName]
}
// 对于Bedrock区域前缀模型如 us.anthropic.claude-sonnet-4-20250514-v1:0
// 尝试去掉区域前缀进行匹配
if (modelName.includes('.anthropic.') || modelName.includes('.claude')) {
// 提取不带区域前缀的模型名
const withoutRegion = modelName.replace(/^(us|eu|apac)\./, '');
const withoutRegion = modelName.replace(/^(us|eu|apac)\./, '')
if (this.pricingData[withoutRegion]) {
logger.debug(`💰 Found pricing for ${modelName} by removing region prefix: ${withoutRegion}`);
return this.pricingData[withoutRegion];
logger.debug(
`💰 Found pricing for ${modelName} by removing region prefix: ${withoutRegion}`
)
return this.pricingData[withoutRegion]
}
}
// 尝试模糊匹配(处理版本号等变化)
const normalizedModel = modelName.toLowerCase().replace(/[_-]/g, '');
const normalizedModel = modelName.toLowerCase().replace(/[_-]/g, '')
for (const [key, value] of Object.entries(this.pricingData)) {
const normalizedKey = key.toLowerCase().replace(/[_-]/g, '');
const normalizedKey = key.toLowerCase().replace(/[_-]/g, '')
if (normalizedKey.includes(normalizedModel) || normalizedModel.includes(normalizedKey)) {
logger.debug(`💰 Found pricing for ${modelName} using fuzzy match: ${key}`);
return value;
logger.debug(`💰 Found pricing for ${modelName} using fuzzy match: ${key}`)
return value
}
}
// 对于Bedrock模型尝试更智能的匹配
if (modelName.includes('anthropic.claude')) {
// 提取核心模型名部分(去掉区域和前缀)
const coreModel = modelName.replace(/^(us|eu|apac)\./, '').replace('anthropic.', '');
const coreModel = modelName.replace(/^(us|eu|apac)\./, '').replace('anthropic.', '')
for (const [key, value] of Object.entries(this.pricingData)) {
if (key.includes(coreModel) || key.replace('anthropic.', '').includes(coreModel)) {
logger.debug(`💰 Found pricing for ${modelName} using Bedrock core model match: ${key}`);
return value;
logger.debug(`💰 Found pricing for ${modelName} using Bedrock core model match: ${key}`)
return value
}
}
}
logger.debug(`💰 No pricing found for model: ${modelName}`);
return null;
logger.debug(`💰 No pricing found for model: ${modelName}`)
return null
}
// 计算使用费用
calculateCost(usage, modelName) {
const pricing = this.getModelPricing(modelName);
const pricing = this.getModelPricing(modelName)
if (!pricing) {
return {
inputCost: 0,
@@ -254,13 +270,15 @@ class PricingService {
cacheReadCost: 0,
totalCost: 0,
hasPricing: false
};
}
}
const inputCost = (usage.input_tokens || 0) * (pricing.input_cost_per_token || 0);
const outputCost = (usage.output_tokens || 0) * (pricing.output_cost_per_token || 0);
const cacheCreateCost = (usage.cache_creation_input_tokens || 0) * (pricing.cache_creation_input_token_cost || 0);
const cacheReadCost = (usage.cache_read_input_tokens || 0) * (pricing.cache_read_input_token_cost || 0);
const inputCost = (usage.input_tokens || 0) * (pricing.input_cost_per_token || 0)
const outputCost = (usage.output_tokens || 0) * (pricing.output_cost_per_token || 0)
const cacheCreateCost =
(usage.cache_creation_input_tokens || 0) * (pricing.cache_creation_input_token_cost || 0)
const cacheReadCost =
(usage.cache_read_input_tokens || 0) * (pricing.cache_read_input_token_cost || 0)
return {
inputCost,
@@ -275,16 +293,24 @@ class PricingService {
cacheCreate: pricing.cache_creation_input_token_cost || 0,
cacheRead: pricing.cache_read_input_token_cost || 0
}
};
}
}
// 格式化价格显示
formatCost(cost) {
if (cost === 0) return '$0.000000';
if (cost < 0.000001) return `$${cost.toExponential(2)}`;
if (cost < 0.01) return `$${cost.toFixed(6)}`;
if (cost < 1) return `$${cost.toFixed(4)}`;
return `$${cost.toFixed(2)}`;
if (cost === 0) {
return '$0.000000'
}
if (cost < 0.000001) {
return `$${cost.toExponential(2)}`
}
if (cost < 0.01) {
return `$${cost.toFixed(6)}`
}
if (cost < 1) {
return `$${cost.toFixed(4)}`
}
return `$${cost.toFixed(2)}`
}
// 获取服务状态
@@ -293,23 +319,25 @@ class PricingService {
initialized: this.pricingData !== null,
lastUpdated: this.lastUpdated,
modelCount: this.pricingData ? Object.keys(this.pricingData).length : 0,
nextUpdate: this.lastUpdated ? new Date(this.lastUpdated.getTime() + this.updateInterval) : null
};
nextUpdate: this.lastUpdated
? new Date(this.lastUpdated.getTime() + this.updateInterval)
: null
}
}
// 强制更新价格数据
async forceUpdate() {
try {
await this._downloadFromRemote();
return { success: true, message: 'Pricing data updated successfully' };
await this._downloadFromRemote()
return { success: true, message: 'Pricing data updated successfully' }
} catch (error) {
logger.error('❌ Force update failed:', error);
logger.info('📋 Force update failed, using fallback pricing data...');
await this.useFallbackPricing();
return {
success: false,
message: `Download failed: ${error.message}. Using fallback pricing data instead.`
};
logger.error('❌ Force update failed:', error)
logger.info('📋 Force update failed, using fallback pricing data...')
await this.useFallbackPricing()
return {
success: false,
message: `Download failed: ${error.message}. Using fallback pricing data instead.`
}
}
}
@@ -318,43 +346,45 @@ class PricingService {
try {
// 如果已有监听器,先关闭
if (this.fileWatcher) {
this.fileWatcher.close();
this.fileWatcher = null;
this.fileWatcher.close()
this.fileWatcher = null
}
// 只有文件存在时才设置监听器
if (!fs.existsSync(this.pricingFile)) {
logger.debug('💰 Pricing file does not exist yet, skipping file watcher setup');
return;
logger.debug('💰 Pricing file does not exist yet, skipping file watcher setup')
return
}
// 使用 fs.watchFile 作为更可靠的文件监听方式
// 它使用轮询,虽然性能稍差,但更可靠
const watchOptions = {
persistent: true,
const watchOptions = {
persistent: true,
interval: 60000 // 每60秒检查一次
};
}
// 记录初始的修改时间
let lastMtime = fs.statSync(this.pricingFile).mtimeMs;
let lastMtime = fs.statSync(this.pricingFile).mtimeMs
fs.watchFile(this.pricingFile, watchOptions, (curr, _prev) => {
// 检查文件是否真的被修改了(不仅仅是访问)
if (curr.mtimeMs !== lastMtime) {
lastMtime = curr.mtimeMs;
logger.debug(`💰 Detected change in pricing file (mtime: ${new Date(curr.mtime).toISOString()})`);
this.handleFileChange();
lastMtime = curr.mtimeMs
logger.debug(
`💰 Detected change in pricing file (mtime: ${new Date(curr.mtime).toISOString()})`
)
this.handleFileChange()
}
});
})
// 保存引用以便清理
this.fileWatcher = {
close: () => fs.unwatchFile(this.pricingFile)
};
}
logger.info('👁️ File watcher set up for model_pricing.json (polling every 60s)');
logger.info('👁️ File watcher set up for model_pricing.json (polling every 60s)')
} catch (error) {
logger.error('❌ Failed to setup file watcher:', error);
logger.error('❌ Failed to setup file watcher:', error)
}
}
@@ -362,14 +392,14 @@ class PricingService {
handleFileChange() {
// 清除之前的定时器
if (this.reloadDebounceTimer) {
clearTimeout(this.reloadDebounceTimer);
clearTimeout(this.reloadDebounceTimer)
}
// 设置新的定时器防抖500ms
this.reloadDebounceTimer = setTimeout(async () => {
logger.info('🔄 Reloading pricing data due to file change...');
await this.reloadPricingData();
}, 500);
logger.info('🔄 Reloading pricing data due to file change...')
await this.reloadPricingData()
}, 500)
}
// 重新加载价格数据
@@ -377,55 +407,57 @@ class PricingService {
try {
// 验证文件是否存在
if (!fs.existsSync(this.pricingFile)) {
logger.warn('💰 Pricing file was deleted, using fallback');
await this.useFallbackPricing();
logger.warn('💰 Pricing file was deleted, using fallback')
await this.useFallbackPricing()
// 重新设置文件监听器fallback会创建新文件
this.setupFileWatcher();
return;
this.setupFileWatcher()
return
}
// 读取文件内容
const data = fs.readFileSync(this.pricingFile, 'utf8');
const data = fs.readFileSync(this.pricingFile, 'utf8')
// 尝试解析JSON
const jsonData = JSON.parse(data);
const jsonData = JSON.parse(data)
// 验证数据结构
if (typeof jsonData !== 'object' || Object.keys(jsonData).length === 0) {
throw new Error('Invalid pricing data structure');
throw new Error('Invalid pricing data structure')
}
// 更新内存中的数据
this.pricingData = jsonData;
this.lastUpdated = new Date();
const modelCount = Object.keys(jsonData).length;
logger.success(`💰 Reloaded pricing data for ${modelCount} models from file`);
this.pricingData = jsonData
this.lastUpdated = new Date()
const modelCount = Object.keys(jsonData).length
logger.success(`💰 Reloaded pricing data for ${modelCount} models from file`)
// 显示一些统计信息
const claudeModels = Object.keys(jsonData).filter(k => k.includes('claude')).length;
const gptModels = Object.keys(jsonData).filter(k => k.includes('gpt')).length;
const geminiModels = Object.keys(jsonData).filter(k => k.includes('gemini')).length;
logger.debug(`💰 Model breakdown: Claude=${claudeModels}, GPT=${gptModels}, Gemini=${geminiModels}`);
const claudeModels = Object.keys(jsonData).filter((k) => k.includes('claude')).length
const gptModels = Object.keys(jsonData).filter((k) => k.includes('gpt')).length
const geminiModels = Object.keys(jsonData).filter((k) => k.includes('gemini')).length
logger.debug(
`💰 Model breakdown: Claude=${claudeModels}, GPT=${gptModels}, Gemini=${geminiModels}`
)
} catch (error) {
logger.error('❌ Failed to reload pricing data:', error);
logger.warn('💰 Keeping existing pricing data in memory');
logger.error('❌ Failed to reload pricing data:', error)
logger.warn('💰 Keeping existing pricing data in memory')
}
}
// 清理资源
cleanup() {
if (this.fileWatcher) {
this.fileWatcher.close();
this.fileWatcher = null;
logger.debug('💰 File watcher closed');
this.fileWatcher.close()
this.fileWatcher = null
logger.debug('💰 File watcher closed')
}
if (this.reloadDebounceTimer) {
clearTimeout(this.reloadDebounceTimer);
this.reloadDebounceTimer = null;
clearTimeout(this.reloadDebounceTimer)
this.reloadDebounceTimer = null
}
}
}
module.exports = new PricingService();
module.exports = new PricingService()

View File

@@ -1,6 +1,6 @@
const redis = require('../models/redis');
const logger = require('../utils/logger');
const { v4: uuidv4 } = require('uuid');
const redis = require('../models/redis')
const logger = require('../utils/logger')
const { v4: uuidv4 } = require('uuid')
/**
* Token 刷新锁服务
@@ -8,30 +8,29 @@ const { v4: uuidv4 } = require('uuid');
*/
class TokenRefreshService {
constructor() {
this.lockTTL = 60; // 锁的TTL: 60秒token刷新通常在30秒内完成
this.lockValue = new Map(); // 存储每个锁的唯一值
this.lockTTL = 60 // 锁的TTL: 60秒token刷新通常在30秒内完成
this.lockValue = new Map() // 存储每个锁的唯一值
}
/**
* 获取分布式锁
* 使用唯一标识符作为值,避免误释放其他进程的锁
*/
async acquireLock(lockKey) {
try {
const client = redis.getClientSafe();
const lockId = uuidv4();
const result = await client.set(lockKey, lockId, 'NX', 'EX', this.lockTTL);
const client = redis.getClientSafe()
const lockId = uuidv4()
const result = await client.set(lockKey, lockId, 'NX', 'EX', this.lockTTL)
if (result === 'OK') {
this.lockValue.set(lockKey, lockId);
logger.debug(`🔒 Acquired lock ${lockKey} with ID ${lockId}, TTL: ${this.lockTTL}s`);
return true;
this.lockValue.set(lockKey, lockId)
logger.debug(`🔒 Acquired lock ${lockKey} with ID ${lockId}, TTL: ${this.lockTTL}s`)
return true
}
return false;
return false
} catch (error) {
logger.error(`Failed to acquire lock ${lockKey}:`, error);
return false;
logger.error(`Failed to acquire lock ${lockKey}:`, error)
return false
}
}
@@ -41,12 +40,12 @@ class TokenRefreshService {
*/
async releaseLock(lockKey) {
try {
const client = redis.getClientSafe();
const lockId = this.lockValue.get(lockKey);
const client = redis.getClientSafe()
const lockId = this.lockValue.get(lockKey)
if (!lockId) {
logger.warn(`⚠️ No lock ID found for ${lockKey}, skipping release`);
return;
logger.warn(`⚠️ No lock ID found for ${lockKey}, skipping release`)
return
}
// Lua 脚本:只有当值匹配时才删除
@@ -56,18 +55,18 @@ class TokenRefreshService {
else
return 0
end
`;
const result = await client.eval(luaScript, 1, lockKey, lockId);
`
const result = await client.eval(luaScript, 1, lockKey, lockId)
if (result === 1) {
this.lockValue.delete(lockKey);
logger.debug(`🔓 Released lock ${lockKey} with ID ${lockId}`);
this.lockValue.delete(lockKey)
logger.debug(`🔓 Released lock ${lockKey} with ID ${lockId}`)
} else {
logger.warn(`⚠️ Lock ${lockKey} was not released - value mismatch or already expired`);
logger.warn(`⚠️ Lock ${lockKey} was not released - value mismatch or already expired`)
}
} catch (error) {
logger.error(`Failed to release lock ${lockKey}:`, error);
logger.error(`Failed to release lock ${lockKey}:`, error)
}
}
@@ -78,8 +77,8 @@ class TokenRefreshService {
* @returns {Promise<boolean>} 是否成功获取锁
*/
async acquireRefreshLock(accountId, platform = 'claude') {
const lockKey = `token_refresh_lock:${platform}:${accountId}`;
return await this.acquireLock(lockKey);
const lockKey = `token_refresh_lock:${platform}:${accountId}`
return await this.acquireLock(lockKey)
}
/**
@@ -88,8 +87,8 @@ class TokenRefreshService {
* @param {string} platform - 平台类型 (claude/gemini)
*/
async releaseRefreshLock(accountId, platform = 'claude') {
const lockKey = `token_refresh_lock:${platform}:${accountId}`;
await this.releaseLock(lockKey);
const lockKey = `token_refresh_lock:${platform}:${accountId}`
await this.releaseLock(lockKey)
}
/**
@@ -99,14 +98,14 @@ class TokenRefreshService {
* @returns {Promise<boolean>} 锁是否存在
*/
async isRefreshLocked(accountId, platform = 'claude') {
const lockKey = `token_refresh_lock:${platform}:${accountId}`;
const lockKey = `token_refresh_lock:${platform}:${accountId}`
try {
const client = redis.getClientSafe();
const exists = await client.exists(lockKey);
return exists === 1;
const client = redis.getClientSafe()
const exists = await client.exists(lockKey)
return exists === 1
} catch (error) {
logger.error(`Failed to check lock status ${lockKey}:`, error);
return false;
logger.error(`Failed to check lock status ${lockKey}:`, error)
return false
}
}
@@ -117,14 +116,14 @@ class TokenRefreshService {
* @returns {Promise<number>} 剩余秒数,-1表示锁不存在
*/
async getLockTTL(accountId, platform = 'claude') {
const lockKey = `token_refresh_lock:${platform}:${accountId}`;
const lockKey = `token_refresh_lock:${platform}:${accountId}`
try {
const client = redis.getClientSafe();
const ttl = await client.ttl(lockKey);
return ttl;
const client = redis.getClientSafe()
const ttl = await client.ttl(lockKey)
return ttl
} catch (error) {
logger.error(`Failed to get lock TTL ${lockKey}:`, error);
return -1;
logger.error(`Failed to get lock TTL ${lockKey}:`, error)
return -1
}
}
@@ -133,12 +132,12 @@ class TokenRefreshService {
* 在进程退出时调用,避免内存泄漏
*/
cleanup() {
this.lockValue.clear();
logger.info('🧹 Cleaned up local lock records');
this.lockValue.clear()
logger.info('🧹 Cleaned up local lock records')
}
}
// 创建单例实例
const tokenRefreshService = new TokenRefreshService();
const tokenRefreshService = new TokenRefreshService()
module.exports = tokenRefreshService;
module.exports = tokenRefreshService

View File

@@ -1,23 +1,23 @@
const claudeAccountService = require('./claudeAccountService');
const claudeConsoleAccountService = require('./claudeConsoleAccountService');
const bedrockAccountService = require('./bedrockAccountService');
const accountGroupService = require('./accountGroupService');
const redis = require('../models/redis');
const logger = require('../utils/logger');
const claudeAccountService = require('./claudeAccountService')
const claudeConsoleAccountService = require('./claudeConsoleAccountService')
const bedrockAccountService = require('./bedrockAccountService')
const accountGroupService = require('./accountGroupService')
const redis = require('../models/redis')
const logger = require('../utils/logger')
class UnifiedClaudeScheduler {
constructor() {
this.SESSION_MAPPING_PREFIX = 'unified_claude_session_mapping:';
this.SESSION_MAPPING_PREFIX = 'unified_claude_session_mapping:'
}
// 🔧 辅助方法:检查账户是否可调度(兼容字符串和布尔值)
_isSchedulable(schedulable) {
// 如果是 undefined 或 null默认为可调度
if (schedulable === undefined || schedulable === null) {
return true;
return true
}
// 明确设置为 false布尔值或 'false'(字符串)时不可调度
return schedulable !== false && schedulable !== 'false';
return schedulable !== false && schedulable !== 'false'
}
// 🎯 统一调度Claude账号官方和Console
@@ -27,177 +27,248 @@ class UnifiedClaudeScheduler {
if (apiKeyData.claudeAccountId) {
// 检查是否是分组
if (apiKeyData.claudeAccountId.startsWith('group:')) {
const groupId = apiKeyData.claudeAccountId.replace('group:', '');
logger.info(`🎯 API key ${apiKeyData.name} is bound to group ${groupId}, selecting from group`);
return await this.selectAccountFromGroup(groupId, sessionHash, requestedModel);
const groupId = apiKeyData.claudeAccountId.replace('group:', '')
logger.info(
`🎯 API key ${apiKeyData.name} is bound to group ${groupId}, selecting from group`
)
return await this.selectAccountFromGroup(groupId, sessionHash, requestedModel)
}
// 普通专属账户
const boundAccount = await redis.getClaudeAccount(apiKeyData.claudeAccountId);
const boundAccount = await redis.getClaudeAccount(apiKeyData.claudeAccountId)
if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') {
logger.info(`🎯 Using bound dedicated Claude OAuth account: ${boundAccount.name} (${apiKeyData.claudeAccountId}) for API key ${apiKeyData.name}`);
logger.info(
`🎯 Using bound dedicated Claude OAuth account: ${boundAccount.name} (${apiKeyData.claudeAccountId}) for API key ${apiKeyData.name}`
)
return {
accountId: apiKeyData.claudeAccountId,
accountType: 'claude-official'
};
}
} else {
logger.warn(`⚠️ Bound Claude OAuth account ${apiKeyData.claudeAccountId} is not available, falling back to pool`);
logger.warn(
`⚠️ Bound Claude OAuth account ${apiKeyData.claudeAccountId} is not available, falling back to pool`
)
}
}
// 2. 检查Claude Console账户绑定
if (apiKeyData.claudeConsoleAccountId) {
const boundConsoleAccount = await claudeConsoleAccountService.getAccount(apiKeyData.claudeConsoleAccountId);
if (boundConsoleAccount && boundConsoleAccount.isActive === true && boundConsoleAccount.status === 'active') {
logger.info(`🎯 Using bound dedicated Claude Console account: ${boundConsoleAccount.name} (${apiKeyData.claudeConsoleAccountId}) for API key ${apiKeyData.name}`);
const boundConsoleAccount = await claudeConsoleAccountService.getAccount(
apiKeyData.claudeConsoleAccountId
)
if (
boundConsoleAccount &&
boundConsoleAccount.isActive === true &&
boundConsoleAccount.status === 'active'
) {
logger.info(
`🎯 Using bound dedicated Claude Console account: ${boundConsoleAccount.name} (${apiKeyData.claudeConsoleAccountId}) for API key ${apiKeyData.name}`
)
return {
accountId: apiKeyData.claudeConsoleAccountId,
accountType: 'claude-console'
};
}
} else {
logger.warn(`⚠️ Bound Claude Console account ${apiKeyData.claudeConsoleAccountId} is not available, falling back to pool`);
logger.warn(
`⚠️ Bound Claude Console account ${apiKeyData.claudeConsoleAccountId} is not available, falling back to pool`
)
}
}
// 3. 检查Bedrock账户绑定
if (apiKeyData.bedrockAccountId) {
const boundBedrockAccountResult = await bedrockAccountService.getAccount(apiKeyData.bedrockAccountId);
const boundBedrockAccountResult = await bedrockAccountService.getAccount(
apiKeyData.bedrockAccountId
)
if (boundBedrockAccountResult.success && boundBedrockAccountResult.data.isActive === true) {
logger.info(`🎯 Using bound dedicated Bedrock account: ${boundBedrockAccountResult.data.name} (${apiKeyData.bedrockAccountId}) for API key ${apiKeyData.name}`);
logger.info(
`🎯 Using bound dedicated Bedrock account: ${boundBedrockAccountResult.data.name} (${apiKeyData.bedrockAccountId}) for API key ${apiKeyData.name}`
)
return {
accountId: apiKeyData.bedrockAccountId,
accountType: 'bedrock'
};
}
} else {
logger.warn(`⚠️ Bound Bedrock account ${apiKeyData.bedrockAccountId} is not available, falling back to pool`);
logger.warn(
`⚠️ Bound Bedrock account ${apiKeyData.bedrockAccountId} is not available, falling back to pool`
)
}
}
// 如果有会话哈希,检查是否有已映射的账户
if (sessionHash) {
const mappedAccount = await this._getSessionMapping(sessionHash);
const mappedAccount = await this._getSessionMapping(sessionHash)
if (mappedAccount) {
// 验证映射的账户是否仍然可用
const isAvailable = await this._isAccountAvailable(mappedAccount.accountId, mappedAccount.accountType);
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;
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);
logger.warn(
`⚠️ Mapped account ${mappedAccount.accountId} is no longer available, selecting new account`
)
await this._deleteSessionMapping(sessionHash)
}
}
}
// 获取所有可用账户(传递请求的模型进行过滤)
const availableAccounts = await this._getAllAvailableAccounts(apiKeyData, requestedModel);
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}`);
throw new Error(
`No available Claude accounts support the requested model: ${requestedModel}`
)
} else {
throw new Error('No available Claude accounts (neither official nor console)');
throw new Error('No available Claude accounts (neither official nor console)')
}
}
// 按优先级和最后使用时间排序
const sortedAccounts = this._sortAccountsByPriority(availableAccounts);
const sortedAccounts = this._sortAccountsByPriority(availableAccounts)
// 选择第一个账户
const selectedAccount = sortedAccounts[0];
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}`);
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}`);
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;
logger.error('❌ Failed to select account for API key:', error)
throw error
}
}
// 📋 获取所有可用账户合并官方和Console
async _getAllAvailableAccounts(apiKeyData, requestedModel = null) {
const availableAccounts = [];
const availableAccounts = []
// 如果API Key绑定了专属账户优先返回
// 1. 检查Claude OAuth账户绑定
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);
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 OAuth account: ${boundAccount.name} (${apiKeyData.claudeAccountId})`);
return [{
...boundAccount,
accountId: boundAccount.id,
accountType: 'claude-official',
priority: parseInt(boundAccount.priority) || 50,
lastUsedAt: boundAccount.lastUsedAt || '0'
}];
logger.info(
`🎯 Using bound dedicated Claude OAuth 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 OAuth account ${apiKeyData.claudeAccountId} is not available`);
logger.warn(`⚠️ Bound Claude OAuth account ${apiKeyData.claudeAccountId} is not available`)
}
}
// 2. 检查Claude Console账户绑定
if (apiKeyData.claudeConsoleAccountId) {
const boundConsoleAccount = await claudeConsoleAccountService.getAccount(apiKeyData.claudeConsoleAccountId);
if (boundConsoleAccount && boundConsoleAccount.isActive === true && boundConsoleAccount.status === 'active') {
const isRateLimited = await claudeConsoleAccountService.isAccountRateLimited(boundConsoleAccount.id);
const boundConsoleAccount = await claudeConsoleAccountService.getAccount(
apiKeyData.claudeConsoleAccountId
)
if (
boundConsoleAccount &&
boundConsoleAccount.isActive === true &&
boundConsoleAccount.status === 'active'
) {
const isRateLimited = await claudeConsoleAccountService.isAccountRateLimited(
boundConsoleAccount.id
)
if (!isRateLimited) {
logger.info(`🎯 Using bound dedicated Claude Console account: ${boundConsoleAccount.name} (${apiKeyData.claudeConsoleAccountId})`);
return [{
...boundConsoleAccount,
accountId: boundConsoleAccount.id,
accountType: 'claude-console',
priority: parseInt(boundConsoleAccount.priority) || 50,
lastUsedAt: boundConsoleAccount.lastUsedAt || '0'
}];
logger.info(
`🎯 Using bound dedicated Claude Console account: ${boundConsoleAccount.name} (${apiKeyData.claudeConsoleAccountId})`
)
return [
{
...boundConsoleAccount,
accountId: boundConsoleAccount.id,
accountType: 'claude-console',
priority: parseInt(boundConsoleAccount.priority) || 50,
lastUsedAt: boundConsoleAccount.lastUsedAt || '0'
}
]
}
} else {
logger.warn(`⚠️ Bound Claude Console account ${apiKeyData.claudeConsoleAccountId} is not available`);
logger.warn(
`⚠️ Bound Claude Console account ${apiKeyData.claudeConsoleAccountId} is not available`
)
}
}
// 3. 检查Bedrock账户绑定
if (apiKeyData.bedrockAccountId) {
const boundBedrockAccountResult = await bedrockAccountService.getAccount(apiKeyData.bedrockAccountId);
const boundBedrockAccountResult = await bedrockAccountService.getAccount(
apiKeyData.bedrockAccountId
)
if (boundBedrockAccountResult.success && boundBedrockAccountResult.data.isActive === true) {
logger.info(`🎯 Using bound dedicated Bedrock account: ${boundBedrockAccountResult.data.name} (${apiKeyData.bedrockAccountId})`);
return [{
...boundBedrockAccountResult.data,
accountId: boundBedrockAccountResult.data.id,
accountType: 'bedrock',
priority: parseInt(boundBedrockAccountResult.data.priority) || 50,
lastUsedAt: boundBedrockAccountResult.data.lastUsedAt || '0'
}];
logger.info(
`🎯 Using bound dedicated Bedrock account: ${boundBedrockAccountResult.data.name} (${apiKeyData.bedrockAccountId})`
)
return [
{
...boundBedrockAccountResult.data,
accountId: boundBedrockAccountResult.data.id,
accountType: 'bedrock',
priority: parseInt(boundBedrockAccountResult.data.priority) || 50,
lastUsedAt: boundBedrockAccountResult.data.lastUsedAt || '0'
}
]
} else {
logger.warn(`⚠️ Bound Bedrock account ${apiKeyData.bedrockAccountId} is not available`);
logger.warn(`⚠️ Bound Bedrock account ${apiKeyData.bedrockAccountId} is not available`)
}
}
// 获取官方Claude账户共享池
const claudeAccounts = await redis.getAllClaudeAccounts();
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) && // 兼容旧数据
this._isSchedulable(account.schedulable)) { // 检查是否可调度
if (
account.isActive === 'true' &&
account.status !== 'error' &&
account.status !== 'blocked' &&
(account.accountType === 'shared' || !account.accountType) && // 兼容旧数据
this._isSchedulable(account.schedulable)
) {
// 检查是否可调度
// 检查是否被限流
const isRateLimited = await claudeAccountService.isAccountRateLimited(account.id);
const isRateLimited = await claudeAccountService.isAccountRateLimited(account.id)
if (!isRateLimited) {
availableAccounts.push({
...account,
@@ -205,44 +276,59 @@ class UnifiedClaudeScheduler {
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`);
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}`);
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' &&
this._isSchedulable(account.schedulable)) { // 检查是否可调度
if (
account.isActive === true &&
account.status === 'active' &&
account.accountType === 'shared' &&
this._isSchedulable(account.schedulable)
) {
// 检查是否可调度
// 检查模型支持(如果有请求的模型)
if (requestedModel && account.supportedModels) {
// 兼容旧格式(数组)和新格式(对象)
if (Array.isArray(account.supportedModels)) {
// 旧格式:数组
if (account.supportedModels.length > 0 && !account.supportedModels.includes(requestedModel)) {
logger.info(`🚫 Claude Console account ${account.name} does not support model ${requestedModel}`);
continue;
if (
account.supportedModels.length > 0 &&
!account.supportedModels.includes(requestedModel)
) {
logger.info(
`🚫 Claude Console account ${account.name} does not support model ${requestedModel}`
)
continue
}
} else if (typeof account.supportedModels === 'object') {
// 新格式:映射表
if (Object.keys(account.supportedModels).length > 0 && !claudeConsoleAccountService.isModelSupported(account.supportedModels, requestedModel)) {
logger.info(`🚫 Claude Console account ${account.name} does not support model ${requestedModel}`);
continue;
if (
Object.keys(account.supportedModels).length > 0 &&
!claudeConsoleAccountService.isModelSupported(account.supportedModels, requestedModel)
) {
logger.info(
`🚫 Claude Console account ${account.name} does not support model ${requestedModel}`
)
continue
}
}
}
// 检查是否被限流
const isRateLimited = await claudeConsoleAccountService.isAccountRateLimited(account.id);
const isRateLimited = await claudeConsoleAccountService.isAccountRateLimited(account.id)
if (!isRateLimited) {
availableAccounts.push({
...account,
@@ -250,45 +336,60 @@ class UnifiedClaudeScheduler {
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})`);
})
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`);
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(
`❌ Claude Console account ${account.name} not eligible - isActive: ${account.isActive}, status: ${account.status}, accountType: ${account.accountType}, schedulable: ${account.schedulable}`
)
}
}
// 获取Bedrock账户共享池
const bedrockAccountsResult = await bedrockAccountService.getAllAccounts();
const bedrockAccountsResult = await bedrockAccountService.getAllAccounts()
if (bedrockAccountsResult.success) {
const bedrockAccounts = bedrockAccountsResult.data;
logger.info(`📋 Found ${bedrockAccounts.length} total Bedrock accounts`);
const bedrockAccounts = bedrockAccountsResult.data
logger.info(`📋 Found ${bedrockAccounts.length} total Bedrock accounts`)
for (const account of bedrockAccounts) {
logger.info(`🔍 Checking Bedrock account: ${account.name} - isActive: ${account.isActive}, accountType: ${account.accountType}, schedulable: ${account.schedulable}`);
if (account.isActive === true &&
account.accountType === 'shared' &&
this._isSchedulable(account.schedulable)) { // 检查是否可调度
logger.info(
`🔍 Checking Bedrock account: ${account.name} - isActive: ${account.isActive}, accountType: ${account.accountType}, schedulable: ${account.schedulable}`
)
if (
account.isActive === true &&
account.accountType === 'shared' &&
this._isSchedulable(account.schedulable)
) {
// 检查是否可调度
availableAccounts.push({
...account,
accountId: account.id,
accountType: 'bedrock',
priority: parseInt(account.priority) || 50,
lastUsedAt: account.lastUsedAt || '0'
});
logger.info(`✅ Added Bedrock account to available pool: ${account.name} (priority: ${account.priority})`);
})
logger.info(
`✅ Added Bedrock account to available pool: ${account.name} (priority: ${account.priority})`
)
} else {
logger.info(`❌ Bedrock account ${account.name} not eligible - isActive: ${account.isActive}, accountType: ${account.accountType}, schedulable: ${account.schedulable}`);
logger.info(
`❌ Bedrock account ${account.name} not eligible - isActive: ${account.isActive}, 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})`);
return availableAccounts;
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})`
)
return availableAccounts
}
// 🔢 按优先级和最后使用时间排序账户
@@ -296,115 +397,123 @@ class UnifiedClaudeScheduler {
return accounts.sort((a, b) => {
// 首先按优先级排序(数字越小优先级越高)
if (a.priority !== b.priority) {
return 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;
});
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);
const account = await redis.getClaudeAccount(accountId)
if (!account || account.isActive !== 'true' || account.status === 'error') {
return false;
return false
}
// 检查是否可调度
if (!this._isSchedulable(account.schedulable)) {
logger.info(`🚫 Account ${accountId} is not schedulable`);
return false;
logger.info(`🚫 Account ${accountId} is not schedulable`)
return false
}
return !(await claudeAccountService.isAccountRateLimited(accountId));
return !(await claudeAccountService.isAccountRateLimited(accountId))
} else if (accountType === 'claude-console') {
const account = await claudeConsoleAccountService.getAccount(accountId);
const account = await claudeConsoleAccountService.getAccount(accountId)
if (!account || !account.isActive || account.status !== 'active') {
return false;
return false
}
// 检查是否可调度
if (!this._isSchedulable(account.schedulable)) {
logger.info(`🚫 Claude Console account ${accountId} is not schedulable`);
return false;
logger.info(`🚫 Claude Console account ${accountId} is not schedulable`)
return false
}
return !(await claudeConsoleAccountService.isAccountRateLimited(accountId));
return !(await claudeConsoleAccountService.isAccountRateLimited(accountId))
} else if (accountType === 'bedrock') {
const accountResult = await bedrockAccountService.getAccount(accountId);
const accountResult = await bedrockAccountService.getAccount(accountId)
if (!accountResult.success || !accountResult.data.isActive) {
return false;
return false
}
// 检查是否可调度
if (!this._isSchedulable(accountResult.data.schedulable)) {
logger.info(`🚫 Bedrock account ${accountId} is not schedulable`);
return false;
logger.info(`🚫 Bedrock account ${accountId} is not schedulable`)
return false
}
// Bedrock账户暂不需要限流检查因为AWS管理限流
return true;
return true
}
return false;
return false
} catch (error) {
logger.warn(`⚠️ Failed to check account availability: ${accountId}`, error);
return false;
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}`);
const client = redis.getClientSafe()
const mappingData = await client.get(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`)
if (mappingData) {
try {
return JSON.parse(mappingData);
return JSON.parse(mappingData)
} catch (error) {
logger.warn('⚠️ Failed to parse session mapping:', error);
return null;
logger.warn('⚠️ Failed to parse session mapping:', error)
return null
}
}
return null;
return null
}
// 💾 设置会话映射
async _setSessionMapping(sessionHash, accountId, accountType) {
const client = redis.getClientSafe();
const mappingData = JSON.stringify({ accountId, accountType });
const client = redis.getClientSafe()
const mappingData = JSON.stringify({ accountId, accountType })
// 设置1小时过期
await client.setex(
`${this.SESSION_MAPPING_PREFIX}${sessionHash}`,
3600,
mappingData
);
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}`);
const client = redis.getClientSafe()
await client.del(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`)
}
// 🚫 标记账户为限流状态
async markAccountRateLimited(accountId, accountType, sessionHash = null, rateLimitResetTimestamp = null) {
async markAccountRateLimited(
accountId,
accountType,
sessionHash = null,
rateLimitResetTimestamp = null
) {
try {
if (accountType === 'claude-official') {
await claudeAccountService.markAccountRateLimited(accountId, sessionHash, rateLimitResetTimestamp);
await claudeAccountService.markAccountRateLimited(
accountId,
sessionHash,
rateLimitResetTimestamp
)
} else if (accountType === 'claude-console') {
await claudeConsoleAccountService.markAccountRateLimited(accountId);
await claudeConsoleAccountService.markAccountRateLimited(accountId)
}
// 删除会话映射
if (sessionHash) {
await this._deleteSessionMapping(sessionHash);
await this._deleteSessionMapping(sessionHash)
}
return { success: true };
return { success: true }
} catch (error) {
logger.error(`❌ Failed to mark account as rate limited: ${accountId} (${accountType})`, error);
throw error;
logger.error(
`❌ Failed to mark account as rate limited: ${accountId} (${accountType})`,
error
)
throw error
}
}
@@ -412,15 +521,18 @@ class UnifiedClaudeScheduler {
async removeAccountRateLimit(accountId, accountType) {
try {
if (accountType === 'claude-official') {
await claudeAccountService.removeAccountRateLimit(accountId);
await claudeAccountService.removeAccountRateLimit(accountId)
} else if (accountType === 'claude-console') {
await claudeConsoleAccountService.removeAccountRateLimit(accountId);
await claudeConsoleAccountService.removeAccountRateLimit(accountId)
}
return { success: true };
return { success: true }
} catch (error) {
logger.error(`❌ Failed to remove rate limit for account: ${accountId} (${accountType})`, error);
throw error;
logger.error(
`❌ Failed to remove rate limit for account: ${accountId} (${accountType})`,
error
)
throw error
}
}
@@ -428,25 +540,25 @@ class UnifiedClaudeScheduler {
async isAccountRateLimited(accountId, accountType) {
try {
if (accountType === 'claude-official') {
return await claudeAccountService.isAccountRateLimited(accountId);
return await claudeAccountService.isAccountRateLimited(accountId)
} else if (accountType === 'claude-console') {
return await claudeConsoleAccountService.isAccountRateLimited(accountId);
return await claudeConsoleAccountService.isAccountRateLimited(accountId)
}
return false;
return false
} catch (error) {
logger.error(`❌ Failed to check rate limit status: ${accountId} (${accountType})`, error);
return false;
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 };
await claudeConsoleAccountService.blockAccount(accountId, reason)
return { success: true }
} catch (error) {
logger.error(`❌ Failed to block console account: ${accountId}`, error);
throw error;
logger.error(`❌ Failed to block console account: ${accountId}`, error)
throw error
}
}
@@ -454,127 +566,149 @@ class UnifiedClaudeScheduler {
async selectAccountFromGroup(groupId, sessionHash = null, requestedModel = null) {
try {
// 获取分组信息
const group = await accountGroupService.getGroup(groupId);
const group = await accountGroupService.getGroup(groupId)
if (!group) {
throw new Error(`Group ${groupId} not found`);
throw new Error(`Group ${groupId} not found`)
}
logger.info(`👥 Selecting account from group: ${group.name} (${group.platform})`);
logger.info(`👥 Selecting account from group: ${group.name} (${group.platform})`)
// 如果有会话哈希,检查是否有已映射的账户
if (sessionHash) {
const mappedAccount = await this._getSessionMapping(sessionHash);
const mappedAccount = await this._getSessionMapping(sessionHash)
if (mappedAccount) {
// 验证映射的账户是否属于这个分组
const memberIds = await accountGroupService.getGroupMembers(groupId);
const memberIds = await accountGroupService.getGroupMembers(groupId)
if (memberIds.includes(mappedAccount.accountId)) {
const isAvailable = await this._isAccountAvailable(mappedAccount.accountId, mappedAccount.accountType);
const isAvailable = await this._isAccountAvailable(
mappedAccount.accountId,
mappedAccount.accountType
)
if (isAvailable) {
logger.info(`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`);
return mappedAccount;
logger.info(
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
)
return mappedAccount
}
}
// 如果映射的账户不可用或不在分组中,删除映射
await this._deleteSessionMapping(sessionHash);
await this._deleteSessionMapping(sessionHash)
}
}
// 获取分组内的所有账户
const memberIds = await accountGroupService.getGroupMembers(groupId);
const memberIds = await accountGroupService.getGroupMembers(groupId)
if (memberIds.length === 0) {
throw new Error(`Group ${group.name} has no members`);
throw new Error(`Group ${group.name} has no members`)
}
const availableAccounts = [];
const availableAccounts = []
// 获取所有成员账户的详细信息
for (const memberId of memberIds) {
let account = null;
let accountType = null;
let account = null
let accountType = null
// 根据平台类型获取账户
if (group.platform === 'claude') {
// 先尝试官方账户
account = await redis.getClaudeAccount(memberId);
account = await redis.getClaudeAccount(memberId)
if (account?.id) {
accountType = 'claude-official';
accountType = 'claude-official'
} else {
// 尝试Console账户
account = await claudeConsoleAccountService.getAccount(memberId);
account = await claudeConsoleAccountService.getAccount(memberId)
if (account) {
accountType = 'claude-console';
accountType = 'claude-console'
}
}
} else if (group.platform === 'gemini') {
// Gemini暂时不支持预留接口
logger.warn('⚠️ Gemini group scheduling not yet implemented');
continue;
logger.warn('⚠️ Gemini group scheduling not yet implemented')
continue
}
if (!account) {
logger.warn(`⚠️ Account ${memberId} not found in group ${group.name}`);
continue;
logger.warn(`⚠️ Account ${memberId} not found in group ${group.name}`)
continue
}
// 检查账户是否可用
const isActive = accountType === 'claude-official'
? account.isActive === 'true'
: account.isActive === true;
const status = accountType === 'claude-official'
? account.status !== 'error' && account.status !== 'blocked'
: account.status === 'active';
const isActive =
accountType === 'claude-official'
? account.isActive === 'true'
: account.isActive === true
const status =
accountType === 'claude-official'
? account.status !== 'error' && account.status !== 'blocked'
: account.status === 'active'
if (isActive && status && this._isSchedulable(account.schedulable)) {
// 检查模型支持Console账户
if (accountType === 'claude-console' && requestedModel && account.supportedModels && account.supportedModels.length > 0) {
if (
accountType === 'claude-console' &&
requestedModel &&
account.supportedModels &&
account.supportedModels.length > 0
) {
if (!account.supportedModels.includes(requestedModel)) {
logger.info(`🚫 Account ${account.name} in group does not support model ${requestedModel}`);
continue;
logger.info(
`🚫 Account ${account.name} in group does not support model ${requestedModel}`
)
continue
}
}
// 检查是否被限流
const isRateLimited = await this.isAccountRateLimited(account.id, accountType);
const isRateLimited = await this.isAccountRateLimited(account.id, accountType)
if (!isRateLimited) {
availableAccounts.push({
...account,
accountId: account.id,
accountType: accountType,
accountType,
priority: parseInt(account.priority) || 50,
lastUsedAt: account.lastUsedAt || '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}`)
}
// 使用现有的优先级排序逻辑
const sortedAccounts = this._sortAccountsByPriority(availableAccounts);
const sortedAccounts = this._sortAccountsByPriority(availableAccounts)
// 选择第一个账户
const selectedAccount = sortedAccounts[0];
const selectedAccount = sortedAccounts[0]
// 如果有会话哈希,建立新的映射
if (sessionHash) {
await this._setSessionMapping(sessionHash, selectedAccount.accountId, selectedAccount.accountType);
logger.info(`🎯 Created new sticky session mapping in group: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) for session ${sessionHash}`);
await this._setSessionMapping(
sessionHash,
selectedAccount.accountId,
selectedAccount.accountType
)
logger.info(
`🎯 Created new sticky session mapping in group: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) for session ${sessionHash}`
)
}
logger.info(`🎯 Selected account from group ${group.name}: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) with priority ${selectedAccount.priority}`);
logger.info(
`🎯 Selected account from group ${group.name}: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) with priority ${selectedAccount.priority}`
)
return {
accountId: selectedAccount.accountId,
accountType: selectedAccount.accountType
};
}
} catch (error) {
logger.error(`❌ Failed to select account from group ${groupId}:`, error);
throw error;
logger.error(`❌ Failed to select account from group ${groupId}:`, error)
throw error
}
}
}
module.exports = new UnifiedClaudeScheduler();
module.exports = new UnifiedClaudeScheduler()

View File

@@ -1,21 +1,21 @@
const geminiAccountService = require('./geminiAccountService');
const accountGroupService = require('./accountGroupService');
const redis = require('../models/redis');
const logger = require('../utils/logger');
const geminiAccountService = require('./geminiAccountService')
const accountGroupService = require('./accountGroupService')
const redis = require('../models/redis')
const logger = require('../utils/logger')
class UnifiedGeminiScheduler {
constructor() {
this.SESSION_MAPPING_PREFIX = 'unified_gemini_session_mapping:';
this.SESSION_MAPPING_PREFIX = 'unified_gemini_session_mapping:'
}
// 🔧 辅助方法:检查账户是否可调度(兼容字符串和布尔值)
_isSchedulable(schedulable) {
// 如果是 undefined 或 null默认为可调度
if (schedulable === undefined || schedulable === null) {
return true;
return true
}
// 明确设置为 false布尔值或 'false'(字符串)时不可调度
return schedulable !== false && schedulable !== 'false';
return schedulable !== false && schedulable !== 'false'
}
// 🎯 统一调度Gemini账号
@@ -25,143 +25,183 @@ class UnifiedGeminiScheduler {
if (apiKeyData.geminiAccountId) {
// 检查是否是分组
if (apiKeyData.geminiAccountId.startsWith('group:')) {
const groupId = apiKeyData.geminiAccountId.replace('group:', '');
logger.info(`🎯 API key ${apiKeyData.name} is bound to group ${groupId}, selecting from group`);
return await this.selectAccountFromGroup(groupId, sessionHash, requestedModel, apiKeyData);
const groupId = apiKeyData.geminiAccountId.replace('group:', '')
logger.info(
`🎯 API key ${apiKeyData.name} is bound to group ${groupId}, selecting from group`
)
return await this.selectAccountFromGroup(groupId, sessionHash, requestedModel, apiKeyData)
}
// 普通专属账户
const boundAccount = await geminiAccountService.getAccount(apiKeyData.geminiAccountId);
const boundAccount = await geminiAccountService.getAccount(apiKeyData.geminiAccountId)
if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') {
logger.info(`🎯 Using bound dedicated Gemini account: ${boundAccount.name} (${apiKeyData.geminiAccountId}) for API key ${apiKeyData.name}`);
logger.info(
`🎯 Using bound dedicated Gemini account: ${boundAccount.name} (${apiKeyData.geminiAccountId}) for API key ${apiKeyData.name}`
)
return {
accountId: apiKeyData.geminiAccountId,
accountType: 'gemini'
};
}
} else {
logger.warn(`⚠️ Bound Gemini account ${apiKeyData.geminiAccountId} is not available, falling back to pool`);
logger.warn(
`⚠️ Bound Gemini account ${apiKeyData.geminiAccountId} is not available, falling back to pool`
)
}
}
// 如果有会话哈希,检查是否有已映射的账户
if (sessionHash) {
const mappedAccount = await this._getSessionMapping(sessionHash);
const mappedAccount = await this._getSessionMapping(sessionHash)
if (mappedAccount) {
// 验证映射的账户是否仍然可用
const isAvailable = await this._isAccountAvailable(mappedAccount.accountId, mappedAccount.accountType);
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;
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);
logger.warn(
`⚠️ Mapped account ${mappedAccount.accountId} is no longer available, selecting new account`
)
await this._deleteSessionMapping(sessionHash)
}
}
}
// 获取所有可用账户
const availableAccounts = await this._getAllAvailableAccounts(apiKeyData, requestedModel);
const availableAccounts = await this._getAllAvailableAccounts(apiKeyData, requestedModel)
if (availableAccounts.length === 0) {
// 提供更详细的错误信息
if (requestedModel) {
throw new Error(`No available Gemini accounts support the requested model: ${requestedModel}`);
throw new Error(
`No available Gemini accounts support the requested model: ${requestedModel}`
)
} else {
throw new Error('No available Gemini accounts');
throw new Error('No available Gemini accounts')
}
}
// 按优先级和最后使用时间排序
const sortedAccounts = this._sortAccountsByPriority(availableAccounts);
const sortedAccounts = this._sortAccountsByPriority(availableAccounts)
// 选择第一个账户
const selectedAccount = sortedAccounts[0];
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}`);
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}`);
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;
logger.error('❌ Failed to select account for API key:', error)
throw error
}
}
// 📋 获取所有可用账户
async _getAllAvailableAccounts(apiKeyData, requestedModel = null) {
const availableAccounts = [];
const availableAccounts = []
// 如果API Key绑定了专属账户优先返回
if (apiKeyData.geminiAccountId) {
const boundAccount = await geminiAccountService.getAccount(apiKeyData.geminiAccountId);
const boundAccount = await geminiAccountService.getAccount(apiKeyData.geminiAccountId)
if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') {
const isRateLimited = await this.isAccountRateLimited(boundAccount.id);
const isRateLimited = await this.isAccountRateLimited(boundAccount.id)
if (!isRateLimited) {
// 检查模型支持
if (requestedModel && boundAccount.supportedModels && boundAccount.supportedModels.length > 0) {
if (
requestedModel &&
boundAccount.supportedModels &&
boundAccount.supportedModels.length > 0
) {
// 处理可能带有 models/ 前缀的模型名
const normalizedModel = requestedModel.replace('models/', '');
const modelSupported = boundAccount.supportedModels.some(model =>
model.replace('models/', '') === normalizedModel
);
const normalizedModel = requestedModel.replace('models/', '')
const modelSupported = boundAccount.supportedModels.some(
(model) => model.replace('models/', '') === normalizedModel
)
if (!modelSupported) {
logger.warn(`⚠️ Bound Gemini account ${boundAccount.name} does not support model ${requestedModel}`);
return availableAccounts;
logger.warn(
`⚠️ Bound Gemini account ${boundAccount.name} does not support model ${requestedModel}`
)
return availableAccounts
}
}
logger.info(`🎯 Using bound dedicated Gemini account: ${boundAccount.name} (${apiKeyData.geminiAccountId})`);
return [{
...boundAccount,
accountId: boundAccount.id,
accountType: 'gemini',
priority: parseInt(boundAccount.priority) || 50,
lastUsedAt: boundAccount.lastUsedAt || '0'
}];
logger.info(
`🎯 Using bound dedicated Gemini account: ${boundAccount.name} (${apiKeyData.geminiAccountId})`
)
return [
{
...boundAccount,
accountId: boundAccount.id,
accountType: 'gemini',
priority: parseInt(boundAccount.priority) || 50,
lastUsedAt: boundAccount.lastUsedAt || '0'
}
]
}
} else {
logger.warn(`⚠️ Bound Gemini account ${apiKeyData.geminiAccountId} is not available`);
logger.warn(`⚠️ Bound Gemini account ${apiKeyData.geminiAccountId} is not available`)
}
}
// 获取所有Gemini账户共享池
const geminiAccounts = await geminiAccountService.getAllAccounts();
const geminiAccounts = await geminiAccountService.getAllAccounts()
for (const account of geminiAccounts) {
if (account.isActive === 'true' &&
account.status !== 'error' &&
(account.accountType === 'shared' || !account.accountType) && // 兼容旧数据
this._isSchedulable(account.schedulable)) { // 检查是否可调度
if (
account.isActive === 'true' &&
account.status !== 'error' &&
(account.accountType === 'shared' || !account.accountType) && // 兼容旧数据
this._isSchedulable(account.schedulable)
) {
// 检查是否可调度
// 检查token是否过期
const isExpired = geminiAccountService.isTokenExpired(account);
const isExpired = geminiAccountService.isTokenExpired(account)
if (isExpired && !account.refreshToken) {
logger.warn(`⚠️ Gemini account ${account.name} token expired and no refresh token available`);
continue;
logger.warn(
`⚠️ Gemini account ${account.name} token expired and no refresh token available`
)
continue
}
// 检查模型支持
if (requestedModel && account.supportedModels && account.supportedModels.length > 0) {
// 处理可能带有 models/ 前缀的模型名
const normalizedModel = requestedModel.replace('models/', '');
const modelSupported = account.supportedModels.some(model =>
model.replace('models/', '') === normalizedModel
);
const normalizedModel = requestedModel.replace('models/', '')
const modelSupported = account.supportedModels.some(
(model) => model.replace('models/', '') === normalizedModel
)
if (!modelSupported) {
logger.debug(`⏭️ Skipping Gemini account ${account.name} - doesn't support model ${requestedModel}`);
continue;
logger.debug(
`⏭️ Skipping Gemini account ${account.name} - doesn't support model ${requestedModel}`
)
continue
}
}
// 检查是否被限流
const isRateLimited = await this.isAccountRateLimited(account.id);
const isRateLimited = await this.isAccountRateLimited(account.id)
if (!isRateLimited) {
availableAccounts.push({
...account,
@@ -169,13 +209,13 @@ class UnifiedGeminiScheduler {
accountType: 'gemini',
priority: parseInt(account.priority) || 50, // 默认优先级50
lastUsedAt: account.lastUsedAt || '0'
});
})
}
}
}
logger.info(`📊 Total available Gemini accounts: ${availableAccounts.length}`);
return availableAccounts;
logger.info(`📊 Total available Gemini accounts: ${availableAccounts.length}`)
return availableAccounts
}
// 🔢 按优先级和最后使用时间排序账户
@@ -183,90 +223,89 @@ class UnifiedGeminiScheduler {
return accounts.sort((a, b) => {
// 首先按优先级排序(数字越小优先级越高)
if (a.priority !== b.priority) {
return 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;
});
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 === 'gemini') {
const account = await geminiAccountService.getAccount(accountId);
const account = await geminiAccountService.getAccount(accountId)
if (!account || account.isActive !== 'true' || account.status === 'error') {
return false;
return false
}
// 检查是否可调度
if (!this._isSchedulable(account.schedulable)) {
logger.info(`🚫 Gemini account ${accountId} is not schedulable`);
return false;
logger.info(`🚫 Gemini account ${accountId} is not schedulable`)
return false
}
return !(await this.isAccountRateLimited(accountId));
return !(await this.isAccountRateLimited(accountId))
}
return false;
return false
} catch (error) {
logger.warn(`⚠️ Failed to check account availability: ${accountId}`, error);
return false;
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}`);
const client = redis.getClientSafe()
const mappingData = await client.get(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`)
if (mappingData) {
try {
return JSON.parse(mappingData);
return JSON.parse(mappingData)
} catch (error) {
logger.warn('⚠️ Failed to parse session mapping:', error);
return null;
logger.warn('⚠️ Failed to parse session mapping:', error)
return null
}
}
return null;
return null
}
// 💾 设置会话映射
async _setSessionMapping(sessionHash, accountId, accountType) {
const client = redis.getClientSafe();
const mappingData = JSON.stringify({ accountId, accountType });
const client = redis.getClientSafe()
const mappingData = JSON.stringify({ accountId, accountType })
// 设置1小时过期
await client.setex(
`${this.SESSION_MAPPING_PREFIX}${sessionHash}`,
3600,
mappingData
);
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}`);
const client = redis.getClientSafe()
await client.del(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`)
}
// 🚫 标记账户为限流状态
async markAccountRateLimited(accountId, accountType, sessionHash = null) {
try {
if (accountType === 'gemini') {
await geminiAccountService.setAccountRateLimited(accountId, true);
await geminiAccountService.setAccountRateLimited(accountId, true)
}
// 删除会话映射
if (sessionHash) {
await this._deleteSessionMapping(sessionHash);
await this._deleteSessionMapping(sessionHash)
}
return { success: true };
return { success: true }
} catch (error) {
logger.error(`❌ Failed to mark account as rate limited: ${accountId} (${accountType})`, error);
throw error;
logger.error(
`❌ Failed to mark account as rate limited: ${accountId} (${accountType})`,
error
)
throw error
}
}
@@ -274,33 +313,38 @@ class UnifiedGeminiScheduler {
async removeAccountRateLimit(accountId, accountType) {
try {
if (accountType === 'gemini') {
await geminiAccountService.setAccountRateLimited(accountId, false);
await geminiAccountService.setAccountRateLimited(accountId, false)
}
return { success: true };
return { success: true }
} catch (error) {
logger.error(`❌ Failed to remove rate limit for account: ${accountId} (${accountType})`, error);
throw error;
logger.error(
`❌ Failed to remove rate limit for account: ${accountId} (${accountType})`,
error
)
throw error
}
}
// 🔍 检查账户是否处于限流状态
async isAccountRateLimited(accountId) {
try {
const account = await geminiAccountService.getAccount(accountId);
if (!account) return false;
if (account.rateLimitStatus === 'limited' && account.rateLimitedAt) {
const limitedAt = new Date(account.rateLimitedAt).getTime();
const now = Date.now();
const limitDuration = 60 * 60 * 1000; // 1小时
return now < (limitedAt + limitDuration);
const account = await geminiAccountService.getAccount(accountId)
if (!account) {
return false
}
return false;
if (account.rateLimitStatus === 'limited' && account.rateLimitedAt) {
const limitedAt = new Date(account.rateLimitedAt).getTime()
const now = Date.now()
const limitDuration = 60 * 60 * 1000 // 1小时
return now < limitedAt + limitDuration
}
return false
} catch (error) {
logger.error(`❌ Failed to check rate limit status: ${accountId}`, error);
return false;
logger.error(`❌ Failed to check rate limit status: ${accountId}`, error)
return false
}
}
@@ -308,79 +352,89 @@ class UnifiedGeminiScheduler {
async selectAccountFromGroup(groupId, sessionHash = null, requestedModel = null) {
try {
// 获取分组信息
const group = await accountGroupService.getGroup(groupId);
const group = await accountGroupService.getGroup(groupId)
if (!group) {
throw new Error(`Group ${groupId} not found`);
}
if (group.platform !== 'gemini') {
throw new Error(`Group ${group.name} is not a Gemini group`);
throw new Error(`Group ${groupId} not found`)
}
logger.info(`👥 Selecting account from Gemini group: ${group.name}`);
if (group.platform !== 'gemini') {
throw new Error(`Group ${group.name} is not a Gemini group`)
}
logger.info(`👥 Selecting account from Gemini group: ${group.name}`)
// 如果有会话哈希,检查是否有已映射的账户
if (sessionHash) {
const mappedAccount = await this._getSessionMapping(sessionHash);
const mappedAccount = await this._getSessionMapping(sessionHash)
if (mappedAccount) {
// 验证映射的账户是否属于这个分组
const memberIds = await accountGroupService.getGroupMembers(groupId);
const memberIds = await accountGroupService.getGroupMembers(groupId)
if (memberIds.includes(mappedAccount.accountId)) {
const isAvailable = await this._isAccountAvailable(mappedAccount.accountId, mappedAccount.accountType);
const isAvailable = await this._isAccountAvailable(
mappedAccount.accountId,
mappedAccount.accountType
)
if (isAvailable) {
logger.info(`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`);
return mappedAccount;
logger.info(
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
)
return mappedAccount
}
}
// 如果映射的账户不可用或不在分组中,删除映射
await this._deleteSessionMapping(sessionHash);
await this._deleteSessionMapping(sessionHash)
}
}
// 获取分组内的所有账户
const memberIds = await accountGroupService.getGroupMembers(groupId);
const memberIds = await accountGroupService.getGroupMembers(groupId)
if (memberIds.length === 0) {
throw new Error(`Group ${group.name} has no members`);
throw new Error(`Group ${group.name} has no members`)
}
const availableAccounts = [];
const availableAccounts = []
// 获取所有成员账户的详细信息
for (const memberId of memberIds) {
const account = await geminiAccountService.getAccount(memberId);
const account = await geminiAccountService.getAccount(memberId)
if (!account) {
logger.warn(`⚠️ Gemini account ${memberId} not found in group ${group.name}`);
continue;
logger.warn(`⚠️ Gemini account ${memberId} not found in group ${group.name}`)
continue
}
// 检查账户是否可用
if (account.isActive === 'true' &&
account.status !== 'error' &&
this._isSchedulable(account.schedulable)) {
if (
account.isActive === 'true' &&
account.status !== 'error' &&
this._isSchedulable(account.schedulable)
) {
// 检查token是否过期
const isExpired = geminiAccountService.isTokenExpired(account);
const isExpired = geminiAccountService.isTokenExpired(account)
if (isExpired && !account.refreshToken) {
logger.warn(`⚠️ Gemini account ${account.name} in group token expired and no refresh token available`);
continue;
logger.warn(
`⚠️ Gemini account ${account.name} in group token expired and no refresh token available`
)
continue
}
// 检查模型支持
if (requestedModel && account.supportedModels && account.supportedModels.length > 0) {
// 处理可能带有 models/ 前缀的模型名
const normalizedModel = requestedModel.replace('models/', '');
const modelSupported = account.supportedModels.some(model =>
model.replace('models/', '') === normalizedModel
);
const normalizedModel = requestedModel.replace('models/', '')
const modelSupported = account.supportedModels.some(
(model) => model.replace('models/', '') === normalizedModel
)
if (!modelSupported) {
logger.debug(`⏭️ Skipping Gemini account ${account.name} in group - doesn't support model ${requestedModel}`);
continue;
logger.debug(
`⏭️ Skipping Gemini account ${account.name} in group - doesn't support model ${requestedModel}`
)
continue
}
}
// 检查是否被限流
const isRateLimited = await this.isAccountRateLimited(account.id);
const isRateLimited = await this.isAccountRateLimited(account.id)
if (!isRateLimited) {
availableAccounts.push({
...account,
@@ -388,38 +442,46 @@ class UnifiedGeminiScheduler {
accountType: 'gemini',
priority: parseInt(account.priority) || 50,
lastUsedAt: account.lastUsedAt || '0'
});
})
}
}
}
if (availableAccounts.length === 0) {
throw new Error(`No available accounts in Gemini group ${group.name}`);
throw new Error(`No available accounts in Gemini group ${group.name}`)
}
// 使用现有的优先级排序逻辑
const sortedAccounts = this._sortAccountsByPriority(availableAccounts);
const sortedAccounts = this._sortAccountsByPriority(availableAccounts)
// 选择第一个账户
const selectedAccount = sortedAccounts[0];
const selectedAccount = sortedAccounts[0]
// 如果有会话哈希,建立新的映射
if (sessionHash) {
await this._setSessionMapping(sessionHash, selectedAccount.accountId, selectedAccount.accountType);
logger.info(`🎯 Created new sticky session mapping in group: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) for session ${sessionHash}`);
await this._setSessionMapping(
sessionHash,
selectedAccount.accountId,
selectedAccount.accountType
)
logger.info(
`🎯 Created new sticky session mapping in group: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) for session ${sessionHash}`
)
}
logger.info(`🎯 Selected account from Gemini group ${group.name}: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) with priority ${selectedAccount.priority}`);
logger.info(
`🎯 Selected account from Gemini group ${group.name}: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) with priority ${selectedAccount.priority}`
)
return {
accountId: selectedAccount.accountId,
accountType: selectedAccount.accountType
};
}
} catch (error) {
logger.error(`❌ Failed to select account from Gemini group ${groupId}:`, error);
throw error;
logger.error(`❌ Failed to select account from Gemini group ${groupId}:`, error)
throw error
}
}
}
module.exports = new UnifiedGeminiScheduler();
module.exports = new UnifiedGeminiScheduler()