Procházet zdrojové kódy

feat: 添加 Cookie 验证接口并优化作品同步逻辑

- 新增 verifyCookie API 用于内嵌浏览器登录时验证 Cookie 并获取账号信息
- 在小红书账号信息收集中集成 Cookie 验证,补充作品数和粉丝数等数据
- 优化作品同步逻辑,支持规范化的视频 ID 并添加防误删保护机制
- 在 verifyCookie 方法中增强对 JSON 格式 Cookie 的支持
- 更新组件类型声明,添加多个 Element Plus 组件
Ethanfly před 2 dny
rodič
revize
73571e43db

+ 8 - 0
client/src/api/accounts.ts

@@ -73,6 +73,14 @@ export const accountsApi = {
     return request.get(`/api/accounts/${id}/check-status`, { timeout: 120000 }); // 2分钟超时
   },
 
+  // 验证 Cookie 并获取账号信息(用于内嵌浏览器登录)
+  verifyCookie(data: {
+    platform: string;
+    cookieData: string;
+  }): Promise<{ success: boolean; message?: string; accountInfo?: { accountId: string; accountName: string; avatarUrl: string; fansCount: number; worksCount: number } }> {
+    return request.post('/api/accounts/verify-cookie', data, { timeout: 120000 });
+  },
+
   // 扫码登录
   getQRCode(platform: string): Promise<QRCodeInfo> {
     return request.post('/api/accounts/qrcode', { platform });

+ 6 - 0
client/src/components.d.ts

@@ -16,8 +16,12 @@ declare module 'vue' {
     ElButton: typeof import('element-plus/es')['ElButton']
     ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
     ElContainer: typeof import('element-plus/es')['ElContainer']
+    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
+    ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
+    ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDivider: typeof import('element-plus/es')['ElDivider']
     ElDrawer: typeof import('element-plus/es')['ElDrawer']
@@ -45,6 +49,8 @@ declare module 'vue' {
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
+    ElText: typeof import('element-plus/es')['ElText']
+    ElUpload: typeof import('element-plus/es')['ElUpload']
     Icons: typeof import('./components/icons/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']

+ 20 - 0
client/src/components/BrowserTab.vue

@@ -2335,6 +2335,26 @@ async function collectXiaohongshuAccountInfo() {
   if (worksCount === 0) {
     console.log('[小红书] 首页无作品数,将通过后台刷新获取');
   }
+
+  if (cookieData.value) {
+    try {
+      const verifyResult = await accountsApi.verifyCookie({
+        platform: 'xiaohongshu',
+        cookieData: cookieData.value,
+      });
+      if (verifyResult?.success && verifyResult.accountInfo) {
+        worksCount = verifyResult.accountInfo.worksCount || 0;
+        info.fans_count = verifyResult.accountInfo.fansCount || 0;
+        if (verifyResult.accountInfo.avatarUrl) info.avatar = verifyResult.accountInfo.avatarUrl;
+        if (verifyResult.accountInfo.accountName) info.name = verifyResult.accountInfo.accountName;
+      } else {
+        worksCount = 0;
+      }
+    } catch (e) {
+      console.warn('[小红书] verify-cookie failed:', e);
+      worksCount = 0;
+    }
+  }
   
   console.log('[小红书] 笔记数:', worksCount);
   

+ 110 - 78
server/src/services/AccountService.ts

@@ -128,11 +128,11 @@ export class AccountService {
     if (!account) {
       throw new AppError('账号不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.ACCOUNT_NOT_FOUND);
     }
-    
+
     if (!account.cookieData) {
       throw new AppError('账号没有 Cookie 数据', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR);
     }
-    
+
     // 尝试解密 Cookie
     let decryptedCookies: string;
     try {
@@ -143,7 +143,7 @@ export class AccountService {
       logger.warn(`[AccountService] Cookie 解密失败,使用原始数据,账号: ${account.accountName}`);
       decryptedCookies = account.cookieData;
     }
-    
+
     // 验证 Cookie 格式
     try {
       const parsed = JSON.parse(decryptedCookies);
@@ -156,7 +156,7 @@ export class AccountService {
     } catch {
       logger.warn(`[AccountService] Cookie 不是 JSON 格式,可能是字符串格式`);
     }
-    
+
     return decryptedCookies;
   }
 
@@ -170,11 +170,11 @@ export class AccountService {
     };
   }): Promise<PlatformAccountType> {
     const platform = data.platform as PlatformType;
-    
+
     // 解密 Cookie(如果是加密的)
     let cookieData = data.cookieData;
     let decryptedCookies: string;
-    
+
     try {
       // 尝试解密(如果是通过浏览器登录获取的加密Cookie)
       decryptedCookies = CookieManager.decrypt(cookieData);
@@ -182,14 +182,14 @@ export class AccountService {
       // 如果解密失败,可能是直接粘贴的Cookie字符串
       decryptedCookies = cookieData;
     }
-    
+
     // 检查客户端传入的 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
     // - 小红书:使用小红书号(red_num),而不是 Cookie 中的 userid
@@ -197,10 +197,10 @@ export class AccountService {
     // - 视频号/头条:使用 API 返回的真实账号 ID
     const platformsPreferApiId: PlatformType[] = ['douyin', 'xiaohongshu', 'baijiahao', 'weixin_video', 'toutiao'];
     const preferApiId = platformsPreferApiId.includes(platform);
-    
+
     // 确定最终的 accountId
     let finalAccountId: string;
-    
+
     if (preferApiId && isValidClientAccountId) {
       // 对于优先使用 API ID 的平台,先用客户端传入的有效 ID
       finalAccountId = this.normalizeAccountId(platform, clientAccountId);
@@ -217,7 +217,7 @@ export class AccountService {
       finalAccountId = `${platform}_${Date.now()}`;
       logger.warn(`[addAccount] Using timestamp-based accountId as fallback: ${finalAccountId}`);
     }
-    
+
     // 使用传入的账号信息(来自浏览器登录会话),或使用默认值
     const accountInfo = {
       accountId: finalAccountId,
@@ -226,14 +226,14 @@ export class AccountService {
       fansCount: data.accountInfo?.fansCount || 0,
       worksCount: data.accountInfo?.worksCount || 0,
     };
-    
+
     logger.info(`Adding account for ${platform}: ${accountInfo.accountId}, name: ${accountInfo.accountName}`);
 
     // 检查是否已存在相同账号
     const existing = await this.accountRepository.findOne({
       where: { userId, platform, accountId: accountInfo.accountId },
     });
-    
+
     if (existing) {
       // 更新已存在的账号
       await this.accountRepository.update(existing.id, {
@@ -246,15 +246,15 @@ export class AccountService {
         groupId: data.groupId || existing.groupId,
         proxyConfig: data.proxyConfig || existing.proxyConfig,
       });
-      
+
       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!);
     }
 
@@ -301,9 +301,9 @@ export class AccountService {
   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}`);
@@ -440,17 +440,17 @@ export class AccountService {
           // 但是如果 API 调用失败,作为备用方案仍会尝试 AI
           const platformsSkipAI: PlatformType[] = ['douyin', 'xiaohongshu', 'baijiahao'];
           const shouldUseAI = aiService.isAvailable() && !platformsSkipAI.includes(platform);
-          
+
           logger.info(`[refreshAccount] Platform: ${platform}, shouldUseAI: ${shouldUseAI}, aiAvailable: ${aiService.isAvailable()}`);
-          
+
           // ========== AI 辅助刷新(部分平台使用) ==========
           if (shouldUseAI) {
             try {
               logger.info(`[AI Refresh] Starting AI-assisted refresh for account ${accountId} (${platform})`);
-              
+
               // 使用无头浏览器截图,然后 AI 分析
               const aiResult = await this.refreshAccountWithAI(platform, cookieList, accountId);
-              
+
               if (aiResult.needReLogin) {
                 // AI 检测到需要重新登录
                 updateData.status = 'expired';
@@ -478,7 +478,7 @@ export class AccountService {
               // AI 刷新失败,继续使用原有逻辑
             }
           }
-          
+
           // ========== 原有逻辑(AI 失败时的备用方案) ==========
           if (!aiRefreshSuccess) {
             const cookieStatus = await headlessBrowserService.checkCookieStatus(platform, cookieList);
@@ -501,7 +501,7 @@ export class AccountService {
 
                 // 检查是否获取到有效信息(排除默认名称)
                 const defaultNames = [
-                  `${platform}账号`, '未知账号', '抖音账号', '小红书账号', 
+                  `${platform}账号`, '未知账号', '抖音账号', '小红书账号',
                   '快手账号', '视频号账号', 'B站账号', '头条账号', '百家号账号'
                 ];
                 const isValidProfile = profile.accountName && !defaultNames.includes(profile.accountName);
@@ -509,7 +509,7 @@ export class AccountService {
                 if (isValidProfile) {
                   updateData.accountName = profile.accountName;
                   updateData.avatarUrl = profile.avatarUrl;
-                  
+
                   // 仅在粉丝数有效时更新(避免因获取失败导致的归零)
                   if (profile.fansCount !== undefined) {
                     // 如果新粉丝数为 0,但原粉丝数 > 0,可能是获取失败,记录警告并跳过更新
@@ -519,13 +519,13 @@ export class AccountService {
                       updateData.fansCount = profile.fansCount;
                     }
                   }
-                  
+
                   if (profile.worksCount === 0 && (account.worksCount || 0) > 0) {
                     logger.warn(`[refreshAccount] Works count dropped to 0 for ${accountId} (was ${account.worksCount}). Ignoring potential fetch error.`);
                   } else {
                     updateData.worksCount = profile.worksCount;
                   }
-                  
+
                   // 如果获取到了有效的 accountId(如抖音号),也更新它
                   // 这样可以修正之前使用错误 ID(如 Cookie 值)保存的账号
                   if (profile.accountId && !this.isTimestampBasedId(profile.accountId)) {
@@ -536,7 +536,7 @@ export class AccountService {
                       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 状态
@@ -545,7 +545,7 @@ export class AccountService {
               } catch (infoError) {
                 // 获取账号信息失败,但 Cookie 检查已通过,保持 active 状态
                 logger.warn(`Failed to fetch account info for ${accountId}, but cookie is valid:`, infoError);
-                
+
                 // 对于百家号,如果是获取信息失败,可能是分散认证问题,不需要立即标记为失败
                 if (platform === 'baijiahao') {
                   logger.info(`[baijiahao] Account info fetch failed for ${accountId}, but this might be due to distributed auth. Keeping status active.`);
@@ -609,32 +609,32 @@ export class AccountService {
   }> {
     // 使用无头浏览器访问平台后台并截图
     const screenshot = await headlessBrowserService.capturePageScreenshot(platform, cookieList);
-    
+
     if (!screenshot) {
       throw new Error('Failed to capture screenshot');
     }
-    
+
     // 第一步:使用 AI 分析登录状态
     const loginStatus = await aiService.analyzeLoginStatus(screenshot, platform);
-    
+
     logger.info(`[AI Refresh] Login status for account ${accountId}:`, {
       isLoggedIn: loginStatus.isLoggedIn,
       hasVerification: loginStatus.hasVerification,
     });
-    
+
     // 如果 AI 检测到未登录或有验证码,说明需要重新登录
     if (!loginStatus.isLoggedIn || loginStatus.hasVerification) {
       return { needReLogin: true };
     }
-    
+
     // 第二步:使用 AI 提取账号信息
     const accountInfo = await aiService.extractAccountInfo(screenshot, platform);
-    
+
     logger.info(`[AI Refresh] Account info extraction for ${accountId}:`, {
       found: accountInfo.found,
       accountName: accountInfo.accountName,
     });
-    
+
     if (accountInfo.found && accountInfo.accountName) {
       return {
         needReLogin: false,
@@ -645,7 +645,7 @@ export class AccountService {
         },
       };
     }
-    
+
     // AI 未能提取到账号信息,但登录状态正常
     // 返回空结果,让原有逻辑处理
     return { needReLogin: false };
@@ -701,7 +701,7 @@ export class AccountService {
       // 更新账号状态
       if (cookieStatus.needReLogin) {
         await this.accountRepository.update(accountId, { status: 'expired' });
-        wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_UPDATED, { 
+        wsManager.sendToUser(userId, WS_EVENTS.ACCOUNT_UPDATED, {
           account: { ...this.formatAccount(account), status: 'expired' }
         });
         return { isValid: false, needReLogin: true, uncertain: false };
@@ -782,9 +782,41 @@ export class AccountService {
     };
   }> {
     try {
-      // 将 cookie 字符串转换为 cookie 列表格式
-      const cookieList = this.parseCookieString(cookieData, platform);
-      
+      const domainMap: Record<string, string> = {
+        douyin: '.douyin.com',
+        kuaishou: '.kuaishou.com',
+        xiaohongshu: '.xiaohongshu.com',
+        weixin_video: '.qq.com',
+        bilibili: '.bilibili.com',
+        toutiao: '.toutiao.com',
+        baijiahao: '.baidu.com',
+        qie: '.qq.com',
+        dayuhao: '.alibaba.com',
+      };
+
+      let cookieList: { name: string; value: string; domain: string; path: string }[] = [];
+      try {
+        const parsed = JSON.parse(cookieData) as Array<{
+          name?: string;
+          value?: string;
+          domain?: string;
+          path?: string;
+        }>;
+        if (Array.isArray(parsed) && parsed.length > 0) {
+          const domain = domainMap[platform] || `.${platform}.com`;
+          cookieList = parsed
+            .filter(c => c?.name && c?.value)
+            .map(c => ({
+              name: String(c.name),
+              value: String(c.value),
+              domain: c.domain ? String(c.domain) : domain,
+              path: c.path ? String(c.path) : '/',
+            }));
+        }
+      } catch {
+        cookieList = this.parseCookieString(cookieData, platform);
+      }
+
       if (cookieList.length === 0) {
         return { success: false, message: 'Cookie 格式无效' };
       }
@@ -793,10 +825,10 @@ export class AccountService {
       // 如果能成功获取到有效信息,说明登录是有效的
       try {
         const profile = await headlessBrowserService.fetchAccountInfo(platform, cookieList);
-        
+
         // 检查是否获取到有效信息(排除默认名称)
         const defaultNames = [
-          `${platform}账号`, '未知账号', '抖音账号', '小红书账号', 
+          `${platform}账号`, '未知账号', '抖音账号', '小红书账号',
           '快手账号', '视频号账号', 'B站账号', '头条账号', '百家号账号'
         ];
         const isValidProfile = profile.accountName && !defaultNames.includes(profile.accountName);
@@ -814,16 +846,16 @@ export class AccountService {
             },
           };
         }
-        
+
         // 未能获取有效信息,再验证 Cookie 是否有效
         logger.info(`[verifyCookieAndGetInfo] Could not get valid profile for ${platform}, checking cookie validity...`);
       } catch (infoError) {
         logger.warn(`Failed to fetch account info for ${platform}:`, infoError);
       }
-      
+
       // 账号信息获取失败或无效,检查 Cookie 是否有效
       const cookieStatus = await headlessBrowserService.checkCookieStatus(platform, cookieList);
-      
+
       if (cookieStatus.isValid) {
         // Cookie 有效但未能获取账号信息,返回基本成功
         return {
@@ -847,9 +879,9 @@ export class AccountService {
       return { success: false, message: '无法验证登录状态,请稍后重试' };
     } catch (error) {
       logger.error(`Failed to verify cookie for ${platform}:`, error);
-      return { 
-        success: false, 
-        message: error instanceof Error ? error.message : '验证失败' 
+      return {
+        success: false,
+        message: error instanceof Error ? error.message : '验证失败'
       };
     }
   }
@@ -857,10 +889,10 @@ export class AccountService {
   /**
    * 将 cookie 字符串解析为 cookie 列表
    */
-  private parseCookieString(cookieString: string, platform: PlatformType): { 
-    name: string; 
-    value: string; 
-    domain: string; 
+  private parseCookieString(cookieString: string, platform: PlatformType): {
+    name: string;
+    value: string;
+    domain: string;
     path: string;
   }[] {
     // 获取平台对应的域名
@@ -875,23 +907,23 @@ export class AccountService {
       qie: '.qq.com',
       dayuhao: '.alibaba.com',
     };
-    
+
     const domain = domainMap[platform] || `.${platform}.com`;
-    
+
     // 解析 "name=value; name2=value2" 格式的 cookie 字符串
     const cookies: { name: string; value: string; domain: string; path: string }[] = [];
-    
+
     const pairs = cookieString.split(';');
     for (const pair of pairs) {
       const trimmed = pair.trim();
       if (!trimmed) continue;
-      
+
       const eqIndex = trimmed.indexOf('=');
       if (eqIndex === -1) continue;
-      
+
       const name = trimmed.substring(0, eqIndex).trim();
       const value = trimmed.substring(eqIndex + 1).trim();
-      
+
       if (name && value) {
         cookies.push({
           name,
@@ -901,7 +933,7 @@ export class AccountService {
         });
       }
     }
-    
+
     return cookies;
   }
 
@@ -951,12 +983,12 @@ export class AccountService {
       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 }[];
@@ -969,18 +1001,18 @@ export class AccountService {
           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;
@@ -992,18 +1024,18 @@ export class AccountService {
               // 解码失败,使用原值
             }
           }
-          
+
           // 截取合理长度(避免过长的 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);
@@ -1032,21 +1064,21 @@ export class AccountService {
    */
   private normalizeAccountId(platform: PlatformType, accountId: string): string {
     const shortPrefix = AccountService.SHORT_PREFIX_MAP[platform] || `${platform}_`;
-    
+
     if (!accountId) {
       return `${shortPrefix}${Date.now()}`;
     }
-    
+
     // 如果已经有正确的短前缀,直接返回
     if (accountId.startsWith(shortPrefix)) {
       return accountId;
     }
-    
+
     // 移除任何已有的前缀(短前缀或完整前缀)
     const allShortPrefixes = Object.values(AccountService.SHORT_PREFIX_MAP);
     const allFullPrefixes = Object.keys(AccountService.SHORT_PREFIX_MAP).map(p => `${p}_`);
     const allPrefixes = [...allShortPrefixes, ...allFullPrefixes];
-    
+
     let cleanId = accountId;
     for (const prefix of allPrefixes) {
       if (cleanId.startsWith(prefix)) {
@@ -1054,7 +1086,7 @@ export class AccountService {
         break;
       }
     }
-    
+
     // 添加正确的短前缀
     return `${shortPrefix}${cleanId}`;
   }
@@ -1065,26 +1097,26 @@ export class AccountService {
    */
   private isTimestampBasedId(accountId: string): boolean {
     if (!accountId) return true;
-    
+
     // 检查是否匹配 前缀_时间戳 格式(支持短前缀和完整前缀)
     const timestampPattern = /^[a-z_]+_(\d{13,})$/;
     const match = accountId.match(timestampPattern);
     if (!match) {
       return false;
     }
-    
+
     // 提取数字部分,检查是否是合理的时间戳(2020年到2030年之间)
     if (match[1]) {
       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;
   }
 }

+ 77 - 18
server/src/services/WorkService.ts

@@ -252,26 +252,56 @@ export class WorkService {
     if (accountInfo.worksList && accountInfo.worksList.length > 0) {
       const total = accountInfo.worksList.length;
       for (const workItem of accountInfo.worksList) {
-        // 生成一个唯一的视频ID
-        const platformVideoId = workItem.videoId || `${platform}_${workItem.title}_${workItem.publishTime}`.substring(0, 100);
-        remotePlatformVideoIds.add(platformVideoId);
+        const titleForId = (workItem.title || '').trim();
+        const publishTimeForId = (workItem.publishTime || '').trim();
+        const legacyFallbackId = `${platform}_${titleForId}_${publishTimeForId}`.substring(0, 100);
+        const canonicalVideoId = (workItem.videoId || '').trim() || legacyFallbackId;
+
+        remotePlatformVideoIds.add(canonicalVideoId);
+        if (legacyFallbackId !== canonicalVideoId) {
+          remotePlatformVideoIds.add(legacyFallbackId);
+        }
 
-        // 查找是否已存在
-        const existingWork = await this.workRepository.findOne({
-          where: { accountId: account.id, platformVideoId },
+        let work = await this.workRepository.findOne({
+          where: { accountId: account.id, platformVideoId: canonicalVideoId },
         });
 
-        if (existingWork) {
-          // 更新现有作品
-          await this.workRepository.update(existingWork.id, {
-            title: workItem.title || existingWork.title,
-            coverUrl: workItem.coverUrl || existingWork.coverUrl,
-            duration: workItem.duration || existingWork.duration,
-            status: workItem.status || existingWork.status,
-            playCount: workItem.playCount ?? existingWork.playCount,
-            likeCount: workItem.likeCount ?? existingWork.likeCount,
-            commentCount: workItem.commentCount ?? existingWork.commentCount,
-            shareCount: workItem.shareCount ?? existingWork.shareCount,
+        if (!work && legacyFallbackId !== canonicalVideoId) {
+          const legacyWork = await this.workRepository.findOne({
+            where: { accountId: account.id, platformVideoId: legacyFallbackId },
+          });
+
+          if (legacyWork) {
+            const canonicalWork = await this.workRepository.findOne({
+              where: { accountId: account.id, platformVideoId: canonicalVideoId },
+            });
+
+            if (canonicalWork) {
+              await AppDataSource.getRepository(Comment).update(
+                { workId: legacyWork.id },
+                { workId: canonicalWork.id }
+              );
+              await this.workRepository.delete(legacyWork.id);
+              work = canonicalWork;
+            } else {
+              await this.workRepository.update(legacyWork.id, {
+                platformVideoId: canonicalVideoId,
+              });
+              work = { ...legacyWork, platformVideoId: canonicalVideoId };
+            }
+          }
+        }
+
+        if (work) {
+          await this.workRepository.update(work.id, {
+            title: workItem.title || work.title,
+            coverUrl: workItem.coverUrl || work.coverUrl,
+            duration: workItem.duration || work.duration,
+            status: workItem.status || work.status,
+            playCount: workItem.playCount ?? work.playCount,
+            likeCount: workItem.likeCount ?? work.likeCount,
+            commentCount: workItem.commentCount ?? work.commentCount,
+            shareCount: workItem.shareCount ?? work.shareCount,
           });
         } else {
           // 创建新作品
@@ -279,7 +309,7 @@ export class WorkService {
             userId,
             accountId: account.id,
             platform,
-            platformVideoId,
+            platformVideoId: canonicalVideoId,
             title: workItem.title || '',
             coverUrl: workItem.coverUrl || '',
             duration: workItem.duration || '00:00',
@@ -319,10 +349,39 @@ export class WorkService {
     } else if (remotePlatformVideoIds.size === 0) {
       logger.warn(`[SyncAccountWorks] Skipping local deletions for account ${account.id} because no remote IDs were collected`);
     } else {
+      if (platform === 'weixin_video') {
+        logger.info(`[SyncAccountWorks] Skipping local deletions for ${platform} account ${account.id} to avoid false deletions`);
+        return {
+          syncedCount,
+          worksListLength: accountInfo.worksList?.length || 0,
+          worksCount: accountInfo.worksCount || 0,
+          source: accountInfo.source,
+          pythonAvailable: accountInfo.pythonAvailable,
+        };
+      }
+
       const localWorks = await this.workRepository.find({
         where: { accountId: account.id },
       });
 
+      const matchedCount = localWorks.reduce(
+        (sum, w) => sum + (remotePlatformVideoIds.has(w.platformVideoId) ? 1 : 0),
+        0
+      );
+      const matchRatio = localWorks.length > 0 ? matchedCount / localWorks.length : 1;
+      if (localWorks.length >= 10 && matchRatio < 0.2) {
+        logger.warn(
+          `[SyncAccountWorks] Skipping local deletions for account ${account.id} because remote/local ID match ratio is too low (matched=${matchedCount}/${localWorks.length})`
+        );
+        return {
+          syncedCount,
+          worksListLength: accountInfo.worksList?.length || 0,
+          worksCount: accountInfo.worksCount || 0,
+          source: accountInfo.source,
+          pythonAvailable: accountInfo.pythonAvailable,
+        };
+      }
+
       let deletedCount = 0;
       for (const localWork of localWorks) {
         if (!remotePlatformVideoIds.has(localWork.platformVideoId)) {