first commit

This commit is contained in:
shaw
2025-07-14 18:14:13 +08:00
parent a96a372011
commit b1ca3f307e
31 changed files with 20046 additions and 21 deletions

367
src/app.js Normal file
View File

@@ -0,0 +1,367 @@
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const compression = require('compression');
const path = require('path');
const fs = require('fs');
const bcrypt = require('bcryptjs');
const config = require('../config/config');
const logger = require('./utils/logger');
const redis = require('./models/redis');
const pricingService = require('./services/pricingService');
// Import routes
const apiRoutes = require('./routes/api');
const adminRoutes = require('./routes/admin');
const webRoutes = require('./routes/web');
// Import middleware
const {
corsMiddleware,
requestLogger,
securityMiddleware,
errorHandler,
globalRateLimit,
requestSizeLimit
} = require('./middleware/auth');
class Application {
constructor() {
this.app = express();
this.server = null;
}
async initialize() {
try {
// 🔗 连接Redis
logger.info('🔄 Connecting to Redis...');
await redis.connect();
logger.success('✅ Redis connected successfully');
// 💰 初始化价格服务
logger.info('🔄 Initializing pricing service...');
await pricingService.initialize();
// 🔧 初始化管理员凭据
logger.info('🔄 Initializing admin credentials...');
await this.initializeAdmin();
// 🛡️ 安全中间件
this.app.use(helmet({
contentSecurityPolicy: false, // 允许内联样式和脚本
crossOriginEmbedderPolicy: false
}));
// 🌐 CORS
if (config.web.enableCors) {
this.app.use(cors());
} else {
this.app.use(corsMiddleware);
}
// 📦 压缩
this.app.use(compression());
// 🚦 全局速率限制(仅在生产环境启用)
if (process.env.NODE_ENV === 'production') {
this.app.use(globalRateLimit);
}
// 📏 请求大小限制
this.app.use(requestSizeLimit);
// 📝 请求日志使用自定义logger而不是morgan
this.app.use(requestLogger);
// 🔧 基础中间件
this.app.use(express.json({
limit: '10mb',
verify: (req, res, buf, encoding) => {
// 验证JSON格式
if (buf && buf.length && !buf.toString(encoding || 'utf8').trim()) {
throw new Error('Invalid JSON: empty body');
}
}
}));
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
this.app.use(securityMiddleware);
// 🎯 信任代理
if (config.server.trustProxy) {
this.app.set('trust proxy', 1);
}
// 🛣️ 路由
this.app.use('/api', apiRoutes);
this.app.use('/admin', adminRoutes);
this.app.use('/web', webRoutes);
// 🏠 根路径重定向到管理界面
this.app.get('/', (req, res) => {
res.redirect('/web');
});
// 🏥 增强的健康检查端点
this.app.get('/health', async (req, res) => {
try {
const timer = logger.timer('health-check');
// 检查各个组件健康状态
const [redisHealth, loggerHealth] = await Promise.all([
this.checkRedisHealth(),
this.checkLoggerHealth()
]);
const memory = process.memoryUsage();
const health = {
status: 'healthy',
service: 'claude-relay-service',
version: '1.0.0',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: {
used: Math.round(memory.heapUsed / 1024 / 1024) + 'MB',
total: Math.round(memory.heapTotal / 1024 / 1024) + 'MB',
external: Math.round(memory.external / 1024 / 1024) + 'MB'
},
components: {
redis: redisHealth,
logger: loggerHealth
},
stats: logger.getStats()
};
timer.end('completed');
res.json(health);
} catch (error) {
logger.error('❌ Health check failed:', { error: error.message, stack: error.stack });
res.status(503).json({
status: 'unhealthy',
error: error.message,
timestamp: new Date().toISOString()
});
}
});
// 📊 指标端点
this.app.get('/metrics', async (req, res) => {
try {
const stats = await redis.getSystemStats();
const metrics = {
...stats,
uptime: process.uptime(),
memory: process.memoryUsage(),
timestamp: new Date().toISOString()
};
res.json(metrics);
} catch (error) {
logger.error('❌ Metrics collection failed:', error);
res.status(500).json({ error: 'Failed to collect metrics' });
}
});
// 🚫 404 处理
this.app.use('*', (req, res) => {
res.status(404).json({
error: 'Not Found',
message: `Route ${req.originalUrl} not found`,
timestamp: new Date().toISOString()
});
});
// 🚨 错误处理
this.app.use(errorHandler);
logger.success('✅ Application initialized successfully');
} catch (error) {
logger.error('💥 Application initialization failed:', error);
throw error;
}
}
// 🔧 初始化管理员凭据
async initializeAdmin() {
try {
// 检查Redis中是否已存在管理员凭据
const existingAdmin = await redis.getSession('admin_credentials');
if (!existingAdmin || Object.keys(existingAdmin).length === 0) {
// 尝试从初始化文件读取
const initFilePath = path.join(__dirname, '..', 'data', 'init.json');
if (fs.existsSync(initFilePath)) {
const initData = JSON.parse(fs.readFileSync(initFilePath, 'utf8'));
// 将明文密码哈希化
const saltRounds = 10;
const passwordHash = await bcrypt.hash(initData.adminPassword, saltRounds);
// 存储到Redis
const adminCredentials = {
username: initData.adminUsername,
passwordHash: passwordHash,
createdAt: new Date().toISOString(),
lastLogin: null
};
await redis.setSession('admin_credentials', adminCredentials);
logger.success('✅ Admin credentials initialized from setup data');
} else {
logger.warn('⚠️ No admin credentials found. Please run npm run setup first.');
}
} else {
logger.info(' Admin credentials already exist in Redis');
}
} catch (error) {
logger.error('❌ Failed to initialize admin credentials:', { error: error.message, stack: error.stack });
throw error;
}
}
// 🔍 Redis健康检查
async checkRedisHealth() {
try {
const start = Date.now();
await redis.getClient().ping();
const latency = Date.now() - start;
return {
status: 'healthy',
connected: redis.isConnected,
latency: `${latency}ms`
};
} catch (error) {
return {
status: 'unhealthy',
connected: false,
error: error.message
};
}
}
// 📝 Logger健康检查
async checkLoggerHealth() {
try {
const health = logger.healthCheck();
return {
status: health.healthy ? 'healthy' : 'unhealthy',
...health
};
} catch (error) {
return {
status: 'unhealthy',
error: error.message
};
}
}
async start() {
try {
await this.initialize();
this.server = this.app.listen(config.server.port, config.server.host, () => {
logger.start(`🚀 Claude Relay Service started on ${config.server.host}:${config.server.port}`);
logger.info(`🌐 Web interface: http://${config.server.host}:${config.server.port}/web`);
logger.info(`🔗 API endpoint: http://${config.server.host}:${config.server.port}/api/v1/messages`);
logger.info(`⚙️ Admin API: http://${config.server.host}:${config.server.port}/admin`);
logger.info(`🏥 Health check: http://${config.server.host}:${config.server.port}/health`);
logger.info(`📊 Metrics: http://${config.server.host}:${config.server.port}/metrics`);
});
// 🔄 定期清理任务
this.startCleanupTasks();
// 🛑 优雅关闭
this.setupGracefulShutdown();
} catch (error) {
logger.error('💥 Failed to start server:', error);
process.exit(1);
}
}
startCleanupTasks() {
// 🧹 每小时清理一次过期数据
setInterval(async () => {
try {
logger.info('🧹 Starting scheduled cleanup...');
const apiKeyService = require('./services/apiKeyService');
const claudeAccountService = require('./services/claudeAccountService');
const [expiredKeys, errorAccounts] = await Promise.all([
apiKeyService.cleanupExpiredKeys(),
claudeAccountService.cleanupErrorAccounts()
]);
await redis.cleanup();
logger.success(`🧹 Cleanup completed: ${expiredKeys} expired keys, ${errorAccounts} error accounts reset`);
} catch (error) {
logger.error('❌ Cleanup task failed:', error);
}
}, config.system.cleanupInterval);
logger.info(`🔄 Cleanup tasks scheduled every ${config.system.cleanupInterval / 1000 / 60} minutes`);
}
setupGracefulShutdown() {
const shutdown = async (signal) => {
logger.info(`🛑 Received ${signal}, starting graceful shutdown...`);
if (this.server) {
this.server.close(async () => {
logger.info('🚪 HTTP server closed');
try {
await redis.disconnect();
logger.info('👋 Redis disconnected');
} catch (error) {
logger.error('❌ Error disconnecting Redis:', error);
}
logger.success('✅ Graceful shutdown completed');
process.exit(0);
});
// 强制关闭超时
setTimeout(() => {
logger.warn('⚠️ Forced shutdown due to timeout');
process.exit(1);
}, 10000);
} else {
process.exit(0);
}
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
// 处理未捕获异常
process.on('uncaughtException', (error) => {
logger.error('💥 Uncaught exception:', error);
shutdown('uncaughtException');
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('💥 Unhandled rejection at:', promise, 'reason:', reason);
shutdown('unhandledRejection');
});
}
}
// 启动应用
if (require.main === module) {
const app = new Application();
app.start().catch((error) => {
logger.error('💥 Application startup failed:', error);
process.exit(1);
});
}
module.exports = Application;

532
src/middleware/auth.js Normal file
View File

@@ -0,0 +1,532 @@
const apiKeyService = require('../services/apiKeyService');
const logger = require('../utils/logger');
const redis = require('../models/redis');
const { RateLimiterRedis } = require('rate-limiter-flexible');
// 🔑 API Key验证中间件优化版
const authenticateApiKey = async (req, res, next) => {
const startTime = Date.now();
try {
// 安全提取API Key支持多种格式
const apiKey = req.headers['x-api-key'] ||
req.headers['authorization']?.replace(/^Bearer\s+/i, '') ||
req.headers['api-key'];
if (!apiKey) {
logger.security(`🔒 Missing API key attempt from ${req.ip || 'unknown'}`);
return res.status(401).json({
error: 'Missing API key',
message: 'Please provide an API key in the x-api-key header or Authorization header'
});
}
// 基本API Key格式验证
if (typeof apiKey !== 'string' || apiKey.length < 10 || apiKey.length > 512) {
logger.security(`🔒 Invalid API key format from ${req.ip || 'unknown'}`);
return res.status(401).json({
error: 'Invalid API key format',
message: 'API key format is invalid'
});
}
// 验证API Key带缓存优化
const validation = await apiKeyService.validateApiKey(apiKey);
if (!validation.valid) {
const clientIP = req.ip || req.connection?.remoteAddress || 'unknown';
logger.security(`🔒 Invalid API key attempt: ${validation.error} from ${clientIP}`);
return res.status(401).json({
error: 'Invalid API key',
message: validation.error
});
}
// 检查速率限制(优化:只在验证成功后检查)
const rateLimitResult = await apiKeyService.checkRateLimit(validation.keyData.id);
if (!rateLimitResult.allowed) {
logger.security(`🚦 Rate limit exceeded for key: ${validation.keyData.id} (${validation.keyData.name})`);
return res.status(429).json({
error: 'Rate limit exceeded',
message: `Too many requests. Limit: ${rateLimitResult.limit} requests per minute`,
resetTime: rateLimitResult.resetTime,
retryAfter: rateLimitResult.resetTime
});
}
// 设置标准速率限制响应头
res.setHeader('X-RateLimit-Limit', rateLimitResult.limit);
res.setHeader('X-RateLimit-Remaining', Math.max(0, rateLimitResult.limit - rateLimitResult.current));
res.setHeader('X-RateLimit-Reset', rateLimitResult.resetTime);
res.setHeader('X-RateLimit-Policy', `${rateLimitResult.limit};w=60`);
// 将验证信息添加到请求对象(只包含必要信息)
req.apiKey = {
id: validation.keyData.id,
name: validation.keyData.name,
tokenLimit: validation.keyData.tokenLimit,
requestLimit: validation.keyData.requestLimit,
claudeAccountId: validation.keyData.claudeAccountId
};
req.usage = validation.keyData.usage;
const authDuration = Date.now() - startTime;
logger.api(`🔓 Authenticated request from key: ${validation.keyData.name} (${validation.keyData.id}) in ${authDuration}ms`);
next();
} catch (error) {
const authDuration = Date.now() - startTime;
logger.error(`❌ Authentication middleware error (${authDuration}ms):`, {
error: error.message,
stack: error.stack,
ip: req.ip,
userAgent: req.get('User-Agent'),
url: req.originalUrl
});
res.status(500).json({
error: 'Authentication error',
message: 'Internal server error during authentication'
});
}
};
// 🛡️ 管理员验证中间件(优化版)
const authenticateAdmin = async (req, res, next) => {
const startTime = Date.now();
try {
// 安全提取token支持多种方式
const token = req.headers['authorization']?.replace(/^Bearer\s+/i, '') ||
req.cookies?.adminToken ||
req.headers['x-admin-token'];
if (!token) {
logger.security(`🔒 Missing admin token attempt from ${req.ip || 'unknown'}`);
return res.status(401).json({
error: 'Missing admin token',
message: 'Please provide an admin token'
});
}
// 基本token格式验证
if (typeof token !== 'string' || token.length < 32 || token.length > 512) {
logger.security(`🔒 Invalid admin token format from ${req.ip || 'unknown'}`);
return res.status(401).json({
error: 'Invalid admin token format',
message: 'Admin token format is invalid'
});
}
// 获取管理员会话(带超时处理)
const adminSession = await Promise.race([
redis.getSession(token),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Session lookup timeout')), 5000)
)
]);
if (!adminSession || Object.keys(adminSession).length === 0) {
logger.security(`🔒 Invalid admin token attempt from ${req.ip || 'unknown'}`);
return res.status(401).json({
error: 'Invalid admin token',
message: 'Invalid or expired admin session'
});
}
// 检查会话活跃性(可选:检查最后活动时间)
const now = new Date();
const lastActivity = new Date(adminSession.lastActivity || adminSession.loginTime);
const inactiveDuration = now - lastActivity;
const maxInactivity = 24 * 60 * 60 * 1000; // 24小时
if (inactiveDuration > maxInactivity) {
logger.security(`🔒 Expired admin session for ${adminSession.username} from ${req.ip || 'unknown'}`);
await redis.deleteSession(token); // 清理过期会话
return res.status(401).json({
error: 'Session expired',
message: 'Admin session has expired due to inactivity'
});
}
// 更新最后活动时间(异步,不阻塞请求)
redis.setSession(token, {
...adminSession,
lastActivity: now.toISOString()
}, 86400).catch(error => {
logger.error('Failed to update admin session activity:', error);
});
// 设置管理员信息(只包含必要信息)
req.admin = {
id: adminSession.adminId || 'admin',
username: adminSession.username,
sessionId: token,
loginTime: adminSession.loginTime
};
const authDuration = Date.now() - startTime;
logger.security(`🔐 Admin authenticated: ${adminSession.username} in ${authDuration}ms`);
next();
} catch (error) {
const authDuration = Date.now() - startTime;
logger.error(`❌ Admin authentication error (${authDuration}ms):`, {
error: error.message,
ip: req.ip,
userAgent: req.get('User-Agent'),
url: req.originalUrl
});
res.status(500).json({
error: 'Authentication error',
message: 'Internal server error during admin authentication'
});
}
};
// 注意:使用统计现在直接在/api/v1/messages路由中处理
// 以便从Claude API响应中提取真实的usage数据
// 🚦 CORS中间件优化版
const corsMiddleware = (req, res, next) => {
const origin = req.headers.origin;
// 允许的源(可以从配置文件读取)
const allowedOrigins = [
'http://localhost:3000',
'https://localhost:3000',
'http://127.0.0.1:3000',
'https://127.0.0.1:3000'
];
// 设置CORS头
if (allowedOrigins.includes(origin) || !origin) {
res.header('Access-Control-Allow-Origin', origin || '*');
}
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', [
'Origin',
'X-Requested-With',
'Content-Type',
'Accept',
'Authorization',
'x-api-key',
'api-key',
'x-admin-token'
].join(', '));
res.header('Access-Control-Expose-Headers', [
'X-RateLimit-Limit',
'X-RateLimit-Remaining',
'X-RateLimit-Reset',
'X-RateLimit-Policy'
].join(', '));
res.header('Access-Control-Max-Age', '86400'); // 24小时预检缓存
res.header('Access-Control-Allow-Credentials', 'true');
if (req.method === 'OPTIONS') {
res.status(204).end();
} else {
next();
}
};
// 📝 请求日志中间件(优化版)
const requestLogger = (req, res, next) => {
const start = Date.now();
const requestId = Math.random().toString(36).substring(2, 15);
// 添加请求ID到请求对象
req.requestId = requestId;
res.setHeader('X-Request-ID', requestId);
// 获取客户端信息
const clientIP = req.ip || req.connection?.remoteAddress || req.socket?.remoteAddress || 'unknown';
const userAgent = req.get('User-Agent') || 'unknown';
const referer = req.get('Referer') || 'none';
// 记录请求开始
if (req.originalUrl !== '/health') { // 避免健康检查日志过多
logger.request(`▶️ [${requestId}] ${req.method} ${req.originalUrl} | IP: ${clientIP}`);
}
res.on('finish', () => {
const duration = Date.now() - start;
const contentLength = res.get('Content-Length') || '0';
// 构建日志元数据
const logMetadata = {
requestId,
method: req.method,
url: req.originalUrl,
status: res.statusCode,
duration,
contentLength,
ip: clientIP,
userAgent,
referer
};
// 根据状态码选择日志级别
if (res.statusCode >= 500) {
logger.error(`◀️ [${requestId}] ${req.method} ${req.originalUrl} | ${res.statusCode} | ${duration}ms | ${contentLength}B`, logMetadata);
} else if (res.statusCode >= 400) {
logger.warn(`◀️ [${requestId}] ${req.method} ${req.originalUrl} | ${res.statusCode} | ${duration}ms | ${contentLength}B`, logMetadata);
} else if (req.originalUrl !== '/health') {
logger.request(`◀️ [${requestId}] ${req.method} ${req.originalUrl} | ${res.statusCode} | ${duration}ms | ${contentLength}B`, logMetadata);
}
// API Key相关日志
if (req.apiKey) {
logger.api(`📱 [${requestId}] Request from ${req.apiKey.name} (${req.apiKey.id}) | ${duration}ms`);
}
// 慢请求警告
if (duration > 5000) {
logger.warn(`🐌 [${requestId}] Slow request detected: ${duration}ms for ${req.method} ${req.originalUrl}`);
}
});
res.on('error', (error) => {
const duration = Date.now() - start;
logger.error(`💥 [${requestId}] Response error after ${duration}ms:`, error);
});
next();
};
// 🛡️ 安全中间件(增强版)
const securityMiddleware = (req, res, next) => {
// 设置基础安全头
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// 添加更多安全头
res.setHeader('X-DNS-Prefetch-Control', 'off');
res.setHeader('X-Download-Options', 'noopen');
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
// Cross-Origin-Opener-Policy (仅对可信来源设置)
const host = req.get('host') || '';
const isLocalhost = host.includes('localhost') || host.includes('127.0.0.1') || host.includes('0.0.0.0');
const isHttps = req.secure || req.headers['x-forwarded-proto'] === 'https';
if (isLocalhost || isHttps) {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
res.setHeader('Origin-Agent-Cluster', '?1');
}
// Content Security Policy (适用于web界面)
if (req.path.startsWith('/web') || req.path === '/') {
res.setHeader('Content-Security-Policy', [
'default-src \'self\'',
'script-src \'self\' \'unsafe-inline\' \'unsafe-eval\' https://unpkg.com https://cdn.tailwindcss.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net',
'style-src \'self\' \'unsafe-inline\' https://cdn.tailwindcss.com https://cdnjs.cloudflare.com',
'font-src \'self\' https://cdnjs.cloudflare.com',
'img-src \'self\' data:',
'connect-src \'self\'',
'frame-ancestors \'none\'',
'base-uri \'self\'',
'form-action \'self\''
].join('; '));
}
// Strict Transport Security (HTTPS)
if (req.secure || req.headers['x-forwarded-proto'] === 'https') {
res.setHeader('Strict-Transport-Security', 'max-age=15552000; includeSubDomains');
}
// 移除泄露服务器信息的头
res.removeHeader('X-Powered-By');
res.removeHeader('Server');
// 防止信息泄露
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
next();
};
// 🚨 错误处理中间件(增强版)
const errorHandler = (error, req, res, _next) => {
const requestId = req.requestId || 'unknown';
const isDevelopment = process.env.NODE_ENV === 'development';
// 记录详细错误信息
logger.error(`💥 [${requestId}] Unhandled error:`, {
error: error.message,
stack: error.stack,
url: req.originalUrl,
method: req.method,
ip: req.ip || 'unknown',
userAgent: req.get('User-Agent') || 'unknown',
apiKey: req.apiKey ? req.apiKey.id : 'none',
admin: req.admin ? req.admin.username : 'none'
});
// 确定HTTP状态码
let statusCode = 500;
let errorMessage = 'Internal Server Error';
let userMessage = 'Something went wrong';
if (error.status && error.status >= 400 && error.status < 600) {
statusCode = error.status;
}
// 根据错误类型提供友好的错误消息
switch (error.name) {
case 'ValidationError':
statusCode = 400;
errorMessage = 'Validation Error';
userMessage = 'Invalid input data';
break;
case 'CastError':
statusCode = 400;
errorMessage = 'Cast Error';
userMessage = 'Invalid data format';
break;
case 'MongoError':
case 'RedisError':
statusCode = 503;
errorMessage = 'Database Error';
userMessage = 'Database temporarily unavailable';
break;
case 'TimeoutError':
statusCode = 408;
errorMessage = 'Request Timeout';
userMessage = 'Request took too long to process';
break;
default:
if (error.message && !isDevelopment) {
// 在生产环境中,只显示安全的错误消息
if (error.message.includes('ECONNREFUSED')) {
userMessage = 'Service temporarily unavailable';
} else if (error.message.includes('timeout')) {
userMessage = 'Request timeout';
}
}
}
// 设置响应头
res.setHeader('X-Request-ID', requestId);
// 构建错误响应
const errorResponse = {
error: errorMessage,
message: isDevelopment ? error.message : userMessage,
requestId,
timestamp: new Date().toISOString()
};
// 在开发环境中包含更多调试信息
if (isDevelopment) {
errorResponse.stack = error.stack;
errorResponse.url = req.originalUrl;
errorResponse.method = req.method;
}
res.status(statusCode).json(errorResponse);
};
// 🌐 全局速率限制中间件(延迟初始化)
let rateLimiter = null;
const getRateLimiter = () => {
if (!rateLimiter) {
try {
const client = redis.getClient();
if (!client) {
logger.warn('⚠️ Redis client not available for rate limiter');
return null;
}
rateLimiter = new RateLimiterRedis({
storeClient: client,
keyPrefix: 'global_rate_limit',
points: 1000, // 请求数量
duration: 900, // 15分钟 (900秒)
blockDuration: 900, // 阻塞时间15分钟
});
logger.info('✅ Rate limiter initialized successfully');
} catch (error) {
logger.warn('⚠️ Rate limiter initialization failed, using fallback', { error: error.message });
return null;
}
}
return rateLimiter;
};
const globalRateLimit = async (req, res, next) => {
// 跳过健康检查和内部请求
if (req.path === '/health' || req.path === '/api/health') {
return next();
}
const limiter = getRateLimiter();
if (!limiter) {
// 如果Redis不可用直接跳过速率限制
return next();
}
const clientIP = req.ip || req.connection?.remoteAddress || 'unknown';
try {
await limiter.consume(clientIP);
next();
} catch (rejRes) {
const remainingPoints = rejRes.remainingPoints || 0;
const msBeforeNext = rejRes.msBeforeNext || 900000;
logger.security(`🚦 Global rate limit exceeded for IP: ${clientIP}`);
res.set({
'Retry-After': Math.round(msBeforeNext / 1000) || 900,
'X-RateLimit-Limit': 1000,
'X-RateLimit-Remaining': remainingPoints,
'X-RateLimit-Reset': new Date(Date.now() + msBeforeNext).toISOString()
});
res.status(429).json({
error: 'Too Many Requests',
message: 'Too many requests from this IP, please try again later.',
retryAfter: Math.round(msBeforeNext / 1000)
});
}
};
// 📊 请求大小限制中间件
const requestSizeLimit = (req, res, next) => {
const maxSize = 10 * 1024 * 1024; // 10MB
const contentLength = parseInt(req.headers['content-length'] || '0');
if (contentLength > maxSize) {
logger.security(`🚨 Request too large: ${contentLength} bytes from ${req.ip}`);
return res.status(413).json({
error: 'Payload Too Large',
message: 'Request body size exceeds limit',
limit: '10MB'
});
}
next();
};
module.exports = {
authenticateApiKey,
authenticateAdmin,
corsMiddleware,
requestLogger,
securityMiddleware,
errorHandler,
globalRateLimit,
requestSizeLimit
};

