app.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. import express from 'express';
  2. import cors from 'cors';
  3. import helmet from 'helmet';
  4. import morgan from 'morgan';
  5. import compression from 'compression';
  6. import { createServer } from 'http';
  7. import { createConnection, createServer as createNetServer } from 'net';
  8. import { exec } from 'child_process';
  9. import { promisify } from 'util';
  10. import { config } from './config/index.js';
  11. import { errorHandler } from './middleware/error.js';
  12. import { setupRoutes } from './routes/index.js';
  13. import { setupWebSocket } from './websocket/index.js';
  14. import { initDatabase } from './models/index.js';
  15. import { initRedis } from './config/redis.js';
  16. import { logger } from './utils/logger.js';
  17. import { taskScheduler } from './scheduler/index.js';
  18. import { registerTaskExecutors } from './services/taskExecutors.js';
  19. import { taskQueueService } from './services/TaskQueueService.js';
  20. import { browserLoginService } from './services/BrowserLoginService.js';
  21. import { wsManager } from './websocket/index.js';
  22. const execAsync = promisify(exec);
  23. const app = express();
  24. const httpServer = createServer(app);
  25. // 中间件
  26. app.use(helmet({
  27. crossOriginResourcePolicy: { policy: 'cross-origin' },
  28. }));
  29. app.use(cors({
  30. origin: config.cors.origin,
  31. credentials: true,
  32. }));
  33. app.use(compression());
  34. app.use(morgan('combined', {
  35. stream: { write: (message) => logger.info(message.trim()) },
  36. }));
  37. app.use(express.json({ limit: '50mb' }));
  38. app.use(express.urlencoded({ extended: true, limit: '50mb' }));
  39. // 静态文件
  40. app.use('/uploads', express.static(config.upload.path));
  41. // 健康检查
  42. app.get('/api/health', (_req, res) => {
  43. res.json({
  44. status: 'ok',
  45. version: config.version,
  46. uptime: process.uptime(),
  47. timestamp: new Date().toISOString(),
  48. });
  49. });
  50. // API 路由
  51. setupRoutes(app);
  52. // 错误处理
  53. app.use(errorHandler);
  54. // WebSocket
  55. setupWebSocket(httpServer);
  56. // 检查端口是否被占用(改进版:使用 net.Server 来测试)
  57. async function checkPortInUse(port: number): Promise<boolean> {
  58. return new Promise((resolve) => {
  59. const testServer = createNetServer();
  60. testServer.once('error', (err: NodeJS.ErrnoException) => {
  61. if (err.code === 'EADDRINUSE') {
  62. resolve(true); // 端口被占用
  63. } else {
  64. resolve(false);
  65. }
  66. });
  67. testServer.once('listening', () => {
  68. testServer.close(() => {
  69. resolve(false); // 端口未被占用
  70. });
  71. });
  72. testServer.listen(port);
  73. });
  74. }
  75. // 获取占用端口的进程ID(跨平台)
  76. async function getProcessOnPort(port: number): Promise<number | null> {
  77. const isWindows = process.platform === 'win32';
  78. try {
  79. if (isWindows) {
  80. // Windows: 使用 netstat 命令,改进解析逻辑
  81. const { stdout } = await execAsync(`netstat -ano | findstr :${port}`);
  82. const lines = stdout.trim().split('\n');
  83. for (const line of lines) {
  84. // 匹配 LISTENING 状态的连接
  85. if (line.includes('LISTENING')) {
  86. const parts = line.trim().split(/\s+/);
  87. const pid = parseInt(parts[parts.length - 1], 10);
  88. if (!isNaN(pid) && pid > 0) {
  89. logger.info(`[Port Check] Found PID ${pid} listening on port ${port}`);
  90. return pid;
  91. }
  92. }
  93. }
  94. } else {
  95. const { stdout } = await execAsync(`lsof -i :${port} -t`);
  96. const pid = parseInt(stdout.trim().split('\n')[0], 10);
  97. if (!isNaN(pid)) return pid;
  98. }
  99. } catch (error) {
  100. // 命令执行失败,可能端口没有被占用
  101. logger.debug(`[Port Check] Command failed (port might not be in use):`, error);
  102. }
  103. return null;
  104. }
  105. // 终止进程(跨平台)
  106. async function killProcess(pid: number): Promise<boolean> {
  107. const isWindows = process.platform === 'win32';
  108. const currentPid = process.pid;
  109. // 不要杀死自己的进程
  110. if (pid === currentPid) {
  111. logger.warn(`[Port Check] Cannot kill own process (PID: ${pid})`);
  112. return false;
  113. }
  114. try {
  115. if (isWindows) {
  116. await execAsync(`taskkill /PID ${pid} /F`);
  117. } else {
  118. await execAsync(`kill -9 ${pid}`);
  119. }
  120. // 等待进程完全终止
  121. await new Promise(resolve => setTimeout(resolve, 2000));
  122. return true;
  123. } catch (error) {
  124. logger.error(`Failed to kill process ${pid}:`, error);
  125. return false;
  126. }
  127. }
  128. // 检查并释放端口(增加重试机制)
  129. async function ensurePortAvailable(port: number, maxRetries: number = 3): Promise<void> {
  130. for (let attempt = 1; attempt <= maxRetries; attempt++) {
  131. const inUse = await checkPortInUse(port);
  132. if (!inUse) {
  133. logger.info(`[Port Check] Port ${port} is available`);
  134. return;
  135. }
  136. logger.warn(`[Port Check] Attempt ${attempt}/${maxRetries}: Port ${port} is already in use`);
  137. const pid = await getProcessOnPort(port);
  138. if (pid) {
  139. logger.info(`[Port Check] Found process ${pid} using port ${port}, attempting to terminate...`);
  140. const killed = await killProcess(pid);
  141. if (killed) {
  142. logger.info(`[Port Check] Successfully terminated process ${pid}`);
  143. // 等待更长时间让端口释放
  144. await new Promise(resolve => setTimeout(resolve, 2000));
  145. // 再次检查端口
  146. const stillInUse = await checkPortInUse(port);
  147. if (!stillInUse) {
  148. logger.info(`[Port Check] Port ${port} is now available`);
  149. return;
  150. }
  151. }
  152. } else {
  153. logger.warn(`[Port Check] Port ${port} is in use but could not identify the process`);
  154. }
  155. // 如果还有重试机会,等待一会儿
  156. if (attempt < maxRetries) {
  157. logger.info(`[Port Check] Waiting before retry...`);
  158. await new Promise(resolve => setTimeout(resolve, 3000));
  159. }
  160. }
  161. throw new Error(`Port ${port} is still in use after ${maxRetries} attempts. Please manually kill the process or use a different port.`);
  162. }
  163. // 启动服务
  164. async function bootstrap() {
  165. // 确保端口可用
  166. try {
  167. await ensurePortAvailable(config.port);
  168. } catch (error) {
  169. logger.error('Port availability check failed:', error);
  170. process.exit(1);
  171. }
  172. let dbConnected = false;
  173. let redisConnected = false;
  174. // 尝试初始化数据库
  175. try {
  176. await initDatabase();
  177. logger.info('Database connected');
  178. dbConnected = true;
  179. } catch (error) {
  180. logger.warn('Database connection failed - running in limited mode');
  181. logger.warn('Please install MySQL and create the database, or use Docker');
  182. logger.error('Database error:', error instanceof Error ? error.message : error);
  183. }
  184. // 尝试初始化 Redis
  185. try {
  186. await initRedis();
  187. logger.info('Redis connected');
  188. redisConnected = true;
  189. } catch (error) {
  190. logger.warn('Redis connection failed - some features may not work');
  191. }
  192. // 只有在数据库连接成功时才启动调度器和注册任务执行器
  193. if (dbConnected) {
  194. registerTaskExecutors();
  195. taskScheduler.start();
  196. // 启动任务队列 Worker
  197. taskQueueService.startWorker();
  198. }
  199. // 注册浏览器登录服务的事件监听(用于 AI 分析结果的 WebSocket 推送)
  200. setupBrowserLoginEvents();
  201. // 启动 HTTP 服务
  202. httpServer.listen(config.port, config.host, () => {
  203. logger.info(`Server running on http://${config.host}:${config.port}`);
  204. logger.info(`Environment: ${config.env}`);
  205. if (!dbConnected) {
  206. logger.warn('⚠️ Running without database - API endpoints will not work');
  207. logger.warn('⚠️ Please configure MySQL in .env file');
  208. }
  209. });
  210. }
  211. /**
  212. * 设置浏览器登录服务的事件监听
  213. * 用于通过 WebSocket 推送 AI 分析结果给前端
  214. */
  215. function setupBrowserLoginEvents(): void {
  216. // AI 分析结果事件
  217. browserLoginService.on('aiAnalysis', (data: {
  218. sessionId: string;
  219. userId?: number;
  220. status: string;
  221. analysis: {
  222. isLoggedIn: boolean;
  223. hasVerification: boolean;
  224. verificationType?: string;
  225. verificationDescription?: string;
  226. pageDescription: string;
  227. suggestedAction?: string;
  228. };
  229. }) => {
  230. if (data.userId) {
  231. wsManager.sendToUser(data.userId, 'login:aiAnalysis', {
  232. sessionId: data.sessionId,
  233. status: data.status,
  234. analysis: data.analysis,
  235. });
  236. }
  237. });
  238. // 验证码检测事件
  239. browserLoginService.on('verificationNeeded', (data: {
  240. sessionId: string;
  241. userId?: number;
  242. verificationType?: string;
  243. description?: string;
  244. suggestedAction?: string;
  245. }) => {
  246. if (data.userId) {
  247. wsManager.sendToUser(data.userId, 'login:verificationNeeded', {
  248. sessionId: data.sessionId,
  249. verificationType: data.verificationType,
  250. description: data.description,
  251. suggestedAction: data.suggestedAction,
  252. });
  253. }
  254. });
  255. // 导航建议事件
  256. browserLoginService.on('navigationSuggestion', (data: {
  257. sessionId: string;
  258. userId?: number;
  259. guide: unknown;
  260. }) => {
  261. if (data.userId) {
  262. wsManager.sendToUser(data.userId, 'login:navigationSuggestion', {
  263. sessionId: data.sessionId,
  264. guide: data.guide,
  265. });
  266. }
  267. });
  268. // 登录结果事件(也通过 WebSocket 推送)
  269. browserLoginService.on('loginResult', (data: {
  270. sessionId: string;
  271. userId?: number;
  272. status: string;
  273. cookies?: string;
  274. accountInfo?: unknown;
  275. error?: string;
  276. message?: string;
  277. }) => {
  278. logger.info(`[BrowserLogin] Login result event: ${data.sessionId}, status: ${data.status}`);
  279. if (data.userId) {
  280. wsManager.sendToUser(data.userId, 'login:result', {
  281. sessionId: data.sessionId,
  282. status: data.status,
  283. accountInfo: data.accountInfo,
  284. error: data.error,
  285. message: data.message,
  286. });
  287. }
  288. });
  289. logger.info('Browser login events registered');
  290. }
  291. // 优雅关闭
  292. process.on('SIGTERM', async () => {
  293. logger.info('SIGTERM received, shutting down gracefully');
  294. taskScheduler.stop();
  295. await taskQueueService.close();
  296. httpServer.close(() => {
  297. logger.info('Server closed');
  298. process.exit(0);
  299. });
  300. });
  301. bootstrap();
  302. export { app, httpServer };