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;