678
src/models/redis.js Normal file
View File

@@ -0,0 +1,678 @@
const Redis = require('ioredis');
const config = require('../../config/config');
const logger = require('../utils/logger');
class RedisClient {
constructor() {
this.client = null;
this.isConnected = false;
}
async connect() {
try {
this.client = new Redis({
host: config.redis.host,
port: config.redis.port,
password: config.redis.password,
db: config.redis.db,
retryDelayOnFailover: config.redis.retryDelayOnFailover,
maxRetriesPerRequest: config.redis.maxRetriesPerRequest,
lazyConnect: config.redis.lazyConnect
});
this.client.on('connect', () => {
this.isConnected = true;
logger.info('🔗 Redis connected successfully');
});
this.client.on('error', (err) => {
this.isConnected = false;
logger.error('❌ Redis connection error:', err);
});
this.client.on('close', () => {
this.isConnected = false;
logger.warn('⚠️ Redis connection closed');
});
await this.client.connect();
return this.client;
} catch (error) {
logger.error('💥 Failed to connect to Redis:', error);
throw error;
}
}
async disconnect() {
if (this.client) {
await this.client.quit();
this.isConnected = false;
logger.info('👋 Redis disconnected');
}
}
getClient() {
if (!this.client || !this.isConnected) {
logger.warn('⚠️ Redis client is not connected');
return null;
}
return this.client;
}
// 安全获取客户端(用于关键操作)
getClientSafe() {
if (!this.client || !this.isConnected) {
throw new Error('Redis client is not connected');
}
return this.client;
}
// 🔑 API Key 相关操作
async setApiKey(keyId, keyData, hashedKey = null) {
const key = `apikey:${keyId}`;
const client = this.getClientSafe();
// 维护哈希映射表(用于快速查找)
// hashedKey参数是实际的哈希值用于建立映射
if (hashedKey) {
await client.hset('apikey:hash_map', hashedKey, keyId);
}
await client.hset(key, keyData);
await client.expire(key, 86400 * 365); // 1年过期
}
async getApiKey(keyId) {
const key = `apikey:${keyId}`;
return await this.client.hgetall(key);
}
async deleteApiKey(keyId) {
const key = `apikey:${keyId}`;
// 获取要删除的API Key哈希值以便从映射表中移除
const keyData = await this.client.hgetall(key);
if (keyData && keyData.apiKey) {
// keyData.apiKey现在存储的是哈希值直接从映射表删除
await this.client.hdel('apikey:hash_map', keyData.apiKey);
}
return await this.client.del(key);
}
async getAllApiKeys() {
const keys = await this.client.keys('apikey:*');
const apiKeys = [];
for (const key of keys) {
// 过滤掉hash_map它不是真正的API Key
if (key === 'apikey:hash_map') {
continue;
}
const keyData = await this.client.hgetall(key);
if (keyData && Object.keys(keyData).length > 0) {
apiKeys.push({ id: key.replace('apikey:', ''), ...keyData });
}
}
return apiKeys;
}
// 🔍 通过哈希值查找API Key性能优化
async findApiKeyByHash(hashedKey) {
// 使用反向映射表hash -> keyId
const keyId = await this.client.hget('apikey:hash_map', hashedKey);
if (!keyId) {
return null;
}
const keyData = await this.client.hgetall(`apikey:${keyId}`);
if (keyData && Object.keys(keyData).length > 0) {
return { id: keyId, ...keyData };
}
// 如果数据不存在,清理映射表
await this.client.hdel('apikey:hash_map', hashedKey);
return null;
}
// 📊 使用统计相关操作支持缓存token统计和模型信息
async incrementTokenUsage(keyId, tokens, inputTokens = 0, outputTokens = 0, cacheCreateTokens = 0, cacheReadTokens = 0, model = 'unknown') {
const key = `usage:${keyId}`;
const today = new Date().toISOString().split('T')[0];
const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
const daily = `usage:daily:${keyId}:${today}`;
const monthly = `usage:monthly:${keyId}:${currentMonth}`;
// 按模型统计的键
const modelDaily = `usage:model:daily:${model}:${today}`;
const modelMonthly = `usage:model:monthly:${model}:${currentMonth}`;
// API Key级别的模型统计
const keyModelDaily = `usage:${keyId}:model:daily:${model}:${today}`;
const keyModelMonthly = `usage:${keyId}:model:monthly:${model}:${currentMonth}`;
// 智能处理输入输出token分配
const finalInputTokens = inputTokens || 0;
const finalOutputTokens = outputTokens || (finalInputTokens > 0 ? 0 : tokens);
const finalCacheCreateTokens = cacheCreateTokens || 0;
const finalCacheReadTokens = cacheReadTokens || 0;
// 重新计算真实的总token数包括缓存token
const totalTokens = finalInputTokens + finalOutputTokens + finalCacheCreateTokens + finalCacheReadTokens;
// 核心token不包括缓存- 用于与历史数据兼容
const coreTokens = finalInputTokens + finalOutputTokens;
await Promise.all([
// 核心token统计保持向后兼容
this.client.hincrby(key, 'totalTokens', coreTokens),
this.client.hincrby(key, 'totalInputTokens', finalInputTokens),
this.client.hincrby(key, 'totalOutputTokens', finalOutputTokens),
// 缓存token统计新增
this.client.hincrby(key, 'totalCacheCreateTokens', finalCacheCreateTokens),
this.client.hincrby(key, 'totalCacheReadTokens', finalCacheReadTokens),
this.client.hincrby(key, 'totalAllTokens', totalTokens), // 包含所有类型的总token
// 请求计数
this.client.hincrby(key, 'totalRequests', 1),
// 每日统计
this.client.hincrby(daily, 'tokens', coreTokens),
this.client.hincrby(daily, 'inputTokens', finalInputTokens),
this.client.hincrby(daily, 'outputTokens', finalOutputTokens),
this.client.hincrby(daily, 'cacheCreateTokens', finalCacheCreateTokens),
this.client.hincrby(daily, 'cacheReadTokens', finalCacheReadTokens),
this.client.hincrby(daily, 'allTokens', totalTokens),
this.client.hincrby(daily, 'requests', 1),
// 每月统计
this.client.hincrby(monthly, 'tokens', coreTokens),
this.client.hincrby(monthly, 'inputTokens', finalInputTokens),
this.client.hincrby(monthly, 'outputTokens', finalOutputTokens),
this.client.hincrby(monthly, 'cacheCreateTokens', finalCacheCreateTokens),
this.client.hincrby(monthly, 'cacheReadTokens', finalCacheReadTokens),
this.client.hincrby(monthly, 'allTokens', totalTokens),
this.client.hincrby(monthly, 'requests', 1),
// 按模型统计 - 每日
this.client.hincrby(modelDaily, 'inputTokens', finalInputTokens),
this.client.hincrby(modelDaily, 'outputTokens', finalOutputTokens),
this.client.hincrby(modelDaily, 'cacheCreateTokens', finalCacheCreateTokens),
this.client.hincrby(modelDaily, 'cacheReadTokens', finalCacheReadTokens),
this.client.hincrby(modelDaily, 'allTokens', totalTokens),
this.client.hincrby(modelDaily, 'requests', 1),
// 按模型统计 - 每月
this.client.hincrby(modelMonthly, 'inputTokens', finalInputTokens),
this.client.hincrby(modelMonthly, 'outputTokens', finalOutputTokens),
this.client.hincrby(modelMonthly, 'cacheCreateTokens', finalCacheCreateTokens),
this.client.hincrby(modelMonthly, 'cacheReadTokens', finalCacheReadTokens),
this.client.hincrby(modelMonthly, 'allTokens', totalTokens),
this.client.hincrby(modelMonthly, 'requests', 1),
// API Key级别的模型统计 - 每日
this.client.hincrby(keyModelDaily, 'inputTokens', finalInputTokens),
this.client.hincrby(keyModelDaily, 'outputTokens', finalOutputTokens),
this.client.hincrby(keyModelDaily, 'cacheCreateTokens', finalCacheCreateTokens),
this.client.hincrby(keyModelDaily, 'cacheReadTokens', finalCacheReadTokens),
this.client.hincrby(keyModelDaily, 'allTokens', totalTokens),
this.client.hincrby(keyModelDaily, 'requests', 1),
// API Key级别的模型统计 - 每月
this.client.hincrby(keyModelMonthly, 'inputTokens', finalInputTokens),
this.client.hincrby(keyModelMonthly, 'outputTokens', finalOutputTokens),
this.client.hincrby(keyModelMonthly, 'cacheCreateTokens', finalCacheCreateTokens),
this.client.hincrby(keyModelMonthly, 'cacheReadTokens', finalCacheReadTokens),
this.client.hincrby(keyModelMonthly, 'allTokens', totalTokens),
this.client.hincrby(keyModelMonthly, 'requests', 1),
// 设置过期时间
this.client.expire(daily, 86400 * 32), // 32天过期
this.client.expire(monthly, 86400 * 365), // 1年过期
this.client.expire(modelDaily, 86400 * 32), // 模型每日统计32天过期
this.client.expire(modelMonthly, 86400 * 365), // 模型每月统计1年过期
this.client.expire(keyModelDaily, 86400 * 32), // API Key模型每日统计32天过期
this.client.expire(keyModelMonthly, 86400 * 365) // API Key模型每月统计1年过期
]);
}
async getUsageStats(keyId) {
const totalKey = `usage:${keyId}`;
const today = new Date().toISOString().split('T')[0];
const dailyKey = `usage:daily:${keyId}:${today}`;
const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
const monthlyKey = `usage:monthly:${keyId}:${currentMonth}`;
const [total, daily, monthly] = await Promise.all([
this.client.hgetall(totalKey),
this.client.hgetall(dailyKey),
this.client.hgetall(monthlyKey)
]);
// 获取API Key的创建时间来计算平均值
const keyData = await this.client.hgetall(`apikey:${keyId}`);
const createdAt = keyData.createdAt ? new Date(keyData.createdAt) : new Date();
const now = new Date();
const daysSinceCreated = Math.max(1, Math.ceil((now - createdAt) / (1000 * 60 * 60 * 24)));
const totalTokens = parseInt(total.totalTokens) || 0;
const totalRequests = parseInt(total.totalRequests) || 0;
// 计算平均RPM (requests per minute) 和 TPM (tokens per minute)
const totalMinutes = Math.max(1, daysSinceCreated * 24 * 60);
const avgRPM = totalRequests / totalMinutes;
const avgTPM = totalTokens / totalMinutes;
// 处理旧数据兼容性支持缓存token
const handleLegacyData = (data) => {
// 优先使用total*字段(存储时使用的字段)
const tokens = parseInt(data.totalTokens) || parseInt(data.tokens) || 0;
const inputTokens = parseInt(data.totalInputTokens) || parseInt(data.inputTokens) || 0;
const outputTokens = parseInt(data.totalOutputTokens) || parseInt(data.outputTokens) || 0;
const requests = parseInt(data.totalRequests) || parseInt(data.requests) || 0;
// 新增缓存token字段
const cacheCreateTokens = parseInt(data.totalCacheCreateTokens) || parseInt(data.cacheCreateTokens) || 0;
const cacheReadTokens = parseInt(data.totalCacheReadTokens) || parseInt(data.cacheReadTokens) || 0;
const allTokens = parseInt(data.totalAllTokens) || parseInt(data.allTokens) || 0;
const totalFromSeparate = inputTokens + outputTokens;
if (totalFromSeparate === 0 && tokens > 0) {
// 旧数据:没有输入输出分离
return {
tokens,
inputTokens: Math.round(tokens * 0.3), // 假设30%为输入
outputTokens: Math.round(tokens * 0.7), // 假设70%为输出
cacheCreateTokens: 0, // 旧数据没有缓存token
cacheReadTokens: 0,
allTokens: tokens, // 对于旧数据allTokens等于tokens
requests
};
} else {
// 新数据或无数据
return {
tokens,
inputTokens,
outputTokens,
cacheCreateTokens,
cacheReadTokens,
allTokens: allTokens || (inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens), // 计算或使用存储的值
requests
};
}
};
const totalData = handleLegacyData(total);
const dailyData = handleLegacyData(daily);
const monthlyData = handleLegacyData(monthly);
return {
total: totalData,
daily: dailyData,
monthly: monthlyData,
averages: {
rpm: Math.round(avgRPM * 100) / 100, // 保留2位小数
tpm: Math.round(avgTPM * 100) / 100,
dailyRequests: Math.round((totalRequests / daysSinceCreated) * 100) / 100,
dailyTokens: Math.round((totalTokens / daysSinceCreated) * 100) / 100
}
};
}
// 🧹 清空所有API Key的使用统计数据
async resetAllUsageStats() {
const client = this.getClientSafe();
const stats = {
deletedKeys: 0,
deletedDailyKeys: 0,
deletedMonthlyKeys: 0,
resetApiKeys: 0
};
try {
// 获取所有API Key ID
const apiKeyIds = [];
const apiKeyKeys = await client.keys('apikey:*');
for (const key of apiKeyKeys) {
if (key === 'apikey:hash_map') continue; // 跳过哈希映射表
const keyId = key.replace('apikey:', '');
apiKeyIds.push(keyId);
}
// 清空每个API Key的使用统计
for (const keyId of apiKeyIds) {
// 删除总体使用统计
const usageKey = `usage:${keyId}`;
const deleted = await client.del(usageKey);
if (deleted > 0) {
stats.deletedKeys++;
}
// 删除该API Key的每日统计使用精确的keyId匹配
const dailyKeys = await client.keys(`usage:daily:${keyId}:*`);
if (dailyKeys.length > 0) {
await client.del(...dailyKeys);
stats.deletedDailyKeys += dailyKeys.length;
}
// 删除该API Key的每月统计使用精确的keyId匹配
const monthlyKeys = await client.keys(`usage:monthly:${keyId}:*`);
if (monthlyKeys.length > 0) {
await client.del(...monthlyKeys);
stats.deletedMonthlyKeys += monthlyKeys.length;
}
// 重置API Key的lastUsedAt字段
const keyData = await client.hgetall(`apikey:${keyId}`);
if (keyData && Object.keys(keyData).length > 0) {
keyData.lastUsedAt = '';
await client.hset(`apikey:${keyId}`, keyData);
stats.resetApiKeys++;
}
}
// 额外清理删除所有可能遗漏的usage相关键
const allUsageKeys = await client.keys('usage:*');
if (allUsageKeys.length > 0) {
await client.del(...allUsageKeys);
stats.deletedKeys += allUsageKeys.length;
}
return stats;
} catch (error) {
throw new Error(`Failed to reset usage stats: ${error.message}`);
}
}
// 🏢 Claude 账户管理
async setClaudeAccount(accountId, accountData) {
const key = `claude:account:${accountId}`;
await this.client.hset(key, accountData);
}
async getClaudeAccount(accountId) {
const key = `claude:account:${accountId}`;
return await this.client.hgetall(key);
}
async getAllClaudeAccounts() {
const keys = await this.client.keys('claude:account:*');
const accounts = [];
for (const key of keys) {
const accountData = await this.client.hgetall(key);
if (accountData && Object.keys(accountData).length > 0) {
accounts.push({ id: key.replace('claude:account:', ''), ...accountData });
}
}
return accounts;
}
async deleteClaudeAccount(accountId) {
const key = `claude:account:${accountId}`;
return await this.client.del(key);
}
// 🔐 会话管理
async setSession(sessionId, sessionData, ttl = 86400) {
const key = `session:${sessionId}`;
await this.client.hset(key, sessionData);
await this.client.expire(key, ttl);
}
async getSession(sessionId) {
const key = `session:${sessionId}`;
return await this.client.hgetall(key);
}
async deleteSession(sessionId) {
const key = `session:${sessionId}`;
return await this.client.del(key);
}
// 🗝️ API Key哈希索引管理
async setApiKeyHash(hashedKey, keyData, ttl = 0) {
const key = `apikey_hash:${hashedKey}`;
await this.client.hset(key, keyData);
if (ttl > 0) {
await this.client.expire(key, ttl);
}
}
async getApiKeyHash(hashedKey) {
const key = `apikey_hash:${hashedKey}`;
return await this.client.hgetall(key);
}
async deleteApiKeyHash(hashedKey) {
const key = `apikey_hash:${hashedKey}`;
return await this.client.del(key);
}
// 🔗 OAuth会话管理
async setOAuthSession(sessionId, sessionData, ttl = 600) { // 10分钟过期
const key = `oauth:${sessionId}`;
await this.client.hset(key, sessionData);
await this.client.expire(key, ttl);
}
async getOAuthSession(sessionId) {
const key = `oauth:${sessionId}`;
return await this.client.hgetall(key);
}
async deleteOAuthSession(sessionId) {
const key = `oauth:${sessionId}`;
return await this.client.del(key);
}
// 🚦 速率限制
async checkRateLimit(identifier, limit = 100, window = 60) {
const key = `ratelimit:${identifier}`;
const current = await this.client.incr(key);
if (current === 1) {
await this.client.expire(key, window);
}
return {
allowed: current <= limit,
current,
limit,
resetTime: await this.client.ttl(key)
};
}
// 📈 系统统计
async getSystemStats() {
const keys = await Promise.all([
this.client.keys('apikey:*'),
this.client.keys('claude:account:*'),
this.client.keys('usage:*')
]);
return {
totalApiKeys: keys[0].length,
totalClaudeAccounts: keys[1].length,
totalUsageRecords: keys[2].length
};
}
// 📊 获取今日系统统计
async getTodayStats() {
try {
const today = new Date().toISOString().split('T')[0];
const dailyKeys = await this.client.keys(`usage:daily:*:${today}`);
let totalRequestsToday = 0;
let totalTokensToday = 0;
let totalInputTokensToday = 0;
let totalOutputTokensToday = 0;
let totalCacheCreateTokensToday = 0;
let totalCacheReadTokensToday = 0;
// 批量获取所有今日数据,提高性能
if (dailyKeys.length > 0) {
const pipeline = this.client.pipeline();
dailyKeys.forEach(key => pipeline.hgetall(key));
const results = await pipeline.exec();
for (const [error, dailyData] of results) {
if (error || !dailyData) continue;
totalRequestsToday += parseInt(dailyData.requests) || 0;
const currentDayTokens = parseInt(dailyData.tokens) || 0;
totalTokensToday += currentDayTokens;
// 处理旧数据兼容性如果有总token但没有输入输出分离则使用总token作为输出token
const inputTokens = parseInt(dailyData.inputTokens) || 0;
const outputTokens = parseInt(dailyData.outputTokens) || 0;
const cacheCreateTokens = parseInt(dailyData.cacheCreateTokens) || 0;
const cacheReadTokens = parseInt(dailyData.cacheReadTokens) || 0;
const totalTokensFromSeparate = inputTokens + outputTokens;
if (totalTokensFromSeparate === 0 && currentDayTokens > 0) {
// 旧数据没有输入输出分离假设70%为输出30%为输入(基于一般对话比例)
totalOutputTokensToday += Math.round(currentDayTokens * 0.7);
totalInputTokensToday += Math.round(currentDayTokens * 0.3);
} else {
// 新数据:使用实际的输入输出分离
totalInputTokensToday += inputTokens;
totalOutputTokensToday += outputTokens;
}
// 添加cache token统计
totalCacheCreateTokensToday += cacheCreateTokens;
totalCacheReadTokensToday += cacheReadTokens;
}
}
// 获取今日创建的API Key数量批量优化
const allApiKeys = await this.client.keys('apikey:*');
let apiKeysCreatedToday = 0;
if (allApiKeys.length > 0) {
const pipeline = this.client.pipeline();
allApiKeys.forEach(key => pipeline.hget(key, 'createdAt'));
const results = await pipeline.exec();
for (const [error, createdAt] of results) {
if (!error && createdAt && createdAt.startsWith(today)) {
apiKeysCreatedToday++;
}
}
}
return {
requestsToday: totalRequestsToday,
tokensToday: totalTokensToday,
inputTokensToday: totalInputTokensToday,
outputTokensToday: totalOutputTokensToday,
cacheCreateTokensToday: totalCacheCreateTokensToday,
cacheReadTokensToday: totalCacheReadTokensToday,
apiKeysCreatedToday
};
} catch (error) {
console.error('Error getting today stats:', error);
return {
requestsToday: 0,
tokensToday: 0,
inputTokensToday: 0,
outputTokensToday: 0,
cacheCreateTokensToday: 0,
cacheReadTokensToday: 0,
apiKeysCreatedToday: 0
};
}
}
// 📈 获取系统总的平均RPM和TPM
async getSystemAverages() {
try {
const allApiKeys = await this.client.keys('apikey:*');
let totalRequests = 0;
let totalTokens = 0;
let totalInputTokens = 0;
let totalOutputTokens = 0;
let oldestCreatedAt = new Date();
// 批量获取所有usage数据和key数据提高性能
const usageKeys = allApiKeys.map(key => `usage:${key.replace('apikey:', '')}`);
const pipeline = this.client.pipeline();
// 添加所有usage查询
usageKeys.forEach(key => pipeline.hgetall(key));
// 添加所有key数据查询
allApiKeys.forEach(key => pipeline.hgetall(key));
const results = await pipeline.exec();
const usageResults = results.slice(0, usageKeys.length);
const keyResults = results.slice(usageKeys.length);
for (let i = 0; i < allApiKeys.length; i++) {
const totalData = usageResults[i][1] || {};
const keyData = keyResults[i][1] || {};
totalRequests += parseInt(totalData.totalRequests) || 0;
totalTokens += parseInt(totalData.totalTokens) || 0;
totalInputTokens += parseInt(totalData.totalInputTokens) || 0;
totalOutputTokens += parseInt(totalData.totalOutputTokens) || 0;
const createdAt = keyData.createdAt ? new Date(keyData.createdAt) : new Date();
if (createdAt < oldestCreatedAt) {
oldestCreatedAt = createdAt;
}
}
const now = new Date();
// 保持与个人API Key计算一致的算法按天计算然后转换为分钟
const daysSinceOldest = Math.max(1, Math.ceil((now - oldestCreatedAt) / (1000 * 60 * 60 * 24)));
const totalMinutes = daysSinceOldest * 24 * 60;
return {
systemRPM: Math.round((totalRequests / totalMinutes) * 100) / 100,
systemTPM: Math.round((totalTokens / totalMinutes) * 100) / 100,
totalInputTokens,
totalOutputTokens,
totalTokens
};
} catch (error) {
console.error('Error getting system averages:', error);
return {
systemRPM: 0,
systemTPM: 0,
totalInputTokens: 0,
totalOutputTokens: 0,
totalTokens: 0
};
}
}
// 🧹 清理过期数据
async cleanup() {
try {
const patterns = [
'usage:daily:*',
'ratelimit:*',
'session:*',
'oauth:*'
];
for (const pattern of patterns) {
const keys = await this.client.keys(pattern);
const pipeline = this.client.pipeline();
for (const key of keys) {
const ttl = await this.client.ttl(key);
if (ttl === -1) { // 没有设置过期时间的键
if (key.startsWith('oauth:')) {
pipeline.expire(key, 600); // OAuth会话设置10分钟过期
} else {
pipeline.expire(key, 86400); // 其他设置1天过期
}
}
}
await pipeline.exec();
}
logger.info('🧹 Redis cleanup completed');
} catch (error) {
logger.error('❌ Redis cleanup failed:', error);
}
}
}
module.exports = new RedisClient();

