瀏覽代碼

Enhance account info extraction and validation in AI login process; add server API integration for account verification and improve logging for better debugging.

Ethanfly 13 小時之前
父節點
當前提交
1d5ac0a27f

+ 63 - 3
client/src/components/BrowserTab.vue

@@ -512,8 +512,16 @@ async function handleAILoginSuccess(currentScreenshot: string) {
     }
   }
   
-  // 检查是否成功提取到账号信息
-  if (extractedAccountInfo && extractedAccountInfo.accountName) {
+  // 检查是否成功提取到有效的账号信息(排除默认值)
+  const defaultNames = [
+    '抖音账号', '小红书账号', '快手账号', '视频号账号', 'B站账号', 
+    '头条账号', '百家号账号', '企鹅号账号', '大鱼号账号', '未知账号'
+  ];
+  const isValidAccountInfo = extractedAccountInfo && 
+    extractedAccountInfo.accountName && 
+    !defaultNames.includes(extractedAccountInfo.accountName);
+  
+  if (isValidAccountInfo) {
     // 成功!保存 cookies 并设置登录成功状态
     const cookies = await getCookies();
     if (cookies && cookies.length > 0) {
@@ -530,6 +538,7 @@ async function handleAILoginSuccess(currentScreenshot: string) {
       ElMessage.success('登录成功!已获取账号信息');
     }
   } else {
+    console.log('[AI] Account info is invalid or default, not triggering save prompt');
     // 提取失败,尝试导航到个人中心页面获取账号信息
     console.log('[AI] Failed to extract account info, trying to navigate to profile page...');
     
@@ -547,7 +556,12 @@ async function handleAILoginSuccess(currentScreenshot: string) {
           const retryResult = await aiApi.extractAccountInfo(retryScreenshot, platform.value);
           console.log('[AI] Retry extract result after navigation:', retryResult);
           
-          if (retryResult.found && retryResult.accountName) {
+          // 验证提取的账号信息是否有效
+          const retryIsValid = retryResult.found && 
+            retryResult.accountName && 
+            !defaultNames.includes(retryResult.accountName);
+          
+          if (retryIsValid) {
             extractedAccountInfo = {
               accountId: retryResult.accountId || `${platform.value}_${Date.now()}`,
               accountName: retryResult.accountName,
@@ -585,6 +599,52 @@ async function handleAILoginSuccess(currentScreenshot: string) {
       }
     }
     
+    // AI 提取失败,尝试通过服务端 API 获取账号信息
+    console.log('[AI] Trying to get account info via server API...');
+    
+    try {
+      // 先获取 cookies
+      const cookies = await getCookies();
+      if (cookies && cookies.length > 0) {
+        cookieData.value = formatCookies(cookies);
+        
+        // 调用服务端 API 验证 Cookie 并获取账号信息
+        const verifyResult = await accountsApi.verifyLoginCookie(platform.value, cookieData.value);
+        
+        console.log('[AI] Server verify result:', verifyResult);
+        
+        // 检查服务端返回的账号信息是否有效
+        if (verifyResult.success && verifyResult.accountInfo) {
+          const serverAccountInfo = verifyResult.accountInfo;
+          if (serverAccountInfo.accountName && !defaultNames.includes(serverAccountInfo.accountName)) {
+            extractedAccountInfo = {
+              accountId: serverAccountInfo.accountId || `${platform.value}_${Date.now()}`,
+              accountName: serverAccountInfo.accountName,
+              avatarUrl: serverAccountInfo.avatarUrl || '',
+              fansCount: serverAccountInfo.fansCount || 0,
+              worksCount: serverAccountInfo.worksCount || 0,
+            };
+            
+            accountInfo.value = extractedAccountInfo;
+            loginStatus.value = 'success';
+            stopAutoCheck();
+            stopAIAnalysis();
+            
+            if (!hasShownSuccessMessage) {
+              hasShownSuccessMessage = true;
+              ElMessage.success('登录成功!已获取账号信息');
+            }
+            
+            fetchingAccountInfo.value = false;
+            isVerifying = false;
+            return;
+          }
+        }
+      }
+    } catch (serverError) {
+      console.warn('[AI] Server API failed:', serverError);
+    }
+    
     // 仍然失败,显示提示
     if (aiAnalysis.value) {
       aiAnalysis.value.suggestedAction = '无法获取账号信息,请手动进入个人中心页面,或点击头像查看账号信息';

+ 28 - 0
server/python/app.py

@@ -126,6 +126,28 @@ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %
 # 让 werkzeug 日志显示
 werkzeug_logger = logging.getLogger('werkzeug')
 werkzeug_logger.setLevel(logging.INFO)
+# 添加 StreamHandler 确保输出到控制台
+handler = logging.StreamHandler(sys.stdout)
+handler.setLevel(logging.DEBUG)
+werkzeug_logger.addHandler(handler)
+
+# 添加请求钩子,打印所有收到的请求
+@app.before_request
+def log_request_info():
+    """在处理每个请求前打印详细信息"""
+    print(f"\n{'='*60}", flush=True)
+    print(f"[HTTP Request] {request.method} {request.path}", flush=True)
+    print(f"[HTTP Request] From: {request.remote_addr}", flush=True)
+    if request.content_type and 'json' in request.content_type:
+        try:
+            data = request.get_json(silent=True)
+            if data:
+                # 打印部分参数,避免太长
+                keys = list(data.keys()) if data else []
+                print(f"[HTTP Request] JSON keys: {keys}", flush=True)
+        except:
+            pass
+    print(f"{'='*60}\n", flush=True)
 
 # 全局配置
 HEADLESS_MODE = os.environ.get('HEADLESS', 'true').lower() == 'true'
@@ -346,8 +368,14 @@ def publish_ai_assisted():
         ...
     }
     """
+    # 立即打印请求日志,确保能看到
+    print("\n" + "!" * 60, flush=True)
+    print("!!! [AI-Assisted Publish] 收到请求 !!!", flush=True)
+    print("!" * 60 + "\n", flush=True)
+    
     try:
         data = request.json
+        print(f"[AI-Assisted Publish] 请求数据: platform={data.get('platform')}, title={data.get('title')}", flush=True)
         
         # 获取参数
         platform = data.get("platform", "").lower()

+ 1 - 1
server/python/platforms/douyin.py

@@ -479,7 +479,7 @@ class DouyinPublisher(BasePublisher):
             
             # 调用作品列表 API
             cursor = page * page_size
-            api_url = f"https://creator.douyin.com/janus/douyin/creator/pc/work_list?scene=star_atlas&device_platform=android&count={page_size}&max_cursor={cursor}&cookie_enabled=true&browser_language=zh-CN&browser_platform=Win32&browser_name=Mozilla&browser_online=true&timezone_name=Asia%2FShanghai&aid=1128"
+            api_url = f"https://creator.douyin.com/janus/douyin/creator/pc/work_list?status=0&scene=star_atlas&device_platform=android&count={page_size}&max_cursor={cursor}&cookie_enabled=true&browser_language=zh-CN&browser_platform=Win32&browser_name=Mozilla&browser_online=true&timezone_name=Asia%2FShanghai&aid=1128"
             
             response = await self.page.evaluate(f'''
                 async () => {{

+ 214 - 16
server/src/services/AccountService.ts

@@ -160,21 +160,43 @@ export class AccountService {
       decryptedCookies = cookieData;
     }
     
-    // 从 Cookie 中提取账号 ID(作为后备方案)
-    let accountIdFromCookie = `${platform}_${Date.now()}`;
-    try {
-      const cookieList = JSON.parse(decryptedCookies);
-      const uidCookie = cookieList.find((c: { name: string }) => 
-        ['passport_uid', 'uid', 'ssid', 'DedeUserID', 'userid'].includes(c.name)
-      );
-      if (uidCookie?.value) {
-        accountIdFromCookie = `${platform}_${uidCookie.value}`;
-      }
-    } catch {}
+    // 检查客户端传入的 accountId 是否有效(不是纯时间戳)
+    const clientAccountId = data.accountInfo?.accountId;
+    const isValidClientAccountId = clientAccountId && !this.isTimestampBasedId(clientAccountId);
+    
+    // 从 Cookie 中提取账号 ID(部分平台的 Cookie 包含真实用户 ID)
+    const accountIdFromCookie = this.extractAccountIdFromCookie(platform, decryptedCookies);
+    
+    // 某些平台应优先使用 API 返回的真实 ID,而不是 Cookie 中的值
+    // - 抖音:使用抖音号(unique_id,如 Ethanfly9392),而不是 Cookie 中的 passport_uid
+    // - 百家号:使用 new_uc_id,而不是 Cookie 中的 BDUSS
+    // - 视频号/头条:使用 API 返回的真实账号 ID
+    const platformsPreferApiId: PlatformType[] = ['douyin', 'baijiahao', 'weixin_video', 'toutiao'];
+    const preferApiId = platformsPreferApiId.includes(platform);
+    
+    // 确定最终的 accountId
+    let finalAccountId: string;
+    
+    if (preferApiId && isValidClientAccountId) {
+      // 对于优先使用 API ID 的平台,先用客户端传入的有效 ID
+      finalAccountId = this.normalizeAccountId(platform, clientAccountId);
+      logger.info(`[addAccount] Using API-based accountId for ${platform}: ${finalAccountId}`);
+    } else if (accountIdFromCookie) {
+      // 其他平台优先使用 Cookie 中提取的 ID
+      finalAccountId = accountIdFromCookie;
+      logger.info(`[addAccount] Using accountId from cookie: ${finalAccountId}`);
+    } else if (isValidClientAccountId) {
+      // 再次尝试客户端 ID
+      finalAccountId = this.normalizeAccountId(platform, clientAccountId);
+      logger.info(`[addAccount] Using valid client accountId: ${finalAccountId}`);
+    } else {
+      finalAccountId = `${platform}_${Date.now()}`;
+      logger.warn(`[addAccount] Using timestamp-based accountId as fallback: ${finalAccountId}`);
+    }
     
     // 使用传入的账号信息(来自浏览器登录会话),或使用默认值
     const accountInfo = {
-      accountId: data.accountInfo?.accountId || accountIdFromCookie,
+      accountId: finalAccountId,
       accountName: data.accountInfo?.accountName || `${platform}账号`,
       avatarUrl: data.accountInfo?.avatarUrl || null,
       fansCount: data.accountInfo?.fansCount || 0,
@@ -203,6 +225,12 @@ export class AccountService {
       
       const updated = await this.accountRepository.findOne({ where: { id: existing.id } });
       wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_UPDATED, { account: this.formatAccount(updated!) });
+      
+      // 异步刷新账号信息(获取准确的粉丝数、作品数等)
+      this.refreshAccountAsync(userId, existing.id, platform).catch(err => {
+        logger.warn(`[addAccount] Background refresh failed for existing account ${existing.id}:`, err);
+      });
+      
       return this.formatAccount(updated!);
     }
 
@@ -226,9 +254,32 @@ export class AccountService {
     // 通知其他客户端
     wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_ADDED, { account: this.formatAccount(account) });
 
+    // 异步刷新账号信息(获取准确的粉丝数、作品数等)
+    // 不阻塞返回,后台执行
+    this.refreshAccountAsync(userId, account.id, platform).catch(err => {
+      logger.warn(`[addAccount] Background refresh failed for account ${account.id}:`, err);
+    });
+
     return this.formatAccount(account);
   }
 
+  /**
+   * 异步刷新账号信息(用于添加账号后获取准确数据)
+   */
+  private async refreshAccountAsync(userId: number, accountId: number, platform: PlatformType): Promise<void> {
+    // 延迟 2 秒执行,等待前端处理完成
+    await new Promise(resolve => setTimeout(resolve, 2000));
+    
+    logger.info(`[addAccount] Starting background refresh for account ${accountId} (${platform})`);
+    
+    try {
+      await this.refreshAccount(userId, accountId);
+      logger.info(`[addAccount] Background refresh completed for account ${accountId}`);
+    } catch (error) {
+      logger.warn(`[addAccount] Background refresh failed for account ${accountId}:`, error);
+    }
+  }
+
   async updateAccount(
     userId: number,
     accountId: number,
@@ -311,8 +362,12 @@ export class AccountService {
         }
 
         if (cookieList.length > 0 && !cookieParseError) {
-          // ========== AI 辅助刷新(优先) ==========
-          if (aiService.isAvailable()) {
+          // 抖音和小红书直接使用 API 获取准确数据,不使用 AI(因为 AI 可能识别错误)
+          const platformsSkipAI: PlatformType[] = ['douyin', 'xiaohongshu'];
+          const shouldUseAI = aiService.isAvailable() && !platformsSkipAI.includes(platform);
+          
+          // ========== AI 辅助刷新(部分平台使用) ==========
+          if (shouldUseAI) {
             try {
               logger.info(`[AI Refresh] Starting AI-assisted refresh for account ${accountId} (${platform})`);
               
@@ -376,7 +431,19 @@ export class AccountService {
                   updateData.avatarUrl = profile.avatarUrl;
                   updateData.fansCount = profile.fansCount;
                   updateData.worksCount = profile.worksCount;
-                  logger.info(`Refreshed account info for ${platform}: ${profile.accountName}, fans: ${profile.fansCount}`);
+                  
+                  // 如果获取到了有效的 accountId(如抖音号),也更新它
+                  // 这样可以修正之前使用错误 ID(如 Cookie 值)保存的账号
+                  if (profile.accountId && !this.isTimestampBasedId(profile.accountId)) {
+                    const newAccountId = this.normalizeAccountId(platform, profile.accountId);
+                    // 只有当新 ID 与旧 ID 不同时才更新
+                    if (newAccountId !== account.accountId) {
+                      updateData.accountId = newAccountId;
+                      logger.info(`[refreshAccount] Updating accountId from ${account.accountId} to ${newAccountId}`);
+                    }
+                  }
+                  
+                  logger.info(`Refreshed account info for ${platform}: ${profile.accountName}, fans: ${profile.fansCount}, works: ${profile.worksCount}`);
                 } else {
                   // 获取的信息无效,但 Cookie 有效,保持 active 状态
                   logger.warn(`Could not fetch valid account info for ${accountId}, but cookie is valid`);
@@ -611,7 +678,8 @@ export class AccountService {
           return {
             success: true,
             accountInfo: {
-              accountId: profile.accountId || `${platform}_${Date.now()}`,
+              // 确保 accountId 带有平台前缀
+              accountId: this.normalizeAccountId(platform, profile.accountId || ''),
               accountName: profile.accountName,
               avatarUrl: profile.avatarUrl || '',
               fansCount: profile.fansCount || 0,
@@ -734,4 +802,134 @@ export class AccountService {
       updatedAt: account.updatedAt.toISOString(),
     };
   }
+
+  /**
+   * 从 Cookie 中提取账号 ID(最可靠的方式)
+   * 不同平台使用不同的 Cookie 字段来标识用户
+   */
+  private extractAccountIdFromCookie(platform: PlatformType, cookieString: string): string | null {
+    // 各平台用于标识用户的 Cookie 名称(按优先级排序)
+    const platformCookieNames: Record<string, string[]> = {
+      douyin: ['passport_uid', 'uid_tt', 'ttwid', 'sessionid_ss'],
+      kuaishou: ['userId', 'passToken', 'did'],
+      xiaohongshu: ['customerClientId', 'web_session', 'xsecappid'],
+      weixin_video: ['wxuin', 'pass_ticket', 'uin'],
+      bilibili: ['DedeUserID', 'SESSDATA', 'bili_jct'],
+      toutiao: ['sso_uid', 'sessionid', 'passport_uid'],
+      baijiahao: ['BDUSS', 'STOKEN', 'BAIDUID'],
+      qie: ['uin', 'skey', 'p_uin'],
+      dayuhao: ['login_aliyunid', 'cna', 'munb'],
+    };
+    
+    const targetCookieNames = platformCookieNames[platform] || [];
+    if (targetCookieNames.length === 0) {
+      return null;
+    }
+    
+    try {
+      // 尝试解析 JSON 格式的 Cookie
+      let cookieList: { name: string; value: string }[];
+      try {
+        cookieList = JSON.parse(cookieString);
+      } catch {
+        // 如果不是 JSON,尝试解析 "name=value; name2=value2" 格式
+        cookieList = this.parseCookieString(cookieString, platform).map(c => ({
+          name: c.name,
+          value: c.value,
+        }));
+      }
+      
+      if (!Array.isArray(cookieList) || cookieList.length === 0) {
+        return null;
+      }
+      
+      // 按优先级查找 Cookie
+      for (const cookieName of targetCookieNames) {
+        const cookie = cookieList.find(c => c.name === cookieName);
+        if (cookie?.value) {
+          // 获取 Cookie 值,处理可能的编码
+          let cookieValue = cookie.value;
+          
+          // 处理特殊格式的 Cookie(如 ttwid 可能包含分隔符)
+          if (cookieValue.includes('|')) {
+            cookieValue = cookieValue.split('|')[1] || cookieValue;
+          }
+          if (cookieValue.includes('%')) {
+            try {
+              cookieValue = decodeURIComponent(cookieValue);
+            } catch {
+              // 解码失败,使用原值
+            }
+          }
+          
+          // 截取合理长度(避免过长的 ID)
+          if (cookieValue.length > 64) {
+            cookieValue = cookieValue.slice(0, 64);
+          }
+          
+          const accountId = `${platform}_${cookieValue}`;
+          logger.info(`[extractAccountIdFromCookie] Found ${cookieName} for ${platform}: ${accountId}`);
+          return accountId;
+        }
+      }
+      
+      return null;
+    } catch (error) {
+      logger.warn(`[extractAccountIdFromCookie] Failed to extract accountId from cookie for ${platform}:`, error);
+      return null;
+    }
+  }
+
+  /**
+   * 标准化 accountId 格式,确保带有平台前缀
+   * 例如:1833101008440434 -> baijiahao_1833101008440434
+   */
+  private normalizeAccountId(platform: PlatformType, accountId: string): string {
+    if (!accountId) {
+      return `${platform}_${Date.now()}`;
+    }
+    
+    // 如果已经有平台前缀,直接返回
+    if (accountId.startsWith(`${platform}_`)) {
+      return accountId;
+    }
+    
+    // 如果有其他平台前缀(可能是错误的),替换为正确的前缀
+    const platformPrefixPattern = /^(douyin|kuaishou|xiaohongshu|weixin_video|bilibili|toutiao|baijiahao|qie|dayuhao)_/;
+    if (platformPrefixPattern.test(accountId)) {
+      return accountId.replace(platformPrefixPattern, `${platform}_`);
+    }
+    
+    // 没有前缀,添加平台前缀
+    return `${platform}_${accountId}`;
+  }
+
+  /**
+   * 检查 accountId 是否是基于时间戳生成的(不可靠的 ID)
+   * 时间戳 ID 格式通常是:platform_1737619200000
+   */
+  private isTimestampBasedId(accountId: string): boolean {
+    if (!accountId) return true;
+    
+    // 检查是否匹配 platform_时间戳 格式
+    const timestampPattern = /^[a-z_]+_\d{13,}$/;
+    if (!timestampPattern.test(accountId)) {
+      return false;
+    }
+    
+    // 提取数字部分,检查是否是合理的时间戳(2020年到2030年之间)
+    const match = accountId.match(/_(\d{13,})$/);
+    if (match) {
+      const timestamp = parseInt(match[1]);
+      const minTimestamp = new Date('2020-01-01').getTime(); // 1577836800000
+      const maxTimestamp = new Date('2030-01-01').getTime(); // 1893456000000
+      
+      if (timestamp >= minTimestamp && timestamp <= maxTimestamp) {
+        logger.info(`[isTimestampBasedId] Detected timestamp-based ID: ${accountId}`);
+        return true;
+      }
+    }
+    
+    return false;
+  }
 }

+ 272 - 30
server/src/services/HeadlessBrowserService.ts

@@ -535,7 +535,9 @@ class HeadlessBrowserService {
             try {
               const accountInfo = await this.fetchAccountInfoWithPlaywright(platform, cookies);
               accountInfo.worksList = worksList;
+              // 直接使用 Python API 获取的作品数量(最准确,排除了已删除/私密视频)
               accountInfo.worksCount = worksList.length;
+              logger.info(`[fetchAccountInfo] Using Python API works count for ${platform}: ${accountInfo.worksCount}`);
               return accountInfo;
             } catch (playwrightError) {
               logger.warn(`[Playwright] Failed to get account info for ${platform}:`, playwrightError);
@@ -593,6 +595,9 @@ class HeadlessBrowserService {
         case 'xiaohongshu':
           accountInfo = await this.fetchXiaohongshuAccountInfo(page, context, cookies);
           break;
+        case 'weixin_video':
+          accountInfo = await this.fetchWeixinVideoAccountInfo(page, context, cookies);
+          break;
         default:
           accountInfo = this.getDefaultAccountInfo(platform);
       }
@@ -672,10 +677,22 @@ class HeadlessBrowserService {
           // 监听 work_list 接口 - 获取作品列表
           if (url.includes('/work_list') || url.includes('/janus/douyin/creator/pc/work_list')) {
             const data = await response.json();
-            if (data?.aweme_list) {
-              // 获取总数
-              if (data.total !== undefined) {
-                capturedData.total = data.total;
+            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,使用 aweme_list 长度
+                  capturedData.total = (capturedData.total || 0) + data.aweme_list.length;
+                }
               }
               // 解析作品列表
               capturedData.worksList = data.aweme_list.map((aweme: Record<string, unknown>) => {
@@ -697,7 +714,7 @@ class HeadlessBrowserService {
                   },
                 };
               });
-              logger.info(`[Douyin API] work_list: total=${capturedData.total}, items=${capturedData.worksList?.length}`);
+              logger.info(`[Douyin API] work_list: itemsCount=${capturedData.total}, aweme_list_length=${capturedData.worksList?.length}`);
             }
           }
 
@@ -943,11 +960,13 @@ class HeadlessBrowserService {
 
       // 通过 API 获取作品列表
       logger.info('[Douyin] Fetching works via API...');
-      const apiWorks = await this.fetchWorksDirectApi(page);
+      const apiResult = await this.fetchWorksDirectApi(page);
 
-      if (apiWorks.length > 0) {
-        worksCount = apiWorks.length;
-        worksList = apiWorks.map(w => ({
+      if (apiResult.works.length > 0) {
+        // 使用 items 累计数量作为作品数(apiResult.total 现在是累计的 items.length)
+        // 如果 total 为 0,则使用 works 列表长度
+        worksCount = apiResult.total > 0 ? apiResult.total : apiResult.works.length;
+        worksList = apiResult.works.map(w => ({
           videoId: w.awemeId,
           title: w.title,
           coverUrl: w.coverUrl,
@@ -959,7 +978,7 @@ class HeadlessBrowserService {
           commentCount: w.commentCount,
           shareCount: 0,
         }));
-        logger.info(`[Douyin] Got ${worksCount} works from API`);
+        logger.info(`[Douyin] Got ${apiResult.works.length} works from API, total count: ${worksCount}`);
       } else if (capturedData.worksList && capturedData.worksList.length > 0) {
         // 如果直接 API 调用失败,使用监听到的数据
         worksCount = capturedData.total || capturedData.worksList.length;
@@ -1099,6 +1118,180 @@ class HeadlessBrowserService {
   }
 
   /**
+   * 获取微信视频号账号信息
+   */
+  private async fetchWeixinVideoAccountInfo(
+    page: Page,
+    _context: BrowserContext,
+    cookies: CookieData[]
+  ): Promise<AccountInfo> {
+    let accountId = `weixin_video_${Date.now()}`;
+    let accountName = '视频号账号';
+    let avatarUrl = '';
+    let fansCount = 0;
+    let worksCount = 0;
+
+    try {
+      // 从 Cookie 中提取用户标识
+      const uinCookie = cookies.find(c => c.name === 'wxuin' || c.name === 'uin');
+      if (uinCookie?.value) {
+        accountId = `weixin_video_${uinCookie.value}`;
+      }
+
+      // 访问视频号创作者平台首页
+      await page.goto('https://channels.weixin.qq.com/platform/home', {
+        waitUntil: 'domcontentloaded',
+        timeout: 30000,
+      });
+
+      await page.waitForTimeout(3000);
+
+      // 检查是否需要登录
+      const currentUrl = page.url();
+      if (currentUrl.includes('login') || currentUrl.includes('passport')) {
+        logger.warn('[WeixinVideo] Cookie expired, needs login');
+        return { accountId, accountName, avatarUrl, fansCount, worksCount };
+      }
+
+      // 从页面提取账号信息
+      const accountData = await page.evaluate(() => {
+        const result: { name?: string; avatar?: string; fans?: number; works?: number; finderId?: string } = {};
+
+        try {
+          // 查找头像 - 视频号创作者平台头像选择器
+          const avatarSelectors = [
+            '.finder-avatar img',
+            '.account-avatar img',
+            '.user-avatar img',
+            '[class*="avatar"] img',
+            '[class*="Avatar"] img',
+            'img[class*="avatar"]',
+            '.header-user img',
+            '.header img[src*="wx.qlogo"]',
+            '.header img[src*="mmbiz"]',
+            'img[src*="wx.qlogo"]',
+            'img[src*="mmbiz.qpic"]',
+          ];
+
+          for (const selector of avatarSelectors) {
+            const el = document.querySelector(selector) as HTMLImageElement;
+            if (el?.src && el.src.startsWith('http')) {
+              result.avatar = el.src;
+              console.log('[WeixinVideo] Found avatar:', el.src);
+              break;
+            }
+          }
+
+          // 查找用户名
+          const nameSelectors = [
+            '.finder-nickname',
+            '.account-name',
+            '.user-name',
+            '[class*="nickname"]',
+            '[class*="userName"]',
+            '[class*="user-name"]',
+            '.header-user-name',
+            'h2.name',
+            '.name-text',
+          ];
+
+          for (const selector of nameSelectors) {
+            const el = document.querySelector(selector);
+            const text = el?.textContent?.trim();
+            if (text && text.length >= 2 && text.length <= 30) {
+              result.name = text;
+              console.log('[WeixinVideo] Found name:', text);
+              break;
+            }
+          }
+
+          // 查找视频号 ID
+          const bodyText = document.body.innerText || '';
+          const finderIdMatch = bodyText.match(/视频号ID[::]\s*([a-zA-Z0-9_]+)/);
+          if (finderIdMatch) {
+            result.finderId = finderIdMatch[1];
+          }
+
+          // 尝试从页面文本中提取粉丝数和作品数
+          const statsTexts = document.querySelectorAll('[class*="stat"], [class*="count"], [class*="number"]');
+          statsTexts.forEach(el => {
+            const text = el.textContent || '';
+            const parent = el.parentElement?.textContent || '';
+
+            // 粉丝数
+            if (parent.includes('粉丝') || text.includes('粉丝')) {
+              const match = text.match(/(\d+(?:\.\d+)?[万wW]?)/);
+              if (match) {
+                let count = parseFloat(match[1]);
+                if (match[1].includes('万') || match[1].toLowerCase().includes('w')) {
+                  count = count * 10000;
+                }
+                result.fans = Math.floor(count);
+              }
+            }
+
+            // 作品数
+            if (parent.includes('作品') || parent.includes('视频') || text.includes('作品')) {
+              const match = text.match(/(\d+)/);
+              if (match) {
+                result.works = parseInt(match[1], 10);
+              }
+            }
+          });
+
+          // 备选:遍历页面查找用户名(如果上面没找到)
+          if (!result.name) {
+            const allElements = document.querySelectorAll('span, div, h1, h2, h3');
+            for (const el of allElements) {
+              const text = el.textContent?.trim();
+              const rect = (el as HTMLElement).getBoundingClientRect();
+              // 在页面顶部区域查找可能的用户名
+              if (text && rect.top < 200 && rect.width > 0 &&
+                  text.length >= 2 && text.length <= 20 &&
+                  /[\u4e00-\u9fa5a-zA-Z]/.test(text) &&
+                  !/粉丝|关注|作品|视频|数据|登录|注册|设置|首页/.test(text)) {
+                result.name = text;
+                break;
+              }
+            }
+          }
+
+        } catch (e) {
+          console.error('[WeixinVideo] Extract error:', e);
+        }
+
+        return result;
+      });
+
+      logger.info(`[WeixinVideo] Extracted account data:`, accountData);
+
+      // 更新账号信息
+      if (accountData.name) {
+        accountName = accountData.name;
+      }
+      if (accountData.avatar) {
+        avatarUrl = accountData.avatar;
+      }
+      if (accountData.fans !== undefined) {
+        fansCount = accountData.fans;
+      }
+      if (accountData.works !== undefined) {
+        worksCount = accountData.works;
+      }
+      if (accountData.finderId) {
+        accountId = `weixin_video_${accountData.finderId}`;
+      }
+
+      logger.info(`[WeixinVideo] Account info: id=${accountId}, name=${accountName}, avatar=${avatarUrl ? 'yes' : 'no'}, fans=${fansCount}`);
+
+    } catch (error) {
+      logger.warn('Failed to fetch WeixinVideo account info:', error);
+    }
+
+    return { accountId, accountName, avatarUrl, fansCount, worksCount };
+  }
+
+  /**
    * 获取小红书账号信息 - 通过 API 方式获取
    */
   private async fetchXiaohongshuAccountInfo(
@@ -1578,13 +1771,14 @@ class HeadlessBrowserService {
 
         logger.info(`[Xiaohongshu] Fetched ${worksList.length} works via API`);
 
-        // 更新作品数:优先使用从 API tags 获取的总数
-        if (totalNotesCount > 0) {
-          worksCount = totalNotesCount;
-          logger.info(`[Xiaohongshu] Using total notes count from API: ${worksCount}`);
-        } else if (worksList.length > 0) {
+        // 更新作品数:直接使用获取到的 notes 数量(更准确)
+        // 只有当 notes 为空时才使用 tags 中的 notes_count
+        if (worksList.length > 0) {
           worksCount = worksList.length;
-          logger.info(`[Xiaohongshu] Using works list length: ${worksCount}`);
+          logger.info(`[Xiaohongshu] Using actual notes count: ${worksCount}`);
+        } else if (totalNotesCount > 0) {
+          worksCount = totalNotesCount;
+          logger.info(`[Xiaohongshu] Using notes count from tags: ${worksCount}`);
         }
       } catch (worksError) {
         logger.warn('[Xiaohongshu] Failed to fetch works list:', worksError);
@@ -2695,14 +2889,20 @@ class HeadlessBrowserService {
   /**
    * 直接调用抖音 API 获取作品列表
    * 使用新的 work_list 接口,支持分页加载
+   * 返回作品列表和总作品数
+   * 
+   * 注意:需要先导航到作品管理页面才能正确调用 API
    */
-  private async fetchWorksDirectApi(page: Page): Promise<Array<{
-    awemeId: string;
-    title: string;
-    coverUrl: string;
-    commentCount: number;
-    createTime?: number;
-  }>> {
+  private async fetchWorksDirectApi(page: Page): Promise<{
+    works: Array<{
+      awemeId: string;
+      title: string;
+      coverUrl: string;
+      commentCount: number;
+      createTime?: number;
+    }>;
+    total: number;
+  }> {
     const works: Array<{
       awemeId: string;
       title: string;
@@ -2710,8 +2910,29 @@ class HeadlessBrowserService {
       commentCount: number;
       createTime?: number;
     }> = [];
+    let totalCount = 0; // 从 API 获取的总作品数
 
     try {
+      // 首先导航到作品管理页面,确保 API 有正确的上下文和权限
+      const contentManageUrl = 'https://creator.douyin.com/creator-micro/content/manage';
+      const currentUrl = page.url();
+      
+      if (!currentUrl.includes('/content/manage')) {
+        logger.info(`[DirectAPI] Navigating to content manage page...`);
+        await page.goto(contentManageUrl, {
+          waitUntil: 'domcontentloaded',
+          timeout: 30000,
+        });
+        await page.waitForTimeout(2000);
+        
+        // 检查是否需要登录
+        const newUrl = page.url();
+        if (newUrl.includes('login') || newUrl.includes('passport')) {
+          logger.warn('[DirectAPI] Not logged in, cannot fetch works');
+          return { works, total: 0 };
+        }
+      }
+
       let hasMore = true;
       let maxCursor = 0;
       let pageCount = 0;
@@ -2723,9 +2944,9 @@ class HeadlessBrowserService {
 
         const data = await page.evaluate(async (cursor: number) => {
           // 使用新的 work_list API 接口
-          // status: 不传或传空表示获取全部状态的作品
-          // count: 每页获取数量,增加到20减少请求次数
-          const url = `https://creator.douyin.com/janus/douyin/creator/pc/work_list?scene=star_atlas&device_platform=android&count=20&max_cursor=${cursor}&cookie_enabled=true&browser_language=zh-CN&browser_platform=Win32&browser_name=Mozilla&browser_online=true&timezone_name=Asia%2FShanghai&aid=1128`;
+          // status: 0 表示获取全部已发布的作品
+          // count: 每页获取数量
+          const url = `https://creator.douyin.com/janus/douyin/creator/pc/work_list?status=0&scene=star_atlas&device_platform=android&count=20&max_cursor=${cursor}&cookie_enabled=true&browser_language=zh-CN&browser_platform=Win32&browser_name=Mozilla&browser_online=true&timezone_name=Asia%2FShanghai&aid=1128`;
 
           const resp = await fetch(url, {
             credentials: 'include',
@@ -2737,10 +2958,31 @@ class HeadlessBrowserService {
           return resp.json();
         }, maxCursor);
 
-        logger.info(`[DirectAPI] API response: has_more=${data?.has_more}, max_cursor=${data?.max_cursor}, aweme_list_length=${data?.aweme_list?.length || 0}`);
+        // 获取作品数
+        const awemeList = data?.aweme_list || [];
+        logger.info(`[DirectAPI] API response: status_code=${data?.status_code}, has_more=${data?.has_more}, max_cursor=${data?.max_cursor}, aweme_list_length=${awemeList.length}`);
+        
+        // 检查 API 返回状态
+        if (data?.status_code !== 0 && data?.status_code !== undefined) {
+          logger.warn(`[DirectAPI] API returned error status_code: ${data.status_code}`);
+          // status_code: 8 表示未授权,可能需要重新登录
+          if (data.status_code === 8) {
+            logger.warn('[DirectAPI] status_code 8: Not authorized, may need re-login');
+          }
+          break;
+        }
+
+        // 优先从第一个作品的 author.aweme_count 获取真实作品数(只在第一页获取)
+        if (pageCount === 1 && awemeList.length > 0) {
+          const firstAweme = awemeList[0];
+          const authorAwemeCount = firstAweme?.author?.aweme_count;
+          if (authorAwemeCount !== undefined && authorAwemeCount > 0) {
+            totalCount = authorAwemeCount;
+            logger.info(`[DirectAPI] Using author.aweme_count as total works: ${totalCount}`);
+          }
+        }
 
         // 解析 aweme_list 中的作品数据
-        const awemeList = data?.aweme_list || [];
         logger.info(`[DirectAPI] Page ${pageCount}: got ${awemeList.length} works from aweme_list`);
 
         for (const aweme of awemeList) {
@@ -2817,12 +3059,12 @@ class HeadlessBrowserService {
         }
       }
 
-      logger.info(`[DirectAPI] Total fetched ${works.length} works from ${pageCount} pages`);
+      logger.info(`[DirectAPI] Total fetched ${works.length} works from ${pageCount} pages, items count: ${totalCount}`);
     } catch (e) {
       logger.warn('[DirectAPI] Failed to fetch works:', e);
     }
 
-    return works;
+    return { works, total: totalCount };
   }
 
   /**