|
|
@@ -455,6 +455,11 @@ class HeadlessBrowserService {
|
|
|
|
|
|
const config = this.getPlatformConfig(platform);
|
|
|
|
|
|
+ // #6065: 视频号使用专门的平台登录页检查(登录后 URL 含特定路径,非登录时可能不重定向)
|
|
|
+ if (platform === 'weixin_video') {
|
|
|
+ return this.checkWeixinVideoLoginStatusByBrowser(page, context, cookies, browser);
|
|
|
+ }
|
|
|
+
|
|
|
// 访问平台主页
|
|
|
await page.goto(config.homeUrl, {
|
|
|
waitUntil: 'domcontentloaded',
|
|
|
@@ -616,6 +621,107 @@ class HeadlessBrowserService {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * #6065: 视频号登录状态检测 - 通过检查平台页面是否展示账号信息
|
|
|
+ * 视频号 Cookie 失效时可能不重定向到登录页(URL 不变),需要正向信号判断
|
|
|
+ */
|
|
|
+ private async checkWeixinVideoLoginStatusByBrowser(
|
|
|
+ page: Page,
|
|
|
+ context: BrowserContext,
|
|
|
+ cookies: CookieData[],
|
|
|
+ browser: import('playwright').Browser
|
|
|
+ ): Promise<CookieCheckResult> {
|
|
|
+ try {
|
|
|
+ // 视频号创作者平台需要等待加载完成后检测页面内容
|
|
|
+ await page.goto('https://channels.weixin.qq.com/platform', {
|
|
|
+ waitUntil: 'domcontentloaded',
|
|
|
+ timeout: 30000,
|
|
|
+ });
|
|
|
+
|
|
|
+ await page.waitForTimeout(5000);
|
|
|
+
|
|
|
+ // 尝试等待网络空闲,给页面足够时间加载
|
|
|
+ try {
|
|
|
+ await page.waitForLoadState('networkidle', { timeout: 15000 });
|
|
|
+ } catch {
|
|
|
+ // 超时继续
|
|
|
+ }
|
|
|
+
|
|
|
+ const url = page.url();
|
|
|
+ const bodyText = await this.getPageBodyTextSafe(page);
|
|
|
+ logger.info(`[Weixin Video] Browser check: URL=${url}, bodyLen=${bodyText.length}`);
|
|
|
+
|
|
|
+ // 检测明确的登录页特征
|
|
|
+ if (url.includes('login.html') || url.includes('/login?') || url.includes('passport')) {
|
|
|
+ logger.info('[Weixin Video] Redirected to login page');
|
|
|
+ await page.close();
|
|
|
+ await context.close();
|
|
|
+ await browser.close();
|
|
|
+ return { isValid: false, needReLogin: true, uncertain: false, reason: 'need_login', source: 'browser' };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检测风控
|
|
|
+ if (this.containsRiskKeywords(url) || this.containsRiskKeywords(bodyText)) {
|
|
|
+ logger.info('[Weixin Video] Detected risk control keywords');
|
|
|
+ await page.close();
|
|
|
+ await context.close();
|
|
|
+ await browser.close();
|
|
|
+ return { isValid: false, needReLogin: true, uncertain: false, reason: 'risk_control', source: 'browser', message: '检测到风控/验证页面' };
|
|
|
+ }
|
|
|
+
|
|
|
+ // #6065: 正向信号检测 - 检查页面上是否存在已登录的账号信息元素
|
|
|
+ // 视频号登录后首页会出现 nickname、头像等元素
|
|
|
+ const positiveSignals = [
|
|
|
+ '.finder-nickname', // 视频号昵称
|
|
|
+ '.avatar img[src]', // 头像图片
|
|
|
+ '[class*="video-count"]', // 视频数
|
|
|
+ '[class*="follower"]', // 关注者
|
|
|
+ 'div.title-name', // 账号名称
|
|
|
+ ];
|
|
|
+
|
|
|
+ let hasPositiveSignal = false;
|
|
|
+ for (const selector of positiveSignals) {
|
|
|
+ try {
|
|
|
+ const count = await page.locator(selector).count();
|
|
|
+ if (count > 0) {
|
|
|
+ hasPositiveSignal = true;
|
|
|
+ logger.info(`[Weixin Video] Found positive signal: ${selector}`);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ // continue
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 额外检查:页面正文是否包含视频号管理页面的特征文本
|
|
|
+ const hasManagementText = bodyText.includes('发表视频') ||
|
|
|
+ bodyText.includes('数据中心') ||
|
|
|
+ bodyText.includes('互动管理') ||
|
|
|
+ bodyText.includes('内容管理');
|
|
|
+
|
|
|
+ if (hasPositiveSignal || hasManagementText) {
|
|
|
+ logger.info(`[Weixin Video] Cookie valid (positive signal=${hasPositiveSignal}, managementText=${hasManagementText})`);
|
|
|
+ await page.close();
|
|
|
+ await context.close();
|
|
|
+ await browser.close();
|
|
|
+ return { isValid: true, needReLogin: false, uncertain: false, reason: 'valid', source: 'browser' };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 没有正向信号也没有登录指标 → 不确定(可能是页面加载慢或网络问题)
|
|
|
+ logger.warn(`[Weixin Video] No positive or negative signals detected, marking as uncertain`);
|
|
|
+ await page.close();
|
|
|
+ await context.close();
|
|
|
+ await browser.close();
|
|
|
+ return { isValid: false, needReLogin: false, uncertain: true, reason: 'uncertain', source: 'browser', message: '无法确定视频号登录状态' };
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('[Weixin Video] checkLoginByBrowser error:', error);
|
|
|
+ try {
|
|
|
+ await browser.close();
|
|
|
+ } catch { /* ignore */ }
|
|
|
+ return { isValid: false, needReLogin: false, uncertain: true, reason: 'uncertain', source: 'browser', message: error instanceof Error ? error.message : 'Weixin video check error' };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 访问平台后台页面并截图(用于 AI 分析)
|
|
|
* @param platform 平台类型
|
|
|
* @param cookies Cookie 数据
|
|
|
@@ -921,8 +1027,14 @@ class HeadlessBrowserService {
|
|
|
}
|
|
|
} else {
|
|
|
logger.info(`[Python API] Service not available for baijiahao, falling back to direct API`);
|
|
|
- // Python 不可用时,回退到 Node 直连 API(可能仍会遇到分散认证问题)
|
|
|
- info = await this.fetchBaijiahaoAccountInfoDirectApi(cookies);
|
|
|
+ // #6085: Python 不可用时回退到 Node 直连 API,需捕获分散认证等 errno 异常
|
|
|
+ try {
|
|
|
+ info = await this.fetchBaijiahaoAccountInfoDirectApi(cookies);
|
|
|
+ } catch (apiError) {
|
|
|
+ logger.warn(`[Baijiahao] Direct API failed: ${apiError instanceof Error ? apiError.message : apiError}`);
|
|
|
+ // 分散认证等 errno 非 0 错误不一定是 cookie 失效,返回默认信息而非抛出异常
|
|
|
+ info = this.getDefaultAccountInfo(platform);
|
|
|
+ }
|
|
|
info.source = 'api';
|
|
|
info.pythonAvailable = false;
|
|
|
}
|
|
|
@@ -1315,6 +1427,26 @@ class HeadlessBrowserService {
|
|
|
logger.warn('[Douyin] Failed to navigate to data center:', error);
|
|
|
}
|
|
|
|
|
|
+ // #6088: 如果还没有获取到作品列表,主动访问内容管理页面触发 work_list API
|
|
|
+ if (!capturedData.worksList || capturedData.worksList.length === 0) {
|
|
|
+ logger.info('[Douyin] No works captured yet, navigating to content manage page to trigger work_list API...');
|
|
|
+ try {
|
|
|
+ await page.goto('https://creator.douyin.com/creator-micro/content/manage', {
|
|
|
+ waitUntil: 'domcontentloaded',
|
|
|
+ timeout: 15000,
|
|
|
+ });
|
|
|
+ await page.waitForTimeout(5000);
|
|
|
+
|
|
|
+ if (capturedData.worksList && capturedData.worksList.length > 0) {
|
|
|
+ logger.info(`[Douyin] Captured ${capturedData.worksList.length} works from content manage page`);
|
|
|
+ } else {
|
|
|
+ logger.warn('[Douyin] Still no works captured from content manage page');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ logger.warn('[Douyin] Failed to navigate to content manage page:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 检查登录状态 - 如果没有从 API 获取到,通过 URL 判断
|
|
|
if (!isLoggedIn) {
|
|
|
const currentUrl = page.url();
|
|
|
@@ -2280,8 +2412,9 @@ class HeadlessBrowserService {
|
|
|
|
|
|
const fetchNotesPage = async (pageNum: number) => {
|
|
|
return await page.evaluate(async (p) => {
|
|
|
+ // #6071: 添加 page_size=20 确保每页返回足够多的笔记(默认可能只有10条)
|
|
|
const response = await fetch(
|
|
|
- `https://edith.xiaohongshu.com/web_api/sns/v5/creator/note/user/posted?tab=0&page=${p}`,
|
|
|
+ `https://edith.xiaohongshu.com/web_api/sns/v5/creator/note/user/posted?tab=0&page=${p}&page_size=20`,
|
|
|
{
|
|
|
method: 'GET',
|
|
|
credentials: 'include',
|
|
|
@@ -2557,8 +2690,14 @@ class HeadlessBrowserService {
|
|
|
logger.info(`[Baijiahao API] appinfo response: errno=${appInfoData.errno}, errmsg=${appInfoData.errmsg}`);
|
|
|
|
|
|
if (appInfoData.errno !== 0) {
|
|
|
- logger.error(`[Baijiahao API] appinfo API error: errno=${appInfoData.errno}, errmsg=${appInfoData.errmsg}`);
|
|
|
- throw new Error(`appinfo API error: ${appInfoData.errmsg || 'Unknown error'}`);
|
|
|
+ // #6085: errno 非 0 不一定是 cookie 失效(如 errno=10001402 分散认证),
|
|
|
+ // 只有 errno=110 才明确表示未登录,其他 errno 返回默认信息避免同步中断
|
|
|
+ if (appInfoData.errno === 110) {
|
|
|
+ logger.error(`[Baijiahao API] Not logged in (errno=110)`);
|
|
|
+ throw new Error(`appinfo API error: errno=110, cookie expired`);
|
|
|
+ }
|
|
|
+ logger.warn(`[Baijiahao API] appinfo returned errno=${appInfoData.errno}, errmsg=${appInfoData.errmsg}, returning default info`);
|
|
|
+ return accountInfo;
|
|
|
}
|
|
|
|
|
|
if (!appInfoData.data?.user) {
|
|
|
@@ -4118,7 +4257,7 @@ class HeadlessBrowserService {
|
|
|
/**
|
|
|
* 通过 Python API 获取评论 - 分作品逐个获取
|
|
|
*/
|
|
|
- private async fetchCommentsViaPythonApi(platform: 'douyin' | 'xiaohongshu' | 'weixin', cookies: CookieData[]): Promise<WorkComments[]> {
|
|
|
+ private async fetchCommentsViaPythonApi(platform: 'douyin' | 'xiaohongshu' | 'weixin' | 'baijiahao', cookies: CookieData[]): Promise<WorkComments[]> {
|
|
|
const allWorkComments: WorkComments[] = [];
|
|
|
const cookieString = JSON.stringify(cookies);
|
|
|
const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
|
|
|
@@ -5089,6 +5228,30 @@ class HeadlessBrowserService {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * #6073: 获取百家号评论 - 优先使用 Python API
|
|
|
+ * 注: CommentService 调用了此方法但之前未实现,导致运行时 TypeError
|
|
|
+ */
|
|
|
+ async fetchBaijiahaoCommentsViaApi(cookies: CookieData[]): Promise<WorkComments[]> {
|
|
|
+ const pythonAvailable = await this.checkPythonServiceAvailable();
|
|
|
+ if (pythonAvailable) {
|
|
|
+ logger.info('[Baijiahao Comments] Using Python API...');
|
|
|
+ try {
|
|
|
+ const result = await this.fetchCommentsViaPythonApi('baijiahao', cookies);
|
|
|
+ if (result.length > 0) {
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ logger.info('[Baijiahao Comments] Python API returned empty');
|
|
|
+ } catch (pythonError) {
|
|
|
+ logger.warn('[Baijiahao Comments] Python API failed:', pythonError);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 百家号暂无 Playwright 评论抓取方案,返回空数组
|
|
|
+ logger.warn('[Baijiahao Comments] No fallback available, returning empty');
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 获取微信视频号评论 - 优先使用 Python API
|
|
|
*/
|
|
|
async fetchWeixinVideoCommentsViaApi(cookies: CookieData[]): Promise<WorkComments[]> {
|