901
src/routes/admin.js Normal file
View File

@@ -0,0 +1,901 @@
const express = require('express');
const apiKeyService = require('../services/apiKeyService');
const claudeAccountService = require('../services/claudeAccountService');
const redis = require('../models/redis');
const { authenticateAdmin } = require('../middleware/auth');
const logger = require('../utils/logger');
const oauthHelper = require('../utils/oauthHelper');
const CostCalculator = require('../utils/costCalculator');
const pricingService = require('../services/pricingService');
const router = express.Router();
// 🔑 API Keys 管理
// 获取所有API Keys
router.get('/api-keys', authenticateAdmin, async (req, res) => {
try {
const apiKeys = await apiKeyService.getAllApiKeys();
res.json({ success: true, data: apiKeys });
} catch (error) {
logger.error('❌ Failed to get API keys:', error);
res.status(500).json({ error: 'Failed to get API keys', message: error.message });
}
});
// 创建新的API Key
router.post('/api-keys', authenticateAdmin, async (req, res) => {
try {
const {
name,
description,
tokenLimit,
requestLimit,
expiresAt,
claudeAccountId
} = req.body;
// 输入验证
if (!name || typeof name !== 'string' || name.trim().length === 0) {
return res.status(400).json({ error: 'Name is required and must be a non-empty string' });
}
if (name.length > 100) {
return res.status(400).json({ error: 'Name must be less than 100 characters' });
}
if (description && (typeof description !== 'string' || description.length > 500)) {
return res.status(400).json({ error: 'Description must be a string with less than 500 characters' });
}
if (tokenLimit && (!Number.isInteger(Number(tokenLimit)) || Number(tokenLimit) < 0)) {
return res.status(400).json({ error: 'Token limit must be a non-negative integer' });
}
if (requestLimit && (!Number.isInteger(Number(requestLimit)) || Number(requestLimit) < 0)) {
return res.status(400).json({ error: 'Request limit must be a non-negative integer' });
}
const newKey = await apiKeyService.generateApiKey({
name,
description,
tokenLimit,
requestLimit,
expiresAt,
claudeAccountId
});
logger.success(`🔑 Admin created new API key: ${name}`);
res.json({ success: true, data: newKey });
} catch (error) {
logger.error('❌ Failed to create API key:', error);
res.status(500).json({ error: 'Failed to create API key', message: error.message });
}
});
// 更新API Key
router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
try {
const { keyId } = req.params;
const updates = req.body;
await apiKeyService.updateApiKey(keyId, updates);
logger.success(`📝 Admin updated API key: ${keyId}`);
res.json({ success: true, message: 'API key updated successfully' });
} catch (error) {
logger.error('❌ Failed to update API key:', error);
res.status(500).json({ error: 'Failed to update API key', message: error.message });
}
});
// 删除API Key
router.delete('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
try {
const { keyId } = req.params;
await apiKeyService.deleteApiKey(keyId);
logger.success(`🗑️ Admin deleted API key: ${keyId}`);
res.json({ success: true, message: 'API key deleted successfully' });
} catch (error) {
logger.error('❌ Failed to delete API key:', error);
res.status(500).json({ error: 'Failed to delete API key', message: error.message });
}
});
// 🏢 Claude 账户管理
// 生成OAuth授权URL
router.post('/claude-accounts/generate-auth-url', authenticateAdmin, async (req, res) => {
try {
const { proxy } = req.body; // 接收代理配置
const oauthParams = await oauthHelper.generateOAuthParams();
// 将codeVerifier和state临时存储到Redis用于后续验证
const sessionId = require('crypto').randomUUID();
await redis.setOAuthSession(sessionId, {
codeVerifier: oauthParams.codeVerifier,
state: oauthParams.state,
codeChallenge: oauthParams.codeChallenge,
proxy: proxy || null, // 存储代理配置
createdAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString() // 10分钟过期
});
logger.success('🔗 Generated OAuth authorization URL with proxy support');
res.json({
success: true,
data: {
authUrl: oauthParams.authUrl,
sessionId: sessionId,
instructions: [
'1. 复制上面的链接到浏览器中打开',
'2. 登录您的 Anthropic 账户',
'3. 同意应用权限',
'4. 复制浏览器地址栏中的完整 URL',
'5. 在添加账户表单中粘贴完整的回调 URL 和授权码'
]
}
});
} catch (error) {
logger.error('❌ Failed to generate OAuth URL:', error);
res.status(500).json({ error: 'Failed to generate OAuth URL', message: error.message });
}
});
// 验证授权码并获取token
router.post('/claude-accounts/exchange-code', authenticateAdmin, async (req, res) => {
try {
const { sessionId, authorizationCode, callbackUrl } = req.body;
if (!sessionId || (!authorizationCode && !callbackUrl)) {
return res.status(400).json({ error: 'Session ID and authorization code (or callback URL) are required' });
}
// 从Redis获取OAuth会话信息
const oauthSession = await redis.getOAuthSession(sessionId);
if (!oauthSession) {
return res.status(400).json({ error: 'Invalid or expired OAuth session' });
}
// 检查会话是否过期
if (new Date() > new Date(oauthSession.expiresAt)) {
await redis.deleteOAuthSession(sessionId);
return res.status(400).json({ error: 'OAuth session has expired, please generate a new authorization URL' });
}
// 统一处理授权码输入可能是直接的code或完整的回调URL
let finalAuthCode;
const inputValue = callbackUrl || authorizationCode;
try {
finalAuthCode = oauthHelper.parseCallbackUrl(inputValue);
} catch (parseError) {
return res.status(400).json({ error: 'Failed to parse authorization input', message: parseError.message });
}
// 交换访问令牌
const tokenData = await oauthHelper.exchangeCodeForTokens(
finalAuthCode,
oauthSession.codeVerifier,
oauthSession.state,
oauthSession.proxy // 传递代理配置
);
// 清理OAuth会话
await redis.deleteOAuthSession(sessionId);
logger.success('🎉 Successfully exchanged authorization code for tokens');
res.json({
success: true,
data: {
claudeAiOauth: tokenData
}
});
} catch (error) {
logger.error('❌ Failed to exchange authorization code:', {
error: error.message,
sessionId: req.body.sessionId,
// 不记录完整的授权码,只记录长度和前几个字符
codeLength: req.body.callbackUrl ? req.body.callbackUrl.length : (req.body.authorizationCode ? req.body.authorizationCode.length : 0),
codePrefix: req.body.callbackUrl ? req.body.callbackUrl.substring(0, 10) + '...' : (req.body.authorizationCode ? req.body.authorizationCode.substring(0, 10) + '...' : 'N/A')
});
res.status(500).json({ error: 'Failed to exchange authorization code', message: error.message });
}
});
// 获取所有Claude账户
router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
try {
const accounts = await claudeAccountService.getAllAccounts();
res.json({ success: true, data: accounts });
} catch (error) {
logger.error('❌ Failed to get Claude accounts:', error);
res.status(500).json({ error: 'Failed to get Claude accounts', message: error.message });
}
});
// 创建新的Claude账户
router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
try {
const {
name,
description,
email,
password,
refreshToken,
claudeAiOauth,
proxy
} = req.body;
if (!name) {
return res.status(400).json({ error: 'Name is required' });
}
const newAccount = await claudeAccountService.createAccount({
name,
description,
email,
password,
refreshToken,
claudeAiOauth,
proxy
});
logger.success(`🏢 Admin created new Claude account: ${name}`);
res.json({ success: true, data: newAccount });
} catch (error) {
logger.error('❌ Failed to create Claude account:', error);
res.status(500).json({ error: 'Failed to create Claude account', message: error.message });
}
});
// 更新Claude账户
router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params;
const updates = req.body;
await claudeAccountService.updateAccount(accountId, updates);
logger.success(`📝 Admin updated Claude account: ${accountId}`);
res.json({ success: true, message: 'Claude account updated successfully' });
} catch (error) {
logger.error('❌ Failed to update Claude account:', error);
res.status(500).json({ error: 'Failed to update Claude account', message: error.message });
}
});
// 删除Claude账户
router.delete('/claude-accounts/:accountId', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params;
await claudeAccountService.deleteAccount(accountId);
logger.success(`🗑️ Admin deleted Claude account: ${accountId}`);
res.json({ success: true, message: 'Claude account deleted successfully' });
} catch (error) {
logger.error('❌ Failed to delete Claude account:', error);
res.status(500).json({ error: 'Failed to delete Claude account', message: error.message });
}
});
// 刷新Claude账户token
router.post('/claude-accounts/:accountId/refresh', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params;
const result = await claudeAccountService.refreshAccountToken(accountId);
logger.success(`🔄 Admin refreshed token for Claude account: ${accountId}`);
res.json({ success: true, data: result });
} catch (error) {
logger.error('❌ Failed to refresh Claude account token:', error);
res.status(500).json({ error: 'Failed to refresh token', message: error.message });
}
});
// 📊 系统统计
// 获取系统概览
router.get('/dashboard', authenticateAdmin, async (req, res) => {
try {
const [, apiKeys, accounts, todayStats, systemAverages] = await Promise.all([
redis.getSystemStats(),
apiKeyService.getAllApiKeys(),
claudeAccountService.getAllAccounts(),
redis.getTodayStats(),
redis.getSystemAverages()
]);
// 计算使用统计包含cache tokens
const totalTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.tokens || 0), 0);
const totalRequestsUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.requests || 0), 0);
const totalInputTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.inputTokens || 0), 0);
const totalOutputTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.outputTokens || 0), 0);
const totalCacheCreateTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.cacheCreateTokens || 0), 0);
const totalCacheReadTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.cacheReadTokens || 0), 0);
const totalAllTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.allTokens || 0), 0);
const activeApiKeys = apiKeys.filter(key => key.isActive).length;
const activeAccounts = accounts.filter(acc => acc.isActive && acc.status === 'active').length;
const dashboard = {
overview: {
totalApiKeys: apiKeys.length,
activeApiKeys,
totalClaudeAccounts: accounts.length,
activeClaudeAccounts: activeAccounts,
totalTokensUsed,
totalRequestsUsed,
totalInputTokensUsed,
totalOutputTokensUsed,
totalCacheCreateTokensUsed,
totalCacheReadTokensUsed,
totalAllTokensUsed
},
recentActivity: {
apiKeysCreatedToday: todayStats.apiKeysCreatedToday,
requestsToday: todayStats.requestsToday,
tokensToday: todayStats.tokensToday,
inputTokensToday: todayStats.inputTokensToday,
outputTokensToday: todayStats.outputTokensToday,
cacheCreateTokensToday: todayStats.cacheCreateTokensToday || 0,
cacheReadTokensToday: todayStats.cacheReadTokensToday || 0
},
systemAverages: {
rpm: systemAverages.systemRPM,
tpm: systemAverages.systemTPM
},
systemHealth: {
redisConnected: redis.isConnected,
claudeAccountsHealthy: activeAccounts > 0,
uptime: process.uptime()
}
};
res.json({ success: true, data: dashboard });
} catch (error) {
logger.error('❌ Failed to get dashboard data:', error);
res.status(500).json({ error: 'Failed to get dashboard data', message: error.message });
}
});
// 获取使用统计
router.get('/usage-stats', authenticateAdmin, async (req, res) => {
try {
const { period = 'daily' } = req.query; // daily, monthly
// 获取基础API Key统计
const apiKeys = await apiKeyService.getAllApiKeys();
const stats = apiKeys.map(key => ({
keyId: key.id,
keyName: key.name,
usage: key.usage
}));
res.json({ success: true, data: { period, stats } });
} catch (error) {
logger.error('❌ Failed to get usage stats:', error);
res.status(500).json({ error: 'Failed to get usage stats', message: error.message });
}
});
// 获取按模型的使用统计和费用
router.get('/model-stats', authenticateAdmin, async (req, res) => {
try {
const { period = 'daily' } = req.query; // daily, monthly
const today = new Date().toISOString().split('T')[0];
const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
logger.info(`📊 Getting global model stats, period: ${period}, today: ${today}, currentMonth: ${currentMonth}`);
const client = redis.getClientSafe();
// 获取所有模型的统计数据
const pattern = period === 'daily' ? `usage:model:daily:*:${today}` : `usage:model:monthly:*:${currentMonth}`;
logger.info(`📊 Searching pattern: ${pattern}`);
const keys = await client.keys(pattern);
logger.info(`📊 Found ${keys.length} matching keys:`, keys);
const modelStats = [];
for (const key of keys) {
const match = key.match(period === 'daily' ?
/usage:model:daily:(.+):\d{4}-\d{2}-\d{2}$/ :
/usage:model:monthly:(.+):\d{4}-\d{2}$/
);
if (!match) {
logger.warn(`📊 Pattern mismatch for key: ${key}`);
continue;
}
const model = match[1];
const data = await client.hgetall(key);
logger.info(`📊 Model ${model} data:`, data);
if (data && Object.keys(data).length > 0) {
const usage = {
input_tokens: parseInt(data.inputTokens) || 0,
output_tokens: parseInt(data.outputTokens) || 0,
cache_creation_input_tokens: parseInt(data.cacheCreateTokens) || 0,
cache_read_input_tokens: parseInt(data.cacheReadTokens) || 0
};
// 计算费用
const costData = CostCalculator.calculateCost(usage, model);
modelStats.push({
model,
period,
requests: parseInt(data.requests) || 0,
inputTokens: usage.input_tokens,
outputTokens: usage.output_tokens,
cacheCreateTokens: usage.cache_creation_input_tokens,
cacheReadTokens: usage.cache_read_input_tokens,
allTokens: parseInt(data.allTokens) || 0,
usage: {
requests: parseInt(data.requests) || 0,
inputTokens: usage.input_tokens,
outputTokens: usage.output_tokens,
cacheCreateTokens: usage.cache_creation_input_tokens,
cacheReadTokens: usage.cache_read_input_tokens,
totalTokens: usage.input_tokens + usage.output_tokens + usage.cache_creation_input_tokens + usage.cache_read_input_tokens
},
costs: costData.costs,
formatted: costData.formatted,
pricing: costData.pricing
});
}
}
// 按总费用排序
modelStats.sort((a, b) => b.costs.total - a.costs.total);
logger.info(`📊 Returning ${modelStats.length} global model stats for period ${period}:`, modelStats);
res.json({ success: true, data: modelStats });
} catch (error) {
logger.error('❌ Failed to get model stats:', error);
res.status(500).json({ error: 'Failed to get model stats', message: error.message });
}
});
// 🔧 系统管理
// 清理过期数据
router.post('/cleanup', authenticateAdmin, async (req, res) => {
try {
const [expiredKeys, errorAccounts] = await Promise.all([
apiKeyService.cleanupExpiredKeys(),
claudeAccountService.cleanupErrorAccounts()
]);
await redis.cleanup();
logger.success(`🧹 Admin triggered cleanup: ${expiredKeys} expired keys, ${errorAccounts} error accounts`);
res.json({
success: true,
message: 'Cleanup completed',
data: {
expiredKeysRemoved: expiredKeys,
errorAccountsReset: errorAccounts
}
});
} catch (error) {
logger.error('❌ Cleanup failed:', error);
res.status(500).json({ error: 'Cleanup failed', message: error.message });
}
});
// 获取使用趋势数据
router.get('/usage-trend', authenticateAdmin, async (req, res) => {
try {
const { days = 7 } = req.query;
const daysCount = parseInt(days) || 7;
const client = redis.getClientSafe();
const trendData = [];
const today = new Date();
// 获取过去N天的数据
for (let i = 0; i < daysCount; i++) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split('T')[0];
// 汇总当天所有API Key的使用数据
const pattern = `usage:daily:*:${dateStr}`;
const keys = await client.keys(pattern);
let dayInputTokens = 0;
let dayOutputTokens = 0;
let dayRequests = 0;
let dayCacheCreateTokens = 0;
let dayCacheReadTokens = 0;
let dayCost = 0;
for (const key of keys) {
const data = await client.hgetall(key);
if (data) {
dayInputTokens += parseInt(data.inputTokens) || 0;
dayOutputTokens += parseInt(data.outputTokens) || 0;
dayRequests += parseInt(data.requests) || 0;
dayCacheCreateTokens += parseInt(data.cacheCreateTokens) || 0;
dayCacheReadTokens += parseInt(data.cacheReadTokens) || 0;
}
}
// 计算当天费用(使用通用模型价格估算)
const usage = {
input_tokens: dayInputTokens,
output_tokens: dayOutputTokens,
cache_creation_input_tokens: dayCacheCreateTokens,
cache_read_input_tokens: dayCacheReadTokens
};
const costResult = CostCalculator.calculateCost(usage, 'unknown');
dayCost = costResult.costs.total;
trendData.push({
date: dateStr,
inputTokens: dayInputTokens,
outputTokens: dayOutputTokens,
requests: dayRequests,
cacheCreateTokens: dayCacheCreateTokens,
cacheReadTokens: dayCacheReadTokens,
totalTokens: dayInputTokens + dayOutputTokens + dayCacheCreateTokens + dayCacheReadTokens,
cost: dayCost,
formattedCost: CostCalculator.formatCost(dayCost)
});
}
// 按日期正序排列
trendData.sort((a, b) => new Date(a.date) - new Date(b.date));
res.json({ success: true, data: trendData });
} catch (error) {
logger.error('❌ Failed to get usage trend:', error);
res.status(500).json({ error: 'Failed to get usage trend', message: error.message });
}
});
// 获取单个API Key的模型统计
router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) => {
try {
const { keyId } = req.params;
const { period = 'monthly', startDate, endDate } = req.query;
logger.info(`📊 Getting model stats for API key: ${keyId}, period: ${period}, startDate: ${startDate}, endDate: ${endDate}`);
const client = redis.getClientSafe();
const today = new Date().toISOString().split('T')[0];
const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
let searchPatterns = [];
if (period === 'custom' && startDate && endDate) {
// 自定义日期范围,生成多个日期的搜索模式
const start = new Date(startDate);
const end = new Date(endDate);
// 确保日期范围有效
if (start > end) {
return res.status(400).json({ error: 'Start date must be before or equal to end date' });
}
// 限制最大范围为31天
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
if (daysDiff > 31) {
return res.status(400).json({ error: 'Date range cannot exceed 31 days' });
}
// 生成日期范围内所有日期的搜索模式
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const dateStr = d.toISOString().split('T')[0];
searchPatterns.push(`usage:${keyId}:model:daily:*:${dateStr}`);
}
logger.info(`📊 Custom date range patterns: ${searchPatterns.length} days from ${startDate} to ${endDate}`);
} else {
// 原有的预设期间逻辑
const pattern = period === 'daily' ?
`usage:${keyId}:model:daily:*:${today}` :
`usage:${keyId}:model:monthly:*:${currentMonth}`;
searchPatterns = [pattern];
logger.info(`📊 Preset period pattern: ${pattern}`);
}
// 汇总所有匹配的数据
const modelStatsMap = new Map();
const modelStats = []; // 定义结果数组
for (const pattern of searchPatterns) {
const keys = await client.keys(pattern);
logger.info(`📊 Pattern ${pattern} found ${keys.length} keys`);
for (const key of keys) {
const match = key.match(/usage:.+:model:daily:(.+):\d{4}-\d{2}-\d{2}$/) ||
key.match(/usage:.+:model:monthly:(.+):\d{4}-\d{2}$/);
if (!match) {
logger.warn(`📊 Pattern mismatch for key: ${key}`);
continue;
}
const model = match[1];
const data = await client.hgetall(key);
if (data && Object.keys(data).length > 0) {
// 累加同一模型的数据
if (!modelStatsMap.has(model)) {
modelStatsMap.set(model, {
requests: 0,
inputTokens: 0,
outputTokens: 0,
cacheCreateTokens: 0,
cacheReadTokens: 0,
allTokens: 0
});
}
const stats = modelStatsMap.get(model);
stats.requests += parseInt(data.requests) || 0;
stats.inputTokens += parseInt(data.inputTokens) || 0;
stats.outputTokens += parseInt(data.outputTokens) || 0;
stats.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0;
stats.cacheReadTokens += parseInt(data.cacheReadTokens) || 0;
stats.allTokens += parseInt(data.allTokens) || 0;
}
}
}
// 将汇总的数据转换为最终结果
for (const [model, stats] of modelStatsMap) {
logger.info(`📊 Model ${model} aggregated data:`, stats);
const usage = {
input_tokens: stats.inputTokens,
output_tokens: stats.outputTokens,
cache_creation_input_tokens: stats.cacheCreateTokens,
cache_read_input_tokens: stats.cacheReadTokens
};
// 使用CostCalculator计算费用
const costData = CostCalculator.calculateCost(usage, model);
modelStats.push({
model,
requests: stats.requests,
inputTokens: stats.inputTokens,
outputTokens: stats.outputTokens,
cacheCreateTokens: stats.cacheCreateTokens,
cacheReadTokens: stats.cacheReadTokens,
allTokens: stats.allTokens,
// 添加费用信息
costs: costData.costs,
formatted: costData.formatted,
pricing: costData.pricing,
usingDynamicPricing: costData.usingDynamicPricing
});
}
// 如果没有找到模型级别的详细数据,尝试从汇总数据中生成展示
if (modelStats.length === 0) {
logger.info(`📊 No detailed model stats found, trying to get aggregate data for API key ${keyId}`);
// 尝试从API Keys列表中获取usage数据作为备选方案
try {
const apiKeys = await apiKeyService.getAllApiKeys();
const targetApiKey = apiKeys.find(key => key.id === keyId);
if (targetApiKey && targetApiKey.usage) {
logger.info(`📊 Found API key usage data from getAllApiKeys for ${keyId}:`, targetApiKey.usage);
// 从汇总数据创建展示条目
let usageData;
if (period === 'custom' || period === 'daily') {
// 对于自定义或日统计使用daily数据或total数据
usageData = targetApiKey.usage.daily || targetApiKey.usage.total;
} else {
// 对于月统计使用monthly数据或total数据
usageData = targetApiKey.usage.monthly || targetApiKey.usage.total;
}
if (usageData && usageData.allTokens > 0) {
const usage = {
input_tokens: usageData.inputTokens || 0,
output_tokens: usageData.outputTokens || 0,
cache_creation_input_tokens: usageData.cacheCreateTokens || 0,
cache_read_input_tokens: usageData.cacheReadTokens || 0
};
// 对于汇总数据,使用默认模型计算费用
const costData = CostCalculator.calculateCost(usage, 'claude-3-5-sonnet-20241022');
modelStats.push({
model: '总体使用 (历史数据)',
requests: usageData.requests || 0,
inputTokens: usageData.inputTokens || 0,
outputTokens: usageData.outputTokens || 0,
cacheCreateTokens: usageData.cacheCreateTokens || 0,
cacheReadTokens: usageData.cacheReadTokens || 0,
allTokens: usageData.allTokens || 0,
// 添加费用信息
costs: costData.costs,
formatted: costData.formatted,
pricing: costData.pricing,
usingDynamicPricing: costData.usingDynamicPricing
});
logger.info('📊 Generated display data from API key usage stats');
} else {
logger.info(`📊 No usage data found for period ${period} in API key data`);
}
} else {
logger.info(`📊 API key ${keyId} not found or has no usage data`);
}
} catch (error) {
logger.error('❌ Error fetching API key usage data:', error);
}
}
// 按总token数降序排列
modelStats.sort((a, b) => b.allTokens - a.allTokens);
logger.info(`📊 Returning ${modelStats.length} model stats for API key ${keyId}:`, modelStats);
res.json({ success: true, data: modelStats });
} catch (error) {
logger.error('❌ Failed to get API key model stats:', error);
res.status(500).json({ error: 'Failed to get API key model stats', message: error.message });
}
});
// 计算总体使用费用
router.get('/usage-costs', authenticateAdmin, async (req, res) => {
try {
const { period = 'all' } = req.query; // all, today, monthly
logger.info(`💰 Calculating usage costs for period: ${period}`);
// 获取所有API Keys的使用统计
const apiKeys = await apiKeyService.getAllApiKeys();
let totalCosts = {
inputCost: 0,
outputCost: 0,
cacheCreateCost: 0,
cacheReadCost: 0,
totalCost: 0
};
let modelCosts = {};
// 按模型统计费用
const client = redis.getClientSafe();
const today = new Date().toISOString().split('T')[0];
const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
let pattern;
if (period === 'today') {
pattern = `usage:model:daily:*:${today}`;
} else if (period === 'monthly') {
pattern = `usage:model:monthly:*:${currentMonth}`;
} else {
// 全部时间使用API Key汇总数据
for (const apiKey of apiKeys) {
if (apiKey.usage && apiKey.usage.total) {
const usage = {
input_tokens: apiKey.usage.total.inputTokens || 0,
output_tokens: apiKey.usage.total.outputTokens || 0,
cache_creation_input_tokens: apiKey.usage.total.cacheCreateTokens || 0,
cache_read_input_tokens: apiKey.usage.total.cacheReadTokens || 0
};
// 计算未知模型的费用(汇总数据)
const costResult = CostCalculator.calculateCost(usage, 'unknown');
totalCosts.inputCost += costResult.costs.input;
totalCosts.outputCost += costResult.costs.output;
totalCosts.cacheCreateCost += costResult.costs.cacheWrite;
totalCosts.cacheReadCost += costResult.costs.cacheRead;
totalCosts.totalCost += costResult.costs.total;
}
}
res.json({
success: true,
data: {
period,
totalCosts: {
...totalCosts,
formatted: {
inputCost: CostCalculator.formatCost(totalCosts.inputCost),
outputCost: CostCalculator.formatCost(totalCosts.outputCost),
cacheCreateCost: CostCalculator.formatCost(totalCosts.cacheCreateCost),
cacheReadCost: CostCalculator.formatCost(totalCosts.cacheReadCost),
totalCost: CostCalculator.formatCost(totalCosts.totalCost)
}
},
modelCosts: [],
pricingServiceStatus: pricingService.getStatus()
}
});
return;
}
// 对于今日或本月从Redis获取详细的模型统计
const keys = await client.keys(pattern);
for (const key of keys) {
const match = key.match(period === 'today' ?
/usage:model:daily:(.+):\d{4}-\d{2}-\d{2}$/ :
/usage:model:monthly:(.+):\d{4}-\d{2}$/
);
if (!match) continue;
const model = match[1];
const data = await client.hgetall(key);
if (data && Object.keys(data).length > 0) {
const usage = {
input_tokens: parseInt(data.inputTokens) || 0,
output_tokens: parseInt(data.outputTokens) || 0,
cache_creation_input_tokens: parseInt(data.cacheCreateTokens) || 0,
cache_read_input_tokens: parseInt(data.cacheReadTokens) || 0
};
const costResult = CostCalculator.calculateCost(usage, model);
// 累加总费用
totalCosts.inputCost += costResult.costs.input;
totalCosts.outputCost += costResult.costs.output;
totalCosts.cacheCreateCost += costResult.costs.cacheWrite;
totalCosts.cacheReadCost += costResult.costs.cacheRead;
totalCosts.totalCost += costResult.costs.total;
// 记录模型费用
modelCosts[model] = {
model,
requests: parseInt(data.requests) || 0,
usage,
costs: costResult.costs,
formatted: costResult.formatted,
usingDynamicPricing: costResult.usingDynamicPricing
};
}
}
res.json({
success: true,
data: {
period,
totalCosts: {
...totalCosts,
formatted: {
inputCost: CostCalculator.formatCost(totalCosts.inputCost),
outputCost: CostCalculator.formatCost(totalCosts.outputCost),
cacheCreateCost: CostCalculator.formatCost(totalCosts.cacheCreateCost),
cacheReadCost: CostCalculator.formatCost(totalCosts.cacheReadCost),
totalCost: CostCalculator.formatCost(totalCosts.totalCost)
}
},
modelCosts: Object.values(modelCosts).sort((a, b) => b.costs.total - a.costs.total),
pricingServiceStatus: pricingService.getStatus()
}
});
} catch (error) {
logger.error('❌ Failed to calculate usage costs:', error);
res.status(500).json({ error: 'Failed to calculate usage costs', message: error.message });
}
});
module.exports = router;

