فهرست منبع

fix: 修复4处内存泄漏

1. BrowserLoginService.ts: sessionTimers Map 保存定时器引用,closeSession 时取消定时器并立即删除 session(不再延迟60秒)

2. app.ts: SIGTERM 处理中添加 wsManager.close() 清理 WebSocket 心跳定时器和 captchaListeners

3. websocket/index.ts: captchaListeners 添加5分钟 TTL 超时自动删除,调用后立即清理

4. TaskQueueService.ts: 添加每10分钟定时清理机制,每个用户最多保留100个已完成任务
ethanfly 3 روز پیش
والد
کامیت
c253a48328
4فایلهای تغییر یافته به همراه86 افزوده شده و 13 حذف شده
  1. 2 0
      server/src/app.ts
  2. 15 6
      server/src/services/BrowserLoginService.ts
  3. 41 0
      server/src/services/TaskQueueService.ts
  4. 28 7
      server/src/websocket/index.ts

+ 2 - 0
server/src/app.ts

@@ -296,6 +296,8 @@ process.on('SIGTERM', async () => {
   await taskQueueService.close();
   // 关闭所有无头/有头浏览器,释放资源
   await BrowserManager.closeBrowser();
+  // 修复: 关闭 WebSocket manager,清理心跳定时器和 captchaListeners
+  wsManager.close();
   httpServer.close(() => {
     logger.info('Server closed');
     process.exit(0);

+ 15 - 6
server/src/services/BrowserLoginService.ts

@@ -137,6 +137,8 @@ interface LoginSession {
  */
 class BrowserLoginService extends EventEmitter {
   private sessions: Map<string, LoginSession> = new Map();
+  // 修复: 跟踪 session 定时器以便取消,防止内存泄漏
+  private sessionTimers: Map<string, NodeJS.Timeout> = new Map();
 
   /**
    * 开始浏览器登录会话
@@ -201,10 +203,11 @@ class BrowserLoginService extends EventEmitter {
         this.startAIMonitoring(sessionId);
       }
 
-      // 设置超时(5分钟)
-      setTimeout(() => {
+      // 设置超时(5分钟)- 保存定时器引用以便取消
+      const timer = setTimeout(() => {
         this.handleTimeout(sessionId);
       }, 5 * 60 * 1000);
+      this.sessionTimers.set(sessionId, timer);
 
       logger.info(`Browser login session started: ${sessionId} for platform: ${platform}`);
 
@@ -1435,11 +1438,19 @@ class BrowserLoginService extends EventEmitter {
 
   /**
    * 关闭会话
+   * 修复: 取消定时器、立即删除 session,不再延迟 60 秒
    */
   private async closeSession(sessionId: string): Promise<void> {
     const session = this.sessions.get(sessionId);
     if (!session) return;
 
+    // 修复: 取消 5 分钟超时定时器
+    const timer = this.sessionTimers.get(sessionId);
+    if (timer) {
+      clearTimeout(timer);
+      this.sessionTimers.delete(sessionId);
+    }
+
     try {
       // 停止并销毁 AI 助手(异步清理截图)
       if (session.aiAssistant) {
@@ -1460,10 +1471,8 @@ class BrowserLoginService extends EventEmitter {
       logger.error(`Error closing session ${sessionId}:`, error);
     }
 
-    // 保留会话信息一段时间供查询
-    setTimeout(() => {
-      this.sessions.delete(sessionId);
-    }, 60000);
+    // 修复: 立即删除 session,不再延迟
+    this.sessions.delete(sessionId);
   }
 
   /**

+ 41 - 0
server/src/services/TaskQueueService.ts

@@ -37,6 +37,42 @@ class TaskQueueService {
   // 取消控制器 Map<taskId, AbortController>
   private taskAbortControllers: Map<string, AbortController> = new Map();
 
+  // 修复: 每个用户最大保留任务数
+  private maxTasksPerUser = 100;
+
+  // 修复: 定时清理定时器
+  private cleanupTimer: NodeJS.Timeout | null = null;
+
+  constructor() {
+    // 修复: 每10分钟清理一次过旧的已完成任务
+    this.cleanupTimer = setInterval(() => {
+      this.performPeriodicCleanup();
+    }, 10 * 60 * 1000);
+  }
+
+  /**
+   * 修复: 定期清理过旧已完成任务
+   */
+  private performPeriodicCleanup(): void {
+    for (const [userId, taskList] of this.userTasks.entries()) {
+      // 保留所有 pending/running 任务
+      const activeTasks = taskList.filter(t => ['pending', 'running'].includes(t.status));
+      // 清理已完成超过50个的旧任务
+      const completedTasks = taskList.filter(t => 
+        !['pending', 'running'].includes(t.status)
+      ).sort((a, b) => 
+        new Date(b.completedAt || 0).getTime() - new Date(a.completedAt || 0).getTime()
+      );
+      const keptCompletedTasks = completedTasks.slice(0, this.maxTasksPerUser);
+      const validTasks = [...activeTasks, ...keptCompletedTasks];
+      
+      if (validTasks.length < taskList.length) {
+        this.userTasks.set(userId, validTasks);
+        logger.info(`[TaskQueue] Cleaned up ${taskList.length - validTasks.length} old tasks for user ${userId}`);
+      }
+    }
+  }
+
   /**
    * 注册任务执行器
    */
@@ -331,6 +367,11 @@ class TaskQueueService {
    * 关闭服务
    */
   async close(): Promise<void> {
+    // 修复: 清理定时器
+    if (this.cleanupTimer) {
+      clearInterval(this.cleanupTimer);
+      this.cleanupTimer = null;
+    }
     logger.info('Memory Task Queue Service closed');
   }
 }

+ 28 - 7
server/src/websocket/index.ts

@@ -14,8 +14,8 @@ interface AuthenticatedWebSocket extends WebSocket {
 class WebSocketManager {
   private wss: WebSocketServer | null = null;
   private clients: Map<number, Set<AuthenticatedWebSocket>> = new Map();
-  // 验证码回调监听器
-  private captchaListeners: Map<string, (code: string) => void> = new Map();
+  // 修复: captchaListeners 存储 callback 和 timer,以便超时自动删除
+  private captchaListeners: Map<string, { callback: (code: string) => void; timer: NodeJS.Timeout }> = new Map();
   // 心跳检测定时器
   private heartbeatTimer: NodeJS.Timeout | null = null;
 
@@ -100,7 +100,11 @@ class WebSocketManager {
     const listener = this.captchaListeners.get(captchaTaskId);
     if (listener) {
       logger.info(`[WS] Captcha submitted for task ${captchaTaskId}`);
-      listener(code);
+      // 修复: 调用 listener.callback
+      listener.callback(code);
+      // 调用后删除监听器
+      clearTimeout(listener.timer);
+      this.captchaListeners.delete(captchaTaskId);
     } else {
       logger.warn(`[WS] No listener for captcha task ${captchaTaskId}`);
     }
@@ -204,10 +208,20 @@ class WebSocketManager {
   }
   
   /**
-   * 注册验证码提交监听
+   * 注册验证码提交监听(5分钟超时后自动删除)
    */
   onCaptchaSubmit(captchaTaskId: string, callback: (code: string) => void): void {
-    this.captchaListeners.set(captchaTaskId, callback);
+    // 修复: 清除旧的定时器
+    const existing = this.captchaListeners.get(captchaTaskId);
+    if (existing) {
+      clearTimeout(existing.timer);
+    }
+    // 5分钟超时后自动删除
+    const timer = setTimeout(() => {
+      this.captchaListeners.delete(captchaTaskId);
+      logger.info(`[WS] Captcha listener timed out for ${captchaTaskId}`);
+    }, 5 * 60 * 1000);
+    this.captchaListeners.set(captchaTaskId, { callback, timer });
     logger.info(`[WS] Registered captcha listener for ${captchaTaskId}`);
   }
   
@@ -215,14 +229,21 @@ class WebSocketManager {
    * 移除验证码监听
    */
   removeCaptchaListener(captchaTaskId: string): void {
-    this.captchaListeners.delete(captchaTaskId);
-    logger.info(`[WS] Removed captcha listener for ${captchaTaskId}`);
+    const existing = this.captchaListeners.get(captchaTaskId);
+    if (existing) {
+      clearTimeout(existing.timer);
+      this.captchaListeners.delete(captchaTaskId);
+      logger.info(`[WS] Removed captcha listener for ${captchaTaskId}`);
+    }
   }
   
   /**
    * 清理所有监听器
    */
   clearAllCaptchaListeners(): void {
+    for (const { timer } of this.captchaListeners.values()) {
+      clearTimeout(timer);
+    }
     this.captchaListeners.clear();
     logger.info('[WS] Cleared all captcha listeners');
   }