ソースを参照

Implement CDP network interception feature in Electron app; add handlers for enabling, disabling, and updating network patterns. Enhance preload script to expose new API methods for network interception and update BrowserTab component to utilize CDP for API data extraction.

Ethanfly 5 時間 前
コミット
86b79a45ce

+ 117 - 0
client/dist-electron/main.js

@@ -402,4 +402,121 @@ ipcMain.handle("webview-click-by-text", async (_event, webContentsId, text) => {
     return false;
   }
 });
+const networkInterceptors = /* @__PURE__ */ new Map();
+ipcMain.handle("enable-network-intercept", async (_event, webContentsId, patterns) => {
+  var _a;
+  try {
+    const wc = webContents.fromId(webContentsId);
+    if (!wc) {
+      console.error("[CDP] 找不到 webContents:", webContentsId);
+      return false;
+    }
+    if (networkInterceptors.has(webContentsId)) {
+      try {
+        wc.debugger.detach();
+      } catch (e) {
+      }
+    }
+    networkInterceptors.set(webContentsId, {
+      patterns,
+      pendingRequests: /* @__PURE__ */ new Map()
+    });
+    try {
+      wc.debugger.attach("1.3");
+    } catch (err) {
+      const error = err;
+      if (!((_a = error.message) == null ? void 0 : _a.includes("Already attached"))) {
+        throw err;
+      }
+    }
+    await wc.debugger.sendCommand("Network.enable");
+    wc.debugger.on("message", async (_e, method, params) => {
+      const config = networkInterceptors.get(webContentsId);
+      if (!config) return;
+      if (method === "Network.responseReceived") {
+        const { requestId, response } = params;
+        if (!requestId || !(response == null ? void 0 : response.url)) return;
+        if (response.url.includes("baijiahao.baidu.com")) {
+          if (response.url.includes("/pcui/") || response.url.includes("/article")) {
+            console.log(`[CDP DEBUG] 百家号 API: ${response.url}`);
+          }
+        }
+        for (const pattern of config.patterns) {
+          if (response.url.includes(pattern.match)) {
+            config.pendingRequests.set(requestId, {
+              url: response.url,
+              timestamp: Date.now()
+            });
+            console.log(`[CDP] 匹配到 API: ${pattern.key} - ${response.url}`);
+            break;
+          }
+        }
+      }
+      if (method === "Network.loadingFinished") {
+        const { requestId } = params;
+        if (!requestId) return;
+        const pending = config.pendingRequests.get(requestId);
+        if (!pending) return;
+        config.pendingRequests.delete(requestId);
+        try {
+          const result = await wc.debugger.sendCommand("Network.getResponseBody", { requestId });
+          let body = result.body;
+          if (result.base64Encoded) {
+            body = Buffer.from(body, "base64").toString("utf8");
+          }
+          const data = JSON.parse(body);
+          let matchedKey = "";
+          for (const pattern of config.patterns) {
+            if (pending.url.includes(pattern.match)) {
+              matchedKey = pattern.key;
+              break;
+            }
+          }
+          if (matchedKey) {
+            console.log(`[CDP] 获取到响应: ${matchedKey}`, JSON.stringify(data).substring(0, 200));
+            mainWindow == null ? void 0 : mainWindow.webContents.send("network-intercept-data", {
+              webContentsId,
+              key: matchedKey,
+              url: pending.url,
+              data
+            });
+          }
+        } catch (err) {
+          console.warn(`[CDP] 获取响应体失败:`, err);
+        }
+      }
+    });
+    console.log(`[CDP] 已启用网络拦截,webContentsId: ${webContentsId}, patterns:`, patterns.map((p) => p.key));
+    return true;
+  } catch (error) {
+    console.error("[CDP] 启用网络拦截失败:", error);
+    return false;
+  }
+});
+ipcMain.handle("disable-network-intercept", async (_event, webContentsId) => {
+  try {
+    const wc = webContents.fromId(webContentsId);
+    if (wc) {
+      try {
+        wc.debugger.detach();
+      } catch (e) {
+      }
+    }
+    networkInterceptors.delete(webContentsId);
+    console.log(`[CDP] 已禁用网络拦截,webContentsId: ${webContentsId}`);
+    return true;
+  } catch (error) {
+    console.error("[CDP] 禁用网络拦截失败:", error);
+    return false;
+  }
+});
+ipcMain.handle("update-network-patterns", async (_event, webContentsId, patterns) => {
+  const config = networkInterceptors.get(webContentsId);
+  if (config) {
+    config.patterns = patterns;
+    console.log(`[CDP] 已更新 patterns,webContentsId: ${webContentsId}`);
+    return true;
+  }
+  return false;
+});
 //# sourceMappingURL=main.js.map

ファイルの差分が大きいため隠しています
+ 0 - 0
client/dist-electron/main.js.map


ファイルの差分が大きいため隠しています
+ 11 - 1
client/dist-electron/preload.js


+ 164 - 0
client/electron/main.ts

@@ -532,3 +532,167 @@ ipcMain.handle('webview-click-by-text', async (_event: unknown, webContentsId: n
     return false;
   }
 });
