/** * @file server.js * @brief Express 服务器入口 */ 'use strict'; const express = require('express'); const path = require('path'); const Config = require('./src/core/config'); const { initDB } = require('./src/core/database'); // ==================== 初始化 ==================== initDB(); const app = express(); // 中间件 app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true })); // 静态文件 app.use(express.static(path.join(__dirname, 'public'))); // 截图静态服务:data/screenshots/ → /screenshots/ app.use('/screenshots', express.static(path.join(Config.DATA_DIR, 'screenshots'))); // ==================== API 路由 ==================== app.use('/api/accounts', require('./src/routes/accounts')); app.use('/api/settings', require('./src/routes/settings')); app.use('/api/recharge-credentials', require('./src/routes/recharge_credentials')); app.use('/api/workspace-refund-credentials', require('./src/routes/workspace_refund_credentials')); app.use('/api/google-account-credentials', require('./src/routes/google_account_credentials')); app.use('/api/feishu', require('./src/routes/feishu')); app.use('/api/quota-monitor', require('./src/routes/quota_monitor')); app.use('/api/operations', require('./src/routes/operations')); app.use('/api/diagnostics', require('./src/routes/diagnostics')); app.use('/api/static-proxies', require('./src/routes/static_proxies')); app.use('/api/templates', require('./src/routes/templates')); app.use('/api/backups', require('./src/routes/backups')); // ==================== 前端路由 (SPA) ==================== app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // ==================== 错误处理 ==================== app.use((err, req, res, next) => { // body-parser 的解析异常可能携带原始请求体;充值请求中包含 API Secret // 或卡密,因此全局错误日志和响应都不能输出 err/body/message 原文。 const status = Number.isInteger(err && err.status) && err.status >= 400 && err.status < 600 ? err.status : 500; const type = String((err && err.type) || 'request_error') .replace(/[^a-z0-9_.-]/gi, '') .slice(0, 80) || 'request_error'; console.error(`[Server] Request failed: type=${type} status=${status}`); res.status(status).json({ success: false, msg: status >= 400 && status < 500 ? '请求格式无效' : '服务器内部错误', }); }); // ==================== 启动服务器 ==================== // 端口优先级:环境变量 PORT > src/core/config.js 中的 PORT > 3000 // 注意:启动程序.bat 会读取 config.js 中的 PORT 来打开浏览器,二者需保持一致 const PORT = Number(process.env.PORT) || Config.PORT || 3000; const HOST = process.env.HOST || '0.0.0.0'; const server = app.listen(PORT, HOST, () => { require('./src/services/quota_monitor_scheduler').start(); require('./src/services/refund_deadline_scheduler').start(); require('./src/services/recharge_reconciler').start(); const { networkInterfaces } = require('os'); const nets = networkInterfaces(); let localIP = 'localhost'; for (const name of Object.keys(nets)) { for (const net of nets[name]) { if (net.family === 'IPv4' && !net.internal) { localIP = net.address; break; } } } console.log(`╔═══════════════════════════════════════╗`); console.log(`║ Gemini Go Web - Node.js版 ║`); console.log(`╠═══════════════════════════════════════╣`); console.log(`║ 服务器已启动 ║`); console.log(`║ 本机: http://localhost:${PORT} ║`); console.log(`║ 局域网: http://${localIP}:${PORT} ║`); console.log(`╚═══════════════════════════════════════╝`); }); // 监听失败时给出明确提示(否则只会抛出一段堆栈,启动窗口一闪而过难以排查) server.on('error', (err) => { const code = err && err.code ? err.code : ''; if (code === 'EADDRINUSE') { console.error(`[Server] 启动失败:端口 ${PORT} 已被占用。`); console.error(`[Server] 请关闭仍在运行的旧 "Gemini Go Server" 窗口,或修改 src/core/config.js 中的 PORT 后重试。`); } else if (code === 'EACCES') { console.error(`[Server] 启动失败:没有权限监听 ${HOST}:${PORT},请更换端口或以管理员身份运行。`); } else { console.error(`[Server] 启动失败:${code} ${err && err.message ? err.message : err}`); } process.exit(1); }); module.exports = app;