225
src/routes/api.js Normal file
View File

@@ -0,0 +1,225 @@
const express = require('express');
const claudeRelayService = require('../services/claudeRelayService');
const apiKeyService = require('../services/apiKeyService');
const { authenticateApiKey } = require('../middleware/auth');
const logger = require('../utils/logger');
const router = express.Router();
// 🚀 Claude API messages 端点
router.post('/v1/messages', authenticateApiKey, async (req, res) => {
try {
const startTime = Date.now();
// 严格的输入验证
if (!req.body || typeof req.body !== 'object') {
return res.status(400).json({
error: 'Invalid request',
message: 'Request body must be a valid JSON object'
});
}
if (!req.body.messages || !Array.isArray(req.body.messages)) {
return res.status(400).json({
error: 'Invalid request',
message: 'Missing or invalid field: messages (must be an array)'
});
}
if (req.body.messages.length === 0) {
return res.status(400).json({
error: 'Invalid request',
message: 'Messages array cannot be empty'
});
}
// 检查是否为流式请求
const isStream = req.body.stream === true;
logger.api(`🚀 Processing ${isStream ? 'stream' : 'non-stream'} request for key: ${req.apiKey.name}`);
if (isStream) {
// 流式响应 - 只使用官方真实usage数据
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('Access-Control-Allow-Origin', '*');
let usageDataCaptured = false;
// 使用自定义流处理器来捕获usage数据
await claudeRelayService.relayStreamRequestWithUsageCapture(req.body, req.apiKey, res, (usageData) => {
// 回调函数当检测到完整usage数据时记录真实token使用量
logger.info('🎯 Usage callback triggered with complete data:', JSON.stringify(usageData, null, 2));
if (usageData && usageData.input_tokens !== undefined && usageData.output_tokens !== undefined) {
const inputTokens = usageData.input_tokens || 0;
const outputTokens = usageData.output_tokens || 0;
const cacheCreateTokens = usageData.cache_creation_input_tokens || 0;
const cacheReadTokens = usageData.cache_read_input_tokens || 0;
const model = usageData.model || 'unknown';
// 记录真实的token使用量包含模型信息和所有4种token
apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model).catch(error => {
logger.error('❌ Failed to record stream usage:', error);
});
usageDataCaptured = true;
logger.api(`📊 Stream usage recorded (real) - Model: ${model}, Input: ${inputTokens}, Output: ${outputTokens}, Cache Create: ${cacheCreateTokens}, Cache Read: ${cacheReadTokens}, Total: ${inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens} tokens`);
} else {
logger.warn('⚠️ Usage callback triggered but data is incomplete:', JSON.stringify(usageData));
}
});
// 流式请求完成后 - 如果没有捕获到usage数据记录警告但不进行估算
setTimeout(() => {
if (!usageDataCaptured) {
logger.warn('⚠️ No usage data captured from SSE stream - no statistics recorded (official data only)');
}
}, 1000); // 1秒后检查
} else {
// 非流式响应 - 只使用官方真实usage数据
logger.info('📄 Starting non-streaming request', {
apiKeyId: req.apiKey.id,
apiKeyName: req.apiKey.name
});
const response = await claudeRelayService.relayRequest(req.body, req.apiKey);
logger.info('📡 Claude API response received', {
statusCode: response.statusCode,
headers: JSON.stringify(response.headers),
bodyLength: response.body ? response.body.length : 0
});
res.status(response.statusCode);
// 设置响应头
Object.keys(response.headers).forEach(key => {
if (key.toLowerCase() !== 'content-encoding') {
res.setHeader(key, response.headers[key]);
}
});
let usageRecorded = false;
// 尝试解析JSON响应并提取usage信息
try {
const jsonData = JSON.parse(response.body);
logger.info('📊 Parsed Claude API response:', JSON.stringify(jsonData, null, 2));
// 从Claude API响应中提取usage信息完整的token分类体系
if (jsonData.usage && jsonData.usage.input_tokens !== undefined && jsonData.usage.output_tokens !== undefined) {
const inputTokens = jsonData.usage.input_tokens || 0;
const outputTokens = jsonData.usage.output_tokens || 0;
const cacheCreateTokens = jsonData.usage.cache_creation_input_tokens || 0;
const cacheReadTokens = jsonData.usage.cache_read_input_tokens || 0;
const model = jsonData.model || req.body.model || 'unknown';
// 记录真实的token使用量包含模型信息和所有4种token
await apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model);
usageRecorded = true;
logger.api(`📊 Non-stream usage recorded (real) - Model: ${model}, Input: ${inputTokens}, Output: ${outputTokens}, Cache Create: ${cacheCreateTokens}, Cache Read: ${cacheReadTokens}, Total: ${inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens} tokens`);
} else {
logger.warn('⚠️ No usage data found in Claude API JSON response');
}
res.json(jsonData);
} catch (parseError) {
logger.warn('⚠️ Failed to parse Claude API response as JSON:', parseError.message);
logger.info('📄 Raw response body:', response.body);
res.send(response.body);
}
// 如果没有记录usage只记录警告不进行估算
if (!usageRecorded) {
logger.warn('⚠️ No usage data recorded for non-stream request - no statistics recorded (official data only)');
}
}
const duration = Date.now() - startTime;
logger.api(`✅ Request completed in ${duration}ms for key: ${req.apiKey.name}`);
} catch (error) {
logger.error('❌ Claude relay error:', error);
if (!res.headersSent) {
res.status(500).json({
error: 'Relay service error',
message: error.message
});
}
}
});
// 🏥 健康检查端点
router.get('/health', async (req, res) => {
try {
const healthStatus = await claudeRelayService.healthCheck();
res.status(healthStatus.healthy ? 200 : 503).json({
status: healthStatus.healthy ? 'healthy' : 'unhealthy',
service: 'claude-relay-service',
version: '1.0.0',
...healthStatus
});
} catch (error) {
logger.error('❌ Health check error:', error);
res.status(503).json({
status: 'unhealthy',
service: 'claude-relay-service',
error: error.message,
timestamp: new Date().toISOString()
});
}
});
// 📊 API Key状态检查端点
router.get('/v1/key-info', authenticateApiKey, async (req, res) => {
try {
const usage = await apiKeyService.getUsageStats(req.apiKey.id);
res.json({
keyInfo: {
id: req.apiKey.id,
name: req.apiKey.name,
tokenLimit: req.apiKey.tokenLimit,
requestLimit: req.apiKey.requestLimit,
usage
},
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error('❌ Key info error:', error);
res.status(500).json({
error: 'Failed to get key info',
message: error.message
});
}
});
// 📈 使用统计端点
router.get('/v1/usage', authenticateApiKey, async (req, res) => {
try {
const usage = await apiKeyService.getUsageStats(req.apiKey.id);
res.json({
usage,
limits: {
tokens: req.apiKey.tokenLimit,
requests: req.apiKey.requestLimit
},
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error('❌ Usage stats error:', error);
res.status(500).json({
error: 'Failed to get usage stats',
message: error.message
});
}
});
module.exports = router;

202
src/routes/web.js Normal file
View File

@@ -0,0 +1,202 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const path = require('path');
const fs = require('fs');
const redis = require('../models/redis');
const logger = require('../utils/logger');
const config = require('../../config/config');
const router = express.Router();
// 🏠 服务静态文件
router.use('/assets', express.static(path.join(__dirname, '../../web/assets')));
// 🔒 Web管理界面文件白名单 - 仅允许这些特定文件
const ALLOWED_FILES = {
'index.html': {
path: path.join(__dirname, '../../web/admin/index.html'),
contentType: 'text/html; charset=utf-8'
},
'app.js': {
path: path.join(__dirname, '../../web/admin/app.js'),
contentType: 'application/javascript; charset=utf-8'
},
'style.css': {
path: path.join(__dirname, '../../web/admin/style.css'),
contentType: 'text/css; charset=utf-8'
}
};
// 🛡️ 安全文件服务函数
function serveWhitelistedFile(req, res, filename) {
const fileConfig = ALLOWED_FILES[filename];
if (!fileConfig) {
logger.security(`🚨 Attempted access to non-whitelisted file: ${filename}`);
return res.status(404).json({ error: 'File not found' });
}
try {
// 检查文件是否存在
if (!fs.existsSync(fileConfig.path)) {
logger.error(`❌ Whitelisted file not found: ${fileConfig.path}`);
return res.status(404).json({ error: 'File not found' });
}
// 读取并返回文件内容
const content = fs.readFileSync(fileConfig.path, 'utf8');
res.setHeader('Content-Type', fileConfig.contentType);
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.send(content);
logger.info(`📄 Served whitelisted file: ${filename}`);
} catch (error) {
logger.error(`❌ Error serving file ${filename}:`, error);
res.status(500).json({ error: 'Internal server error' });
}
}
// 🔐 管理员登录
router.post('/auth/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({
error: 'Missing credentials',
message: 'Username and password are required'
});
}
// 从Redis获取管理员信息
const adminData = await redis.getSession('admin_credentials');
if (!adminData || Object.keys(adminData).length === 0) {
return res.status(401).json({
error: 'Invalid credentials',
message: 'Invalid username or password'
});
}
// 验证用户名和密码
const isValidUsername = adminData.username === username;
const isValidPassword = await bcrypt.compare(password, adminData.passwordHash);
if (!isValidUsername || !isValidPassword) {
logger.security(`🔒 Failed login attempt for username: ${username}`);
return res.status(401).json({
error: 'Invalid credentials',
message: 'Invalid username or password'
});
}
// 生成会话token
const sessionId = crypto.randomBytes(32).toString('hex');
// 存储会话
const sessionData = {
username: adminData.username,
loginTime: new Date().toISOString(),
lastActivity: new Date().toISOString()
};
await redis.setSession(sessionId, sessionData, config.security.adminSessionTimeout);
// 更新最后登录时间
adminData.lastLogin = new Date().toISOString();
await redis.setSession('admin_credentials', adminData);
logger.success(`🔐 Admin login successful: ${username}`);
res.json({
success: true,
token: sessionId,
expiresIn: config.security.adminSessionTimeout
});
} catch (error) {
logger.error('❌ Login error:', error);
res.status(500).json({
error: 'Login failed',
message: 'Internal server error'
});
}
});
// 🚪 管理员登出
router.post('/auth/logout', async (req, res) => {
try {
const token = req.headers['authorization']?.replace('Bearer ', '') || req.cookies?.adminToken;
if (token) {
await redis.deleteSession(token);
logger.success('🚪 Admin logout successful');
}
res.json({ success: true, message: 'Logout successful' });
} catch (error) {
logger.error('❌ Logout error:', error);
res.status(500).json({
error: 'Logout failed',
message: 'Internal server error'
});
}
});
// 🔄 刷新token
router.post('/auth/refresh', async (req, res) => {
try {
const token = req.headers['authorization']?.replace('Bearer ', '') || req.cookies?.adminToken;
if (!token) {
return res.status(401).json({
error: 'No token provided',
message: 'Authentication required'
});
}
const sessionData = await redis.getSession(token);
if (!sessionData) {
return res.status(401).json({
error: 'Invalid token',
message: 'Session expired or invalid'
});
}
// 更新最后活动时间
sessionData.lastActivity = new Date().toISOString();
await redis.setSession(token, sessionData, config.security.adminSessionTimeout);
res.json({
success: true,
token: token,
expiresIn: config.security.adminSessionTimeout
});
} catch (error) {
logger.error('❌ Token refresh error:', error);
res.status(500).json({
error: 'Token refresh failed',
message: 'Internal server error'
});
}
});
// 🌐 Web管理界面路由 - 使用固定白名单
router.get('/', (req, res) => {
serveWhitelistedFile(req, res, 'index.html');
});
router.get('/app.js', (req, res) => {
serveWhitelistedFile(req, res, 'app.js');
});
router.get('/style.css', (req, res) => {
serveWhitelistedFile(req, res, 'style.css');
});
module.exports = router;