+
+// ========== CDP 网络拦截功能 ==========
+// 存储每个 webContents 的网络拦截配置
+const networkInterceptors: Map<number, {
+  patterns: Array<{match: string, key: string}>;
+  pendingRequests: Map<string, {url: string, timestamp: number}>;
+}> = new Map();
+
+// 启用 CDP 网络拦截
+ipcMain.handle('enable-network-intercept', async (_event: unknown, webContentsId: number, patterns: Array<{match: string, key: string}>) => {
+  try {
+    const wc = webContents.fromId(webContentsId);
+    if (!wc) {
+      console.error('[CDP] 找不到 webContents:', webContentsId);
+      return false;
+    }
+
+    // 如果已经有拦截器,先清理
+    if (networkInterceptors.has(webContentsId)) {
+      try {
+        wc.debugger.detach();
+      } catch (e) {
+        // 忽略
+      }
+    }
+
+    // 存储配置
+    networkInterceptors.set(webContentsId, {
+      patterns,
+      pendingRequests: new Map()
+    });
+
+    // 附加调试器
+    try {
+      wc.debugger.attach('1.3');
+    } catch (err: unknown) {
+      const error = err as Error;
+      if (!error.message?.includes('Already attached')) {
+        throw err;
+      }
+    }
+
+    // 启用网络监听
+    await wc.debugger.sendCommand('Network.enable');
+
+    // 监听网络响应
+    wc.debugger.on('message', async (_e: unknown, method: string, params: {
+      requestId?: string;
+      response?: { url?: string; status?: number; mimeType?: string };
+      encodedDataLength?: number;
+    }) => {
+      const config = networkInterceptors.get(webContentsId);
+      if (!config) return;
+
+      if (method === 'Network.responseReceived') {
+        const { requestId, response } = params;
+        if (!requestId || !response?.url) return;
+
+        // 调试:打印百家号相关的所有 API 请求
+        if (response.url.includes('baijiahao.baidu.com')) {
+          if (response.url.includes('/pcui/') || response.url.includes('/article')) {
+            console.log(`[CDP DEBUG] 百家号 API: ${response.url}`);
+          }
+        }
+
+        // 检查是否匹配我们关注的 API
+        for (const pattern of config.patterns) {
+          if (response.url.includes(pattern.match)) {
+            // 记录请求,等待响应完成
+            config.pendingRequests.set(requestId, {
+              url: response.url,
+              timestamp: Date.now()
+            });
+            console.log(`[CDP] 匹配到 API: ${pattern.key} - ${response.url}`);
+            break;
+          }
+        }
+      }
+
+      if (method === 'Network.loadingFinished') {
+        const { requestId } = params;
+        if (!requestId) return;
+
+        const pending = config.pendingRequests.get(requestId);
+        if (!pending) return;
+
+        config.pendingRequests.delete(requestId);
+
+        try {
+          // 获取响应体
+          const result = await wc.debugger.sendCommand('Network.getResponseBody', { requestId }) as { body: string; base64Encoded: boolean };
+          let body = result.body;
+          
+          // 如果是 base64 编码,解码
+          if (result.base64Encoded) {
+            body = Buffer.from(body, 'base64').toString('utf8');
+          }
+
+          // 解析 JSON
+          const data = JSON.parse(body);
+
+          // 找到匹配的 key
+          let matchedKey = '';
+          for (const pattern of config.patterns) {
+            if (pending.url.includes(pattern.match)) {
+              matchedKey = pattern.key;
+              break;
+            }
+          }
+
+          if (matchedKey) {
+            console.log(`[CDP] 获取到响应: ${matchedKey}`, JSON.stringify(data).substring(0, 200));
+            // 发送到渲染进程
+            mainWindow?.webContents.send('network-intercept-data', {
+              webContentsId,
+              key: matchedKey,
+              url: pending.url,
+              data
+            });
+          }
+        } catch (err) {
+          console.warn(`[CDP] 获取响应体失败:`, err);
+        }
+      }
+    });
+
+    console.log(`[CDP] 已启用网络拦截,webContentsId: ${webContentsId}, patterns:`, patterns.map(p => p.key));
+    return true;
+  } catch (error) {
+    console.error('[CDP] 启用网络拦截失败:', error);
+    return false;
+  }
+});
+
+// 禁用 CDP 网络拦截
+ipcMain.handle('disable-network-intercept', async (_event: unknown, webContentsId: number) => {
+  try {
+    const wc = webContents.fromId(webContentsId);
+    if (wc) {
+      try {
+        wc.debugger.detach();
+      } catch (e) {
+        // 忽略
+      }
+    }
+    networkInterceptors.delete(webContentsId);
+    console.log(`[CDP] 已禁用网络拦截,webContentsId: ${webContentsId}`);
+    return true;
+  } catch (error) {
+    console.error('[CDP] 禁用网络拦截失败:', error);
+    return false;
+  }
+});
+
+// 更新网络拦截的 patterns
+ipcMain.handle('update-network-patterns', async (_event: unknown, webContentsId: number, patterns: Array<{match: string, key: string}>) => {
+  const config = networkInterceptors.get(webContentsId);
+  if (config) {
+    config.patterns = patterns;
+    console.log(`[CDP] 已更新 patterns,webContentsId: ${webContentsId}`);
+    return true;
+  }
+  return false;
+});

+ 20 - 0
client/electron/preload.ts

