Jelajahi Sumber

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 jam lalu
induk
melakukan
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='调试模式')
     

TEMPAT SAMPAH
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 接口