소스 검색

Enhance account information extraction for Weixin Video platform; implement robust handling of popups and improve logging for better debugging. Update environment configuration to allow flexible host settings and streamline account refresh logic in task scheduler.

Ethanfly 10 시간 전
부모
커밋
8d0d369acc

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

@@ -17,6 +17,7 @@ declare module 'vue' {
     ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
     ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
+    ElCol: typeof import('element-plus/es')['ElCol']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
     ElContainer: typeof import('element-plus/es')['ElContainer']
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
@@ -42,6 +43,7 @@ declare module 'vue' {
     ElProgress: typeof import('element-plus/es')['ElProgress']
     ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
+    ElRow: typeof import('element-plus/es')['ElRow']
     ElSelect: typeof import('element-plus/es')['ElSelect']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']

+ 638 - 127
client/src/components/BrowserTab.vue

@@ -431,10 +431,25 @@ async function handleAILoginSuccess(currentScreenshot: string) {
   fetchingAccountInfo.value = true;
   accountInfo.value = null;
   
-  console.log('[AI] Extracting account info from screenshot...');
+  console.log('[AI] Extracting account info...');
+  
+  // 多次尝试关闭可能的活动弹框(有些弹框可能需要多次点击)
+  console.log('[AI] Trying to close popups...');
+  for (let i = 0; i < 3; i++) {
+    const closed = await closeActivityPopup();
+    if (closed) {
+      console.log(`[AI] Closed popup on attempt ${i + 1}`);
+      await new Promise(resolve => setTimeout(resolve, 500));
+    } else {
+      break;
+    }
+  }
   
-  // 尝试关闭可能的活动弹框
-  await closeActivityPopup();
+  // 对于视频号平台,需要导航到首页才能获取完整的账号信息(视频号ID、作品数、粉丝数)
+  if (platform.value === 'weixin_video') {
+    console.log('[AI] Weixin Video: navigating to home page for complete account info...');
+    await navigateToWeixinVideoHomePage();
+  }
   
   // 最多尝试 3 次提取账号信息
   const MAX_EXTRACT_RETRIES = 3;
@@ -446,69 +461,121 @@ async function handleAILoginSuccess(currentScreenshot: string) {
     worksCount: number;
   } | null = null;
   
-  for (let attempt = 1; attempt <= MAX_EXTRACT_RETRIES; attempt++) {
-    console.log(`[AI] Extract attempt ${attempt}/${MAX_EXTRACT_RETRIES}...`);
+  // 优先从 HTML 中提取账号信息(不受遮罩/弹窗影响)
+  console.log('[AI] Trying to extract account info from HTML first...');
+  try {
+    const htmlExtractResult = await extractAccountInfoFromPage();
+    console.log('[AI] HTML extract result:', htmlExtractResult);
     
-    try {
-      // 每次重试都重新截图
-      const screenshot = attempt === 1 ? currentScreenshot : await captureWebviewScreenshot();
-      if (!screenshot) {
-        console.warn('[AI] No screenshot for extraction');
-        continue;
-      }
+    if (htmlExtractResult && htmlExtractResult.accountName) {
+      // 检查 accountId 是否有效(不是时间戳生成的,也不是空)
+      const accountId = htmlExtractResult.accountId || '';
+      const isValidAccountId = accountId && 
+        !accountId.match(/_\d{13}$/) && // 不是时间戳结尾
+        !accountId.match(/_\d{10}$/);   // 也不是 Unix 时间戳
       
-      // 调用 AI 提取账号信息
-      const extractResult = await aiApi.extractAccountInfo(screenshot, platform.value);
+      // 即使没有有效的 accountId,只要有用户名就可以先保存
+      // 后台刷新会获取正确的 accountId
+      extractedAccountInfo = {
+        accountId: isValidAccountId ? accountId : `${platform.value}_${Date.now()}`,
+        accountName: htmlExtractResult.accountName,
+        avatarUrl: htmlExtractResult.avatarUrl || '',
+        fansCount: htmlExtractResult.fansCount || 0,
+        worksCount: htmlExtractResult.worksCount || 0,
+      };
       
-      console.log('[AI] Extract result:', extractResult);
+      if (isValidAccountId) {
+        console.log('[AI] Successfully extracted complete account info from HTML:', extractedAccountInfo);
+      } else {
+        console.log('[AI] Extracted account name but ID is not valid, will refresh later:', extractedAccountInfo);
+      }
+    }
+  } catch (htmlError) {
+    console.warn('[AI] HTML extraction failed:', htmlError);
+  }
+  
+  // 如果 HTML 提取失败,再用 AI 截图分析
+  if (!extractedAccountInfo) {
+    console.log('[AI] HTML extraction failed, falling back to screenshot AI...');
+    
+    for (let attempt = 1; attempt <= MAX_EXTRACT_RETRIES; attempt++) {
+      console.log(`[AI] Extract attempt ${attempt}/${MAX_EXTRACT_RETRIES}...`);
       
-      if (extractResult.found && extractResult.accountName) {
-        // AI 成功提取到账号信息
-        extractedAccountInfo = {
-          accountId: extractResult.accountId || `${platform.value}_${Date.now()}`,
-          accountName: extractResult.accountName,
-          avatarUrl: '', // AI 返回的是描述,不是 URL,头像需要从页面 JS 获取
-          fansCount: parseInt(String(extractResult.fansCount || 0)) || 0,
-          worksCount: parseInt(String(extractResult.worksCount || 0)) || 0,
-        };
-        
-        // 尝试从页面获取头像 URL(补充 AI 无法获取的信息)
-        try {
-          const avatarUrl = await getAvatarUrlFromPage();
-          if (avatarUrl) {
-            extractedAccountInfo.avatarUrl = avatarUrl;
-          }
-        } catch (e) {
-          console.warn('[AI] Failed to get avatar URL from page:', e);
+      try {
+        // 每次重试都重新截图
+        const screenshot = attempt === 1 ? currentScreenshot : await captureWebviewScreenshot();
+        if (!screenshot) {
+          console.warn('[AI] No screenshot for extraction');
+          continue;
         }
         
-        console.log('[AI] Successfully extracted account info:', extractedAccountInfo);
-        break;
-      } else if (extractResult.navigationGuide) {
-        // AI 提供了导航建议
-        console.log('[AI] Navigation suggestion:', extractResult.navigationGuide);
-        if (aiAnalysis.value) {
-          aiAnalysis.value.suggestedAction = extractResult.navigationGuide || undefined;
+        // 调用 AI 提取账号信息
+        const extractResult = await aiApi.extractAccountInfo(screenshot, platform.value);
+        
+        console.log('[AI] Extract result:', extractResult);
+        
+        if (extractResult.found && extractResult.accountName) {
+          // AI 成功提取到账号信息
+          // 注意:worksCount 设为 0,让后台刷新来获取准确数据(AI 提取的作品数常常不准确)
+          extractedAccountInfo = {
+            accountId: extractResult.accountId || `${platform.value}_${Date.now()}`,
+            accountName: extractResult.accountName,
+            avatarUrl: '', // AI 返回的是描述,不是 URL,头像需要从页面 JS 获取
+            fansCount: parseInt(String(extractResult.fansCount || 0)) || 0,
+            worksCount: 0, // 由后台刷新获取准确数据
+          };
+          
+          // 尝试从页面获取头像 URL(补充 AI 无法获取的信息)
+          try {
+            const avatarUrl = await getAvatarUrlFromPage();
+            if (avatarUrl) {
+              extractedAccountInfo.avatarUrl = avatarUrl;
+            }
+          } catch (e) {
+            console.warn('[AI] Failed to get avatar URL from page:', e);
+          }
+          
+          console.log('[AI] Successfully extracted account info:', extractedAccountInfo);
+          break;
+        } else if (extractResult.navigationGuide) {
+          // AI 提供了导航建议
+          console.log('[AI] Navigation suggestion:', extractResult.navigationGuide);
+          if (aiAnalysis.value) {
+            aiAnalysis.value.suggestedAction = extractResult.navigationGuide || undefined;
+          }
+          
+          // 如果启用了自动操作,尝试执行导航操作
+          if (autoOperationEnabled.value && !isExecutingOperation.value) {
+            console.log('[AI] Attempting auto navigation...');
+            await performAutoOperation(`获取账号信息:${extractResult.navigationGuide}`);
+            // 导航后等待页面加载
+            await new Promise(resolve => setTimeout(resolve, 2000));
+          } else {
+            showAIPanel.value = true;
+          }
         }
         
-        // 如果启用了自动操作,尝试执行导航操作
-        if (autoOperationEnabled.value && !isExecutingOperation.value) {
-          console.log('[AI] Attempting auto navigation...');
-          await performAutoOperation(`获取账号信息:${extractResult.navigationGuide}`);
-          // 导航后等待页面加载
+        // 等待后重试
+        if (attempt < MAX_EXTRACT_RETRIES) {
+          // 如果 AI 提到弹窗遮挡,多次尝试关闭
+          const needClosePopup = extractResult?.navigationGuide?.includes('弹窗') || 
+                                 extractResult?.navigationGuide?.includes('遮挡') ||
+                                 extractResult?.navigationGuide?.includes('关闭');
+          if (needClosePopup) {
+            console.log('[AI] AI detected popup, trying to close...');
+            for (let i = 0; i < 3; i++) {
+              const closed = await closeActivityPopup();
+              if (!closed) break;
+              await new Promise(resolve => setTimeout(resolve, 500));
+            }
+          } else {
+            await closeActivityPopup();
+          }
           await new Promise(resolve => setTimeout(resolve, 2000));
-        } else {
-          showAIPanel.value = true;
         }
+      } catch (error) {
+        console.error(`[AI] Extract attempt ${attempt} failed:`, error);
       }
-      
-      // 等待后重试
-      if (attempt < MAX_EXTRACT_RETRIES) {
-        await closeActivityPopup();
-        await new Promise(resolve => setTimeout(resolve, 2000));
-      }
-    } catch (error) {
-      console.error(`[AI] Extract attempt ${attempt} failed:`, error);
     }
   }
   
@@ -562,12 +629,13 @@ async function handleAILoginSuccess(currentScreenshot: string) {
             !defaultNames.includes(retryResult.accountName);
           
           if (retryIsValid) {
+            // 注意:worksCount 设为 0,让后台刷新来获取准确数据
             extractedAccountInfo = {
               accountId: retryResult.accountId || `${platform.value}_${Date.now()}`,
               accountName: retryResult.accountName,
               avatarUrl: '',
               fansCount: parseInt(String(retryResult.fansCount || 0)) || 0,
-              worksCount: parseInt(String(retryResult.worksCount || 0)) || 0,
+              worksCount: 0, // 由后台刷新获取准确数据
             };
             
             try {
@@ -851,24 +919,59 @@ async function getAvatarUrlFromPage(): Promise<string | null> {
       (function() {
         // 查找可能的头像图片
         const avatarSelectors = [
+          // 通用头像选择器
           '[class*="avatar"] img',
           '[class*="Avatar"] img',
           'img[class*="avatar"]',
           '.user-avatar img',
           '.user-info img',
           '.ant-avatar img',
-          '.header-user img'
+          '.header-user img',
+          // 微信视频号特定选择器
+          'img[src*="wx.qlogo"]',
+          'img[src*="mmbiz.qpic"]',
+          'img[src*="thirdwx"]',
+          '.finder-avatar img',
+          '.account-avatar img',
+          // 顶部导航栏头像
+          '.header img',
+          'nav img',
+          '.navbar img'
         ];
         
         for (const selector of avatarSelectors) {
           const imgs = document.querySelectorAll(selector);
           for (const img of imgs) {
             const src = img.src || '';
-            if (src && src.startsWith('http') && !src.includes('logo') && !src.includes('icon')) {
+            if (src && src.startsWith('http') && !src.includes('logo') && !src.includes('icon') && !src.includes('banner')) {
+              console.log('[getAvatarUrlFromPage] Found avatar:', src);
               return src;
             }
           }
         }
+        
+        // 备选:查找所有圆形或适当大小的图片
+        const allImgs = document.querySelectorAll('img');
+        for (const img of allImgs) {
+          const src = img.src || '';
+          if (!src || !src.startsWith('http')) continue;
+          if (src.includes('logo') || src.includes('icon') || src.includes('banner')) continue;
+          
+          const style = window.getComputedStyle(img);
+          const rect = img.getBoundingClientRect();
+          const isRound = style.borderRadius && (style.borderRadius.includes('50%') || parseInt(style.borderRadius) > 10);
+          const isAvatarSize = rect.width >= 24 && rect.width <= 100;
+          const isInHeader = rect.top < 100;
+          
+          // 微信头像 URL 特征
+          const isWeixinAvatar = src.includes('wx.qlogo') || src.includes('mmbiz') || src.includes('thirdwx');
+          
+          if ((isWeixinAvatar || (isRound && isAvatarSize) || (isInHeader && isAvatarSize))) {
+            console.log('[getAvatarUrlFromPage] Found avatar via fallback:', src);
+            return src;
+          }
+        }
+        
         return null;
       })()
     `;
@@ -1658,16 +1761,202 @@ async function extractAccountInfoFromPage(): Promise<{
         (function() {
           const result = { accountId: '', accountName: '', avatarUrl: '', fansCount: 0, worksCount: 0 };
           
-          const avatarEl = document.querySelector('.user-avatar img, [class*="avatar"] img');
-          if (avatarEl) result.avatarUrl = avatarEl.src || '';
-          
-          const nameEl = document.querySelector('.user-name, [class*="userName"], [class*="nickname"]');
-          if (nameEl) result.accountName = nameEl.textContent?.trim() || '';
-          
-          const uidMatch = document.cookie.match(/customerClientId=([^;]+)/) || 
-                          document.cookie.match(/web_session=([^;]+)/);
-          if (uidMatch) result.accountId = 'xiaohongshu_' + uidMatch[1].slice(0, 16);
+          try {
+            console.log('[Xiaohongshu Extract] Starting extraction...');
+            console.log('[Xiaohongshu Extract] Current URL:', window.location.href);
+            
+            // 1. 查找用户名 - 小红书创作平台用户名通常在右上角或侧边栏
+            const nameSelectors = [
+              // 创作平台顶部用户名(常见位置)
+              '.creator-header [class*="name"]',
+              '.user-info [class*="name"]',
+              // 右上角下拉菜单中的用户名
+              '[class*="dropdown"] [class*="name"]',
+              '[class*="user"] [class*="name"]',
+              // 通用选择器
+              '[class*="userName"]',
+              '[class*="UserName"]',
+              '[class*="user-name"]',
+              '[class*="nickname"]',
+              '[class*="NickName"]',
+              '[class*="nick-name"]',
+              // Ant Design 或其他UI库的组件
+              '.ant-dropdown-trigger [class*="name"]'
+            ];
+            
+            for (const selector of nameSelectors) {
+              const els = document.querySelectorAll(selector);
+              for (const el of els) {
+                const text = el.textContent?.trim() || '';
+                console.log('[Xiaohongshu Extract] Checking selector:', selector, 'text:', text);
+                if (text.length >= 2 && text.length <= 30 && 
+                    !/登录|注册|小红书|创作|发布|数据|内容|消息|设置|帮助|退出|首页|笔记|灵感|学院|活动/.test(text) &&
+                    /[\u4e00-\u9fa5a-zA-Z0-9]/.test(text)) {
+                  result.accountName = text;
+                  console.log('[Xiaohongshu Extract] Found name via selector:', text);
+                  break;
+                }
+              }
+              if (result.accountName) break;
+            }
+            
+            // 2. 备选方案:在整个页面中查找可能是用户名的文本
+            if (!result.accountName) {
+              console.log('[Xiaohongshu Extract] Trying fallback method for name...');
+              // 查找页面右上角区域的文本(通常是用户名所在位置)
+              const allElements = document.querySelectorAll('span, div, p, a');
+              const candidates = [];
+              
+              for (const el of allElements) {
+                const rect = el.getBoundingClientRect();
+                // 只查找右上角区域(通常用户信息在这里)
+                if (rect.right < window.innerWidth * 0.5 || rect.top > 200) continue;
+                
+                // 获取直接文本内容
+                const directText = Array.from(el.childNodes)
+                  .filter(node => node.nodeType === Node.TEXT_NODE)
+                  .map(node => node.textContent?.trim())
+                  .join('')
+                  .trim();
+                
+                if (!directText) continue;
+                
+                if (directText.length >= 2 && directText.length <= 20 &&
+                    /[\u4e00-\u9fa5]/.test(directText) &&
+                    !/登录|注册|小红书|创作|发布|数据|内容|消息|设置|帮助|退出|首页|笔记|灵感|学院|活动|图文|视频|直播|百科/.test(directText)) {
+                  candidates.push({ text: directText, right: rect.right, top: rect.top });
+                }
+              }
+              
+              // 按右上角位置排序
+              candidates.sort((a, b) => (b.right - a.right) + (a.top - b.top));
+              console.log('[Xiaohongshu Extract] Candidates:', candidates.slice(0, 5));
+              
+              if (candidates.length > 0) {
+                result.accountName = candidates[0].text;
+                console.log('[Xiaohongshu Extract] Using fallback name:', result.accountName);
+              }
+            }
+            
+            // 3. 查找头像
+            const avatarSelectors = [
+              // 创作平台头像
+              '.creator-header img',
+              '.user-info img',
+              '[class*="avatar"] img',
+              '[class*="Avatar"] img',
+              'img[class*="avatar"]',
+              'img[class*="Avatar"]',
+              // 右上角用户头像
+              '.ant-avatar img',
+              '.ant-avatar-image'
+            ];
+            
+            for (const selector of avatarSelectors) {
+              const els = document.querySelectorAll(selector);
+              for (const el of els) {
+                const src = el.src || el.style?.backgroundImage?.match(/url\\(["']?([^"')]+)["']?\\)/)?.[1];
+                if (src && src.startsWith('http') && !src.includes('logo') && !src.includes('icon')) {
+                  result.avatarUrl = src;
+                  console.log('[Xiaohongshu Extract] Found avatar:', src);
+                  break;
+                }
+              }
+              if (result.avatarUrl) break;
+            }
+            
+            // 4. 备选:查找所有图片,找到可能是头像的(圆形、适当大小)
+            if (!result.avatarUrl) {
+              const imgs = Array.from(document.querySelectorAll('img'));
+              for (const img of imgs) {
+                const src = img.src || '';
+                const style = window.getComputedStyle(img);
+                const isRound = style.borderRadius && (style.borderRadius.includes('50%') || parseInt(style.borderRadius) > 15);
+                const isAvatarSize = img.width >= 24 && img.width <= 100;
+                const rect = img.getBoundingClientRect();
+                const isInTopArea = rect.top < 150;
+                
+                if ((isRound || isInTopArea) && isAvatarSize && src.startsWith('http') && 
+                    !src.includes('logo') && !src.includes('icon') && !src.includes('banner')) {
+                  result.avatarUrl = src;
+                  console.log('[Xiaohongshu Extract] Found avatar via fallback:', src);
+                  break;
+                }
+              }
+            }
+            
+            // 5. 查找粉丝数
+            const bodyText = document.body.innerText;
+            const fansMatch = bodyText.match(/粉丝[\\s::]*([0-9,\\.]+[万亿]?)/);
+            if (fansMatch) {
+              let fansStr = fansMatch[1].replace(/,/g, '');
+              if (fansStr.includes('万')) {
+                result.fansCount = Math.round(parseFloat(fansStr) * 10000);
+              } else if (fansStr.includes('亿')) {
+                result.fansCount = Math.round(parseFloat(fansStr) * 100000000);
+              } else {
+                result.fansCount = parseInt(fansStr) || 0;
+              }
+              console.log('[Xiaohongshu Extract] Found fans:', result.fansCount);
+            }
+            
+            // 6. 优先从页面获取小红书号作为 account_id
+            // 小红书号格式:通常是 "小红书号:xxxxxxx" 或 "小红书号:xxxxxxx"
+            const xhsIdMatch = bodyText.match(/小红书号[::]\s*([a-zA-Z0-9_]+)/) ||
+                              bodyText.match(/红书号[::]\s*([a-zA-Z0-9_]+)/);
+            if (xhsIdMatch) {
+              result.accountId = 'xiaohongshu_' + xhsIdMatch[1];
+              console.log('[Xiaohongshu Extract] Found 小红书号 from page:', result.accountId);
+            }
+            
+            // 7. 备选:尝试从页面元素中查找小红书号
+            if (!result.accountId) {
+              // 查找可能包含小红书号的元素
+              const idSelectors = [
+                '[class*="redId"]',
+                '[class*="red-id"]',
+                '[class*="userId"]',
+                '[class*="user-id"]'
+              ];
+              for (const selector of idSelectors) {
+                const el = document.querySelector(selector);
+                const text = el?.textContent?.trim();
+                if (text && /^[a-zA-Z0-9_]+$/.test(text) && text.length >= 6 && text.length <= 30) {
+                  result.accountId = 'xiaohongshu_' + text;
+                  console.log('[Xiaohongshu Extract] Found ID from selector:', result.accountId);
+                  break;
+                }
+              }
+            }
+            
+            // 8. 备选:从 URL 中查找用户 ID
+            if (!result.accountId) {
+              const urlIdMatch = window.location.href.match(/user\\/([a-f0-9]+)/);
+              if (urlIdMatch) {
+                result.accountId = 'xiaohongshu_' + urlIdMatch[1];
+                console.log('[Xiaohongshu Extract] Found ID from URL:', result.accountId);
+              }
+            }
+            
+            // 9. 最后备选:从 Cookie 获取
+            if (!result.accountId) {
+              const uidMatch = document.cookie.match(/customerClientId=([^;]+)/) || 
+                              document.cookie.match(/customer-client-id=([^;]+)/) ||
+                              document.cookie.match(/web_session=([^;]+)/);
+              if (uidMatch) {
+                result.accountId = 'xiaohongshu_' + uidMatch[1].slice(0, 20);
+                console.log('[Xiaohongshu Extract] Found ID from cookie:', result.accountId);
+              } else {
+                result.accountId = 'xiaohongshu_' + Date.now();
+                console.log('[Xiaohongshu Extract] Using timestamp as fallback ID');
+              }
+            }
+            
+          } catch (e) {
+            console.error('[Xiaohongshu Extract] Error:', e);
+          }
           
+          console.log('[Xiaohongshu Extract] Final result:', result);
           return result;
         })()
       `,
@@ -1847,74 +2136,188 @@ async function extractAccountInfoFromPage(): Promise<{
           const result = { accountId: '', accountName: '', avatarUrl: '', fansCount: 0, worksCount: 0 };
           
           try {
-            // 1. 查找头像 - 视频号创作者平台头像
-            const imgs = Array.from(document.querySelectorAll('img'));
-            for (const img of imgs) {
+            console.log('[Weixin Extract] Starting extraction...');
+            console.log('[Weixin Extract] Current URL:', window.location.href);
+            
+            // 1. 查找头像 - 视频号创作者平台头像(优先微信特有域名)
+            // 先查找微信头像 URL 特征的图片
+            const allImgs = Array.from(document.querySelectorAll('img'));
+            for (const img of allImgs) {
               const src = img.src || '';
-              const className = img.className || '';
-              const parentClass = img.parentElement?.className || '';
-              
-              // 判断是否是头像图片
-              const isAvatar = className.includes('avatar') || 
-                              parentClass.includes('avatar') ||
-                              src.includes('mmbiz') ||
-                              src.includes('wx.qlogo');
-              
-              if (isAvatar && src.startsWith('http')) {
+              // 微信头像通常包含这些域名
+              if (src.includes('wx.qlogo') || src.includes('mmbiz.qpic') || src.includes('thirdwx.qlogo')) {
                 result.avatarUrl = src;
+                console.log('[Weixin Extract] Found avatar by URL pattern:', src);
                 break;
               }
             }
             
-            // 2. 查找视频号ID - 格式通常是 "视频号ID:xxx" 或页面元素
+            // 2. 如果没找到,尝试通过选择器查找
+            if (!result.avatarUrl) {
+              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',
+                'nav img',
+                '.navbar img'
+              ];
+              
+              for (const selector of avatarSelectors) {
+                const el = document.querySelector(selector);
+                if (el && el.src && el.src.startsWith('http')) {
+                  const src = el.src;
+                  if (!src.includes('logo') && !src.includes('icon') && !src.includes('banner')) {
+                    result.avatarUrl = src;
+                    console.log('[Weixin Extract] Found avatar by selector:', selector, src);
+                    break;
+                  }
+                }
+              }
+            }
+            
+            // 3. 备选:查找所有圆形或适当大小且在页面顶部的图片
+            if (!result.avatarUrl) {
+              for (const img of allImgs) {
+                const src = img.src || '';
+                if (!src || !src.startsWith('http')) continue;
+                if (src.includes('logo') || src.includes('icon') || src.includes('banner')) continue;
+                
+                const style = window.getComputedStyle(img);
+                const rect = img.getBoundingClientRect();
+                const isRound = style.borderRadius && (style.borderRadius.includes('50%') || parseInt(style.borderRadius) > 10);
+                const isAvatarSize = rect.width >= 24 && rect.width <= 80;
+                const isInHeader = rect.top < 120;
+                
+                if ((isRound && isAvatarSize) || (isInHeader && isAvatarSize)) {
+                  result.avatarUrl = src;
+                  console.log('[Weixin Extract] Found avatar by fallback:', src);
+                  break;
+                }
+              }
+            }
+            
+            // 4. 优先查找视频号ID - 格式通常是 "视频号ID:xxx" 或 "视频号ID:xxx"
             const bodyText = document.body.innerText;
-            const finderIdMatch = bodyText.match(/视频号ID[::]\s*([a-zA-Z0-9_]+)/);
+            // 匹配多种格式:视频号ID:xxx、视频号ID:xxx、视频号ID xxx
+            const finderIdMatch = bodyText.match(/视频号ID[::\s]*([a-zA-Z0-9_]+)/) ||
+                                 bodyText.match(/视频号[::\s]*ID[::\s]*([a-zA-Z0-9_]+)/);
             if (finderIdMatch) {
-              result.accountId = 'weixin_' + finderIdMatch[1];
+              result.accountId = 'weixin_video_' + finderIdMatch[1];
+              console.log('[Weixin Extract] Found 视频号ID from page text:', result.accountId);
             }
             
-            // 3. 查找昵称 - 在用户信息区域
-            const nicknameEl = document.querySelector('.finder-nickname, [class*="nickname"], [class*="user-name"], h2.name');
-            if (nicknameEl) {
-              result.accountName = nicknameEl.textContent?.trim() || '';
+            // 4.1 备选:从页面元素中查找视频号ID
+            if (!result.accountId) {
+              // 查找可能包含视频号ID的元素
+              const idSelectors = [
+                '[class*="finder-id"]',
+                '[class*="finderId"]',
+                '[class*="channel-id"]',
+                '[class*="channelId"]'
+              ];
+              for (const selector of idSelectors) {
+                const el = document.querySelector(selector);
+                const text = el?.textContent?.trim();
+                if (text && /^[a-zA-Z0-9_]+$/.test(text) && text.length >= 10 && text.length <= 30) {
+                  result.accountId = 'weixin_video_' + text;
+                  console.log('[Weixin Extract] Found ID from selector:', result.accountId);
+                  break;
+                }
+              }
             }
             
-            // 4. 备选:遍历页面查找看起来像用户名的文本
+            // 5. 查找昵称 - 在用户信息区域
+            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.accountName = text;
+                console.log('[Weixin Extract] Found name by selector:', selector, text);
+                break;
+              }
+            }
+            
+            // 6. 备选:遍历页面查找看起来像用户名的文本
             if (!result.accountName) {
               const candidates = [];
-              document.querySelectorAll('span, div, h2').forEach(el => {
-                const text = el.innerText?.trim() || '';
+              document.querySelectorAll('span, div, h2, h3').forEach(el => {
                 const rect = el.getBoundingClientRect();
-                // 筛选条件:可见、长度适中、包含中文或字母
-                if (rect.width > 0 && text.length >= 2 && text.length <= 20 &&
-                    /[\u4e00-\u9fa5a-zA-Z]/.test(text) &&
-                    !/粉丝|关注|获赞|作品|发布|数据|登录|注册|视频号ID/.test(text)) {
-                  candidates.push({ text, top: rect.top, left: rect.left });
+                // 只在页面顶部区域查找
+                if (rect.top > 200 || rect.width === 0) return;
+                
+                // 获取直接文本内容
+                const directText = Array.from(el.childNodes)
+                  .filter(node => node.nodeType === Node.TEXT_NODE)
+                  .map(node => node.textContent?.trim())
+                  .join('')
+                  .trim();
+                
+                if (directText && directText.length >= 2 && directText.length <= 20 &&
+                    /[\u4e00-\u9fa5a-zA-Z]/.test(directText) &&
+                    !/粉丝|关注|获赞|作品|发布|数据|登录|注册|视频号|首页|创作|评论|消息|设置/.test(directText)) {
+                  candidates.push({ text: directText, top: rect.top, left: rect.left });
                 }
               });
-              // 优先选择靠上方、靠左侧的(通常是用户名位置)
-              candidates.sort((a, b) => (a.top + a.left) - (b.top + b.left));
+              // 优先选择靠上方的
+              candidates.sort((a, b) => a.top - b.top);
+              console.log('[Weixin Extract] Name candidates:', candidates.slice(0, 5));
               if (candidates.length > 0) {
                 result.accountName = candidates[0].text;
               }
             }
             
-            // 5. 从 Cookie 或 URL 获取唯一标识作为备选
-            if (!result.accountId) {
-              const uidMatch = document.cookie.match(/wxuin=([^;]+)/) || 
-                              document.cookie.match(/pass_ticket=([^;]+)/);
-              if (uidMatch) {
-                result.accountId = 'weixin_' + uidMatch[1].slice(0, 16);
+            // 7. 查找粉丝数(视频号使用"关注者"表示粉丝)
+            const fansMatch = bodyText.match(/关注者[\\s::]*([0-9,\\.]+[万亿]?)/) ||
+                             bodyText.match(/粉丝[\\s::]*([0-9,\\.]+[万亿]?)/);
+            if (fansMatch) {
+              let fansStr = fansMatch[1].replace(/,/g, '');
+              if (fansStr.includes('万')) {
+                result.fansCount = Math.round(parseFloat(fansStr) * 10000);
+              } else if (fansStr.includes('亿')) {
+                result.fansCount = Math.round(parseFloat(fansStr) * 100000000);
               } else {
-                result.accountId = 'weixin_' + Date.now();
+                result.fansCount = parseInt(fansStr) || 0;
               }
+              console.log('[Weixin Extract] Found fans (关注者):', result.fansCount);
+            }
+            
+            // 7.1 查找作品数(视频号显示为"视频 X")
+            const worksMatch = bodyText.match(/视频[\\s::]*([0-9]+)/) ||
+                              bodyText.match(/作品[\\s::]*([0-9]+)/);
+            if (worksMatch) {
+              result.worksCount = parseInt(worksMatch[1]) || 0;
+              console.log('[Weixin Extract] Found works (视频):', result.worksCount);
+            }
+            
+            // 8. 不再从 Cookie 获取 ID,只有真正的视频号ID才有效
+            // 如果没有找到视频号ID,返回空字符串,让后续流程处理
+            if (!result.accountId) {
+              console.log('[Weixin Extract] No valid 视频号ID found, will use fallback');
+              // 不设置默认值,让后续流程判断是否需要重新获取
             }
             
           } catch (e) {
             console.error('[Weixin Extract] Error:', e);
           }
           
-          console.log('[Weixin Extract] Result:', result);
+          console.log('[Weixin Extract] Final result:', result);
           return result;
         })()
       `,
@@ -1954,44 +2357,88 @@ async function closeActivityPopup(): Promise<boolean> {
       (function() {
         let closed = false;
         
-        // 百家号活动弹框关闭
-        const baijiahaoClose = document.querySelectorAll(
-          '.dialog-close, .modal-close, .popup-close, ' +
-          '[class*="close-btn"], [class*="closeBtn"], ' +
-          '.ant-modal-close, .el-dialog__close, ' +
-          '[class*="dialog"] [class*="close"], ' +
-          '[class*="modal"] [class*="close"], ' +
-          '[class*="popup"] [class*="close"], ' +
-          'button[aria-label="Close"], ' +
-          '.mask-close, [class*="mask"] [class*="close"]'
-        );
+        // 通用关闭按钮选择器
+        const closeSelectors = [
+          // 通用关闭按钮
+          '.dialog-close, .modal-close, .popup-close',
+          '[class*="close-btn"], [class*="closeBtn"], [class*="close-icon"], [class*="closeIcon"]',
+          '.ant-modal-close, .el-dialog__close, .el-message-box__close',
+          '[class*="dialog"] [class*="close"], [class*="modal"] [class*="close"]',
+          '[class*="popup"] [class*="close"], [class*="toast"] [class*="close"]',
+          'button[aria-label="Close"], button[aria-label="关闭"]',
+          '.mask-close, [class*="mask"] [class*="close"]',
+          // 小红书特定选择器
+          '[class*="guide"] [class*="close"]',
+          '[class*="tip"] [class*="close"]',
+          '[class*="notice"] [class*="close"]',
+          '[class*="banner"] [class*="close"]',
+          '[class*="float"] [class*="close"]',
+          '.close, .icon-close, .btn-close',
+          // X 按钮(通常是 SVG 或特殊字符)
+          'button svg[class*="close"]',
+          '[role="button"][class*="close"]',
+          // 确认/知道了/我知道了 按钮(用于关闭提示)
+          'button:contains("知道了")', 'button:contains("我知道了")',
+          'button:contains("确定")', 'button:contains("关闭")',
+          '[class*="confirm"], [class*="ok-btn"]'
+        ];
+        
+        // 尝试所有关闭按钮
+        for (const selector of closeSelectors) {
+          try {
+            const elements = document.querySelectorAll(selector);
+            for (const btn of elements) {
+              if (btn && btn.offsetParent !== null) {
+                const rect = btn.getBoundingClientRect();
+                // 确保按钮可见且在视口内
+                if (rect.width > 0 && rect.height > 0 && rect.top >= 0 && rect.left >= 0) {
+                  btn.click();
+                  closed = true;
+                  console.log('[closeActivityPopup] Clicked:', selector);
+                }
+              }
+            }
+          } catch (e) {
+            // 忽略无效选择器
+          }
+        }
         
-        for (const btn of baijiahaoClose) {
-          if (btn && btn.offsetParent !== null) {
-            btn.click();
-            closed = true;
-            console.log('[closeActivityPopup] Clicked close button');
+        // 特殊处理:查找包含"关闭"、"知道了"、"确定"文字的按钮
+        const allButtons = document.querySelectorAll('button, [role="button"], a[class*="btn"]');
+        for (const btn of allButtons) {
+          const text = btn.textContent?.trim() || '';
+          if (['关闭', '知道了', '我知道了', '确定', '取消', '跳过', '暂不', '稍后再说'].some(t => text.includes(t))) {
+            if (btn.offsetParent !== null) {
+              const rect = btn.getBoundingClientRect();
+              if (rect.width > 0 && rect.height > 0) {
+                btn.click();
+                closed = true;
+                console.log('[closeActivityPopup] Clicked button with text:', text);
+              }
+            }
           }
         }
         
         // 尝试点击遮罩层外部关闭
         const masks = document.querySelectorAll(
-          '.ant-modal-mask, .el-overlay, [class*="mask"], [class*="overlay"]'
+          '.ant-modal-mask, .el-overlay, [class*="mask"], [class*="overlay"], [class*="backdrop"]'
         );
         for (const mask of masks) {
           const style = window.getComputedStyle(mask);
-          if (style.display !== 'none' && style.visibility !== 'hidden') {
+          if (style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0') {
             // 查找是否有可点击关闭的按钮
-            const closeInMask = mask.querySelector('[class*="close"]');
-            if (closeInMask) {
+            const closeInMask = mask.querySelector('[class*="close"], .close, button');
+            if (closeInMask && closeInMask.offsetParent !== null) {
               closeInMask.click();
               closed = true;
+              console.log('[closeActivityPopup] Clicked close in mask');
             }
           }
         }
         
         // 按 ESC 键尝试关闭
-        document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27 }));
+        document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true }));
+        document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true }));
         
         return closed;
       })()
@@ -2001,7 +2448,7 @@ async function closeActivityPopup(): Promise<boolean> {
     if (result) {
       console.log('[closeActivityPopup] Successfully closed popup');
       // 等待弹框关闭动画
-      await new Promise(resolve => setTimeout(resolve, 500));
+      await new Promise(resolve => setTimeout(resolve, 800));
     }
     return result;
   } catch (error) {
@@ -2012,6 +2459,70 @@ async function closeActivityPopup(): Promise<boolean> {
 
 // 旧的 quickLoginSuccess 函数已被 handleAILoginSuccess 替代
 
+// 视频号:导航到首页获取完整账号信息
+async function navigateToWeixinVideoHomePage(): Promise<boolean> {
+  const webview = webviewRef.value;
+  if (!webview) return false;
+  
+  try {
+    // 获取当前 URL
+    const currentUrl = webview.getURL ? webview.getURL() : (webview.src || '');
+    console.log('[WeixinVideo] Current URL:', currentUrl);
+    
+    // 如果已经在首页,不需要导航
+    if (currentUrl.includes('/platform/home') || currentUrl.includes('/platform/post/list')) {
+      console.log('[WeixinVideo] Already on home page');
+      return true;
+    }
+    
+    // 使用 webview 的 src 属性直接导航(避免 iframe 跨域问题)
+    console.log('[WeixinVideo] Navigating to home page via webview.src...');
+    const targetUrl = 'https://channels.weixin.qq.com/platform/home';
+    
+    // 设置 src 并等待加载完成
+    return new Promise((resolve) => {
+      const onDidFinishLoad = () => {
+        console.log('[WeixinVideo] Page loaded');
+        webview.removeEventListener('did-finish-load', onDidFinishLoad);
+        // 页面加载完成后,等待一段时间让内容渲染
+        setTimeout(async () => {
+          // 再次尝试关闭可能出现的弹窗
+          for (let i = 0; i < 3; i++) {
+            const closed = await closeActivityPopup();
+            if (!closed) break;
+            await new Promise(r => setTimeout(r, 500));
+          }
+          console.log('[WeixinVideo] Navigation complete');
+          resolve(true);
+        }, 2000);
+      };
+      
+      const onDidFailLoad = () => {
+        console.error('[WeixinVideo] Page load failed');
+        webview.removeEventListener('did-fail-load', onDidFailLoad);
+        resolve(false);
+      };
+      
+      webview.addEventListener('did-finish-load', onDidFinishLoad);
+      webview.addEventListener('did-fail-load', onDidFailLoad);
+      
+      // 设置超时
+      setTimeout(() => {
+        webview.removeEventListener('did-finish-load', onDidFinishLoad);
+        webview.removeEventListener('did-fail-load', onDidFailLoad);
+        console.log('[WeixinVideo] Navigation timeout, continuing anyway...');
+        resolve(true);
+      }, 10000);
+      
+      // 执行导航
+      webview.src = targetUrl;
+    });
+  } catch (error) {
+    console.error('[WeixinVideo] Navigation failed:', error);
+    return false;
+  }
+}
+
 // 检查是否有登录 cookie
 function checkHasLoginCookie(cookies: Electron.Cookie[]): boolean {
   // 根据不同平台检查特定的 cookie

+ 6 - 0
client/src/stores/taskQueue.ts

@@ -260,6 +260,12 @@ export const useTaskQueueStore = defineStore('taskQueue', () => {
         break;
     }
     
+    // 处理账号相关事件(直接通过 type 判断)
+    if (data.type === 'account:added' || data.type === 'account:updated' || data.type === 'account:deleted') {
+      console.log('[TaskQueue] Account event received:', data.type);
+      accountRefreshTrigger.value++;
+    }
+    
     // 处理验证码事件
     // 1. 通过 type 判断
     // 2. 或者 payload 中包含 captchaTaskId 字段(兼容 type 为 undefined 的情况)

+ 5 - 0
server/env.example

@@ -13,6 +13,11 @@ NODE_ENV=development
 # 服务端口
 PORT=3000
 
+# 服务监听地址
+# 127.0.0.1 = 仅本地访问(安全,推荐)
+# 0.0.0.0 = 允许局域网/外网访问(需要时再开启)
+HOST=127.0.0.1
+
 # ----------------------------------------
 # 数据库配置 (MySQL)
 # ----------------------------------------

+ 3 - 1
server/python/app.py

@@ -1173,7 +1173,9 @@ def index():
 def main():
     parser = argparse.ArgumentParser(description='多平台视频发布服务')
     parser.add_argument('--port', type=int, default=5005, help='服务端口 (默认: 5005)')
-    parser.add_argument('--host', type=str, default='0.0.0.0', help='监听地址 (默认: 0.0.0.0)')
+    # 从环境变量读取 HOST,默认仅本地访问
+    default_host = os.environ.get('PYTHON_HOST', os.environ.get('HOST', '127.0.0.1'))
+    parser.add_argument('--host', type=str, default=default_host, help='监听地址 (默认: 127.0.0.1,可通过 HOST 环境变量配置)')
     parser.add_argument('--headless', type=str, default='true', help='是否无头模式 (默认: true)')
     parser.add_argument('--debug', action='store_true', help='调试模式')
     

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


+ 40 - 10
server/src/ai/index.ts

@@ -928,17 +928,25 @@ export class QwenAIService {
 - 点击头像可以进入个人主页
 - 侧边栏可能有"我的"或"个人中心"入口`,
       xiaohongshu: `
-小红书平台常见的账号信息位置:
-- 页面左侧或顶部显示创作者信息
-- 侧边栏有"个人中心"或"账号管理"入口`,
+小红书平台常见的账号信息位置和格式:
+- 页面右上角显示账号名称(昵称),通常在头像旁边
+- 账号名称附近或下方会显示"小红书号:xxxxxxx"或"小红书号:xxxxxxx",这是重要的账号ID
+- 小红书号通常是一串字母数字组合,如"ABC123456"
+- "粉丝 X" 或 "粉丝 X/500" 表示粉丝数量
+- 侧边栏有"笔记管理"、"数据看板"等入口
+- 如果看到类似ID格式的字符串(字母+数字组合),很可能就是小红书号`,
       kuaishou: `
 快手平台常见的账号信息位置:
 - 页面顶部显示用户信息
 - 侧边栏有创作者中心入口`,
       weixin_video: `
-视频号平台常见的账号信息位置:
-- 页面左上角显示账号名称
-- 侧边栏有账号设置入口`,
+视频号平台常见的账号信息位置和格式:
+- 页面顶部显示账号名称(如"轻尘网络")
+- 账号名称下方通常显示"视频号ID:xxxxxxx",这个ID是重要信息,请提取冒号后面的字符串
+- "视频 X" 表示作品数量(X是数字)→ 填入 worksCount
+- "关注者 X" 表示粉丝数量(X是数字)→ 填入 fansCount(注意:视频号用"关注者"表示粉丝)
+- 侧边栏有账号设置入口
+- 头像通常是圆形图片,显示在账号名称左侧`,
     };
 
     const platformHint = platformHints[platform] || '';
@@ -953,21 +961,43 @@ export class QwenAIService {
 6. 其他相关信息
 ${platformHint}
 
-重要提示:
+重要提示 - 请务必仔细阅读】
 - 账号名称可能显示在页面顶部、侧边栏、头像旁边等位置
 - 如果看到任何类似用户名或昵称的文字,请提取出来
 - 即使只找到账号名称,也请返回 found: true
 
+【关于数据统计 - 请特别注意区分】:
+- "粉丝"/"粉丝数"/"followers"/"关注者" = 关注该账号的人数 → 填入 fansCount
+- "关注"/"关注数"/"following" = 该账号关注的人数 → 这是关注数,不要填入任何字段
+- "作品"/"作品数"/"视频"/"笔记"/"文章"/"posts"/"videos" = 该账号发布的内容数量 → 填入 worksCount
+- "获赞"/"点赞"/"likes" = 获得的点赞数 → 不要填入作品数
+
+【各平台特殊说明】:
+- 视频号:页面上"关注者 X"中的X是粉丝数,"视频 X"中的X是作品数,"视频号ID:xxx"中冒号后的xxx是账号ID
+- 抖音:页面上"抖音号:xxx"或"抖音号:xxx"中冒号后的字符串是账号ID
+- 小红书:页面上"小红书号:xxx"或"小红书号:xxx"中冒号后的字符串是账号ID(通常是字母数字组合如ABC123456)
+
+【关于弹窗遮挡】:
+- 如果页面有弹窗、对话框、遮罩层等遮挡了主要内容,请在返回结果中说明
+- 如果因为遮挡无法看清账号信息,请设置 found: false,并在 navigationGuide 中说明"页面有弹窗遮挡,请关闭弹窗后重试"
+
+【常见错误 - 请避免】:
+- ❌ 不要把"关注数"(following)当成"作品数"
+- ❌ 不要把"获赞数"当成"作品数"
+- ✅ 作品数通常标注为"作品"、"视频"、"笔记"等
+- ✅ 视频号的"关注者"就是粉丝数
+- 如果页面上没有明确显示作品数量,请返回 worksCount: null
+
 如果当前页面确实没有显示任何账号信息,请告诉我应该如何操作才能看到账号信息。
 
 请严格按照以下JSON格式返回:
 {
   "found": true或false(是否找到账号信息,只要找到账号名称就算找到),
   "accountName": "账号名称/昵称,如果找不到则为null",
-  "accountId": "账号ID,如果找不到则为null",
+  "accountId": "账号ID(如视频号ID:xxx中的xxx、抖音号:xxx中的xxx、小红书号:xxx中的xxx),如果找不到则为null",
   "avatarDescription": "头像描述,如果看不到则为null",
-  "fansCount": "粉丝数量(数字),如果看不到则为null",
-  "worksCount": "作品数量(数字),如果看不到则为null",
+  "fansCount": "粉丝数量(数字,如'关注者 1'则填1,'粉丝 100'则填100),如果看不到则为null",
+  "worksCount": "作品数量(数字,如'视频 4'则填4,'作品 10'则填10),如果看不到或不确定则为null",
   "otherInfo": "其他相关信息,如果没有则为null",
   "navigationGuide": "如果没找到账号信息,请描述具体的操作步骤(如:点击左侧菜单的'个人中心'),如果已找到则为null"
 }`;

+ 2 - 2
server/src/app.ts

@@ -233,8 +233,8 @@ async function bootstrap() {
   setupBrowserLoginEvents();
 
   // 启动 HTTP 服务
-  httpServer.listen(config.port, () => {
-    logger.info(`Server running on port ${config.port}`);
+  httpServer.listen(config.port, config.host, () => {
+    logger.info(`Server running on http://${config.host}:${config.port}`);
     logger.info(`Environment: ${config.env}`);
     if (!dbConnected) {
       logger.warn('⚠️  Running without database - API endpoints will not work');

+ 169 - 11
server/src/automation/platforms/weixin.ts

@@ -180,28 +180,186 @@ export class WeixinAdapter extends BasePlatformAdapter {
       
       if (!this.page) throw new Error('Page not initialized');
       
-      await this.page.goto(this.loginUrl);
+      // 访问视频号创作者平台首页
+      await this.page.goto('https://channels.weixin.qq.com/platform/home');
       await this.page.waitForLoadState('networkidle');
+      await this.page.waitForTimeout(2000);
       
-      // 获取账号信息
-      const accountId = await this.page.$eval('span.finder-uniq-id', el => el.textContent?.trim() || '').catch(() => '');
-      const accountName = await this.page.$eval('h2.finder-nickname', el => el.textContent?.trim() || '').catch(() => '');
-      const avatarUrl = await this.page.$eval('img.avatar', el => el.getAttribute('src') || '').catch(() => '');
+      // 从页面提取账号信息
+      const accountData = await this.page.evaluate(() => {
+        const result: { accountId?: string; accountName?: string; avatarUrl?: string; fansCount?: number; worksCount?: number } = {};
+        
+        try {
+          // ===== 1. 优先使用精确选择器获取视频号 ID =====
+          // 方法1: 通过 #finder-uid-copy 的 data-clipboard-text 属性获取
+          const finderIdCopyEl = document.querySelector('#finder-uid-copy');
+          if (finderIdCopyEl) {
+            const clipboardText = finderIdCopyEl.getAttribute('data-clipboard-text');
+            if (clipboardText && clipboardText.length >= 10) {
+              result.accountId = clipboardText;
+              console.log('[WeixinVideo] Found finder ID from data-clipboard-text:', result.accountId);
+            } else {
+              const text = finderIdCopyEl.textContent?.trim();
+              if (text && /^[a-zA-Z0-9_]+$/.test(text) && text.length >= 10) {
+                result.accountId = text;
+                console.log('[WeixinVideo] Found finder ID from #finder-uid-copy text:', result.accountId);
+              }
+            }
+          }
+          
+          // 方法2: 通过 .finder-uniq-id 选择器获取
+          if (!result.accountId) {
+            const finderUniqIdEl = document.querySelector('.finder-uniq-id');
+            if (finderUniqIdEl) {
+              const clipboardText = finderUniqIdEl.getAttribute('data-clipboard-text');
+              if (clipboardText && clipboardText.length >= 10) {
+                result.accountId = clipboardText;
+                console.log('[WeixinVideo] Found finder ID from .finder-uniq-id data-clipboard-text:', result.accountId);
+              } else {
+                const text = finderUniqIdEl.textContent?.trim();
+                if (text && /^[a-zA-Z0-9_]+$/.test(text) && text.length >= 10) {
+                  result.accountId = text;
+                  console.log('[WeixinVideo] Found finder ID from .finder-uniq-id text:', result.accountId);
+                }
+              }
+            }
+          }
+          
+          // 方法3: 从页面文本中正则匹配
+          if (!result.accountId) {
+            const bodyText = document.body.innerText || '';
+            const finderIdPatterns = [
+              /视频号ID[::\s]*([a-zA-Z0-9_]+)/,
+              /视频号[::\s]*ID[::\s]*([a-zA-Z0-9_]+)/,
+            ];
+            for (const pattern of finderIdPatterns) {
+              const match = bodyText.match(pattern);
+              if (match && match[1] && match[1].length >= 10) {
+                result.accountId = match[1];
+                console.log('[WeixinVideo] Found finder ID from regex:', result.accountId);
+                break;
+              }
+            }
+          }
+          
+          // ===== 2. 获取账号名称 =====
+          const nicknameEl = document.querySelector('h2.finder-nickname') || 
+                            document.querySelector('.finder-nickname');
+          if (nicknameEl) {
+            const text = nicknameEl.textContent?.trim();
+            if (text && text.length >= 2 && text.length <= 30) {
+              result.accountName = text;
+              console.log('[WeixinVideo] Found name:', result.accountName);
+            }
+          }
+          
+          // ===== 3. 获取头像 =====
+          const avatarEl = document.querySelector('img.avatar') as HTMLImageElement;
+          if (avatarEl?.src && avatarEl.src.startsWith('http')) {
+            result.avatarUrl = avatarEl.src;
+          } else {
+            const altAvatarEl = document.querySelector('img[alt="视频号头像"]') as HTMLImageElement;
+            if (altAvatarEl?.src && altAvatarEl.src.startsWith('http')) {
+              result.avatarUrl = altAvatarEl.src;
+            }
+          }
+          
+          // ===== 4. 获取视频数和关注者数 =====
+          const contentInfo = document.querySelector('.finder-content-info');
+          if (contentInfo) {
+            const infoDivs = contentInfo.querySelectorAll('div');
+            infoDivs.forEach(div => {
+              const text = div.textContent || '';
+              const numEl = div.querySelector('.finder-info-num');
+              if (numEl) {
+                const num = parseInt(numEl.textContent?.trim() || '0', 10);
+                if (text.includes('视频') || text.includes('作品')) {
+                  result.worksCount = num;
+                } else if (text.includes('关注者') || text.includes('粉丝')) {
+                  result.fansCount = num;
+                }
+              }
+            });
+          }
+          
+          // 备选:从页面整体文本中匹配
+          if (result.fansCount === undefined || result.worksCount === undefined) {
+            const bodyText = document.body.innerText || '';
+            if (result.fansCount === undefined) {
+              const fansMatch = bodyText.match(/关注者\s*(\d+(?:\.\d+)?[万wW]?)/);
+              if (fansMatch) {
+                let count = parseFloat(fansMatch[1]);
+                if (fansMatch[1].includes('万') || fansMatch[1].toLowerCase().includes('w')) {
+                  count = count * 10000;
+                }
+                result.fansCount = Math.floor(count);
+              }
+            }
+            if (result.worksCount === undefined) {
+              const worksMatch = bodyText.match(/视频\s*(\d+)/);
+              if (worksMatch) {
+                result.worksCount = parseInt(worksMatch[1], 10);
+              }
+            }
+          }
+        } catch (e) {
+          console.error('[WeixinVideo] Extract error:', e);
+        }
+        
+        return result;
+      });
+      
+      logger.info('[Weixin] Extracted account data:', accountData);
+      
+      // 如果首页没有获取到视频号 ID,尝试访问账号设置页面
+      let finalAccountId = accountData.accountId;
+      if (!finalAccountId || finalAccountId.length < 10) {
+        logger.info('[Weixin] Finder ID not found on home page, trying account settings page...');
+        
+        try {
+          await this.page.goto('https://channels.weixin.qq.com/platform/account');
+          await this.page.waitForLoadState('networkidle');
+          await this.page.waitForTimeout(2000);
+          
+          const settingsId = await this.page.evaluate(() => {
+            const bodyText = document.body.innerText || '';
+            const patterns = [
+              /视频号ID[::\s]*([a-zA-Z0-9_]+)/,
+              /视频号[::\s]*ID[::\s]*([a-zA-Z0-9_]+)/,
+              /唯一标识[::\s]*([a-zA-Z0-9_]+)/,
+            ];
+            for (const pattern of patterns) {
+              const match = bodyText.match(pattern);
+              if (match && match[1]) {
+                return match[1];
+              }
+            }
+            return null;
+          });
+          
+          if (settingsId) {
+            finalAccountId = settingsId;
+            logger.info('[Weixin] Found finder ID from settings page:', finalAccountId);
+          }
+        } catch (e) {
+          logger.warn('[Weixin] Failed to fetch from settings page:', e);
+        }
+      }
       
       await this.closeBrowser();
       
       return {
-        accountId: accountId || `weixin_${Date.now()}`,
-        accountName: accountName || '视频号账号',
-        avatarUrl,
-        fansCount: 0,
-        worksCount: 0,
+        accountId: finalAccountId || `weixin_video_${Date.now()}`,
+        accountName: accountData.accountName || '视频号账号',
+        avatarUrl: accountData.avatarUrl || '',
+        fansCount: accountData.fansCount || 0,
+        worksCount: accountData.worksCount || 0,
       };
     } catch (error) {
       logger.error('Weixin getAccountInfo error:', error);
       await this.closeBrowser();
       return {
-        accountId: `weixin_${Date.now()}`,
+        accountId: `weixin_video_${Date.now()}`,
         accountName: '视频号账号',
         avatarUrl: '',
         fansCount: 0,

+ 2 - 0
server/src/config/index.ts

@@ -10,6 +10,8 @@ export const config = {
   env: process.env.NODE_ENV || 'development',
   version: process.env.npm_package_version || '1.0.0',
   port: parseInt(process.env.PORT || '3000', 10),
+  // 服务监听地址:127.0.0.1 仅本地访问,0.0.0.0 允许局域网访问
+  host: process.env.HOST || '127.0.0.1',
 
   // 数据库配置
   database: {

+ 9 - 13
server/src/scheduler/index.ts

@@ -16,32 +16,28 @@ export class TaskScheduler {
   
   /**
    * 启动调度器
+   * 
+   * 注意:账号刷新任务由客户端定时触发,只刷新当前登录用户的账号
+   * 服务端不再自动刷新所有用户的账号
    */
   start(): void {
     logger.info('[Scheduler] ========================================');
     logger.info('[Scheduler] Starting task scheduler...');
     
-    // 每分钟检查定时发布任务
+    // 每分钟检查定时发布任务(只处理到期的定时发布任务)
     this.scheduleJob('check-publish-tasks', '* * * * *', this.checkPublishTasks.bind(this));
     
-    // 每15分钟刷新账号状态
-    this.scheduleJob('refresh-accounts', '*/15 * * * *', this.refreshAccounts.bind(this));
+    // 注意:账号刷新由客户端定时触发,不在服务端自动执行
+    // 这样可以确保只刷新当前登录用户的账号,避免处理其他用户的数据
     
-    // 每天凌晨2点采集数据
-    this.scheduleJob('collect-analytics', '0 2 * * *', this.collectAnalytics.bind(this));
+    // 每天凌晨2点采集数据统计(可选,如果需要服务端采集可以启用)
+    // this.scheduleJob('collect-analytics', '0 2 * * *', this.collectAnalytics.bind(this));
     
     logger.info('[Scheduler] Scheduled jobs:');
     logger.info('[Scheduler]   - check-publish-tasks: every minute (* * * * *)');
-    logger.info('[Scheduler]   - refresh-accounts: every 15 minutes (*/15 * * * *)');
-    logger.info('[Scheduler]   - collect-analytics: daily at 2:00 AM (0 2 * * *)');
+    logger.info('[Scheduler] Note: Account refresh is triggered by client, not server');
     logger.info('[Scheduler] ========================================');
     
-    // 服务器启动时立即刷新一次账号状态
-    setTimeout(() => {
-      logger.info('[Scheduler] Running initial account refresh on startup...');
-      this.refreshAccounts().catch(err => logger.error('[Scheduler] Initial refresh failed:', err));
-    }, 10000); // 延迟10秒,等待其他服务初始化完成
-    
     logger.info('[Scheduler] Task scheduler started successfully');
   }
   

+ 246 - 40
server/src/services/BrowserLoginService.ts

@@ -390,6 +390,7 @@ class BrowserLoginService extends EventEmitter {
 
   /**
    * 处理登录成功
+   * 直接从当前登录成功的页面提取账号信息,不再刷新页面
    */
   private async handleLoginSuccess(sessionId: string): Promise<void> {
     const session = this.sessions.get(sessionId);
@@ -400,7 +401,7 @@ class BrowserLoginService extends EventEmitter {
       session.aiAssistant?.stopMonitoring();
 
       // 等待页面完全稳定
-      logger.info(`Waiting for page to stabilize for session: ${sessionId}`);
+      logger.info(`[BrowserLogin] Waiting for page to stabilize for session: ${sessionId}`);
 
       // 等待网络请求完成
       try {
@@ -411,7 +412,7 @@ class BrowserLoginService extends EventEmitter {
 
       // 额外等待确保所有数据加载
       await session.page.waitForTimeout(2000);
-      logger.info(`Processing login success for session: ${sessionId}`);
+      logger.info(`[BrowserLogin] Processing login success for session: ${sessionId}`);
 
       // 获取所有 Cookie
       const cookies = await session.context.cookies();
@@ -426,50 +427,52 @@ class BrowserLoginService extends EventEmitter {
         message: '登录成功,正在获取账号信息...',
       });
 
-      // 首先尝试使用 AI 在当前页面获取账号信息
-      let accountInfo: AccountInfo | null = null;
+      // 直接从当前页面提取账号信息(不刷新页面)
+      logger.info(`[BrowserLogin] Extracting account info from current page for session: ${sessionId}`);
+      let accountInfo = await this.extractAccountInfoFromCurrentPage(session.page, session.platform);
       
-      if (session.aiAssistant && aiService.isAvailable()) {
-        logger.info(`[BrowserLogin] Trying to get account info with AI for session: ${sessionId}`);
-        const aiResult = await session.aiAssistant.getAccountInfoWithAI(3);
+      // 如果从页面提取失败,尝试使用 AI
+      if (!accountInfo || !accountInfo.accountName) {
+        logger.info(`[BrowserLogin] Page extraction incomplete, trying AI for session: ${sessionId}`);
         
-        if (aiResult.success && aiResult.accountInfo) {
-          logger.info(`[BrowserLogin] AI successfully extracted account info:`, aiResult.accountInfo);
+        if (session.aiAssistant && aiService.isAvailable()) {
+          const aiResult = await session.aiAssistant.getAccountInfoWithAI(3);
           
-          // 将 AI 提取的信息转换为 AccountInfo 格式
-          accountInfo = {
-            accountId: aiResult.accountInfo.accountId || '',
-            accountName: aiResult.accountInfo.accountName || '',
-            avatarUrl: '', // AI 无法直接提取 URL
-            fansCount: this.parseNumberString(aiResult.accountInfo.fansCount),
-            worksCount: this.parseNumberString(aiResult.accountInfo.worksCount),
-          };
-        } else if (aiResult.needNavigation && aiResult.navigationGuide) {
-          logger.info(`[BrowserLogin] AI suggests navigation: ${aiResult.navigationGuide.explanation}`);
-          // 发送导航建议给前端
-          this.emit('navigationSuggestion', {
-            sessionId,
-            guide: aiResult.navigationGuide,
-          });
+          if (aiResult.success && aiResult.accountInfo) {
+            logger.info(`[BrowserLogin] AI successfully extracted account info:`, aiResult.accountInfo);
+            
+            accountInfo = {
+              accountId: aiResult.accountInfo.accountId || accountInfo?.accountId || '',
+              accountName: aiResult.accountInfo.accountName || accountInfo?.accountName || '',
+              avatarUrl: accountInfo?.avatarUrl || '', // 保留页面提取的头像
+              fansCount: this.parseNumberString(aiResult.accountInfo.fansCount) || accountInfo?.fansCount || 0,
+              worksCount: this.parseNumberString(aiResult.accountInfo.worksCount) || accountInfo?.worksCount || 0,
+            };
+          }
         }
       }
-
-      // 如果 AI 没有获取到足够信息,关闭浏览器并使用无头浏览器获取
-      if (!accountInfo || !accountInfo.accountName) {
-        logger.info(`[BrowserLogin] AI info incomplete, closing browser and using headless for session: ${sessionId}`);
+      
+      // 如果仍然没有获取到,使用默认值
+      if (!accountInfo) {
+        // 生成带平台前缀的默认 accountId
+        const defaultPrefix = (session.platform === 'weixin' || session.platform === 'weixin_video') 
+          ? 'weixin_video' 
+          : session.platform;
         
-        // 关闭可见浏览器窗口
-        await this.closeSession(sessionId);
-
-        // 使用无头浏览器后台获取账号信息
-        logger.info(`Fetching account info with headless browser for session: ${sessionId}`);
-        accountInfo = await this.fetchAccountInfoHeadless(session.platform, cookies);
-      } else {
-        // AI 获取成功,关闭浏览器
-        logger.info(`[BrowserLogin] Closing visible browser for session: ${sessionId}`);
-        await this.closeSession(sessionId);
+        accountInfo = {
+          accountId: `${defaultPrefix}_${Date.now()}`,
+          accountName: `${session.platform}账号`,
+          avatarUrl: '',
+          fansCount: 0,
+          worksCount: 0,
+        };
       }
 
+      logger.info(`[BrowserLogin] Final account info:`, accountInfo);
+
+      // 关闭浏览器
+      await this.closeSession(sessionId);
+
       // 加密存储
       const encryptedCookies = CookieManager.encrypt(cookieString);
 
@@ -477,7 +480,7 @@ class BrowserLoginService extends EventEmitter {
       session.cookies = encryptedCookies;
       session.accountInfo = accountInfo;
 
-      logger.info(`Login success for session: ${sessionId}, account: ${accountInfo.accountName}`);
+      logger.info(`[BrowserLogin] Login success for session: ${sessionId}, account: ${accountInfo.accountName}`);
 
       // 发送事件
       this.emit('loginResult', {
@@ -489,12 +492,215 @@ class BrowserLoginService extends EventEmitter {
         rawCookies: cookies,
       });
     } catch (error) {
-      logger.error(`Error handling login success for ${sessionId}:`, error);
+      logger.error(`[BrowserLogin] Error handling login success for ${sessionId}:`, error);
       session.status = 'failed';
       session.error = String(error);
       this.emit('loginResult', { sessionId, userId: session.userId, status: 'failed', error: String(error) });
     }
   }
+  
+  /**
+   * 直接从当前页面提取账号信息(不刷新页面)
+   */
+  private async extractAccountInfoFromCurrentPage(page: Page, platform: PlatformType): Promise<AccountInfo | null> {
+    try {
+      logger.info(`[BrowserLogin] Extracting account info from current page for platform: ${platform}`);
+      
+      const accountData = await page.evaluate((platformType: string) => {
+        const result: { 
+          accountId?: string; 
+          accountName?: string; 
+          avatarUrl?: string; 
+          fansCount?: number; 
+          worksCount?: number;
+        } = {};
+        
+        try {
+          // ===== 视频号平台的选择器 =====
+          if (platformType === 'weixin' || platformType === 'weixin_video') {
+            // 1. 获取视频号 ID
+            // 优先从 #finder-uid-copy 的 data-clipboard-text 属性获取
+            const finderIdCopyEl = document.querySelector('#finder-uid-copy');
+            if (finderIdCopyEl) {
+              const clipboardText = finderIdCopyEl.getAttribute('data-clipboard-text');
+              if (clipboardText && clipboardText.length >= 10) {
+                result.accountId = clipboardText;
+              } else {
+                const text = finderIdCopyEl.textContent?.trim();
+                if (text && /^[a-zA-Z0-9_]+$/.test(text) && text.length >= 10) {
+                  result.accountId = text;
+                }
+              }
+            }
+            
+            // 备选:从 .finder-uniq-id 获取
+            if (!result.accountId) {
+              const finderUniqIdEl = document.querySelector('.finder-uniq-id');
+              if (finderUniqIdEl) {
+                const clipboardText = finderUniqIdEl.getAttribute('data-clipboard-text');
+                if (clipboardText && clipboardText.length >= 10) {
+                  result.accountId = clipboardText;
+                } else {
+                  const text = finderUniqIdEl.textContent?.trim();
+                  if (text && /^[a-zA-Z0-9_]+$/.test(text) && text.length >= 10) {
+                    result.accountId = text;
+                  }
+                }
+              }
+            }
+            
+            // 2. 获取账号名称
+            const nicknameEl = document.querySelector('h2.finder-nickname') || 
+                              document.querySelector('.finder-nickname');
+            if (nicknameEl) {
+              const text = nicknameEl.textContent?.trim();
+              if (text && text.length >= 2 && text.length <= 30) {
+                result.accountName = text;
+              }
+            }
+            
+            // 3. 获取头像 URL
+            // 优先使用 img.avatar
+            const avatarEl = document.querySelector('img.avatar') as HTMLImageElement;
+            if (avatarEl?.src && avatarEl.src.startsWith('http')) {
+              result.avatarUrl = avatarEl.src;
+            } else {
+              // 备选选择器
+              const avatarSelectors = [
+                'img[alt="视频号头像"]',
+                '.finder-info-container img.avatar',
+                'img[src*="wx.qlogo.cn/finderhead"]',
+                'img[src*="wx.qlogo"]',
+                '.avatar img',
+                '[class*="avatar"] img',
+              ];
+              for (const selector of avatarSelectors) {
+                const el = document.querySelector(selector) as HTMLImageElement;
+                if (el?.src && el.src.startsWith('http')) {
+                  result.avatarUrl = el.src;
+                  break;
+                }
+              }
+            }
+            
+            // 4. 获取视频数和关注者数
+            const contentInfo = document.querySelector('.finder-content-info');
+            if (contentInfo) {
+              const infoDivs = contentInfo.querySelectorAll('div');
+              infoDivs.forEach(div => {
+                const text = div.textContent || '';
+                const numEl = div.querySelector('.finder-info-num');
+                if (numEl) {
+                  const num = parseInt(numEl.textContent?.trim() || '0', 10);
+                  if (text.includes('视频') || text.includes('作品')) {
+                    result.worksCount = num;
+                  } else if (text.includes('关注者') || text.includes('粉丝')) {
+                    result.fansCount = num;
+                  }
+                }
+              });
+            }
+            
+            // 备选:从页面文本匹配
+            if (result.fansCount === undefined || result.worksCount === undefined) {
+              const bodyText = document.body.innerText || '';
+              if (result.fansCount === undefined) {
+                const fansMatch = bodyText.match(/关注者\s*(\d+)/) || bodyText.match(/粉丝\s*(\d+)/);
+                if (fansMatch) {
+                  result.fansCount = parseInt(fansMatch[1], 10);
+                }
+              }
+              if (result.worksCount === undefined) {
+                const worksMatch = bodyText.match(/视频\s*(\d+)/) || bodyText.match(/作品\s*(\d+)/);
+                if (worksMatch) {
+                  result.worksCount = parseInt(worksMatch[1], 10);
+                }
+              }
+            }
+          }
+          
+          // ===== 其他平台的选择器(抖音、小红书等) =====
+          // 通用选择器
+          if (!result.accountName) {
+            const nameSelectors = [
+              '.user-name', '.nickname', '[class*="nickname"]', '[class*="userName"]',
+              '.account-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.accountName = text;
+                break;
+              }
+            }
+          }
+          
+          if (!result.avatarUrl) {
+            const avatarSelectors = [
+              'img[class*="avatar"]', '[class*="avatar"] img', 'img.avatar',
+            ];
+            for (const selector of avatarSelectors) {
+              const el = document.querySelector(selector) as HTMLImageElement;
+              if (el?.src && el.src.startsWith('http')) {
+                result.avatarUrl = el.src;
+                break;
+              }
+            }
+          }
+          
+        } catch (e) {
+          console.error('[BrowserLogin] Extract error:', e);
+        }
+        
+        return result;
+      }, platform);
+      
+      logger.info(`[BrowserLogin] Extracted from current page:`, accountData);
+      
+      if (accountData && (accountData.accountName || accountData.accountId)) {
+        // 生成统一格式的 accountId:前缀 + 原始ID
+        let finalAccountId: string;
+        const rawId = accountData.accountId;
+        
+        if (rawId) {
+          // 根据平台添加对应前缀
+          if (platform === 'weixin' || platform === 'weixin_video') {
+            // 视频号:weixin_video_ + 视频号ID
+            finalAccountId = rawId.startsWith('weixin_video_') ? rawId : `weixin_video_${rawId}`;
+          } else if (platform === 'douyin') {
+            finalAccountId = rawId.startsWith('douyin_') ? rawId : `douyin_${rawId}`;
+          } else if (platform === 'xiaohongshu') {
+            finalAccountId = rawId.startsWith('xiaohongshu_') ? rawId : `xiaohongshu_${rawId}`;
+          } else if (platform === 'kuaishou') {
+            finalAccountId = rawId.startsWith('kuaishou_') ? rawId : `kuaishou_${rawId}`;
+          } else if (platform === 'bilibili') {
+            finalAccountId = rawId.startsWith('bilibili_') ? rawId : `bilibili_${rawId}`;
+          } else {
+            finalAccountId = rawId.startsWith(`${platform}_`) ? rawId : `${platform}_${rawId}`;
+          }
+        } else {
+          // 没有获取到ID,使用时间戳
+          finalAccountId = `${platform}_${Date.now()}`;
+        }
+        
+        logger.info(`[BrowserLogin] Final accountId: ${finalAccountId}`);
+        
+        return {
+          accountId: finalAccountId,
+          accountName: accountData.accountName || `${platform}账号`,
+          avatarUrl: accountData.avatarUrl || '',
+          fansCount: accountData.fansCount || 0,
+          worksCount: accountData.worksCount || 0,
+        };
+      }
+      
+      return null;
+    } catch (error) {
+      logger.error(`[BrowserLogin] Failed to extract account info from current page:`, error);
+      return null;
+    }
+  }
 
   /**
    * 解析数字字符串(支持中文单位)

+ 247 - 88
server/src/services/HeadlessBrowserService.ts

@@ -1130,12 +1130,26 @@ class HeadlessBrowserService {
     let avatarUrl = '';
     let fansCount = 0;
     let worksCount = 0;
+    let finderId = '';
 
     try {
       // 从 Cookie 中提取用户标识
-      const uinCookie = cookies.find(c => c.name === 'wxuin' || c.name === 'uin');
-      if (uinCookie?.value) {
-        accountId = `weixin_video_${uinCookie.value}`;
+      // 优先使用 finder_username(视频号唯一标识)
+      const finderUsernameCookie = cookies.find(c => c.name === 'finder_username');
+      if (finderUsernameCookie?.value) {
+        finderId = finderUsernameCookie.value;
+        accountId = `weixin_video_${finderId}`;
+        logger.info(`[WeixinVideo] Found finder_username from cookie: ${finderId}`);
+      }
+
+      // 备选:使用 wxuin 或 uin
+      if (!finderId) {
+        const uinCookie = cookies.find(c => c.name === 'wxuin' || c.name === 'uin');
+        if (uinCookie?.value) {
+          // 仍然保持时间戳格式,后续会尝试从页面获取真实 ID
+          accountId = `weixin_video_${uinCookie.value}`;
+          logger.info(`[WeixinVideo] Using uin from cookie: ${uinCookie.value}`);
+        }
       }
 
       // 访问视频号创作者平台首页
@@ -1158,100 +1172,159 @@ class HeadlessBrowserService {
         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;
+          // ===== 1. 优先使用精确选择器获取视频号 ID =====
+          // 方法1: 通过 #finder-uid-copy 的 data-clipboard-text 属性获取
+          const finderIdCopyEl = document.querySelector('#finder-uid-copy');
+          if (finderIdCopyEl) {
+            const clipboardText = finderIdCopyEl.getAttribute('data-clipboard-text');
+            if (clipboardText && clipboardText.length >= 10) {
+              result.finderId = clipboardText;
+              console.log('[WeixinVideo] Found finder ID from data-clipboard-text:', result.finderId);
+            } else {
+              // 备选:获取元素文本内容
+              const text = finderIdCopyEl.textContent?.trim();
+              if (text && /^[a-zA-Z0-9_]+$/.test(text) && text.length >= 10) {
+                result.finderId = text;
+                console.log('[WeixinVideo] Found finder ID from #finder-uid-copy text:', result.finderId);
+              }
             }
           }
-
-          // 查找用户名
-          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();
+          
+          // 方法2: 通过 .finder-uniq-id 选择器获取
+          if (!result.finderId) {
+            const finderUniqIdEl = document.querySelector('.finder-uniq-id');
+            if (finderUniqIdEl) {
+              const clipboardText = finderUniqIdEl.getAttribute('data-clipboard-text');
+              if (clipboardText && clipboardText.length >= 10) {
+                result.finderId = clipboardText;
+                console.log('[WeixinVideo] Found finder ID from .finder-uniq-id data-clipboard-text:', result.finderId);
+              } else {
+                const text = finderUniqIdEl.textContent?.trim();
+                if (text && /^[a-zA-Z0-9_]+$/.test(text) && text.length >= 10) {
+                  result.finderId = text;
+                  console.log('[WeixinVideo] Found finder ID from .finder-uniq-id text:', result.finderId);
+                }
+              }
+            }
+          }
+          
+          // 方法3: 从页面文本中正则匹配
+          if (!result.finderId) {
+            const bodyText = document.body.innerText || '';
+            const finderIdPatterns = [
+              /视频号ID[::\s]*([a-zA-Z0-9_]+)/,
+              /视频号[::\s]*ID[::\s]*([a-zA-Z0-9_]+)/,
+            ];
+            for (const pattern of finderIdPatterns) {
+              const match = bodyText.match(pattern);
+              if (match && match[1] && match[1].length >= 10) {
+                result.finderId = match[1];
+                console.log('[WeixinVideo] Found finder ID from regex:', result.finderId);
+                break;
+              }
+            }
+          }
+          
+          // ===== 2. 获取账号名称 =====
+          // 优先使用 h2.finder-nickname
+          const nicknameEl = document.querySelector('h2.finder-nickname') || 
+                            document.querySelector('.finder-nickname');
+          if (nicknameEl) {
+            const text = nicknameEl.textContent?.trim();
             if (text && text.length >= 2 && text.length <= 30) {
               result.name = text;
-              console.log('[WeixinVideo] Found name:', text);
-              break;
+              console.log('[WeixinVideo] Found name from .finder-nickname:', result.name);
             }
           }
-
-          // 查找视频号 ID
-          const bodyText = document.body.innerText || '';
-          const finderIdMatch = bodyText.match(/视频号ID[::]\s*([a-zA-Z0-9_]+)/);
-          if (finderIdMatch) {
-            result.finderId = finderIdMatch[1];
+          
+          // 备选选择器
+          if (!result.name) {
+            const nameSelectors = [
+              '.account-name',
+              '[class*="nickname"]',
+              '[class*="userName"]',
+            ];
+            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 from selector:', selector, result.name);
+                break;
+              }
+            }
           }
 
-          // 尝试从页面文本中提取粉丝数和作品数
-          const statsTexts = document.querySelectorAll('[class*="stat"], [class*="count"], [class*="number"]');
-          statsTexts.forEach(el => {
-            const text = el.textContent || '';
-            const parent = el.parentElement?.textContent || '';
+          // ===== 3. 获取头像 =====
+          // 优先使用 img.avatar
+          const avatarEl = document.querySelector('img.avatar') as HTMLImageElement;
+          if (avatarEl?.src && avatarEl.src.startsWith('http')) {
+            result.avatar = avatarEl.src;
+            console.log('[WeixinVideo] Found avatar from img.avatar:', result.avatar);
+          }
+          
+          // 备选选择器
+          if (!result.avatar) {
+            const avatarSelectors = [
+              '.finder-info-container img.avatar',
+              'img[alt="视频号头像"]',
+              'img[src*="wx.qlogo.cn/finderhead"]',
+              'img[src*="wx.qlogo"]',
+            ];
+            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 from selector:', selector);
+                break;
+              }
+            }
+          }
 
-            // 粉丝数
-            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')) {
+          // ===== 4. 获取视频数和关注者数 =====
+          // 使用 .finder-content-info 中的 .finder-info-num
+          const contentInfo = document.querySelector('.finder-content-info');
+          if (contentInfo) {
+            const infoDivs = contentInfo.querySelectorAll('div');
+            infoDivs.forEach(div => {
+              const text = div.textContent || '';
+              const numEl = div.querySelector('.finder-info-num');
+              if (numEl) {
+                const num = parseInt(numEl.textContent?.trim() || '0', 10);
+                if (text.includes('视频') || text.includes('作品')) {
+                  result.works = num;
+                  console.log('[WeixinVideo] Found works from .finder-info-num:', result.works);
+                } else if (text.includes('关注者') || text.includes('粉丝')) {
+                  result.fans = num;
+                  console.log('[WeixinVideo] Found fans from .finder-info-num:', result.fans);
+                }
+              }
+            });
+          }
+          
+          // 备选:从页面整体文本中匹配
+          if (result.fans === undefined || result.works === undefined) {
+            const bodyText = document.body.innerText || '';
+            
+            if (result.fans === undefined) {
+              const fansMatch = bodyText.match(/关注者\s*(\d+(?:\.\d+)?[万wW]?)/) ||
+                               bodyText.match(/粉丝\s*(\d+(?:\.\d+)?[万wW]?)/);
+              if (fansMatch) {
+                let count = parseFloat(fansMatch[1]);
+                if (fansMatch[1].includes('万') || fansMatch[1].toLowerCase().includes('w')) {
                   count = count * 10000;
                 }
                 result.fans = Math.floor(count);
+                console.log('[WeixinVideo] Found fans from text:', result.fans);
               }
             }
-
-            // 作品数
-            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;
+            
+            if (result.works === undefined) {
+              const worksMatch = bodyText.match(/视频\s*(\d+)/) ||
+                                bodyText.match(/作品\s*(\d+)/);
+              if (worksMatch) {
+                result.works = parseInt(worksMatch[1], 10);
+                console.log('[WeixinVideo] Found works from text:', result.works);
               }
             }
           }
@@ -1263,7 +1336,7 @@ class HeadlessBrowserService {
         return result;
       });
 
-      logger.info(`[WeixinVideo] Extracted account data:`, accountData);
+      logger.info(`[WeixinVideo] Extracted account data from home page:`, accountData);
 
       // 更新账号信息
       if (accountData.name) {
@@ -1279,10 +1352,82 @@ class HeadlessBrowserService {
         worksCount = accountData.works;
       }
       if (accountData.finderId) {
+        finderId = accountData.finderId;
         accountId = `weixin_video_${accountData.finderId}`;
       }
 
-      logger.info(`[WeixinVideo] Account info: id=${accountId}, name=${accountName}, avatar=${avatarUrl ? 'yes' : 'no'}, fans=${fansCount}`);
+      // 如果首页没有获取到视频号 ID,尝试访问账号设置页面
+      if (!finderId || finderId.length < 10) {
+        logger.info('[WeixinVideo] Finder ID not found on home page, trying account settings page...');
+        
+        try {
+          // 访问账号设置页面
+          await page.goto('https://channels.weixin.qq.com/platform/account', {
+            waitUntil: 'domcontentloaded',
+            timeout: 30000,
+          });
+          await page.waitForTimeout(2000);
+          
+          // 从账号设置页面提取视频号 ID
+          const settingsData = await page.evaluate(() => {
+            const result: { finderId?: string; name?: string } = {};
+            const bodyText = document.body.innerText || '';
+            
+            // 尝试多种匹配模式
+            const patterns = [
+              /视频号ID[::\s]*([a-zA-Z0-9_]+)/,
+              /视频号[::\s]*ID[::\s]*([a-zA-Z0-9_]+)/,
+              /视频号[::\s]+([a-zA-Z0-9_]{10,})/,
+              /Finder\s*ID[::\s]*([a-zA-Z0-9_]+)/i,
+              /finder_username[::\s]*([a-zA-Z0-9_]+)/i,
+              /唯一标识[::\s]*([a-zA-Z0-9_]+)/,
+            ];
+            
+            for (const pattern of patterns) {
+              const match = bodyText.match(pattern);
+              if (match && match[1]) {
+                result.finderId = match[1];
+                console.log('[WeixinVideo] Found finder ID from settings page:', result.finderId);
+                break;
+              }
+            }
+            
+            // 从元素中查找
+            if (!result.finderId) {
+              const idSelectors = [
+                '[class*="finder-id"]',
+                '[class*="account-id"]',
+                '[class*="unique-id"]',
+                '.finder-uniq-id',
+                'span.finder-uniq-id',
+                '[class*="copy-id"]',
+              ];
+              for (const selector of idSelectors) {
+                const el = document.querySelector(selector);
+                const text = el?.textContent?.trim();
+                if (text && /^[a-zA-Z0-9_]+$/.test(text) && text.length >= 10 && text.length <= 30) {
+                  result.finderId = text;
+                  console.log('[WeixinVideo] Found ID from settings selector:', result.finderId);
+                  break;
+                }
+              }
+            }
+            
+            return result;
+          });
+          
+          logger.info(`[WeixinVideo] Extracted data from settings page:`, settingsData);
+          
+          if (settingsData.finderId) {
+            finderId = settingsData.finderId;
+            accountId = `weixin_video_${settingsData.finderId}`;
+          }
+        } catch (settingsError) {
+          logger.warn('[WeixinVideo] Failed to fetch from settings page:', settingsError);
+        }
+      }
+
+      logger.info(`[WeixinVideo] Final account info: id=${accountId}, name=${accountName}, avatar=${avatarUrl ? 'yes' : 'no'}, fans=${fansCount}`);
 
     } catch (error) {
       logger.warn('Failed to fetch WeixinVideo account info:', error);
@@ -1464,10 +1609,12 @@ class HeadlessBrowserService {
         if (capturedData.userInfo.avatar) {
           avatarUrl = capturedData.userInfo.avatar;
         }
-        if (capturedData.userInfo.userId) {
-          accountId = `xiaohongshu_${capturedData.userInfo.userId}`;
-        } else if (capturedData.userInfo.redId) {
+        // 优先使用小红书号(redId)作为 accountId
+        if (capturedData.userInfo.redId) {
           accountId = `xiaohongshu_${capturedData.userInfo.redId}`;
+          logger.info(`[Xiaohongshu] Using redId as accountId: ${accountId}`);
+        } else if (capturedData.userInfo.userId) {
+          accountId = `xiaohongshu_${capturedData.userInfo.userId}`;
         }
         if (capturedData.userInfo.fans) {
           fansCount = capturedData.userInfo.fans;
@@ -1477,6 +1624,18 @@ class HeadlessBrowserService {
         }
       }
 
+      // 如果还没获取到小红书号,尝试从页面文本中提取
+      if (!accountId.match(/xiaohongshu_[a-zA-Z0-9_]+/) || accountId.includes('_' + Date.now().toString().slice(0, 8))) {
+        const bodyText = await page.textContent('body');
+        // 匹配小红书号格式:小红书号:xxxxxxx
+        const xhsIdMatch = bodyText?.match(/小红书号[::]\s*([a-zA-Z0-9_]+)/) ||
+                          bodyText?.match(/红书号[::]\s*([a-zA-Z0-9_]+)/);
+        if (xhsIdMatch) {
+          accountId = `xiaohongshu_${xhsIdMatch[1]}`;
+          logger.info(`[Xiaohongshu] Found 小红书号 from page text: ${accountId}`);
+        }
+      }
+
       logger.info(`[Xiaohongshu] Account info: id=${accountId}, name=${accountName}, fans=${fansCount}, works=${worksCount}`);
 
       // 获取作品列表 - 通过监听 API 接口