@@ -47,6 +47,20 @@ contextBridge.exposeInMainWorld('electronAPI', {
     ipcRenderer.invoke('webview-get-element-position', webContentsId, selector),
   webviewClickByText: (webContentsId: number, text: string) =>
     ipcRenderer.invoke('webview-click-by-text', webContentsId, text),
+
+  // CDP 网络拦截
+  enableNetworkIntercept: (webContentsId: number, patterns: Array<{match: string, key: string}>) =>
+    ipcRenderer.invoke('enable-network-intercept', webContentsId, patterns),
+  disableNetworkIntercept: (webContentsId: number) =>
+    ipcRenderer.invoke('disable-network-intercept', webContentsId),
+  updateNetworkPatterns: (webContentsId: number, patterns: Array<{match: string, key: string}>) =>
+    ipcRenderer.invoke('update-network-patterns', webContentsId, patterns),
+  onNetworkInterceptData: (callback: (data: { webContentsId: number; key: string; url: string; data: unknown }) => void) => {
+    ipcRenderer.on('network-intercept-data', (_event: unknown, data: { webContentsId: number; key: string; url: string; data: unknown }) => callback(data));
+  },
+  removeNetworkInterceptListener: () => {
+    ipcRenderer.removeAllListeners('network-intercept-data');
+  },
 });
 
 // 类型声明
@@ -72,6 +86,12 @@ declare global {
       webviewSendTextInput: (webContentsId: number, text: string) => Promise<boolean>;
       webviewGetElementPosition: (webContentsId: number, selector: string) => Promise<{ x: number; y: number; width: number; height: number } | null>;
       webviewClickByText: (webContentsId: number, text: string) => Promise<boolean>;
+      // CDP 网络拦截
+      enableNetworkIntercept: (webContentsId: number, patterns: Array<{match: string, key: string}>) => Promise<boolean>;
+      disableNetworkIntercept: (webContentsId: number) => Promise<boolean>;
+      updateNetworkPatterns: (webContentsId: number, patterns: Array<{match: string, key: string}>) => Promise<boolean>;
+      onNetworkInterceptData: (callback: (data: { webContentsId: number; key: string; url: string; data: unknown }) => void) => void;
+      removeNetworkInterceptListener: () => void;
     };
   }
 }

+ 224 - 99
client/src/components/BrowserTab.vue

