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