View File

@@ -0,0 +1,271 @@
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;
}
// 🔑 生成新的API Key
async generateApiKey(options = {}) {
const {
name = 'Unnamed Key',
description = '',
tokenLimit = config.limits.defaultTokenLimit,
requestLimit = config.limits.defaultRequestLimit,
expiresAt = null,
claudeAccountId = null,
isActive = true
} = options;
// 生成简单的API Key (64字符十六进制)
const apiKey = `${this.prefix}${this._generateSecretKey()}`;
const keyId = uuidv4();
const hashedKey = this._hashApiKey(apiKey);
const keyData = {
id: keyId,
name,
description,
apiKey: hashedKey,
tokenLimit: String(tokenLimit ?? 0),
requestLimit: String(requestLimit ?? 0),
isActive: String(isActive),
claudeAccountId: claudeAccountId || '',
createdAt: new Date().toISOString(),
lastUsedAt: '',
expiresAt: expiresAt || '',
createdBy: 'admin' // 可以根据需要扩展用户系统
};
// 保存API Key数据并建立哈希映射
await redis.setApiKey(keyId, keyData, hashedKey);
logger.success(`🔑 Generated new API key: ${name} (${keyId})`);
return {
id: keyId,
apiKey, // 只在创建时返回完整的key
name: keyData.name,
description: keyData.description,
tokenLimit: parseInt(keyData.tokenLimit),
requestLimit: parseInt(keyData.requestLimit),
isActive: keyData.isActive === 'true',
claudeAccountId: keyData.claudeAccountId,
createdAt: keyData.createdAt,
expiresAt: keyData.expiresAt,
createdBy: keyData.createdBy
};
}
// 🔍 验证API Key
async validateApiKey(apiKey) {
try {
if (!apiKey || !apiKey.startsWith(this.prefix)) {
return { valid: false, error: 'Invalid API key format' };
}
// 计算API Key的哈希值
const hashedKey = this._hashApiKey(apiKey);
// 通过哈希值直接查找API Key性能优化
const keyData = await redis.findApiKeyByHash(hashedKey);
if (!keyData) {
return { valid: false, error: 'API key not found' };
}
// 检查是否激活
if (keyData.isActive !== 'true') {
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' };
}
// 检查使用限制
const usage = await redis.getUsageStats(keyData.id);
const tokenLimit = parseInt(keyData.tokenLimit);
const requestLimit = parseInt(keyData.requestLimit);
if (tokenLimit > 0 && usage.total.tokens >= tokenLimit) {
return { valid: false, error: 'Token limit exceeded' };
}
if (requestLimit > 0 && usage.total.requests >= requestLimit) {
return { valid: false, error: 'Request limit exceeded' };
}
// 更新最后使用时间优化只在实际API调用时更新而不是验证时
// 注意lastUsedAt的更新已移至recordUsage方法中
logger.api(`🔓 API key validated successfully: ${keyData.id}`);
return {
valid: true,
keyData: {
id: keyData.id,
name: keyData.name,
claudeAccountId: keyData.claudeAccountId,
tokenLimit: parseInt(keyData.tokenLimit),
requestLimit: parseInt(keyData.requestLimit),
usage
}
};
} catch (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();
// 为每个key添加使用统计
for (const key of apiKeys) {
key.usage = await redis.getUsageStats(key.id);
key.tokenLimit = parseInt(key.tokenLimit);
key.requestLimit = parseInt(key.requestLimit);
key.isActive = key.isActive === 'true';
delete key.apiKey; // 不返回哈希后的key
}
return apiKeys;
} catch (error) {
logger.error('❌ Failed to get API keys:', error);
throw error;
}
}
// 📝 更新API Key
async updateApiKey(keyId, updates) {
try {
const keyData = await redis.getApiKey(keyId);
if (!keyData || Object.keys(keyData).length === 0) {
throw new Error('API key not found');
}
// 允许更新的字段
const allowedUpdates = ['name', 'description', 'tokenLimit', 'requestLimit', 'isActive', 'claudeAccountId', 'expiresAt'];
const updatedData = { ...keyData };
for (const [field, value] of Object.entries(updates)) {
if (allowedUpdates.includes(field)) {
updatedData[field] = (value != null ? value : '').toString();
}
}
updatedData.updatedAt = new Date().toISOString();
// 更新时不需要重新建立哈希映射因为API Key本身没有变化
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;
}
}
// 🗑️ 删除API Key
async deleteApiKey(keyId) {
try {
const result = await redis.deleteApiKey(keyId);
if (result === 0) {
throw new Error('API key not found');
}
logger.success(`🗑️ Deleted API key: ${keyId}`);
return { success: true };
} catch (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') {
try {
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
await redis.incrementTokenUsage(keyId, totalTokens, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model);
// 更新最后使用时间(性能优化:只在实际使用时更新)
const keyData = await redis.getApiKey(keyId);
if (keyData && Object.keys(keyData).length > 0) {
keyData.lastUsedAt = new Date().toISOString();
// 使用记录时不需要重新建立哈希映射
await redis.setApiKey(keyId, keyData);
}
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);
}
}
// 🔐 生成密钥
_generateSecretKey() {
return crypto.randomBytes(32).toString('hex');
}
// 🔒 哈希API Key
_hashApiKey(apiKey) {
return crypto.createHash('sha256').update(apiKey + config.security.encryptionKey).digest('hex');
}
// 📈 获取使用统计
async getUsageStats(keyId) {
return await redis.getUsageStats(keyId);
}
// 🚦 检查速率限制
async checkRateLimit(keyId, limit = null) {
const rateLimit = limit || config.rateLimit.maxRequests;
const window = Math.floor(config.rateLimit.windowMs / 1000);
return await redis.checkRateLimit(`apikey:${keyId}`, rateLimit, window);
}
// 🧹 清理过期的API Keys
async cleanupExpiredKeys() {
try {
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) {
await redis.deleteApiKey(key.id);
cleanedCount++;
}
}
if (cleanedCount > 0) {
logger.success(`🧹 Cleaned up ${cleanedCount} expired API keys`);
}
return cleanedCount;
} catch (error) {
logger.error('❌ Failed to cleanup expired keys:', error);
return 0;
}
}
}
module.exports = new ApiKeyService();

