Просмотр исходного кода

feat: 增强账号状态检测以支持风控和验证码识别

- 扩展 Cookie 状态检测逻辑,区分"需要重新登录"和"不确定"状态
- 在 Python 发布平台基类中增加风控/验证码检测机制
- 更新账号同步任务,当检测到需要重新登录时抛出明确错误
- 修改 API 响应格式,包含 needReLogin 和 uncertain 字段
Ethanfly 11 часов назад
Родитель
Сommit
35bf6b34d3

+ 1 - 1
client/src/api/accounts.ts

@@ -69,7 +69,7 @@ export const accountsApi = {
   },
 
   // 检查账号 Cookie 是否有效
-  checkAccountStatus(id: number): Promise<{ isValid: boolean; needReLogin: boolean }> {
+  checkAccountStatus(id: number): Promise<{ isValid: boolean; needReLogin: boolean; uncertain?: boolean }> {
     return request.get(`/api/accounts/${id}/check-status`, { timeout: 120000 }); // 2分钟超时
   },
 

BIN
server/python/platforms/__pycache__/base.cpython-313.pyc


BIN
server/python/platforms/__pycache__/douyin.cpython-313.pyc


BIN
server/python/platforms/__pycache__/xiaohongshu.cpython-313.pyc


+ 31 - 0
server/python/platforms/base.py

@@ -1054,6 +1054,10 @@ class BasePublisher(ABC):
             
             # 检查页面是否有登录弹窗
             need_login = is_login_page
+
+            # 风控/验证码特征
+            risk_indicators = ['captcha', 'verify', 'challenge', 'risk', 'security', 'safe', 'protect', 'slider']
+            need_verification = any(indicator in current_url.lower() for indicator in risk_indicators)
             
             if not need_login:
                 # 检查页面内容是否有登录提示
@@ -1073,6 +1077,26 @@ class BasePublisher(ABC):
                             break
                     except:
                         pass
+
+            if not need_login and not need_verification:
+                verification_selectors = [
+                    'text="安全验证"',
+                    'text="验证码"',
+                    'text="人机验证"',
+                    'text="滑块"',
+                    'text="请完成验证"',
+                    'text="系统检测到异常"',
+                    'text="访问受限"',
+                    'text="行为异常"',
+                ]
+                for selector in verification_selectors:
+                    try:
+                        if await self.page.locator(selector).count() > 0:
+                            need_verification = True
+                            print(f"[{self.platform_name}] 检测到风控/验证码提示: {selector}")
+                            break
+                    except:
+                        pass
             
             if need_login:
                 return {
@@ -1081,6 +1105,13 @@ class BasePublisher(ABC):
                     "need_login": True,
                     "message": "Cookie 已过期,需要重新登录"
                 }
+            elif need_verification:
+                return {
+                    "success": True,
+                    "valid": False,
+                    "need_login": True,
+                    "message": "触发风控/需要验证"
+                }
             else:
                 return {
                     "success": True,

+ 26 - 25
server/src/routes/accounts.ts

@@ -156,9 +156,9 @@ router.post(
       Number(req.params.id)
     );
     const { needReLogin, ...account } = result;
-    res.json({ 
-      success: true, 
-      data: account,
+    res.json({
+      success: true,
+      data: { ...account, needReLogin: needReLogin || false },
       needReLogin: needReLogin || false,
     });
   })
@@ -176,11 +176,12 @@ router.get(
       req.user!.userId,
       Number(req.params.id)
     );
-    res.json({ 
-      success: true, 
+    res.json({
+      success: true,
       data: {
         isValid: result.isValid,
-        needReLogin: !result.isValid,
+        needReLogin: result.needReLogin,
+        uncertain: result.uncertain,
       }
     });
   })
@@ -198,8 +199,8 @@ router.get(
       req.user!.userId,
       Number(req.params.id)
     );
-    res.json({ 
-      success: true, 
+    res.json({
+      success: true,
       data: {
         cookieData,
       }
@@ -212,8 +213,8 @@ router.post(
   '/refresh-all',
   asyncHandler(async (req, res) => {
     const result = await accountService.refreshAllAccounts(req.user!.userId);
-    res.json({ 
-      success: true, 
+    res.json({
+      success: true,
       data: result,
     });
   })
@@ -277,12 +278,12 @@ router.get(
   asyncHandler(async (req, res) => {
     const { sessionId } = req.params;
     const status = loginServiceManager.getSessionStatus(sessionId);
-    
+
     if (!status) {
       res.status(404).json({ success: false, error: { message: '会话不存在或已过期' } });
       return;
     }
-    
+
     res.json({ success: true, data: status });
   })
 );
@@ -312,17 +313,17 @@ router.post(
   asyncHandler(async (req, res) => {
     const { sessionId } = req.params;
     const { platform, groupId } = req.body;
-    
+
     const status = loginServiceManager.getSessionStatus(sessionId);
-    
+
     if (!status || status.status !== 'success' || !status.cookies) {
-      res.status(400).json({ 
-        success: false, 
-        error: { message: '登录未完成或会话已失效' } 
+      res.status(400).json({
+        success: false,
+        error: { message: '登录未完成或会话已失效' }
       });
       return;
     }
-    
+
     // 使用获取到的 Cookie 和账号信息创建账号
     const account = await accountService.addAccount(req.user!.userId, {
       platform,
@@ -331,7 +332,7 @@ router.post(
       // 传递从浏览器会话中获取的账号信息
       accountInfo: status.accountInfo,
     });
-    
+
     res.status(201).json({ success: true, data: account });
   })
 );
@@ -348,16 +349,16 @@ router.post(
   ],
   asyncHandler(async (req, res) => {
     const { platform, cookieData } = req.body;
-    
+
     try {
       // 使用服务验证 cookie 并获取账号信息
       const result = await accountService.verifyCookieAndGetInfo(
         platform as PlatformType,
         cookieData
       );
-      
-      res.json({ 
-        success: true, 
+
+      res.json({
+        success: true,
         data: {
           success: result.success,
           message: result.message,
@@ -365,8 +366,8 @@ router.post(
         }
       });
     } catch (error) {
-      res.json({ 
-        success: true, 
+      res.json({
+        success: true,
         data: {
           success: false,
           message: error instanceof Error ? error.message : '验证失败',

+ 11 - 11
server/src/routes/publish.ts

@@ -48,15 +48,15 @@ router.post(
     body('videoPath').notEmpty().withMessage('视频路径不能为空'),
     body('title').notEmpty().withMessage('标题不能为空'),
     body('targetAccounts').isArray({ min: 1 }).withMessage('至少选择一个目标账号'),
-    body('publishProxy').optional().isObject(),
+    body('publishProxy').optional({ nullable: true }).isObject().withMessage('发布代理配置无效'),
     validateRequest,
   ],
   asyncHandler(async (req, res) => {
     const userId = req.user!.userId;
-    
+
     // 1. 创建数据库记录
     const task = await publishService.createTask(userId, req.body);
-    
+
     // 2. 如果不是定时任务,加入任务队列
     if (!req.body.scheduledAt) {
       taskQueueService.createTask(userId, {
@@ -68,7 +68,7 @@ router.post(
         },
       });
     }
-    
+
     res.status(201).json({ success: true, data: task });
   })
 );
@@ -96,10 +96,10 @@ router.post(
   asyncHandler(async (req, res) => {
     const userId = req.user!.userId;
     const taskId = Number(req.params.id);
-    
+
     // 1. 更新数据库状态
     const task = await publishService.retryTask(userId, taskId);
-    
+
     // 2. 加入任务队列重新执行
     taskQueueService.createTask(userId, {
       type: 'publish_video',
@@ -109,7 +109,7 @@ router.post(
         publishTaskId: task.id,
       },
     });
-    
+
     res.json({ success: true, data: task });
   })
 );
@@ -139,14 +139,14 @@ router.post(
     const userId = req.user!.userId;
     const taskId = Number(req.params.taskId);
     const accountId = Number(req.params.accountId);
-    
+
     // 调用服务层执行有头浏览器发布
     const result = await publishService.retryAccountWithHeadfulBrowser(
-      userId, 
-      taskId, 
+      userId,
+      taskId,
       accountId
     );
-    
+
     res.json({ success: true, data: result });
   })
 );

+ 41 - 21
server/src/services/AccountService.ts

@@ -481,18 +481,21 @@ export class AccountService {
           
           // ========== 原有逻辑(AI 失败时的备用方案) ==========
           if (!aiRefreshSuccess) {
-            // 第一步:通过浏览器检查 Cookie 是否有效
-            const isValid = await headlessBrowserService.checkCookieValid(platform, cookieList);
-            
-            if (!isValid) {
-              // Cookie 已过期,需要重新登录
+            const cookieStatus = await headlessBrowserService.checkCookieStatus(platform, cookieList);
+
+            if (cookieStatus.needReLogin) {
               updateData.status = 'expired';
               needReLogin = true;
-              logger.warn(`Account ${accountId} (${account.accountName}) cookie expired, need re-login`);
-            } else {
-              // Cookie 有效,尝试获取账号信息
+              logger.warn(
+                `Account ${accountId} (${account.accountName}) needs re-login: reason=${cookieStatus.reason}, source=${cookieStatus.source}, msg=${cookieStatus.message || ''}`
+              );
+            } else if (cookieStatus.uncertain) {
+              logger.warn(
+                `Account ${accountId} (${account.accountName}) cookie status uncertain: source=${cookieStatus.source}, msg=${cookieStatus.message || ''}`
+              );
+            } else if (cookieStatus.isValid) {
               updateData.status = 'active';
-              
+
               try {
                 const profile = await headlessBrowserService.fetchAccountInfo(platform, cookieList);
 
@@ -548,6 +551,10 @@ export class AccountService {
                   logger.info(`[baijiahao] Account info fetch failed for ${accountId}, but this might be due to distributed auth. Keeping status active.`);
                 }
               }
+            } else {
+              logger.warn(
+                `Account ${accountId} (${account.accountName}) cookie invalid without explicit re-login: source=${cookieStatus.source}, reason=${cookieStatus.reason}`
+              );
             }
           }
         }
@@ -647,7 +654,10 @@ export class AccountService {
   /**
    * 检查账号 Cookie 是否有效
    */
-  async checkAccountStatus(userId: number, accountId: number): Promise<{ isValid: boolean }> {
+  async checkAccountStatus(
+    userId: number,
+    accountId: number
+  ): Promise<{ isValid: boolean; needReLogin: boolean; uncertain: boolean }> {
     const account = await this.accountRepository.findOne({
       where: { id: accountId, userId },
     });
@@ -658,7 +668,7 @@ export class AccountService {
     if (!account.cookieData) {
       // 更新状态为过期
       await this.accountRepository.update(accountId, { status: 'expired' });
-      return { isValid: false };
+      return { isValid: false, needReLogin: true, uncertain: false };
     }
 
     const platform = account.platform as PlatformType;
@@ -682,28 +692,34 @@ export class AccountService {
         cookieList = this.parseCookieString(decryptedCookies, platform);
         if (cookieList.length === 0) {
           await this.accountRepository.update(accountId, { status: 'expired' });
-          return { isValid: false };
+          return { isValid: false, needReLogin: true, uncertain: false };
         }
       }
 
-      // 使用 API 检查 Cookie 是否有效
-      const isValid = await headlessBrowserService.checkCookieValid(platform, cookieList);
+      const cookieStatus = await headlessBrowserService.checkCookieStatus(platform, cookieList);
 
       // 更新账号状态
-      if (!isValid) {
+      if (cookieStatus.needReLogin) {
         await this.accountRepository.update(accountId, { status: 'expired' });
         wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_UPDATED, { 
           account: { ...this.formatAccount(account), status: 'expired' }
         });
-      } else if (account.status === 'expired') {
+        return { isValid: false, needReLogin: true, uncertain: false };
+      }
+
+      if (cookieStatus.uncertain) {
+        return { isValid: true, needReLogin: false, uncertain: true };
+      }
+
+      if (cookieStatus.isValid && account.status === 'expired') {
         // 如果之前是过期状态但现在有效了,更新为正常
         await this.accountRepository.update(accountId, { status: 'active' });
       }
 
-      return { isValid };
+      return { isValid: cookieStatus.isValid, needReLogin: false, uncertain: false };
     } catch (error) {
       logger.error(`Failed to check account status ${accountId}:`, error);
-      return { isValid: false };
+      return { isValid: true, needReLogin: false, uncertain: true };
     }
   }
 
@@ -806,9 +822,9 @@ export class AccountService {
       }
       
       // 账号信息获取失败或无效,检查 Cookie 是否有效
-      const isValid = await headlessBrowserService.checkCookieValid(platform, cookieList);
+      const cookieStatus = await headlessBrowserService.checkCookieStatus(platform, cookieList);
       
-      if (isValid) {
+      if (cookieStatus.isValid) {
         // Cookie 有效但未能获取账号信息,返回基本成功
         return {
           success: true,
@@ -821,10 +837,14 @@ export class AccountService {
             worksCount: 0,
           },
         };
-      } else {
+      }
+
+      if (cookieStatus.needReLogin) {
         // Cookie 无效
         return { success: false, message: '登录状态无效或已过期' };
       }
+
+      return { success: false, message: '无法验证登录状态,请稍后重试' };
     } catch (error) {
       logger.error(`Failed to verify cookie for ${platform}:`, error);
       return { 

+ 214 - 35
server/src/services/HeadlessBrowserService.ts

@@ -126,6 +126,22 @@ export interface CookieData {
   path: string;
 }
 
+export type CookieCheckSource = 'python' | 'api' | 'browser';
+export type CookieCheckReason =
+  | 'valid'
+  | 'need_login'
+  | 'risk_control'
+  | 'uncertain';
+
+export interface CookieCheckResult {
+  isValid: boolean;
+  needReLogin: boolean;
+  uncertain: boolean;
+  reason: CookieCheckReason;
+  source: CookieCheckSource;
+  message?: string;
+}
+
 /**
  * 无头浏览器服务 - 用于后台静默获取账号信息
  */
@@ -135,40 +151,86 @@ class HeadlessBrowserService {
    * 优先使用 Python 服务(通过浏览器访问后台检测),回退到 API 检查
    */
   async checkCookieValid(platform: PlatformType, cookies: CookieData[]): Promise<boolean> {
-    logger.info(`[checkCookieValid] Checking cookie for ${platform}, cookie count: ${cookies.length}`);
+    const status = await this.checkCookieStatus(platform, cookies);
+    return status.isValid || status.uncertain;
+  }
+
+  /**
+   * 检查 Cookie 状态(有效 / 需要重新登录 / 不确定)
+   */
+  async checkCookieStatus(platform: PlatformType, cookies: CookieData[]): Promise<CookieCheckResult> {
+    logger.info(`[checkCookieStatus] Checking cookie for ${platform}, cookie count: ${cookies.length}`);
 
-    // 优先使用 Python 服务检查(通过浏览器访问后台页面,检测是否被重定向到登录页)
     const pythonAvailable = await this.checkPythonServiceAvailable();
     if (pythonAvailable) {
       try {
-        const result = await this.checkLoginViaPython(platform, cookies);
-        logger.info(`[checkCookieValid] Python service result for ${platform}: ${result}`);
+        const result = await this.checkLoginStatusViaPython(platform, cookies);
+        logger.info(
+          `[checkCookieStatus] Python result for ${platform}: isValid=${result.isValid}, needReLogin=${result.needReLogin}, uncertain=${result.uncertain}, reason=${result.reason}`
+        );
         return result;
       } catch (error) {
-        logger.warn(`[Python API] Check login failed, falling back to API check:`, error);
+        logger.warn(`[Python API] Check login failed, falling back:`, error);
       }
     } else {
-      logger.info(`[checkCookieValid] Python service not available, using API check`);
+      logger.info(`[checkCookieStatus] Python service not available`);
     }
 
-    // 回退到 API 检查
     const apiConfig = PLATFORM_API_CONFIG[platform];
     if (apiConfig) {
-      const result = await this.checkCookieValidByApi(platform, cookies, apiConfig);
-      logger.info(`[checkCookieValid] API check result for ${platform}: ${result}`);
+      const result = await this.checkCookieStatusByApi(platform, cookies, apiConfig);
+      logger.info(
+        `[checkCookieStatus] API result for ${platform}: isValid=${result.isValid}, needReLogin=${result.needReLogin}, uncertain=${result.uncertain}, reason=${result.reason}`
+      );
       return result;
     }
 
-    // 其他平台使用浏览器检查
-    const result = await this.checkCookieValidByBrowser(platform, cookies);
-    logger.info(`[checkCookieValid] Browser check result for ${platform}: ${result}`);
+    const result = await this.checkCookieStatusByBrowser(platform, cookies);
+    logger.info(
+      `[checkCookieStatus] Browser result for ${platform}: isValid=${result.isValid}, needReLogin=${result.needReLogin}, uncertain=${result.uncertain}, reason=${result.reason}`
+    );
     return result;
   }
 
+  private containsRiskKeywords(text: string): boolean {
+    if (!text) return false;
+    const lowered = text.toLowerCase();
+    const keywords = [
+      '验证码',
+      '安全验证',
+      '人机验证',
+      '滑块',
+      '风控',
+      '风险',
+      '访问受限',
+      '行为异常',
+      '系统检测到异常',
+      '安全校验',
+      'captcha',
+      'verify',
+      'challenge',
+      'risk',
+      'security',
+      'safe',
+      'protect',
+      'blocked',
+    ];
+    return keywords.some(k => lowered.includes(k.toLowerCase()));
+  }
+
+  private async getPageBodyTextSafe(page: Page): Promise<string> {
+    try {
+      const content = await page.textContent('body');
+      return (content || '').slice(0, 8000);
+    } catch {
+      return '';
+    }
+  }
+
   /**
    * 通过 Python 服务检查登录状态(浏览器访问后台页面,检测是否需要登录)
    */
-  private async checkLoginViaPython(platform: PlatformType, cookies: CookieData[]): Promise<boolean> {
+  private async checkLoginStatusViaPython(platform: PlatformType, cookies: CookieData[]): Promise<CookieCheckResult> {
     const pythonUrl = PYTHON_SERVICE_URL;
 
     // 构建 Cookie 字符串
@@ -203,18 +265,48 @@ class HeadlessBrowserService {
       throw new Error(result.error || 'Check login failed');
     }
 
-    return result.valid && !result.need_login;
+    const message = result.message || result.error || '';
+    if (!result.valid || result.need_login) {
+      return {
+        isValid: false,
+        needReLogin: true,
+        uncertain: false,
+        reason: this.containsRiskKeywords(message) ? 'risk_control' : 'need_login',
+        source: 'python',
+        message,
+      };
+    }
+
+    if (this.containsRiskKeywords(message)) {
+      return {
+        isValid: false,
+        needReLogin: true,
+        uncertain: false,
+        reason: 'risk_control',
+        source: 'python',
+        message,
+      };
+    }
+
+    return {
+      isValid: true,
+      needReLogin: false,
+      uncertain: false,
+      reason: 'valid',
+      source: 'python',
+      message,
+    };
   }
 
   /**
    * 通过 API 检查 Cookie 是否有效
    * 注意:API 返回不确定状态时会回退到浏览器检查,避免误判
    */
-  private async checkCookieValidByApi(
+  private async checkCookieStatusByApi(
     platform: PlatformType,
     cookies: CookieData[],
     apiConfig: typeof PLATFORM_API_CONFIG[string]
-  ): Promise<boolean> {
+  ): Promise<CookieCheckResult> {
     try {
       // 构建 Cookie 字符串(所有 Cookie 拼接)
       const cookieString = cookies
@@ -255,7 +347,24 @@ class HeadlessBrowserService {
 
       // 如果 API 明确返回有效,直接返回 true
       if (isValid) {
-        return true;
+        const rawText = JSON.stringify(data).slice(0, 2000);
+        if (this.containsRiskKeywords(rawText)) {
+          return {
+            isValid: false,
+            needReLogin: true,
+            uncertain: false,
+            reason: 'risk_control',
+            source: 'api',
+            message: 'API 返回疑似风控/验证页面',
+          };
+        }
+        return {
+          isValid: true,
+          needReLogin: false,
+          uncertain: false,
+          reason: 'valid',
+          source: 'api',
+        };
       }
 
       // API 返回无效时,检查是否是明确的"未登录"状态
@@ -265,7 +374,13 @@ class HeadlessBrowserService {
 
       if (clearlyNotLoggedIn) {
         logger.info(`[API] Platform ${platform} clearly not logged in (statusCode=${statusCode})`);
-        return false;
+        return {
+          isValid: false,
+          needReLogin: true,
+          uncertain: false,
+          reason: 'need_login',
+          source: 'api',
+        };
       }
 
       // 百家号特殊处理:API 用 Node fetch 调用时可能因分散认证等返回 errno !== 0,但 Cookie 在浏览器内仍有效
@@ -273,28 +388,31 @@ class HeadlessBrowserService {
       if (platform === 'baijiahao') {
         const errno = (data as { errno?: number })?.errno;
 
-        if (errno === 0 && isValid) return true;
+        if (errno === 0 && isValid) {
+          return { isValid: true, needReLogin: false, uncertain: false, reason: 'valid', source: 'api' };
+        }
+
         if (errno === 0 && !isValid) {
           logger.warn(`[API] Baijiahao errno=0 but no user info, falling back to browser check`);
-          return this.checkCookieValidByBrowser(platform, cookies);
+          return this.checkCookieStatusByBrowser(platform, cookies);
         }
 
         // errno 110 通常表示未登录,可直接判无效;其他 errno(如 10001402 分散认证)可能只是接口限制,用浏览器再判一次
         if (errno === 110) {
           logger.warn(`[API] Baijiahao errno=110 (not logged in), cookie invalid`);
-          return false;
+          return { isValid: false, needReLogin: true, uncertain: false, reason: 'need_login', source: 'api' };
         }
         logger.info(`[API] Baijiahao errno=${errno}, falling back to browser check (may be dispersed auth)`);
-        return this.checkCookieValidByBrowser(platform, cookies);
+        return this.checkCookieStatusByBrowser(platform, cookies);
       }
 
       // 不确定的状态(如 status_code=7),回退到浏览器检查
       logger.info(`[API] Uncertain status for ${platform} (statusCode=${statusCode}), falling back to browser check`);
-      return this.checkCookieValidByBrowser(platform, cookies);
+      return this.checkCookieStatusByBrowser(platform, cookies);
     } catch (error) {
       logger.error(`API check cookie error for ${platform}:`, error);
       // API 检查失败时,回退到浏览器检查
-      return this.checkCookieValidByBrowser(platform, cookies);
+      return this.checkCookieStatusByBrowser(platform, cookies);
     }
   }
 
@@ -315,10 +433,10 @@ class HeadlessBrowserService {
    * 通过浏览器检查 Cookie 是否有效(检查是否被重定向到登录页)
    * 注意:网络错误或服务不可用时返回 true(保持原状态),避免误判为过期
    */
-  private async checkCookieValidByBrowser(platform: PlatformType, cookies: CookieData[]): Promise<boolean> {
+  private async checkCookieStatusByBrowser(platform: PlatformType, cookies: CookieData[]): Promise<CookieCheckResult> {
     // 对于抖音平台,使用 check/user 接口检查
     if (platform === 'douyin') {
-      return this.checkDouyinLoginByApi(cookies);
+      return this.checkDouyinLoginStatusByApi(cookies);
     }
 
     const browser = await chromium.launch({ headless: true });
@@ -349,18 +467,52 @@ class HeadlessBrowserService {
 
       // 检查是否被重定向到登录页
       const isLoginPage = config.loginIndicators.some(indicator => url.includes(indicator));
+      const bodyText = await this.getPageBodyTextSafe(page);
+      const isRiskControl = this.containsRiskKeywords(url) || this.containsRiskKeywords(bodyText);
 
       await page.close();
       await context.close();
       await browser.close();
 
-      return !isLoginPage;
+      if (isLoginPage) {
+        return {
+          isValid: false,
+          needReLogin: true,
+          uncertain: false,
+          reason: 'need_login',
+          source: 'browser',
+        };
+      }
+
+      if (isRiskControl) {
+        return {
+          isValid: false,
+          needReLogin: true,
+          uncertain: false,
+          reason: 'risk_control',
+          source: 'browser',
+          message: '检测到风控/验证页面',
+        };
+      }
+
+      return {
+        isValid: true,
+        needReLogin: false,
+        uncertain: false,
+        reason: 'valid',
+        source: 'browser',
+      };
     } catch (error) {
       logger.error(`Browser check cookie error for ${platform}:`, error);
       await browser.close();
-      // 网络错误或服务不可用时返回 true,避免误判为过期
-      logger.warn(`[Browser] Check failed due to error, assuming valid to avoid false expiration`);
-      return true;
+      return {
+        isValid: false,
+        needReLogin: false,
+        uncertain: true,
+        reason: 'uncertain',
+        source: 'browser',
+        message: error instanceof Error ? error.message : 'Browser check error',
+      };
     }
   }
 
@@ -368,10 +520,11 @@ class HeadlessBrowserService {
    * 抖音登录状态检查 - 通过监听 check/user 接口
    * 访问创作者首页,监听 check/user 接口返回的 result 字段判断登录状态
    */
-  async checkDouyinLoginByApi(cookies: CookieData[]): Promise<boolean> {
+  private async checkDouyinLoginStatusByApi(cookies: CookieData[]): Promise<CookieCheckResult> {
     const browser = await chromium.launch({ headless: true });
     let isLoggedIn = false;
     let checkCompleted = false;
+    let isRiskControl = false;
 
     try {
       const context = await browser.newContext({
@@ -419,19 +572,45 @@ class HeadlessBrowserService {
         logger.info(`[Douyin] No check/user response, fallback to URL check: ${currentUrl}, isLoggedIn=${isLoggedIn}`);
       }
 
+      const finalUrl = page.url();
+      const bodyText = await this.getPageBodyTextSafe(page);
+      isRiskControl = this.containsRiskKeywords(finalUrl) || this.containsRiskKeywords(bodyText);
+
       await page.close();
       await context.close();
       await browser.close();
 
-      return isLoggedIn;
+      if (isRiskControl) {
+        return {
+          isValid: false,
+          needReLogin: true,
+          uncertain: false,
+          reason: 'risk_control',
+          source: 'browser',
+          message: '检测到风控/验证页面',
+        };
+      }
+
+      return {
+        isValid: isLoggedIn,
+        needReLogin: !isLoggedIn,
+        uncertain: false,
+        reason: isLoggedIn ? 'valid' : 'need_login',
+        source: 'browser',
+      };
     } catch (error) {
       logger.error('[Douyin] checkDouyinLoginByApi error:', error);
       try {
         await browser.close();
       } catch { }
-      // 网络错误或服务不可用时返回 true,避免误判为过期
-      logger.warn(`[Douyin] Check failed due to error, assuming valid to avoid false expiration`);
-      return true;
+      return {
+        isValid: false,
+        needReLogin: false,
+        uncertain: true,
+        reason: 'uncertain',
+        source: 'browser',
+        message: error instanceof Error ? error.message : 'Douyin check error',
+      };
     }
   }
 

+ 5 - 1
server/src/services/taskExecutors.ts

@@ -104,7 +104,11 @@ async function syncAccountExecutor(task: Task, updateProgress: ProgressUpdater):
     throw new Error('缺少账号ID');
   }
 
-  await accountService.refreshAccount(userId, task.accountId);
+  const refreshResult = await accountService.refreshAccount(userId, task.accountId);
+  if (refreshResult.needReLogin) {
+    updateProgress({ progress: 100, currentStep: '需要重新登录' });
+    throw new Error('账号登录已过期或触发风控,需要重新登录');
+  }
 
   updateProgress({ progress: 100, currentStep: '同步完成' });