Procházet zdrojové kódy

fix: HeadlessBrowserService移除response监听器泄漏,改为命名handler并在finally中off

ethanfly před 3 dny
rodič
revize
bd340c2835
1 změnil soubory, kde provedl 139 přidání a 124 odebrání
  1. 139 124
      server/src/services/HeadlessBrowserService.ts

+ 139 - 124
server/src/services/HeadlessBrowserService.ts

@@ -531,6 +531,23 @@ class HeadlessBrowserService {
     let isLoggedIn = false;
     let checkCompleted = false;
     let isRiskControl = false;
+    let page: import('playwright').Page | null = null;
+
+    // 监听 check/user 接口响应
+    const checkUserHandler = async (response: import('playwright').Response) => {
+      const url = response.url();
+      if (url.includes(DOUYIN_API.CHECK_USER)) {
+        try {
+          const data = await response.json();
+          // result: true 表示已登录
+          isLoggedIn = data?.result === true && data?.status_code === 0;
+          checkCompleted = true;
+          logger.info(`[Douyin] check/user API response: result=${data?.result}, status_code=${data?.status_code}, isLoggedIn=${isLoggedIn}`);
+        } catch {
+          // 忽略解析错误
+        }
+      }
+    };
 
     try {
       const context = await browser.newContext({
@@ -541,23 +558,10 @@ class HeadlessBrowserService {
       });
 
       await context.addCookies(cookies);
-      const page = await context.newPage();
+      page = await context.newPage();
 
-      // 监听 check/user 接口响应
-      page.on('response', async (response) => {
-        const url = response.url();
-        if (url.includes(DOUYIN_API.CHECK_USER)) {
-          try {
-            const data = await response.json();
-            // result: true 表示已登录
-            isLoggedIn = data?.result === true && data?.status_code === 0;
-            checkCompleted = true;
-            logger.info(`[Douyin] check/user API response: result=${data?.result}, status_code=${data?.status_code}, isLoggedIn=${isLoggedIn}`);
-          } catch {
-            // 忽略解析错误
-          }
-        }
-      });
+      // 绑定监听器
+      page.on('response', checkUserHandler);
 
       // 访问创作者首页,触发 check/user 接口
       await page.goto(DOUYIN_API.CREATOR_HOME, {
@@ -617,6 +621,11 @@ class HeadlessBrowserService {
         source: 'browser',
         message: error instanceof Error ? error.message : 'Douyin check error',
       };
+    } finally {
+      // 移除监听器防止内存泄漏
+      if (page) {
+        page.off('response', checkUserHandler);
+      }
     }
   }
 
@@ -1271,124 +1280,127 @@ class HeadlessBrowserService {
       total?: number;
     } = {};
 
-    try {
-      // 从 Cookie 获取用户 ID
-      const uidCookie = cookies.find(c =>
-        ['passport_uid', 'uid', 'ssid'].includes(c.name)
-      );
-      if (uidCookie?.value) {
-        accountId = `douyin_${uidCookie.value}`;
-      }
-
-      // 设置 API 响应监听器
-      page.on('response', async (response) => {
-        const url = response.url();
-        try {
-          // 监听 check/user 接口 - 验证登录状态
-          if (url.includes(DOUYIN_API.CHECK_USER)) {
-            const data = await response.json();
-            isLoggedIn = data?.result === true && data?.status_code === 0;
-            logger.info(`[Douyin API] check/user: isLoggedIn=${isLoggedIn}`);
-          }
+    // 设置 API 响应监听器
+    const responseHandler = async (response: import('playwright').Response) => {
+      const url = response.url();
+      try {
+        // 监听 check/user 接口 - 验证登录状态
+        if (url.includes(DOUYIN_API.CHECK_USER)) {
+          const data = await response.json();
+          isLoggedIn = data?.result === true && data?.status_code === 0;
+          logger.info(`[Douyin API] check/user: isLoggedIn=${isLoggedIn}`);
+        }
 
-          // 监听 work_list 接口 - 获取作品列表
-          if (url.includes('/work_list') || url.includes('/janus/douyin/creator/pc/work_list')) {
-            const data = await response.json();
-            if (data?.aweme_list && data.aweme_list.length > 0) {
-              // 优先从 author.aweme_count 获取真实的作品数(最准确)
-              const firstAweme = data.aweme_list[0];
-              const authorAwemeCount = firstAweme?.author?.aweme_count;
-              if (authorAwemeCount !== undefined && authorAwemeCount > 0) {
-                capturedData.total = authorAwemeCount;
-                logger.info(`[Douyin API] Using author.aweme_count as works count: ${authorAwemeCount}`);
+        // 监听 work_list 接口 - 获取作品列表
+        if (url.includes('/work_list') || url.includes('/janus/douyin/creator/pc/work_list')) {
+          const data = await response.json();
+          if (data?.aweme_list && data.aweme_list.length > 0) {
+            // 优先从 author.aweme_count 获取真实的作品数(最准确)
+            const firstAweme = data.aweme_list[0];
+            const authorAwemeCount = firstAweme?.author?.aweme_count;
+            if (authorAwemeCount !== undefined && authorAwemeCount > 0) {
+              capturedData.total = authorAwemeCount;
+              logger.info(`[Douyin API] Using author.aweme_count as works count: ${authorAwemeCount}`);
+            } else {
+              // 备用方案:使用 items 数组长度
+              const itemsCount = data?.items?.length || 0;
+              if (itemsCount > 0) {
+                capturedData.total = (capturedData.total || 0) + itemsCount;
               } else {
-                // 备用方案:使用 items 数组长度
-                const itemsCount = data?.items?.length || 0;
-                if (itemsCount > 0) {
-                  capturedData.total = (capturedData.total || 0) + itemsCount;
-                } else {
-                  // 如果没有 items,使用 aweme_list 长度
-                  capturedData.total = (capturedData.total || 0) + data.aweme_list.length;
-                }
+                // 如果没有 items,使用 aweme_list 长度
+                capturedData.total = (capturedData.total || 0) + data.aweme_list.length;
               }
-              // 解析作品列表;video_url 使用 video.play_addr.url_list 的第一项
-              capturedData.worksList = data.aweme_list.map((aweme: Record<string, unknown>) => {
-                const statistics = aweme.statistics as Record<string, unknown> || {};
-                const cover = aweme.Cover as { url_list?: string[] } || aweme.video as { cover?: { url_list?: string[] } };
-                const coverUrl = cover?.url_list?.[0] || (cover as { cover?: { url_list?: string[] } })?.cover?.url_list?.[0] || '';
-                const video = aweme.video as { play_addr?: { url_list?: string[] } } | undefined;
-                const videoUrl = video?.play_addr?.url_list?.[0] || '';
-
-                return {
-                  awemeId: String(aweme.aweme_id || ''),
-                  title: String(aweme.item_title || aweme.desc || '').split('\n')[0].slice(0, 50) || '无标题',
-                  coverUrl,
-                  videoUrl,
-                  duration: Number(aweme.duration || 0),
-                  createTime: Number(aweme.create_time || 0),
-                  statistics: {
-                    play_count: Number(statistics.play_count || 0),
-                    digg_count: Number(statistics.digg_count || 0),
-                    comment_count: Number(statistics.comment_count || 0),
-                    share_count: Number(statistics.share_count || 0),
-                    collect_count: Number((statistics as any).collect_count || 0),
-                  },
-                };
-              });
-              logger.info(`[Douyin API] work_list: itemsCount=${capturedData.total}, aweme_list_length=${capturedData.worksList?.length}`);
             }
+            // 解析作品列表;video_url 使用 video.play_addr.url_list 的第一项
+            capturedData.worksList = data.aweme_list.map((aweme: Record<string, unknown>) => {
+              const statistics = aweme.statistics as Record<string, unknown> || {};
+              const cover = aweme.Cover as { url_list?: string[] } || aweme.video as { cover?: { url_list?: string[] } };
+              const coverUrl = cover?.url_list?.[0] || (cover as { cover?: { url_list?: string[] } })?.cover?.url_list?.[0] || '';
+              const video = aweme.video as { play_addr?: { url_list?: string[] } } | undefined;
+              const videoUrl = video?.play_addr?.url_list?.[0] || '';
+
+              return {
+                awemeId: String(aweme.aweme_id || ''),
+                title: String(aweme.item_title || aweme.desc || '').split('\n')[0].slice(0, 50) || '无标题',
+                coverUrl,
+                videoUrl,
+                duration: Number(aweme.duration || 0),
+                createTime: Number(aweme.create_time || 0),
+                statistics: {
+                  play_count: Number(statistics.play_count || 0),
+                  digg_count: Number(statistics.digg_count || 0),
+                  comment_count: Number(statistics.comment_count || 0),
+                  share_count: Number(statistics.share_count || 0),
+                  collect_count: Number((statistics as any).collect_count || 0),
+                },
+              };
+            });
+            logger.info(`[Douyin API] work_list: itemsCount=${capturedData.total}, aweme_list_length=${capturedData.worksList?.length}`);
           }
+        }
 
-          // 监听账号信息接口 - 增加更多可能的接口
-          if (url.includes('/account_base_info') ||
-            url.includes('/user/info') ||
-            url.includes('/creator/user') ||
-            url.includes('/data/overview') ||
-            url.includes('/creator-micro/data') ||
-            url.includes('/home_data')) {
-            const data = await response.json();
-            logger.info(`[Douyin API] Captured response from: ${url.split('?')[0]}`);
-
-            // 处理 data/overview API - 获取总作品数
-            if (url.includes('/data/overview') || url.includes('/creator-micro/data')) {
-              if (data?.data) {
-                capturedData.dataOverview = {
-                  fans_count: data.data.fans_count || data.data.follower_count,
-                  total_works: data.data.total_item_cnt || data.data.works_count || data.data.video_count,
-                  total_play: data.data.total_play_cnt,
-                };
-                logger.info(`[Douyin API] Captured data overview: total_works=${capturedData.dataOverview.total_works}, fans_count=${capturedData.dataOverview.fans_count}`);
-              }
+        // 监听账号信息接口 - 增加更多可能的接口
+        if (url.includes('/account_base_info') ||
+          url.includes('/user/info') ||
+          url.includes('/creator/user') ||
+          url.includes('/data/overview') ||
+          url.includes('/creator-micro/data') ||
+          url.includes('/home_data')) {
+          const data = await response.json();
+          logger.info(`[Douyin API] Captured response from: ${url.split('?')[0]}`);
+
+          // 处理 data/overview API - 获取总作品数
+          if (url.includes('/data/overview') || url.includes('/creator-micro/data')) {
+            if (data?.data) {
+              capturedData.dataOverview = {
+                fans_count: data.data.fans_count || data.data.follower_count,
+                total_works: data.data.total_item_cnt || data.data.works_count || data.data.video_count,
+                total_play: data.data.total_play_cnt,
+              };
+              logger.info(`[Douyin API] Captured data overview: total_works=${capturedData.dataOverview.total_works}, fans_count=${capturedData.dataOverview.fans_count}`);
             }
-            
-            // 尝试多种数据结构
-            const user = data?.user || data?.data?.user || data?.data || data;
-            if (user) {
-              const nickname = user.nickname || user.name || user.nick_name || user.user_name;
-              const avatar = user.avatar_url || user.avatar_thumb?.url_list?.[0] || user.avatar || user.avatar_larger?.url_list?.[0];
-              const uid = user.uid || user.user_id || user.id;
-              const fans = user.follower_count || user.fans_count || user.mplatform_followers_count;
-              // 获取抖音号(unique_id 或 short_id)
-              const uniqueId = user.unique_id || user.short_id || user.douyin_id;
-
-              if (nickname || uid || uniqueId) {
-                capturedData.userInfo = {
-                  nickname: nickname,
-                  avatar: avatar,
-                  uid: uid,
-                  sec_uid: user.sec_uid,
-                  unique_id: uniqueId,
-                  short_id: user.short_id,
-                  follower_count: fans,
-                };
-                logger.info(`[Douyin API] user info captured: nickname=${capturedData.userInfo.nickname}, uid=${capturedData.userInfo.uid}, unique_id=${capturedData.userInfo.unique_id}`);
-              }
+          }
+
+          // 尝试多种数据结构
+          const user = data?.user || data?.data?.user || data?.data || data;
+          if (user) {
+            const nickname = user.nickname || user.name || user.nick_name || user.user_name;
+            const avatar = user.avatar_url || user.avatar_thumb?.url_list?.[0] || user.avatar || user.avatar_larger?.url_list?.[0];
+            const uid = user.uid || user.user_id || user.id;
+            const fans = user.follower_count || user.fans_count || user.mplatform_followers_count;
+            // 获取抖音号(unique_id 或 short_id)
+            const uniqueId = user.unique_id || user.short_id || user.douyin_id;
+
+            if (nickname || uid || uniqueId) {
+              capturedData.userInfo = {
+                nickname: nickname,
+                avatar: avatar,
+                uid: uid,
+                sec_uid: user.sec_uid,
+                unique_id: uniqueId,
+                short_id: user.short_id,
+                follower_count: fans,
+              };
+              logger.info(`[Douyin API] user info captured: nickname=${capturedData.userInfo.nickname}, uid=${capturedData.userInfo.uid}, unique_id=${capturedData.userInfo.unique_id}`);
             }
           }
-        } catch (e) {
-          // 忽略非 JSON 响应
         }
-      });
+      } catch (e) {
+        // 忽略非 JSON 响应
+      }
+    };
+
+    try {
+      // 从 Cookie 获取用户 ID
+      const uidCookie = cookies.find(c =>
+        ['passport_uid', 'uid', 'ssid'].includes(c.name)
+      );
+      if (uidCookie?.value) {
+        accountId = `douyin_${uidCookie.value}`;
+      }
+
+      // 绑定监听器
+      page.on('response', responseHandler);
 
       // 访问主页获取基本信息并触发 check/user 接口
       logger.info('[Douyin] Navigating to creator home...');
@@ -1701,6 +1713,9 @@ class HeadlessBrowserService {
     } catch (error) {
       logger.error('Failed to fetch Douyin account info:', error);
       logger.error('Error details:', error instanceof Error ? error.stack : String(error));
+    } finally {
+      // 移除监听器防止内存泄漏
+      page.off('response', responseHandler);
     }
 
     return { accountId, accountName, avatarUrl, fansCount, worksCount, worksList };