View File

@@ -0,0 +1,452 @@
const { v4: uuidv4 } = require('uuid');
const crypto = require('crypto');
const { SocksProxyAgent } = require('socks-proxy-agent');
const { HttpsProxyAgent } = require('https-proxy-agent');
const axios = require('axios');
const redis = require('../models/redis');
const logger = require('../utils/logger');
const config = require('../../config/config');
class ClaudeAccountService {
constructor() {
this.claudeApiUrl = 'https://console.anthropic.com/v1/oauth/token';
this.claudeOauthClientId = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
// 加密相关常量
this.ENCRYPTION_ALGORITHM = 'aes-256-cbc';
this.ENCRYPTION_SALT = 'salt';
}
// 🏢 创建Claude账户
async createAccount(options = {}) {
const {
name = 'Unnamed Account',
description = '',
email = '',
password = '',
refreshToken = '',
claudeAiOauth = null, // Claude标准格式的OAuth数据
proxy = null, // { type: 'socks5', host: 'localhost', port: 1080, username: '', password: '' }
isActive = true
} = options;
const accountId = uuidv4();
let accountData;
if (claudeAiOauth) {
// 使用Claude标准格式的OAuth数据
accountData = {
id: accountId,
name,
description,
email: this._encryptSensitiveData(email),
password: this._encryptSensitiveData(password),
claudeAiOauth: this._encryptSensitiveData(JSON.stringify(claudeAiOauth)),
accessToken: this._encryptSensitiveData(claudeAiOauth.accessToken),
refreshToken: this._encryptSensitiveData(claudeAiOauth.refreshToken),
expiresAt: claudeAiOauth.expiresAt.toString(),
scopes: claudeAiOauth.scopes.join(' '),
proxy: proxy ? JSON.stringify(proxy) : '',
isActive: isActive.toString(),
createdAt: new Date().toISOString(),
lastUsedAt: '',
lastRefreshAt: '',
status: 'active', // 有OAuth数据的账户直接设为active
errorMessage: ''
};
} else {
// 兼容旧格式
accountData = {
id: accountId,
name,
description,
email: this._encryptSensitiveData(email),
password: this._encryptSensitiveData(password),
refreshToken: this._encryptSensitiveData(refreshToken),
accessToken: '',
expiresAt: '',
scopes: '',
proxy: proxy ? JSON.stringify(proxy) : '',
isActive: isActive.toString(),
createdAt: new Date().toISOString(),
lastUsedAt: '',
lastRefreshAt: '',
status: 'created', // created, active, expired, error
errorMessage: ''
};
}
await redis.setClaudeAccount(accountId, accountData);
logger.success(`🏢 Created Claude account: ${name} (${accountId})`);
return {
id: accountId,
name,
description,
email,
isActive,
proxy,
status: accountData.status,
createdAt: accountData.createdAt,
expiresAt: accountData.expiresAt,
scopes: claudeAiOauth ? claudeAiOauth.scopes : []
};
}
// 🔄 刷新Claude账户token
async refreshAccountToken(accountId) {
try {
const accountData = await redis.getClaudeAccount(accountId);
if (!accountData || Object.keys(accountData).length === 0) {
throw new Error('Account not found');
}
const refreshToken = this._decryptSensitiveData(accountData.refreshToken);
if (!refreshToken) {
throw new Error('No refresh token available');
}
// 创建代理agent
const agent = this._createProxyAgent(accountData.proxy);
const response = await axios.post(this.claudeApiUrl, {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.claudeOauthClientId
}, {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json, text/plain, */*',
'User-Agent': 'claude-relay-service/1.0.0'
},
httpsAgent: agent,
timeout: 30000
});
if (response.status === 200) {
const { access_token, refresh_token, expires_in } = response.data;
// 更新账户数据
accountData.accessToken = this._encryptSensitiveData(access_token);
accountData.refreshToken = this._encryptSensitiveData(refresh_token);
accountData.expiresAt = (Date.now() + (expires_in * 1000)).toString();
accountData.lastRefreshAt = new Date().toISOString();
accountData.status = 'active';
accountData.errorMessage = '';
await redis.setClaudeAccount(accountId, accountData);
logger.success(`🔄 Refreshed token for account: ${accountData.name} (${accountId})`);
return {
success: true,
accessToken: access_token,
expiresAt: accountData.expiresAt
};
} else {
throw new Error(`Token refresh failed with status: ${response.status}`);
}
} catch (error) {
logger.error(`❌ Failed to refresh token for account ${accountId}:`, error);
// 更新错误状态
const accountData = await redis.getClaudeAccount(accountId);
if (accountData) {
accountData.status = 'error';
accountData.errorMessage = error.message;
await redis.setClaudeAccount(accountId, accountData);
}
throw error;
}
}
// 🎯 获取有效的访问token
async getValidAccessToken(accountId) {
try {
const accountData = await redis.getClaudeAccount(accountId);
if (!accountData || Object.keys(accountData).length === 0) {
throw new Error('Account not found');
}
if (accountData.isActive !== 'true') {
throw new Error('Account is disabled');
}
// 检查token是否过期
const expiresAt = parseInt(accountData.expiresAt);
const now = Date.now();
if (!expiresAt || now >= (expiresAt - 10000)) { // 10秒提前刷新
logger.info(`🔄 Token expired/expiring for account ${accountId}, refreshing...`);
const refreshResult = await this.refreshAccountToken(accountId);
return refreshResult.accessToken;
}
const accessToken = this._decryptSensitiveData(accountData.accessToken);
if (!accessToken) {
throw new Error('No access token available');
}
// 更新最后使用时间
accountData.lastUsedAt = new Date().toISOString();
await redis.setClaudeAccount(accountId, accountData);
return accessToken;
} catch (error) {
logger.error(`❌ Failed to get valid access token for account ${accountId}:`, error);
throw error;
}
}
// 📋 获取所有Claude账户
async getAllAccounts() {
try {
const accounts = await redis.getAllClaudeAccounts();
// 处理返回数据,移除敏感信息
return accounts.map(account => ({
id: account.id,
name: account.name,
description: account.description,
email: account.email ? this._maskEmail(this._decryptSensitiveData(account.email)) : '',
isActive: account.isActive === 'true',
proxy: account.proxy ? JSON.parse(account.proxy) : null,
status: account.status,
errorMessage: account.errorMessage,
createdAt: account.createdAt,
lastUsedAt: account.lastUsedAt,
lastRefreshAt: account.lastRefreshAt,
expiresAt: account.expiresAt
}));
} catch (error) {
logger.error('❌ Failed to get Claude accounts:', error);
throw error;
}
}
// 📝 更新Claude账户
async updateAccount(accountId, updates) {
try {
const accountData = await redis.getClaudeAccount(accountId);
if (!accountData || Object.keys(accountData).length === 0) {
throw new Error('Account not found');
}
const allowedUpdates = ['name', 'description', 'email', 'password', 'refreshToken', 'proxy', 'isActive'];
const updatedData = { ...accountData };
for (const [field, value] of Object.entries(updates)) {
if (allowedUpdates.includes(field)) {
if (['email', 'password', 'refreshToken'].includes(field)) {
updatedData[field] = this._encryptSensitiveData(value);
} else if (field === 'proxy') {
updatedData[field] = value ? JSON.stringify(value) : '';
} else {
updatedData[field] = value.toString();
}
}
}
updatedData.updatedAt = new Date().toISOString();
await redis.setClaudeAccount(accountId, updatedData);
logger.success(`📝 Updated Claude account: ${accountId}`);
return { success: true };
} catch (error) {
logger.error('❌ Failed to update Claude account:', error);
throw error;
}
}
// 🗑️ 删除Claude账户
async deleteAccount(accountId) {
try {
const result = await redis.deleteClaudeAccount(accountId);
if (result === 0) {
throw new Error('Account not found');
}
logger.success(`🗑️ Deleted Claude account: ${accountId}`);
return { success: true };
} catch (error) {
logger.error('❌ Failed to delete Claude account:', error);
throw error;
}
}
// 🎯 智能选择可用账户
async selectAvailableAccount() {
try {
const accounts = await redis.getAllClaudeAccounts();
const activeAccounts = accounts.filter(account =>
account.isActive === 'true' &&
account.status !== 'error'
);
if (activeAccounts.length === 0) {
throw new Error('No active Claude accounts available');
}
// 优先选择最近刷新过token的账户
const sortedAccounts = activeAccounts.sort((a, b) => {
const aLastRefresh = new Date(a.lastRefreshAt || 0).getTime();
const bLastRefresh = new Date(b.lastRefreshAt || 0).getTime();
return bLastRefresh - aLastRefresh;
});
return sortedAccounts[0].id;
} catch (error) {
logger.error('❌ Failed to select available account:', error);
throw error;
}
}
// 🌐 创建代理agent
_createProxyAgent(proxyConfig) {
if (!proxyConfig) {
return null;
}
try {
const proxy = JSON.parse(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');
// 将IV和加密数据一起返回用:分隔
return iv.toString('hex') + ':' + encrypted;
} catch (error) {
logger.error('❌ Encryption error:', error);
return data;
}
}
// 🔓 解密敏感数据
_decryptSensitiveData(encryptedData) {
if (!encryptedData) return '';
try {
// 检查是否是新格式包含IV
if (encryptedData.includes(':')) {
// 新格式iv:encryptedData
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;
}
}
// 旧格式或格式错误,尝试旧方式解密(向后兼容)
// 注意在新版本Node.js中这将失败但我们会捕获错误
try {
const decipher = crypto.createDecipher('aes-256-cbc', config.security.encryptionKey);
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (oldError) {
// 如果旧方式也失败,返回原数据
logger.warn('⚠️ Could not decrypt data, returning as-is:', oldError.message);
return encryptedData;
}
} catch (error) {
logger.error('❌ Decryption error:', error);
return encryptedData;
}
}
// 🔑 生成加密密钥(辅助方法)
_generateEncryptionKey() {
return crypto.scryptSync(config.security.encryptionKey, this.ENCRYPTION_SALT, 32);
}
// 🎭 掩码邮箱地址
_maskEmail(email) {
if (!email || !email.includes('@')) return email;
const [username, domain] = email.split('@');
const maskedUsername = username.length > 2
? `${username.slice(0, 2)}***${username.slice(-1)}`
: `${username.slice(0, 1)}***`;
return `${maskedUsername}@${domain}`;
}
// 🧹 清理错误账户
async cleanupErrorAccounts() {
try {
const accounts = await redis.getAllClaudeAccounts();
let cleanedCount = 0;
for (const account of accounts) {
if (account.status === 'error' && account.lastRefreshAt) {
const lastRefresh = new Date(account.lastRefreshAt);
const now = new Date();
const hoursSinceLastRefresh = (now - lastRefresh) / (1000 * 60 * 60);
// 如果错误状态超过24小时尝试重新激活
if (hoursSinceLastRefresh > 24) {
account.status = 'created';
account.errorMessage = '';
await redis.setClaudeAccount(account.id, account);
cleanedCount++;
}
}
}
if (cleanedCount > 0) {
logger.success(`🧹 Reset ${cleanedCount} error accounts`);
}
return cleanedCount;
} catch (error) {
logger.error('❌ Failed to cleanup error accounts:', error);
return 0;
}
}
}
module.exports = new ClaudeAccountService();

View File

@@ -0,0 +1,526 @@
const https = require('https');
const { SocksProxyAgent } = require('socks-proxy-agent');
const { HttpsProxyAgent } = require('https-proxy-agent');
const claudeAccountService = require('./claudeAccountService');
const logger = require('../utils/logger');
const config = require('../../config/config');
class ClaudeRelayService {
constructor() {
this.claudeApiUrl = config.claude.apiUrl;
this.apiVersion = config.claude.apiVersion;
this.betaHeader = config.claude.betaHeader;
this.systemPrompt = config.claude.systemPrompt;
}
// 🚀 转发请求到Claude API
async relayRequest(requestBody, apiKeyData) {
try {
// 选择可用的Claude账户
const accountId = apiKeyData.claudeAccountId || await claudeAccountService.selectAvailableAccount();
logger.info(`📤 Processing API request for key: ${apiKeyData.name || apiKeyData.id}, account: ${accountId}`);
// 获取有效的访问token
const accessToken = await claudeAccountService.getValidAccessToken(accountId);
// 处理请求体
const processedBody = this._processRequestBody(requestBody);
// 获取代理配置
const proxyAgent = await this._getProxyAgent(accountId);
// 发送请求到Claude API
const response = await this._makeClaudeRequest(processedBody, accessToken, proxyAgent);
// 记录成功的API调用
const inputTokens = requestBody.messages ?
requestBody.messages.reduce((sum, msg) => sum + (msg.content?.length || 0), 0) / 4 : 0; // 粗略估算
const outputTokens = response.content ?
response.content.reduce((sum, content) => sum + (content.text?.length || 0), 0) / 4 : 0;
logger.info(`✅ API request completed - Key: ${apiKeyData.name}, Account: ${accountId}, Model: ${requestBody.model}, Input: ~${Math.round(inputTokens)} tokens, Output: ~${Math.round(outputTokens)} tokens`);
return response;
} catch (error) {
logger.error(`❌ Claude relay request failed for key: ${apiKeyData.name || apiKeyData.id}:`, error.message);
throw error;
}
}
// 🔄 处理请求体
_processRequestBody(body) {
if (!body) return body;
// 深拷贝请求体
const processedBody = JSON.parse(JSON.stringify(body));
// 移除cache_control中的ttl字段
this._stripTtlFromCacheControl(processedBody);
// 只有在配置了系统提示时才添加
if (this.systemPrompt && this.systemPrompt.trim()) {
const systemPrompt = {
type: 'text',
text: this.systemPrompt
};
if (processedBody.system) {
if (Array.isArray(processedBody.system)) {
// 如果system数组存在但为空或者没有有效内容则添加系统提示
const hasValidContent = processedBody.system.some(item =>
item && item.text && item.text.trim()
);
if (!hasValidContent) {
processedBody.system = [systemPrompt];
} else {
processedBody.system.unshift(systemPrompt);
}
} else {
throw new Error('system field must be an array');
}
} else {
processedBody.system = [systemPrompt];
}
} else {
// 如果没有配置系统提示且system字段为空则删除它
if (processedBody.system && Array.isArray(processedBody.system)) {
const hasValidContent = processedBody.system.some(item =>
item && item.text && item.text.trim()
);
if (!hasValidContent) {
delete processedBody.system;
}
}
}
return processedBody;
}
// 🧹 移除TTL字段
_stripTtlFromCacheControl(body) {
if (!body || typeof body !== 'object') return;
const processContentArray = (contentArray) => {
if (!Array.isArray(contentArray)) return;
contentArray.forEach(item => {
if (item && typeof item === 'object' && item.cache_control) {
if (item.cache_control.ttl) {
delete item.cache_control.ttl;
logger.debug('🧹 Removed ttl from cache_control');
}
}
});
};
if (Array.isArray(body.system)) {
processContentArray(body.system);
}
if (Array.isArray(body.messages)) {
body.messages.forEach(message => {
if (message && Array.isArray(message.content)) {
processContentArray(message.content);
}
});
}
}
// 🌐 获取代理Agent
async _getProxyAgent(accountId) {
try {
const accountData = await claudeAccountService.getAllAccounts();
const account = accountData.find(acc => acc.id === accountId);
if (!account || !account.proxy) {
return null;
}
const proxy = account.proxy;
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('⚠️ Failed to create proxy agent:', error);
}
return null;
}
// 🔗 发送请求到Claude API
async _makeClaudeRequest(body, accessToken, proxyAgent) {
return new Promise((resolve, reject) => {
const url = new URL(this.claudeApiUrl);
const options = {
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'anthropic-version': this.apiVersion,
'User-Agent': 'claude-relay-service/1.0.0'
},
agent: proxyAgent,
timeout: config.proxy.timeout
};
if (this.betaHeader) {
options.headers['anthropic-beta'] = this.betaHeader;
}
const req = https.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
try {
const response = {
statusCode: res.statusCode,
headers: res.headers,
body: responseData
};
logger.debug(`🔗 Claude API response: ${res.statusCode}`);
resolve(response);
} catch (error) {
logger.error('❌ Failed to parse Claude API response:', error);
reject(error);
}
});
});
req.on('error', (error) => {
logger.error('❌ Claude API request error:', error);
reject(error);
});
req.on('timeout', () => {
req.destroy();
logger.error('❌ Claude API request timeout');
reject(new Error('Request timeout'));
});
// 写入请求体
req.write(JSON.stringify(body));
req.end();
});
}
// 🌊 处理流式响应带usage数据捕获
async relayStreamRequestWithUsageCapture(requestBody, apiKeyData, responseStream, usageCallback) {
try {
// 选择可用的Claude账户
const accountId = apiKeyData.claudeAccountId || await claudeAccountService.selectAvailableAccount();
logger.info(`📡 Processing streaming API request with usage capture for key: ${apiKeyData.name || apiKeyData.id}, account: ${accountId}`);
// 获取有效的访问token
const accessToken = await claudeAccountService.getValidAccessToken(accountId);
// 处理请求体
const processedBody = this._processRequestBody(requestBody);
// 获取代理配置
const proxyAgent = await this._getProxyAgent(accountId);
// 发送流式请求并捕获usage数据
return await this._makeClaudeStreamRequestWithUsageCapture(processedBody, accessToken, proxyAgent, responseStream, usageCallback);
} catch (error) {
logger.error('❌ Claude stream relay with usage capture failed:', error);
throw error;
}
}
// 🌊 发送流式请求到Claude API带usage数据捕获
async _makeClaudeStreamRequestWithUsageCapture(body, accessToken, proxyAgent, responseStream, usageCallback) {
return new Promise((resolve, reject) => {
const url = new URL(this.claudeApiUrl);
const options = {
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'anthropic-version': this.apiVersion,
'User-Agent': 'claude-relay-service/1.0.0'
},
agent: proxyAgent,
timeout: config.proxy.timeout
};
if (this.betaHeader) {
options.headers['anthropic-beta'] = this.betaHeader;
}
const req = https.request(options, (res) => {
// 设置响应头
responseStream.statusCode = res.statusCode;
Object.keys(res.headers).forEach(key => {
responseStream.setHeader(key, res.headers[key]);
});
let buffer = '';
let finalUsageReported = false; // 防止重复统计的标志
let collectedUsageData = {}; // 收集来自不同事件的usage数据
// 监听数据块解析SSE并寻找usage信息
res.on('data', (chunk) => {
const chunkStr = chunk.toString();
// 记录原始SSE数据块
logger.info('📡 Raw SSE chunk received:', {
length: chunkStr.length,
content: chunkStr
});
buffer += chunkStr;
// 处理完整的SSE行
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // 保留最后的不完整行
// 转发已处理的完整行到客户端
if (lines.length > 0) {
const linesToForward = lines.join('\n') + (lines.length > 0 ? '\n' : '');
responseStream.write(linesToForward);
}
for (const line of lines) {
// 记录每个SSE行
if (line.trim()) {
logger.info('📄 SSE Line:', line);
}
// 解析SSE数据寻找usage信息
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) {
// message_start包含input tokens、cache tokens和模型信息
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;
logger.info('📊 Collected input/cache data from message_start:', JSON.stringify(collectedUsageData));
}
// message_delta包含最终的output tokens
if (data.type === 'message_delta' && data.usage && data.usage.output_tokens !== undefined) {
collectedUsageData.output_tokens = data.usage.output_tokens || 0;
logger.info('📊 Collected output data from message_delta:', JSON.stringify(collectedUsageData));
// 如果已经收集到了input数据现在有了output数据可以统计了
if (collectedUsageData.input_tokens !== undefined && !finalUsageReported) {
logger.info('🎯 Complete usage data collected, triggering callback');
usageCallback(collectedUsageData);
finalUsageReported = true;
}
}
} catch (parseError) {
// 忽略JSON解析错误继续处理
logger.debug('🔍 SSE line not JSON or no usage data:', line.slice(0, 100));
}
}
}
});
res.on('end', () => {
// 处理缓冲区中剩余的数据
if (buffer.trim()) {
responseStream.write(buffer);
}
responseStream.end();
// 检查是否捕获到usage数据
if (!finalUsageReported) {
logger.warn('⚠️ Stream completed but no usage data was captured! This indicates a problem with SSE parsing or Claude API response format.');
}
logger.debug('🌊 Claude stream response with usage capture completed');
resolve();
});
});
req.on('error', (error) => {
logger.error('❌ Claude stream request error:', error);
if (!responseStream.headersSent) {
responseStream.writeHead(500, { 'Content-Type': 'application/json' });
}
if (!responseStream.destroyed) {
responseStream.end(JSON.stringify({ error: 'Upstream request failed' }));
}
reject(error);
});
req.on('timeout', () => {
req.destroy();
logger.error('❌ Claude stream request timeout');
if (!responseStream.headersSent) {
responseStream.writeHead(504, { 'Content-Type': 'application/json' });
}
if (!responseStream.destroyed) {
responseStream.end(JSON.stringify({ error: 'Request timeout' }));
}
reject(new Error('Request timeout'));
});
// 处理客户端断开连接
responseStream.on('close', () => {
logger.debug('🔌 Client disconnected, cleaning up stream');
if (!req.destroyed) {
req.destroy();
}
});
// 写入请求体
req.write(JSON.stringify(body));
req.end();
});
}
// 🌊 发送流式请求到Claude API
async _makeClaudeStreamRequest(body, accessToken, proxyAgent, responseStream) {
return new Promise((resolve, reject) => {
const url = new URL(this.claudeApiUrl);
const options = {
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'anthropic-version': this.apiVersion,
'User-Agent': 'claude-relay-service/1.0.0'
},
agent: proxyAgent,
timeout: config.proxy.timeout
};
if (this.betaHeader) {
options.headers['anthropic-beta'] = this.betaHeader;
}
const req = https.request(options, (res) => {
// 设置响应头
responseStream.statusCode = res.statusCode;
Object.keys(res.headers).forEach(key => {
responseStream.setHeader(key, res.headers[key]);
});
// 管道响应数据
res.pipe(responseStream);
res.on('end', () => {
logger.debug('🌊 Claude stream response completed');
resolve();
});
});
req.on('error', (error) => {
logger.error('❌ Claude stream request error:', error);
if (!responseStream.headersSent) {
responseStream.writeHead(500, { 'Content-Type': 'application/json' });
}
if (!responseStream.destroyed) {
responseStream.end(JSON.stringify({ error: 'Upstream request failed' }));
}
reject(error);
});
req.on('timeout', () => {
req.destroy();
logger.error('❌ Claude stream request timeout');
if (!responseStream.headersSent) {
responseStream.writeHead(504, { 'Content-Type': 'application/json' });
}
if (!responseStream.destroyed) {
responseStream.end(JSON.stringify({ error: 'Request timeout' }));
}
reject(new Error('Request timeout'));
});
// 处理客户端断开连接
responseStream.on('close', () => {
logger.debug('🔌 Client disconnected, cleaning up stream');
if (!req.destroyed) {
req.destroy();
}
});
// 写入请求体
req.write(JSON.stringify(body));
req.end();
});
}
// 🔄 重试逻辑
async _retryRequest(requestFunc, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await requestFunc();
} catch (error) {
lastError = error;
if (i < maxRetries - 1) {
const delay = Math.pow(2, i) * 1000; // 指数退避
logger.warn(`⏳ Retry ${i + 1}/${maxRetries} in ${delay}ms: ${error.message}`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
// 🎯 健康检查
async healthCheck() {
try {
const accounts = await claudeAccountService.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('❌ Health check failed:', error);
return {
healthy: false,
error: error.message,
timestamp: new Date().toISOString()
};
}
}
}
module.exports = new ClaudeRelayService();

View File

@@ -0,0 +1,234 @@
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.pricingData = null;
this.lastUpdated = null;
this.updateInterval = 24 * 60 * 60 * 1000; // 24小时
}
// 初始化价格服务
async initialize() {
try {
// 确保data目录存在
if (!fs.existsSync(this.dataDir)) {
fs.mkdirSync(this.dataDir, { recursive: true });
logger.info('📁 Created data directory');
}
// 检查是否需要下载或更新价格数据
await this.checkAndUpdatePricing();
// 设置定时更新
setInterval(() => {
this.checkAndUpdatePricing();
}, this.updateInterval);
logger.success('💰 Pricing service initialized successfully');
} catch (error) {
logger.error('❌ Failed to initialize pricing service:', error);
}
}
// 检查并更新价格数据
async checkAndUpdatePricing() {
try {
const needsUpdate = this.needsUpdate();
if (needsUpdate) {
logger.info('🔄 Updating model pricing data...');
await this.downloadPricingData();
} else {
// 如果不需要更新,加载现有数据
await this.loadPricingData();
}
} catch (error) {
logger.error('❌ Failed to check/update pricing:', error);
// 如果更新失败,尝试加载现有数据
await this.loadPricingData();
}
}
// 检查是否需要更新
needsUpdate() {
if (!fs.existsSync(this.pricingFile)) {
logger.info('📋 Pricing file not found, will download');
return true;
}
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;
}
return false;
}
// 下载价格数据
downloadPricingData() {
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;
}
let data = '';
response.on('data', (chunk) => {
data += chunk;
});
response.on('end', () => {
try {
const jsonData = JSON.parse(data);
// 保存到文件
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`);
resolve();
} catch (error) {
reject(new Error(`Failed to parse pricing data: ${error.message}`));
}
});
});
request.on('error', (error) => {
reject(new Error(`Failed to download pricing data: ${error.message}`));
});
request.setTimeout(30000, () => {
request.destroy();
reject(new Error('Download timeout'));
});
});
}
// 加载本地价格数据
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`);
} else {
logger.warn('💰 No pricing data file found');
this.pricingData = {};
}
} catch (error) {
logger.error('❌ Failed to load pricing data:', error);
this.pricingData = {};
}
}
// 获取模型价格信息
getModelPricing(modelName) {
if (!this.pricingData || !modelName) {
return null;
}
// 尝试直接匹配
if (this.pricingData[modelName]) {
return this.pricingData[modelName];
}
// 尝试模糊匹配(处理版本号等变化)
const normalizedModel = modelName.toLowerCase().replace(/[_-]/g, '');
for (const [key, value] of Object.entries(this.pricingData)) {
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(`💰 No pricing found for model: ${modelName}`);
return null;
}
// 计算使用费用
calculateCost(usage, modelName) {
const pricing = this.getModelPricing(modelName);
if (!pricing) {
return {
inputCost: 0,
outputCost: 0,
cacheCreateCost: 0,
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);
return {
inputCost,
outputCost,
cacheCreateCost,
cacheReadCost,
totalCost: inputCost + outputCost + cacheCreateCost + cacheReadCost,
hasPricing: true,
pricing: {
input: pricing.input_cost_per_token || 0,
output: pricing.output_cost_per_token || 0,
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)}`;
}
// 获取服务状态
getStatus() {
return {
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
};
}
// 强制更新价格数据
async forceUpdate() {
try {
await this.downloadPricingData();
return { success: true, message: 'Pricing data updated successfully' };
} catch (error) {
logger.error('❌ Force update failed:', error);
return { success: false, message: error.message };
}
}
}
module.exports = new PricingService();

224
src/utils/costCalculator.js Normal file
View File

@@ -0,0 +1,224 @@
const pricingService = require('../services/pricingService');
// Claude模型价格配置 (USD per 1M tokens) - 备用定价
const MODEL_PRICING = {
// Claude 3.5 Sonnet
'claude-3-5-sonnet-20241022': {
input: 3.00,
output: 15.00,
cacheWrite: 3.75,
cacheRead: 0.30
},
'claude-sonnet-4-20250514': {
input: 3.00,
output: 15.00,
cacheWrite: 3.75,
cacheRead: 0.30
},
// Claude 3.5 Haiku
'claude-3-5-haiku-20241022': {
input: 0.25,
output: 1.25,
cacheWrite: 0.30,
cacheRead: 0.03
},
// Claude 3 Opus
'claude-3-opus-20240229': {
input: 15.00,
output: 75.00,
cacheWrite: 18.75,
cacheRead: 1.50
},
// Claude 3 Sonnet
'claude-3-sonnet-20240229': {
input: 3.00,
output: 15.00,
cacheWrite: 3.75,
cacheRead: 0.30
},
// Claude 3 Haiku
'claude-3-haiku-20240307': {
input: 0.25,
output: 1.25,
cacheWrite: 0.30,
cacheRead: 0.03
},
// 默认定价(用于未知模型)
'unknown': {
input: 3.00,
output: 15.00,
cacheWrite: 3.75,
cacheRead: 0.30
}
};
class CostCalculator {
/**
* 计算单次请求的费用
* @param {Object} usage - 使用量数据
* @param {number} usage.input_tokens - 输入token数量
* @param {number} usage.output_tokens - 输出token数量
* @param {number} usage.cache_creation_input_tokens - 缓存创建token数量
* @param {number} usage.cache_read_input_tokens - 缓存读取token数量
* @param {string} model - 模型名称
* @returns {Object} 费用详情
*/
static calculateCost(usage, model = 'unknown') {
const inputTokens = usage.input_tokens || 0;
const outputTokens = usage.output_tokens || 0;
const cacheCreateTokens = usage.cache_creation_input_tokens || 0;
const cacheReadTokens = usage.cache_read_input_tokens || 0;
// 优先使用动态价格服务
const pricingData = pricingService.getModelPricing(model);
let pricing;
let usingDynamicPricing = false;
if (pricingData) {
// 转换动态价格格式为内部格式
pricing = {
input: (pricingData.input_cost_per_token || 0) * 1000000, // 转换为per 1M tokens
output: (pricingData.output_cost_per_token || 0) * 1000000,
cacheWrite: (pricingData.cache_creation_input_token_cost || 0) * 1000000,
cacheRead: (pricingData.cache_read_input_token_cost || 0) * 1000000
};
usingDynamicPricing = true;
} else {
// 回退到静态价格
pricing = MODEL_PRICING[model] || MODEL_PRICING['unknown'];
}
// 计算各类型token的费用 (USD)
const inputCost = (inputTokens / 1000000) * pricing.input;
const outputCost = (outputTokens / 1000000) * pricing.output;
const cacheWriteCost = (cacheCreateTokens / 1000000) * pricing.cacheWrite;
const cacheReadCost = (cacheReadTokens / 1000000) * pricing.cacheRead;
const totalCost = inputCost + outputCost + cacheWriteCost + cacheReadCost;
return {
model,
pricing,
usingDynamicPricing,
usage: {
inputTokens,
outputTokens,
cacheCreateTokens,
cacheReadTokens,
totalTokens: inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
},
costs: {
input: inputCost,
output: outputCost,
cacheWrite: cacheWriteCost,
cacheRead: cacheReadCost,
total: totalCost
},
// 格式化的费用字符串
formatted: {
input: this.formatCost(inputCost),
output: this.formatCost(outputCost),
cacheWrite: this.formatCost(cacheWriteCost),
cacheRead: this.formatCost(cacheReadCost),
total: this.formatCost(totalCost)
}
};
}
/**
* 计算聚合使用量的费用
* @param {Object} aggregatedUsage - 聚合使用量数据
* @param {string} model - 模型名称
* @returns {Object} 费用详情
*/
static calculateAggregatedCost(aggregatedUsage, model = 'unknown') {
const usage = {
input_tokens: aggregatedUsage.inputTokens || aggregatedUsage.totalInputTokens || 0,
output_tokens: aggregatedUsage.outputTokens || aggregatedUsage.totalOutputTokens || 0,
cache_creation_input_tokens: aggregatedUsage.cacheCreateTokens || aggregatedUsage.totalCacheCreateTokens || 0,
cache_read_input_tokens: aggregatedUsage.cacheReadTokens || aggregatedUsage.totalCacheReadTokens || 0
};
return this.calculateCost(usage, model);
}
/**
* 获取模型定价信息
* @param {string} model - 模型名称
* @returns {Object} 定价信息
*/
static getModelPricing(model = 'unknown') {
return MODEL_PRICING[model] || MODEL_PRICING['unknown'];
}
/**
* 获取所有支持的模型和定价
* @returns {Object} 所有模型定价
*/
static getAllModelPricing() {
return { ...MODEL_PRICING };
}
/**
* 验证模型是否支持
* @param {string} model - 模型名称
* @returns {boolean} 是否支持
*/
static isModelSupported(model) {
return !!MODEL_PRICING[model];
}
/**
* 格式化费用显示
* @param {number} cost - 费用金额
* @param {number} decimals - 小数位数
* @returns {string} 格式化的费用字符串
*/
static formatCost(cost, decimals = 6) {
if (cost >= 1) {
return `$${cost.toFixed(2)}`;
} else if (cost >= 0.001) {
return `$${cost.toFixed(4)}`;
} else {
return `$${cost.toFixed(decimals)}`;
}
}
/**
* 计算费用节省(使用缓存的节省)
* @param {Object} usage - 使用量数据
* @param {string} model - 模型名称
* @returns {Object} 节省信息
*/
static calculateCacheSavings(usage, model = 'unknown') {
const pricing = this.getModelPricing(model);
const cacheReadTokens = usage.cache_read_input_tokens || 0;
// 如果这些token不使用缓存需要按正常input价格计费
const normalCost = (cacheReadTokens / 1000000) * pricing.input;
const cacheCost = (cacheReadTokens / 1000000) * pricing.cacheRead;
const savings = normalCost - cacheCost;
const savingsPercentage = normalCost > 0 ? (savings / normalCost) * 100 : 0;
return {
normalCost,
cacheCost,
savings,
savingsPercentage,
formatted: {
normalCost: this.formatCost(normalCost),
cacheCost: this.formatCost(cacheCost),
savings: this.formatCost(savings),
savingsPercentage: `${savingsPercentage.toFixed(1)}%`
}
};
}
}
module.exports = CostCalculator;

290
src/utils/logger.js Normal file
View File

@@ -0,0 +1,290 @@
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const config = require('../../config/config');
const path = require('path');
const fs = require('fs');
const os = require('os');
// 📝 增强的日志格式
const createLogFormat = (colorize = false) => {
const formats = [
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'stack'] })
];
if (colorize) {
formats.push(winston.format.colorize());
}
formats.push(
winston.format.printf(({ level, message, timestamp, stack, metadata, ...rest }) => {
const emoji = {
error: '❌',
warn: '⚠️ ',
info: ' ',
debug: '🐛',
verbose: '📝'
};
let logMessage = `${emoji[level] || '📝'} [${timestamp}] ${level.toUpperCase()}: ${message}`;
// 添加元数据
if (metadata && Object.keys(metadata).length > 0) {
logMessage += ` | ${JSON.stringify(metadata)}`;
}
// 添加其他属性
const additionalData = { ...rest };
delete additionalData.level;
delete additionalData.message;
delete additionalData.timestamp;
delete additionalData.stack;
if (Object.keys(additionalData).length > 0) {
logMessage += ` | ${JSON.stringify(additionalData)}`;
}
return stack ? `${logMessage}\n${stack}` : logMessage;
})
);
return winston.format.combine(...formats);
};
const logFormat = createLogFormat(false);
const consoleFormat = createLogFormat(true);
// 📁 确保日志目录存在并设置权限
if (!fs.existsSync(config.logging.dirname)) {
fs.mkdirSync(config.logging.dirname, { recursive: true, mode: 0o755 });
}
// 🔄 增强的日志轮转配置
const createRotateTransport = (filename, level = null) => {
const transport = new DailyRotateFile({
filename: path.join(config.logging.dirname, filename),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: config.logging.maxSize,
maxFiles: config.logging.maxFiles,
auditFile: path.join(config.logging.dirname, `.${filename.replace('%DATE%', 'audit')}.json`),
format: logFormat
});
if (level) {
transport.level = level;
}
// 监听轮转事件
transport.on('rotate', (oldFilename, newFilename) => {
console.log(`📦 Log rotated: ${oldFilename} -> ${newFilename}`);
});
transport.on('new', (newFilename) => {
console.log(`📄 New log file created: ${newFilename}`);
});
transport.on('archive', (zipFilename) => {
console.log(`🗜️ Log archived: ${zipFilename}`);
});
return transport;
};
const dailyRotateFileTransport = createRotateTransport('claude-relay-%DATE%.log');
const errorFileTransport = createRotateTransport('claude-relay-error-%DATE%.log', 'error');
// 🔒 创建专门的安全日志记录器
const securityLogger = winston.createLogger({
level: 'warn',
format: logFormat,
transports: [
createRotateTransport('claude-relay-security-%DATE%.log', 'warn')
],
silent: false
});
// 🌟 增强的 Winston logger
const logger = winston.createLogger({
level: config.logging.level,
format: logFormat,
transports: [
// 📄 文件输出
dailyRotateFileTransport,
errorFileTransport,
// 🖥️ 控制台输出
new winston.transports.Console({
format: consoleFormat,
handleExceptions: false,
handleRejections: false
})
],
// 🚨 异常处理
exceptionHandlers: [
new winston.transports.File({
filename: path.join(config.logging.dirname, 'exceptions.log'),
format: logFormat,
maxsize: 10485760, // 10MB
maxFiles: 5
}),
new winston.transports.Console({
format: consoleFormat
})
],
// 🔄 未捕获异常处理
rejectionHandlers: [
new winston.transports.File({
filename: path.join(config.logging.dirname, 'rejections.log'),
format: logFormat,
maxsize: 10485760, // 10MB
maxFiles: 5
}),
new winston.transports.Console({
format: consoleFormat
})
],
// 防止进程退出
exitOnError: false
});
// 🎯 增强的自定义方法
logger.success = (message, metadata = {}) => {
logger.info(`${message}`, { type: 'success', ...metadata });
};
logger.start = (message, metadata = {}) => {
logger.info(`🚀 ${message}`, { type: 'startup', ...metadata });
};
logger.request = (method, url, status, duration, metadata = {}) => {
const emoji = status >= 400 ? '🔴' : status >= 300 ? '🟡' : '🟢';
const level = status >= 400 ? 'error' : status >= 300 ? 'warn' : 'info';
logger[level](`${emoji} ${method} ${url} - ${status} (${duration}ms)`, {
type: 'request',
method,
url,
status,
duration,
...metadata
});
};
logger.api = (message, metadata = {}) => {
logger.info(`🔗 ${message}`, { type: 'api', ...metadata });
};
logger.security = (message, metadata = {}) => {
const securityData = {
type: 'security',
timestamp: new Date().toISOString(),
pid: process.pid,
hostname: os.hostname(),
...metadata
};
// 记录到主日志
logger.warn(`🔒 ${message}`, securityData);
// 记录到专门的安全日志文件
try {
securityLogger.warn(`🔒 ${message}`, securityData);
} catch (error) {
// 如果安全日志文件不可用,只记录到主日志
console.warn('Security logger not available:', error.message);
}
};
logger.database = (message, metadata = {}) => {
logger.debug(`💾 ${message}`, { type: 'database', ...metadata });
};
logger.performance = (message, metadata = {}) => {
logger.info(`${message}`, { type: 'performance', ...metadata });
};
logger.audit = (message, metadata = {}) => {
logger.info(`📋 ${message}`, {
type: 'audit',
timestamp: new Date().toISOString(),
pid: process.pid,
...metadata
});
};
// 🔧 性能监控方法
logger.timer = (label) => {
const start = Date.now();
return {
end: (message = '', metadata = {}) => {
const duration = Date.now() - start;
logger.performance(`${label} ${message}`, { duration, ...metadata });
return duration;
}
};
};
// 📊 日志统计
logger.stats = {
requests: 0,
errors: 0,
warnings: 0
};
// 重写原始方法以统计
const originalError = logger.error;
const originalWarn = logger.warn;
const originalInfo = logger.info;
logger.error = function(message, metadata = {}) {
logger.stats.errors++;
return originalError.call(this, message, metadata);
};
logger.warn = function(message, metadata = {}) {
logger.stats.warnings++;
return originalWarn.call(this, message, metadata);
};
logger.info = function(message, metadata = {}) {
if (metadata.type === 'request') {
logger.stats.requests++;
}
return originalInfo.call(this, message, metadata);
};
// 📈 获取日志统计
logger.getStats = () => ({ ...logger.stats });
// 🧹 清理统计
logger.resetStats = () => {
logger.stats.requests = 0;
logger.stats.errors = 0;
logger.stats.warnings = 0;
};
// 📡 健康检查
logger.healthCheck = () => {
try {
const testMessage = 'Logger health check';
logger.debug(testMessage);
return { healthy: true, timestamp: new Date().toISOString() };
} catch (error) {
return { healthy: false, error: error.message, timestamp: new Date().toISOString() };
}
};
// 🎬 启动日志记录系统
logger.start('Logger initialized', {
level: config.logging.level,
directory: config.logging.dirname,
maxSize: config.logging.maxSize,
maxFiles: config.logging.maxFiles
});
module.exports = logger;

307
src/utils/oauthHelper.js Normal file
View File

@@ -0,0 +1,307 @@
/**
* OAuth助手工具
* 基于claude-code-login.js中的OAuth流程实现
*/
const crypto = require('crypto');
const { SocksProxyAgent } = require('socks-proxy-agent');
const { HttpsProxyAgent } = require('https-proxy-agent');
const axios = require('axios');
const logger = require('./logger');
// OAuth 配置常量 - 从claude-code-login.js提取
const OAUTH_CONFIG = {
AUTHORIZE_URL: 'https://claude.ai/oauth/authorize',
TOKEN_URL: 'https://console.anthropic.com/v1/oauth/token',
CLIENT_ID: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
REDIRECT_URI: 'https://console.anthropic.com/oauth/code/callback',
SCOPES: 'org:create_api_key user:profile user:inference'
};
/**
* 生成随机的 state 参数
* @returns {string} 随机生成的 state (64字符hex)
*/
function generateState() {
return crypto.randomBytes(32).toString('hex');
}
/**
* 生成随机的 code verifierPKCE
* @returns {string} base64url 编码的随机字符串
*/
function generateCodeVerifier() {
return crypto.randomBytes(32).toString('base64url');
}
/**
* 生成 code challengePKCE
* @param {string} codeVerifier - code verifier 字符串
* @returns {string} SHA256 哈希后的 base64url 编码字符串
*/
function generateCodeChallenge(codeVerifier) {
return crypto.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
}
/**
* 生成授权 URL
* @param {string} codeChallenge - PKCE code challenge
* @param {string} state - state 参数
* @returns {string} 完整的授权 URL
*/
function generateAuthUrl(codeChallenge, state) {
const params = new URLSearchParams({
code: 'true',
client_id: OAUTH_CONFIG.CLIENT_ID,
response_type: 'code',
redirect_uri: OAUTH_CONFIG.REDIRECT_URI,
scope: OAUTH_CONFIG.SCOPES,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state: state
});
return `${OAUTH_CONFIG.AUTHORIZE_URL}?${params.toString()}`;
}
/**
* 生成OAuth授权URL和相关参数
* @returns {{authUrl: string, codeVerifier: string, state: string, codeChallenge: string}}
*/
function generateOAuthParams() {
const state = generateState();
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
const authUrl = generateAuthUrl(codeChallenge, state);
return {
authUrl,
codeVerifier,
state,
codeChallenge
};
}
/**
* 创建代理agent
* @param {object|null} proxyConfig - 代理配置对象
* @returns {object|null} 代理agent或null
*/
function createProxyAgent(proxyConfig) {
if (!proxyConfig) {
return null;
}
try {
if (proxyConfig.type === 'socks5') {
const auth = proxyConfig.username && proxyConfig.password ? `${proxyConfig.username}:${proxyConfig.password}@` : '';
const socksUrl = `socks5://${auth}${proxyConfig.host}:${proxyConfig.port}`;
return new SocksProxyAgent(socksUrl);
} else if (proxyConfig.type === 'http' || proxyConfig.type === 'https') {
const auth = proxyConfig.username && proxyConfig.password ? `${proxyConfig.username}:${proxyConfig.password}@` : '';
const httpUrl = `${proxyConfig.type}://${auth}${proxyConfig.host}:${proxyConfig.port}`;
return new HttpsProxyAgent(httpUrl);
}
} catch (error) {
console.warn('⚠️ Invalid proxy configuration:', error);
}
return null;
}
/**
* 使用授权码交换访问令牌
* @param {string} authorizationCode - 授权码
* @param {string} codeVerifier - PKCE code verifier
* @param {string} state - state 参数
* @param {object|null} proxyConfig - 代理配置(可选)
* @returns {Promise<object>} Claude格式的token响应
*/
async function exchangeCodeForTokens(authorizationCode, codeVerifier, state, proxyConfig = null) {
// 清理授权码移除URL片段
const cleanedCode = authorizationCode.split('#')[0]?.split('&')[0] ?? authorizationCode;
const params = {
grant_type: 'authorization_code',
client_id: OAUTH_CONFIG.CLIENT_ID,
code: cleanedCode,
redirect_uri: OAUTH_CONFIG.REDIRECT_URI,
code_verifier: codeVerifier,
state: state
};
// 创建代理agent
const agent = createProxyAgent(proxyConfig);
try {
logger.debug('🔄 Attempting OAuth token exchange', {
url: OAUTH_CONFIG.TOKEN_URL,
codeLength: cleanedCode.length,
codePrefix: cleanedCode.substring(0, 10) + '...',
hasProxy: !!proxyConfig,
proxyType: proxyConfig?.type || 'none'
});
const response = await axios.post(OAUTH_CONFIG.TOKEN_URL, params, {
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.9',
'Referer': 'https://claude.ai/',
'Origin': 'https://claude.ai'
},
httpsAgent: agent,
timeout: 30000
});
logger.success('✅ OAuth token exchange successful', {
status: response.status,
hasAccessToken: !!response.data?.access_token,
hasRefreshToken: !!response.data?.refresh_token,
scopes: response.data?.scope
});
const data = response.data;
// 返回Claude格式的token数据
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresAt: (Math.floor(Date.now() / 1000) + data.expires_in) * 1000,
scopes: data.scope ? data.scope.split(' ') : ['user:inference', 'user:profile'],
isMax: true
};
} catch (error) {
// 处理axios错误响应
if (error.response) {
// 服务器返回了错误状态码
const status = error.response.status;
const errorData = error.response.data;
logger.error('❌ OAuth token exchange failed with server error', {
status: status,
statusText: error.response.statusText,
headers: error.response.headers,
data: errorData,
codeLength: cleanedCode.length,
codePrefix: cleanedCode.substring(0, 10) + '...'
});
// 尝试从错误响应中提取有用信息
let errorMessage = `HTTP ${status}`;
if (errorData) {
if (typeof errorData === 'string') {
errorMessage += `: ${errorData}`;
} else if (errorData.error) {
errorMessage += `: ${errorData.error}`;
if (errorData.error_description) {
errorMessage += ` - ${errorData.error_description}`;
}
} else {
errorMessage += `: ${JSON.stringify(errorData)}`;
}
}
throw new Error(`Token exchange failed: ${errorMessage}`);
} else if (error.request) {
// 请求被发送但没有收到响应
logger.error('❌ OAuth token exchange failed with network error', {
message: error.message,
code: error.code,
hasProxy: !!proxyConfig
});
throw new Error('Token exchange failed: No response from server (network error or timeout)');
} else {
// 其他错误
logger.error('❌ OAuth token exchange failed with unknown error', {
message: error.message,
stack: error.stack
});
throw new Error(`Token exchange failed: ${error.message}`);
}
}
}
/**
* 解析回调 URL 或授权码
* @param {string} input - 完整的回调 URL 或直接的授权码
* @returns {string} 授权码
*/
function parseCallbackUrl(input) {
if (!input || typeof input !== 'string') {
throw new Error('请提供有效的授权码或回调 URL');
}
const trimmedInput = input.trim();
// 情况1: 尝试作为完整URL解析
if (trimmedInput.startsWith('http://') || trimmedInput.startsWith('https://')) {
try {
const urlObj = new URL(trimmedInput);
const authorizationCode = urlObj.searchParams.get('code');
if (!authorizationCode) {
throw new Error('回调 URL 中未找到授权码 (code 参数)');
}
return authorizationCode;
} catch (error) {
if (error.message.includes('回调 URL 中未找到授权码')) {
throw error;
}
throw new Error('无效的 URL 格式,请检查回调 URL 是否正确');
}
}
// 情况2: 直接的授权码可能包含URL fragments
// 参考claude-code-login.js的处理方式移除URL fragments和参数
const cleanedCode = trimmedInput.split('#')[0]?.split('&')[0] ?? trimmedInput;
// 验证授权码格式Claude的授权码通常是base64url格式
if (!cleanedCode || cleanedCode.length < 10) {
throw new Error('授权码格式无效,请确保复制了完整的 Authorization Code');
}
// 基本格式验证:授权码应该只包含字母、数字、下划线、连字符
const validCodePattern = /^[A-Za-z0-9_-]+$/;
if (!validCodePattern.test(cleanedCode)) {
throw new Error('授权码包含无效字符,请检查是否复制了正确的 Authorization Code');
}
return cleanedCode;
}
/**
* 格式化为Claude标准格式
* @param {object} tokenData - token数据
* @returns {object} claudeAiOauth格式的数据
*/
function formatClaudeCredentials(tokenData) {
return {
claudeAiOauth: {
accessToken: tokenData.accessToken,
refreshToken: tokenData.refreshToken,
expiresAt: tokenData.expiresAt,
scopes: tokenData.scopes,
isMax: tokenData.isMax
}
};
}
module.exports = {
OAUTH_CONFIG,
generateOAuthParams,
exchangeCodeForTokens,
parseCallbackUrl,
formatClaudeCredentials,
generateState,
generateCodeVerifier,
generateCodeChallenge,
generateAuthUrl,
createProxyAgent
};