@@ -1434,7 +1434,10 @@ function handleDomReady() {
   
   // 管理模式下不自动检测登录状态
   if (!isAdminMode.value) {
-    // 注入 API 拦截脚本
+    // 优先使用 CDP 网络拦截(更底层更可靠)
+    enableCDPNetworkIntercept();
+    
+    // 同时保留 JS 注入作为备用(某些情况下 CDP 可能不可用)
     injectApiInterceptScript();
     // 设置 console-message 监听器来接收 API 数据
     setupApiDataListener();
@@ -1554,7 +1557,7 @@ function getApiPatternsForPlatform(): Array<{match: string, key: string}> {
   return patterns[platform.value] || [];
 }
 
-// 设置 console-message 监听器来接收 API 拦截数据
+// 设置 console-message 监听器来接收 API 拦截数据(JS 注入方式的备用)
 function setupApiDataListener() {
   const webview = webviewRef.value;
   if (!webview) return;
@@ -1574,6 +1577,90 @@ function setupApiDataListener() {
   });
 }
 
+// ========== CDP 网络拦截(更底层的方式)==========
+let cdpInterceptEnabled = false;
+
+// 启用 CDP 网络拦截
+async function enableCDPNetworkIntercept() {
+  const webview = webviewRef.value;
+  if (!webview || cdpInterceptEnabled) return;
+  
+  try {
+    const webContentsId = webview.getWebContentsId();
+    if (!webContentsId) {
+      console.warn('[CDP] 无法获取 webContentsId');
+      return;
+    }
+    
+    const patterns = getApiPatternsForPlatform();
+    if (patterns.length === 0) {
+      console.log('[CDP] 当前平台无需拦截的 API');
+      return;
+    }
+    
+    const result = await window.electronAPI?.enableNetworkIntercept(webContentsId, patterns);
+    if (result) {
+      cdpInterceptEnabled = true;
+      console.log('[CDP] 网络拦截已启用,webContentsId:', webContentsId);
+    }
+  } catch (err) {
+    console.error('[CDP] 启用网络拦截失败:', err);
+  }
+}
+
+// 禁用 CDP 网络拦截
+async function disableCDPNetworkIntercept() {
+  const webview = webviewRef.value;
+  if (!webview || !cdpInterceptEnabled) return;
+  
+  try {
+    const webContentsId = webview.getWebContentsId();
+    if (webContentsId) {
+      await window.electronAPI?.disableNetworkIntercept(webContentsId);
+    }
+    cdpInterceptEnabled = false;
+    console.log('[CDP] 网络拦截已禁用');
+  } catch (err) {
+    console.error('[CDP] 禁用网络拦截失败:', err);
+  }
+}
+
+// 更新 CDP 拦截的 patterns
+async function updateCDPPatterns() {
+  const webview = webviewRef.value;
+  if (!webview || !cdpInterceptEnabled) return;
+  
+  try {
+    const webContentsId = webview.getWebContentsId();
+    const patterns = getApiPatternsForPlatform();
+    if (webContentsId && patterns.length > 0) {
+      await window.electronAPI?.updateNetworkPatterns(webContentsId, patterns);
+      console.log('[CDP] patterns 已更新');
+    }
+  } catch (err) {
+    console.error('[CDP] 更新 patterns 失败:', err);
+  }
+}
+
+// 设置 CDP 数据监听器
+function setupCDPDataListener() {
+  window.electronAPI?.onNetworkInterceptData((data) => {
+    const webview = webviewRef.value;
+    if (!webview) return;
+    
+    const webContentsId = webview.getWebContentsId();
+    if (data.webContentsId === webContentsId) {
+      console.log('[CDP] 收到 API 数据:', data.key, data.url);
+      apiResponseData.value[data.key] = data.data;
+    }
+  });
+}
+
+// 清理 CDP 监听器
+function cleanupCDPListener() {
+  window.electronAPI?.removeNetworkInterceptListener();
+}
+
 // 拦截自定义协议链接,防止弹出系统对话框
 function setupProtocolInterception() {
   const webview = webviewRef.value;
@@ -2284,9 +2371,9 @@ async function collectWeixinVideoAccountInfo() {
 
 /**
  * 百家号登录流程:
- * 1. 跳转到账号设置页面从 HTML 提取基本信息(头像、昵称、ID)
+ * 1. 监听 API /builder/app/appinfo 获取基本信息(头像、昵称、ID)
  * 2. 监听 API /cms-ui/rights/growth/get_info 获取粉丝数
- * 3. 跳转到作品管理页,监听 API /pcui/article/lists 获取作品数
+ * 3. 跳转到作品管理页获取作品数
  * 4. 账号ID使用 bjh_ 前缀
  */
 async function collectBaijiahaoAccountInfo() {
@@ -2297,121 +2384,150 @@ async function collectBaijiahaoAccountInfo() {
   console.log('[百家号] 步骤0: 获取 Cookie...');
   await getCookieData();
   
-  console.log('[百家号] 步骤1: 跳转到账号设置页面获取基本信息...');
+  console.log('[百家号] 步骤1: 获取账号基本信息 (appinfo API)...');
   
-  // 跳转到账号设置页面获取头像、昵称、ID
-  const settingsUrl = 'https://baijiahao.baidu.com/builder/rc/settings/accountSet';
-  console.log('[百家号] 正在跳转到:', settingsUrl);
+  // 先检查是否已有 appinfo 数据(可能由 CDP 拦截到)
+  let appInfo = apiResponseData.value['appInfo'];
   
-  // 使用 Promise 等待导航完成
-  await new Promise<void>((resolve) => {
-    const onNavigate = () => {
-      console.log('[百家号] 导航开始...');
-    };
-    const onLoad = () => {
-      console.log('[百家号] 页面加载完成,当前URL:', webview.getURL());
-      webview.removeEventListener('did-start-loading', onNavigate);
-      webview.removeEventListener('did-stop-loading', onLoad);
-      resolve();
-    };
-    webview.addEventListener('did-start-loading', onNavigate);
-    webview.addEventListener('did-stop-loading', onLoad);
-    webview.loadURL(settingsUrl);
+  if (!appInfo) {
+    // 刷新首页以触发 appinfo API
+    apiResponseData.value = {};
+    webview.loadURL('https://baijiahao.baidu.com/builder/rc/home');
+    await waitForPageLoad(webview, 5000);
+    await sleep(1000);
     
-    // 超时保护
-    setTimeout(() => {
-      webview.removeEventListener('did-start-loading', onNavigate);
-      webview.removeEventListener('did-stop-loading', onLoad);
-      resolve();
-    }, 10000);
-  });
-  
-  await sleep(3000); // 额外等待页面渲染
+    // 等待 appinfo API 数据
+    appInfo = await waitForApiData('appInfo', 10000).catch(() => null);
+  }
   
-  const currentUrl = webview.getURL();
-  console.log('[百家号] 当前页面 URL:', currentUrl);
+  console.log('[百家号] appInfo 数据:', appInfo);
   
-  // 验证是否跳转成功
-  if (!currentUrl.includes('settings') && !currentUrl.includes('accountSet')) {
-    console.warn('[百家号] 跳转失败,尝试重新跳转...');
-    webview.loadURL(settingsUrl);
-    await sleep(5000);
-    console.log('[百家号] 重试后 URL:', webview.getURL());
-  }
+  // 如果 API 获取失败,回退到页面提取
+  let user: { name?: string; avatar?: string; app_id?: string } = {};
   
-  const pageInfo = await webview.executeJavaScript(`
-    (function() {
-      const result = {};
-      
-      // 用户名:class 包含 userName
-      const nameEl = document.querySelector('[class*="userName"]');
-      if (nameEl) {
-        const text = nameEl.childNodes[0]?.textContent?.trim();
-        if (text) result.name = text;
-      }
-      
-      // 头像:class 包含 userImg
-      const avatarEl = document.querySelector('[class*="userImg"]:not([class*="Mask"]):not([class*="Hover"]):not([class*="Box"])');
-      if (avatarEl?.src) result.avatar = avatarEl.src;
-      
-      // 百家号 ID:class 包含 idName
-      const idEl = document.querySelector('[class*="idName"]');
-      if (idEl) {
-        const idText = idEl.textContent?.trim();
-        if (idText && /^\\d+$/.test(idText)) {
-          result.app_id = idText;
+  if (appInfo?.data?.user) {
+    user = appInfo.data.user;
+    console.log('[百家号] 从 API 获取到用户信息:', user);
+  } else {
+    console.log('[百家号] API 获取失败,尝试从页面提取...');
+    
+    // 跳转到账号设置页面
+    webview.loadURL('https://baijiahao.baidu.com/builder/rc/settings/accountSet');
+    await waitForPageLoad(webview, 8000);
+    await sleep(2000);
+    
+    const pageInfo = await webview.executeJavaScript(`
+      (function() {
+        const result = {};
+        
+        // 用户名
+        const nameEl = document.querySelector('[class*="userName"]');
+        if (nameEl) {
+          const text = nameEl.childNodes[0]?.textContent?.trim();
+          if (text) result.name = text;
         }
-      }
-      
-      // 备选:从"更多信息"区域提取 ID
-      if (!result.app_id) {
-        const morInfoItems = document.querySelectorAll('[class*="morInfoItem"]');
-        for (const item of morInfoItems) {
-          const label = item.querySelector('[class*="morInfoLabel"]');
-          const value = item.querySelector('[class*="morInfoValue"]');
-          if (label?.textContent?.includes('ID') && value) {
-            const match = value.textContent?.match(/(\\d{10,})/);
-            if (match) result.app_id = match[1];
+        
+        // 头像
+        const avatarEl = document.querySelector('[class*="userImg"]:not([class*="Mask"]):not([class*="Hover"]):not([class*="Box"])');
+        if (avatarEl?.src) result.avatar = avatarEl.src;
+        
+        // ID
+        const idEl = document.querySelector('[class*="idName"]');
+        if (idEl) {
+          const idText = idEl.textContent?.trim();
+          if (idText && /^\\d+$/.test(idText)) {
+            result.app_id = idText;
           }
         }
-      }
-      
-      console.log('[百家号] 从页面提取的信息:', result);
-      return result;
-    })()
-  `).catch(() => ({}));
-  
-  console.log('[百家号] 页面提取结果:', pageInfo);
+        
+        if (!result.app_id) {
+          const morInfoItems = document.querySelectorAll('[class*="morInfoItem"]');
+          for (const item of morInfoItems) {
+            const label = item.querySelector('[class*="morInfoLabel"]');
+            const value = item.querySelector('[class*="morInfoValue"]');
+            if (label?.textContent?.includes('ID') && value) {
+              const match = value.textContent?.match(/(\\d{10,})/);
+              if (match) result.app_id = match[1];
+            }
+          }
+        }
+        
+        return result;
+      })()
+    `).catch(() => ({}));
+    
+    if (pageInfo && (pageInfo.name || pageInfo.app_id)) {
+      user = pageInfo;
+      console.log('[百家号] 从页面提取到用户信息:', user);
+    }
+  }
   
-  if (!pageInfo || (!pageInfo.name && !pageInfo.app_id)) {
+  if (!user.name && !user.app_id) {
     throw new Error('无法获取百家号账号信息');
   }
   
-  const user = pageInfo;
   console.log('[百家号] 基本信息:', user);
   
-  // 步骤2: 返回首页获取粉丝数
-  console.log('[百家号] 步骤2: 跳转首页获取粉丝数...');
-  apiResponseData.value = {}; // 清空 API 数据
-  webview.loadURL('https://baijiahao.baidu.com/builder/rc/home');
-  await waitForPageLoad(webview, 5000);
-  injectApiInterceptScript();
+  // 步骤2: 获取粉丝数
+  console.log('[百家号] 步骤2: 获取粉丝数...');
+  
+  // 检查是否已有 growthInfo 数据
+  let growthInfo = apiResponseData.value['growthInfo'];
+  
+  if (!growthInfo) {
+    // 确保在首页
+    const currentUrl = webview.getURL();
+    if (!currentUrl.includes('/builder/rc/home')) {
+      apiResponseData.value = {};
+      webview.loadURL('https://baijiahao.baidu.com/builder/rc/home');
+      await waitForPageLoad(webview, 5000);
+      await sleep(1000);
+    }
+    growthInfo = await waitForApiData('growthInfo', 8000).catch(() => null);
+  }
   
-  const growthInfo = await waitForApiData('growthInfo', 8000).catch(() => null);
   const fansCount = growthInfo?.data?.total_fans || growthInfo?.total_fans || 0;
   console.log('[百家号] 粉丝数:', fansCount);
   
-  // 步骤3: 跳转到作品管理页获取作品数
-  console.log('[百家号] 步骤3: 跳转到作品管理页获取作品数...');
-  apiResponseData.value = {}; // 清空 API 数据
-  webview.loadURL('https://baijiahao.baidu.com/builder/rc/content');
-  await waitForPageLoad(webview, 5000);
-  injectApiInterceptScript();
+  // 步骤3: 直接发起 API 请求获取作品数
+  console.log('[百家号] 步骤3: 发起 API 请求获取作品数...');
   
-  const worksCount = await waitForApiData('articleList', 15000).then(data => {
-    const list = data?.data?.list || data?.list || [];
-    return list.length;
-  }).catch(() => 0);
+  // 直接在 webview 中发起 API 请求(会自动携带 cookie)
+  const worksCount = await webview.executeJavaScript(`
+    (async function() {
+      try {
+        const apiUrl = 'https://baijiahao.baidu.com/pcui/article/lists?currentPage=1&pageSize=10&search=&type=&collection=&startDate=&endDate=&clearBeforeFetch=false&dynamic=0';
+        console.log('[百家号] 发起 API 请求:', apiUrl);
+        
+        const response = await fetch(apiUrl, {
+          method: 'GET',
+          credentials: 'include', // 携带 cookie
+          headers: {
+            'Accept': 'application/json, text/plain, */*',
+            'Content-Type': 'application/json',
+          }
+        });
+        
+        const data = await response.json();
+        console.log('[百家号] API 响应:', JSON.stringify(data).substring(0, 200));
+        
+        if (data.errno === 0 && data.data) {
+          const list = data.data.list || [];
+          console.log('[百家号] 从 API 获取作品数:', list.length);
+          return list.length;
+        } else {
+          console.log('[百家号] API 返回错误:', data.errno, data.errmsg);
+          return 0;
+        }
+      } catch (err) {
+        console.error('[百家号] API 请求失败:', err);
+        return 0;
+      }
+    })()
+  `).catch((err) => {
+    console.error('[百家号] executeJavaScript 失败:', err);
+    return 0;
+  });
   
   console.log('[百家号] 作品数:', worksCount);
   
@@ -3684,6 +3800,9 @@ onMounted(async () => {
   console.log('[BrowserTab] isAdminMode:', isAdminMode.value);
   console.log('[BrowserTab] initialUrl:', initialUrl.value);
   
+  // 设置 CDP 网络拦截数据监听器
+  setupCDPDataListener();
+  
   // 检查 AI 服务可用性(非管理模式)
   if (!isAdminMode.value) {
     await checkAIAvailability();
@@ -3739,6 +3858,8 @@ async function setPresetCookies(cookieDataStr: string) {
       kuaishou: '.kuaishou.com',
       weixin_video: '.qq.com',
       bilibili: '.bilibili.com',
+      baijiahao: '.baidu.com',
+      toutiao: '.toutiao.com',
     };
     const rootDomain = platformDomainMap[platform.value] || '';
     
@@ -3812,6 +3933,10 @@ onUnmounted(() => {
   stopAutoCheck();
   stopAIAnalysis();
   
+  // 清理 CDP 网络拦截
+  disableCDPNetworkIntercept();
+  cleanupCDPListener();
+  
   // 清理 webview session
   if (window.electronAPI?.clearWebviewCookies) {
     window.electronAPI.clearWebviewCookies(webviewPartition.value);

+ 195 - 15
server/src/services/HeadlessBrowserService.ts

@@ -33,12 +33,16 @@ const PLATFORM_API_CONFIG: Record<string, {
     },
   },
   baijiahao: {
-    // 使用设置信息接口检查 Cookie 有效性
-    checkUrl: 'https://baijiahao.baidu.com/user-ui/cms/settingInfo',
+    // 使用 appinfo 接口检查 Cookie 有效性
+    checkUrl: 'https://baijiahao.baidu.com/builder/app/appinfo',
     isValidResponse: (data: unknown) => {
-      const resp = data as { errno?: number; data?: { name?: string } };
-      // errno 为 0 且有 name 表示 Cookie 有效
-      return resp?.errno === 0 && !!resp?.data?.name;
+      const resp = data as { errno?: number; ret?: { errno?: number; no?: number }; data?: { user?: { name?: string; app_id?: string } } };
+      logger.info(`[Baijiahao] API response: errno=${resp?.errno}, ret.errno=${resp?.ret?.errno}, user.name=${resp?.data?.user?.name}, user.app_id=${resp?.data?.user?.app_id}`);
+      // errno/ret.errno 为 0 且有 user 信息表示 Cookie 有效
+      // 兼容多种返回格式
+      const isErrnoOk = resp?.errno === 0 || resp?.ret?.errno === 0 || resp?.ret?.no === 0;
+      const hasUserInfo = !!(resp?.data?.user?.name || resp?.data?.user?.app_id);
+      return isErrnoOk && hasUserInfo;
     },
   },
 };
@@ -102,25 +106,34 @@ class HeadlessBrowserService {
    * 优先使用 Python 服务(通过浏览器访问后台检测),回退到 API 检查
    */
   async checkCookieValid(platform: PlatformType, cookies: CookieData[]): Promise<boolean> {
+    logger.info(`[checkCookieValid] Checking cookie for ${platform}, cookie count: ${cookies.length}`);
+    
     // 优先使用 Python 服务检查(通过浏览器访问后台页面,检测是否被重定向到登录页)
     const pythonAvailable = await this.checkPythonServiceAvailable();
     if (pythonAvailable) {
       try {
         const result = await this.checkLoginViaPython(platform, cookies);
+        logger.info(`[checkCookieValid] Python service result for ${platform}: ${result}`);
         return result;
       } catch (error) {
         logger.warn(`[Python API] Check login failed, falling back to API check:`, error);
       }
+    } else {
+      logger.info(`[checkCookieValid] Python service not available, using API check`);
     }
 
     // 回退到 API 检查
     const apiConfig = PLATFORM_API_CONFIG[platform];
     if (apiConfig) {
-      return this.checkCookieValidByApi(platform, cookies, apiConfig);
+      const result = await this.checkCookieValidByApi(platform, cookies, apiConfig);
+      logger.info(`[checkCookieValid] API check result for ${platform}: ${result}`);
+      return result;
     }
 
     // 其他平台使用浏览器检查
-    return this.checkCookieValidByBrowser(platform, cookies);
+    const result = await this.checkCookieValidByBrowser(platform, cookies);
+    logger.info(`[checkCookieValid] Browser check result for ${platform}: ${result}`);
+    return result;
   }
 
   /**
@@ -202,10 +215,14 @@ class HeadlessBrowserService {
       });
 
       const data = await response.json();
+      logger.info(`[API] Raw response for ${platform}:`, JSON.stringify(data).substring(0, 500));
+      
       const isValid = apiConfig.isValidResponse(data);
-      const statusCode = (data as { status_code?: number })?.status_code;
+      const statusCode = (data as { status_code?: number; errno?: number; ret?: { errno?: number } })?.status_code 
+        ?? (data as { errno?: number })?.errno 
+        ?? (data as { ret?: { errno?: number } })?.ret?.errno;
 
-      logger.info(`API check cookie for ${platform}: valid=${isValid}, status_code=${statusCode}`);
+      logger.info(`API check cookie for ${platform}: valid=${isValid}, statusCode=${statusCode}`);
 
       // 如果 API 明确返回有效,直接返回 true
       if (isValid) {
@@ -213,17 +230,28 @@ class HeadlessBrowserService {
       }
 
       // API 返回无效时,检查是否是明确的"未登录"状态
-      // status_code 为 2 或 8 通常表示未登录/登录过期
-      // 其他状态码可能是临时错误,需要进一步确认
+      // 抖音: status_code 为 2 或 8 通常表示未登录/登录过期
+      // 百家号: errno 为非 0 可能表示未登录,但需要根据具体错误码判断
       const clearlyNotLoggedIn = statusCode === 2 || statusCode === 8;
 
       if (clearlyNotLoggedIn) {
-        logger.info(`[API] Platform ${platform} clearly not logged in (status_code=${statusCode})`);
+        logger.info(`[API] Platform ${platform} clearly not logged in (statusCode=${statusCode})`);
         return false;
       }
 
+      // 百家号特殊处理:如果 errno 为 0 但没有用户信息,可能是其他问题,回退浏览器检查
+      // 如果 errno 不为 0,也回退浏览器检查以避免误判
+      if (platform === 'baijiahao') {
+        const errno = (data as { errno?: number; ret?: { errno?: number } })?.errno 
+          ?? (data as { ret?: { errno?: number } })?.ret?.errno;
+        if (errno !== 0 && errno !== undefined) {
+          logger.info(`[API] Baijiahao errno=${errno}, falling back to browser check`);
+          return this.checkCookieValidByBrowser(platform, cookies);
+        }
+      }
+
       // 不确定的状态(如 status_code=7),回退到浏览器检查
-      logger.info(`[API] Uncertain status for ${platform} (status_code=${statusCode}), falling back to browser check`);
+      logger.info(`[API] Uncertain status for ${platform} (statusCode=${statusCode}), falling back to browser check`);
       return this.checkCookieValidByBrowser(platform, cookies);
     } catch (error) {
       logger.error(`API check cookie error for ${platform}:`, error);
@@ -513,9 +541,9 @@ class HeadlessBrowserService {
    * 获取账号信息(优先使用 Python API,回退到无头浏览器)
    */
   async fetchAccountInfo(platform: PlatformType, cookies: CookieData[]): Promise<AccountInfo> {
-    // 百家号使用 Python API 获取账号信息
+    // 百家号使用直接 API 获取账号信息和作品列表
     if (platform === 'baijiahao') {
-      return this.fetchAccountInfoViaPython(platform, cookies);
+      return this.fetchBaijiahaoAccountInfoDirectApi(cookies);
     }
 
     // 对于支持的平台,尝试使用 Python API 获取作品列表和账号信息
@@ -2014,6 +2042,158 @@ class HeadlessBrowserService {
   }
 
   /**
+   * 百家号 - 直接通过 API 获取账号信息和作品列表
+   */
+  private async fetchBaijiahaoAccountInfoDirectApi(cookies: CookieData[]): Promise<AccountInfo> {
+    logger.info(`[Baijiahao API] Fetching account info via direct API...`);
+    
+    const cookieString = cookies.map(c => `${c.name}=${c.value}`).join('; ');
+    const headers = {
+      'Accept': 'application/json, text/plain, */*',
+      'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
+      'Cookie': cookieString,
+      'Referer': 'https://baijiahao.baidu.com/builder/rc/home',
+      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+    };
+
+    let accountInfo: AccountInfo = this.getDefaultAccountInfo('baijiahao');
+
+    try {
+      // 1. 获取账号基本信息 (appinfo API)
+      const appInfoResponse = await fetch('https://baijiahao.baidu.com/builder/app/appinfo', {
+        method: 'GET',
+        headers,
+      });
+      
+      if (appInfoResponse.ok) {
+        const appInfoData = await appInfoResponse.json() as {
+          errno?: number;
+          data?: {
+            user?: {
+              name?: string;
+              avatar?: string;
+              app_id?: string;
+            };
+          };
+        };
+        
+        if (appInfoData.errno === 0 && appInfoData.data?.user) {
+          const user = appInfoData.data.user;
+          accountInfo.accountId = user.app_id ? `bjh_${user.app_id}` : accountInfo.accountId;
+          accountInfo.accountName = user.name || accountInfo.accountName;
+          accountInfo.avatarUrl = user.avatar || '';
+          logger.info(`[Baijiahao API] Got account info: ${accountInfo.accountName}`);
+        }
+      }
+
+      // 2. 获取粉丝数 (growthInfo API)
+      const growthInfoResponse = await fetch('https://baijiahao.baidu.com/cms-ui/rights/growth/get_info', {
+        method: 'GET',
+        headers,
+      });
+      
+      if (growthInfoResponse.ok) {
+        const growthData = await growthInfoResponse.json() as {
+          errno?: number;
+          data?: {
+            total_fans?: number;
+          };
+        };
+        
+        if (growthData.errno === 0 && growthData.data) {
+          accountInfo.fansCount = growthData.data.total_fans || 0;
+          logger.info(`[Baijiahao API] Got fans count: ${accountInfo.fansCount}`);
+        }
+      }
+
+      // 3. 获取作品列表 (分页获取所有作品)
+      const worksList: WorkItem[] = [];
+      let currentPage = 1;
+      const pageSize = 20;
+      let hasMore = true;
+
+      while (hasMore) {
+        const listUrl = `https://baijiahao.baidu.com/pcui/article/lists?currentPage=${currentPage}&pageSize=${pageSize}&search=&type=&collection=&startDate=&endDate=&clearBeforeFetch=false&dynamic=0`;
+        
+        logger.info(`[Baijiahao API] Fetching works page ${currentPage}...`);
+        
+        const listResponse = await fetch(listUrl, {
+          method: 'GET',
+          headers,
+        });
+
+        if (!listResponse.ok) {
+          logger.warn(`[Baijiahao API] Failed to fetch works list: ${listResponse.status}`);
+          break;
+        }
+
+        const listData = await listResponse.json() as {
+          errno?: number;
+          errmsg?: string;
+          data?: {
+            list?: Array<{
+              id?: string;
+              title?: string;
+              cover_images?: string[];
+              create_time?: string;
+              status?: string;
+              read_count?: number;
+              like_count?: number;
+              comment_count?: number;
+              share_count?: number;
+            }>;
+            total?: number;
+          };
+        };
+
+        if (listData.errno !== 0) {
+          logger.warn(`[Baijiahao API] API returned error: ${listData.errno} - ${listData.errmsg}`);
+          break;
+        }
+
+        const list = listData.data?.list || [];
+        logger.info(`[Baijiahao API] Got ${list.length} works on page ${currentPage}`);
+
+        for (const item of list) {
+          worksList.push({
+            videoId: item.id || `bjh_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+            title: item.title || '',
+            coverUrl: item.cover_images?.[0] || '',
+            duration: '00:00',
+            publishTime: item.create_time || new Date().toISOString(),
+            status: item.status || 'published',
+            playCount: item.read_count || 0,
+            likeCount: item.like_count || 0,
+            commentCount: item.comment_count || 0,
+            shareCount: item.share_count || 0,
+          });
+        }
+
+        // 检查是否还有更多
+        if (list.length < pageSize) {
+          hasMore = false;
+        } else {
+          currentPage++;
+          // 防止无限循环,最多获取 10 页
+          if (currentPage > 10) {
+            logger.warn(`[Baijiahao API] Reached max pages (10), stopping`);
+            hasMore = false;
+          }
+        }
+      }
+
+      accountInfo.worksList = worksList;
+      accountInfo.worksCount = worksList.length;
+      logger.info(`[Baijiahao API] Total works fetched: ${worksList.length}`);
+
+      return accountInfo;
+    } catch (error) {
+      logger.error(`[Baijiahao API] Failed to fetch account info:`, error);
+      return accountInfo;
+    }
+  }
+
+  /**
    * 通过 Python API 获取账号信息
    */
   private async fetchAccountInfoViaPython(platform: PlatformType, cookies: CookieData[]): Promise<AccountInfo> {

+ 2 - 2
shared/src/constants/platforms.ts

@@ -127,8 +127,8 @@ export const PLATFORMS: Record<PlatformType, PlatformInfo> = {
     nameEn: 'Baijiahao',
     icon: 'baidu',
     color: '#2932E1',
-    loginUrl: 'https://baijiahao.baidu.com/',
-    creatorUrl: 'https://baijiahao.baidu.com/',
+    loginUrl: 'https://baijiahao.baidu.com/builder/theme/bjh/login',
+    creatorUrl: 'https://baijiahao.baidu.com/builder/rc/home',
     maxTitleLength: 30,
     maxDescriptionLength: 500,
     maxTags: 10,

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません