diff --git a/application/common/library/scrm/AntiBotConfigBuilder.php b/application/common/library/scrm/AntiBotConfigBuilder.php index 4b47a68..84ea33d 100644 --- a/application/common/library/scrm/AntiBotConfigBuilder.php +++ b/application/common/library/scrm/AntiBotConfigBuilder.php @@ -147,12 +147,16 @@ class AntiBotConfigBuilder { if ($ticketType === 'whatshub') { return [ - 'postCfReadyUrlContains' => 'work-order-sharing', - 'postCfReadySelector' => '.el-input__inner', - 'postCfReadyApiUrl' => '/api/whatshub-counter/workShare/open/statistics', - 'postCfReadyTimeoutMs' => 60000, - 'postCfSettleMs' => 500, - 'postCfSpaWaitMs' => 4000, + 'postCfReadyUrlContains' => 'work-order-sharing', + // 业务页表格就绪即可;密码框为 vxe-form,勿用泛化 .el-input__inner + 'postCfReadySelector' => '.vxe-table, .vxe-grid', + 'postCfReadySelectorOptional' => true, + // statistics 仅在密码验证后触发,禁止 pre-auth 等待(见 Node skipPostCfReadyApi) + 'postCfReadyTimeoutMs' => 60000, + 'postCfSettleMs' => 500, + 'postCfSpaWaitMs' => 4000, + 'skipShortLinkCfPurgeOnReuse' => true, + 'authSkipIfNoPasswordDialog' => true, ]; } diff --git a/application/common/library/scrm/spider/WhatshubSpider.php b/application/common/library/scrm/spider/WhatshubSpider.php index b139bcc..e6a0afd 100644 --- a/application/common/library/scrm/spider/WhatshubSpider.php +++ b/application/common/library/scrm/spider/WhatshubSpider.php @@ -52,12 +52,13 @@ class WhatshubSpider extends AbstractScrmSpider } /** - * 混合同步:cURL 拉号码 + Node 监听 statistics 算完成量;任一失败则完整 Node fallback + * 混合同步:cURL 拉号码 + Node 监听 statistics 算完成量 + * statistics 失败时清 session 重试;仍失败则 fallback 完整 Node 仅取完成量 */ public function run(): UnifiedScrmData { $apiResult = $this->tryFetchViaShareCodeApi(); - $statsCount = $this->tryFetchStatisticsViaNode(); + $statsCount = $this->tryFetchStatisticsViaNode(false); if ($apiResult instanceof UnifiedScrmData && $statsCount !== null) { $apiResult->todayNewCount = $statsCount; @@ -68,26 +69,82 @@ class WhatshubSpider extends AbstractScrmSpider return $apiResult; } + // cURL 已成功:清 session 再试 statistics;仍失败则完整 Node 只取完成量 + if ($apiResult instanceof UnifiedScrmData) { + SplitTicketSyncLogger::log('spider', 'whatshub stats retry after purge session', [ + 'numberCount' => count($apiResult->numbers), + ]); + $statsCount = $this->tryFetchStatisticsViaNode(true); + if ($statsCount !== null) { + $apiResult->todayNewCount = $statsCount; + SplitTicketSyncLogger::log('spider', 'whatshub hybrid path ok (retry)', [ + 'numberCount' => count($apiResult->numbers), + 'todayNewCount' => $statsCount, + ]); + return $apiResult; + } + + SplitTicketSyncLogger::log('spider', 'whatshub stats fallback full node for complete count', [ + 'pageUrl' => $this->pageUrl, + 'numberCount' => count($apiResult->numbers), + ]); + try { + $fullResult = parent::run(); + $apiResult->todayNewCount = $fullResult->todayNewCount; + SplitTicketSyncLogger::log('spider', 'whatshub hybrid path ok (full node stats)', [ + 'numberCount' => count($apiResult->numbers), + 'todayNewCount' => $apiResult->todayNewCount, + ]); + return $apiResult; + } catch (\Throwable $e) { + SplitTicketSyncLogger::log('spider', 'whatshub stats fallback full node failed', [ + 'error' => $e->getMessage(), + ]); + } + + throw new \RuntimeException('Whatshub statistics 同步失败(号码已通过 API 获取,无法更新完成量)'); + } + SplitTicketSyncLogger::log('spider', 'whatshub hybrid incomplete, fallback full node', [ 'pageUrl' => $this->pageUrl, - 'apiOk' => $apiResult instanceof UnifiedScrmData, + 'apiOk' => false, 'statisticsOk' => $statsCount !== null, ]); return parent::run(); } + /** Whatshub vxe 密码表单:placeholder 含「密码」+ 确认按钮,与业务页搜索框区分 */ + private const AUTH_PASSWORD_INPUT = '.vxe-form .el-input__inner[placeholder*="密码"]'; + + private const AUTH_CONFIRM_BUTTON = '.vxe-form .vxe-button-group button.theme--primary[type="submit"]'; + /** * Node 监听 statistics API,返回 dayNewFans + reDayNewFans 总和;失败返回 null + * + * @param bool $isRetry 是否为清 session 后的二次尝试 */ - private function tryFetchStatisticsViaNode(): ?int + private function tryFetchStatisticsViaNode(bool $isRetry = false): ?int { $config = $this->getSpiderConfig(); $antiBot = $config['antiBot'] ?? null; + if (is_array($antiBot)) { + // statistics-only:早挂拦截器、识别 vxe 密码框、session 有效时 skip auth + $antiBot = array_merge($antiBot, [ + 'skipPostCfReadyApi' => true, + 'authSkipIfNoPasswordDialog' => true, + 'authSelectorTimeoutMs' => 30000, + 'earlyApiIntercept' => true, + ]); + if ($isRetry) { + $antiBot['purgeSession'] = true; + } + } SplitTicketSyncLogger::log('spider', 'whatshub stats node request', [ - 'pageUrl' => $this->pageUrl, - 'api' => self::API_DETAILS, + 'pageUrl' => $this->pageUrl, + 'api' => self::API_DETAILS, + 'isRetry' => $isRetry, ]); $result = $this->requestNode('/api/auth-and-intercept', [ @@ -101,6 +158,7 @@ class WhatshubSpider extends AbstractScrmSpider SplitTicketSyncLogger::log('spider', 'whatshub stats node failed', [ 'error' => $result['error'] ?? '未知', 'code' => $result['code'] ?? null, + 'page' => $result['page'] ?? null, ]); return null; } @@ -109,6 +167,7 @@ class WhatshubSpider extends AbstractScrmSpider if (!isset($intercepted[self::API_DETAILS]['data']) || !is_array($intercepted[self::API_DETAILS]['data'])) { SplitTicketSyncLogger::log('spider', 'whatshub stats node missing payload', [ 'intercepted' => array_keys(is_array($intercepted) ? $intercepted : []), + 'page' => $result['page'] ?? null, ]); return null; } @@ -287,10 +346,10 @@ class WhatshubSpider extends AbstractScrmSpider 'listMethod' => 'POST', 'paginationMode' => self::MODE_UI, 'authActions' => [ - // Element Plus 密码弹窗:仅注入可见 input - ['type' => 'vue_fill', 'selector' => '.el-input__inner', 'value' => $this->password], + // Whatshub 密码 UI 为 vxe-form + Element input,限定在表单内避免误填搜索框 + ['type' => 'vue_fill', 'selector' => self::AUTH_PASSWORD_INPUT, 'value' => $this->password], ['type' => 'wait', 'ms' => 500], - ['type' => 'vue_click', 'selector' => '.vxe-button-group .theme--primary'], + ['type' => 'vue_click', 'selector' => self::AUTH_CONFIRM_BUTTON], ['type' => 'wait', 'ms' => 2200], ], 'antiBot' => AntiBotConfigBuilder::build('whatshub', $this->pageUrl), diff --git a/patches/application/common/library/scrm/AntiBotConfigBuilder.php b/patches/application/common/library/scrm/AntiBotConfigBuilder.php index 4b47a68..84ea33d 100644 --- a/patches/application/common/library/scrm/AntiBotConfigBuilder.php +++ b/patches/application/common/library/scrm/AntiBotConfigBuilder.php @@ -147,12 +147,16 @@ class AntiBotConfigBuilder { if ($ticketType === 'whatshub') { return [ - 'postCfReadyUrlContains' => 'work-order-sharing', - 'postCfReadySelector' => '.el-input__inner', - 'postCfReadyApiUrl' => '/api/whatshub-counter/workShare/open/statistics', - 'postCfReadyTimeoutMs' => 60000, - 'postCfSettleMs' => 500, - 'postCfSpaWaitMs' => 4000, + 'postCfReadyUrlContains' => 'work-order-sharing', + // 业务页表格就绪即可;密码框为 vxe-form,勿用泛化 .el-input__inner + 'postCfReadySelector' => '.vxe-table, .vxe-grid', + 'postCfReadySelectorOptional' => true, + // statistics 仅在密码验证后触发,禁止 pre-auth 等待(见 Node skipPostCfReadyApi) + 'postCfReadyTimeoutMs' => 60000, + 'postCfSettleMs' => 500, + 'postCfSpaWaitMs' => 4000, + 'skipShortLinkCfPurgeOnReuse' => true, + 'authSkipIfNoPasswordDialog' => true, ]; } diff --git a/patches/application/common/library/scrm/spider/WhatshubSpider.php b/patches/application/common/library/scrm/spider/WhatshubSpider.php index b139bcc..e6a0afd 100644 --- a/patches/application/common/library/scrm/spider/WhatshubSpider.php +++ b/patches/application/common/library/scrm/spider/WhatshubSpider.php @@ -52,12 +52,13 @@ class WhatshubSpider extends AbstractScrmSpider } /** - * 混合同步:cURL 拉号码 + Node 监听 statistics 算完成量;任一失败则完整 Node fallback + * 混合同步:cURL 拉号码 + Node 监听 statistics 算完成量 + * statistics 失败时清 session 重试;仍失败则 fallback 完整 Node 仅取完成量 */ public function run(): UnifiedScrmData { $apiResult = $this->tryFetchViaShareCodeApi(); - $statsCount = $this->tryFetchStatisticsViaNode(); + $statsCount = $this->tryFetchStatisticsViaNode(false); if ($apiResult instanceof UnifiedScrmData && $statsCount !== null) { $apiResult->todayNewCount = $statsCount; @@ -68,26 +69,82 @@ class WhatshubSpider extends AbstractScrmSpider return $apiResult; } + // cURL 已成功:清 session 再试 statistics;仍失败则完整 Node 只取完成量 + if ($apiResult instanceof UnifiedScrmData) { + SplitTicketSyncLogger::log('spider', 'whatshub stats retry after purge session', [ + 'numberCount' => count($apiResult->numbers), + ]); + $statsCount = $this->tryFetchStatisticsViaNode(true); + if ($statsCount !== null) { + $apiResult->todayNewCount = $statsCount; + SplitTicketSyncLogger::log('spider', 'whatshub hybrid path ok (retry)', [ + 'numberCount' => count($apiResult->numbers), + 'todayNewCount' => $statsCount, + ]); + return $apiResult; + } + + SplitTicketSyncLogger::log('spider', 'whatshub stats fallback full node for complete count', [ + 'pageUrl' => $this->pageUrl, + 'numberCount' => count($apiResult->numbers), + ]); + try { + $fullResult = parent::run(); + $apiResult->todayNewCount = $fullResult->todayNewCount; + SplitTicketSyncLogger::log('spider', 'whatshub hybrid path ok (full node stats)', [ + 'numberCount' => count($apiResult->numbers), + 'todayNewCount' => $apiResult->todayNewCount, + ]); + return $apiResult; + } catch (\Throwable $e) { + SplitTicketSyncLogger::log('spider', 'whatshub stats fallback full node failed', [ + 'error' => $e->getMessage(), + ]); + } + + throw new \RuntimeException('Whatshub statistics 同步失败(号码已通过 API 获取,无法更新完成量)'); + } + SplitTicketSyncLogger::log('spider', 'whatshub hybrid incomplete, fallback full node', [ 'pageUrl' => $this->pageUrl, - 'apiOk' => $apiResult instanceof UnifiedScrmData, + 'apiOk' => false, 'statisticsOk' => $statsCount !== null, ]); return parent::run(); } + /** Whatshub vxe 密码表单:placeholder 含「密码」+ 确认按钮,与业务页搜索框区分 */ + private const AUTH_PASSWORD_INPUT = '.vxe-form .el-input__inner[placeholder*="密码"]'; + + private const AUTH_CONFIRM_BUTTON = '.vxe-form .vxe-button-group button.theme--primary[type="submit"]'; + /** * Node 监听 statistics API,返回 dayNewFans + reDayNewFans 总和;失败返回 null + * + * @param bool $isRetry 是否为清 session 后的二次尝试 */ - private function tryFetchStatisticsViaNode(): ?int + private function tryFetchStatisticsViaNode(bool $isRetry = false): ?int { $config = $this->getSpiderConfig(); $antiBot = $config['antiBot'] ?? null; + if (is_array($antiBot)) { + // statistics-only:早挂拦截器、识别 vxe 密码框、session 有效时 skip auth + $antiBot = array_merge($antiBot, [ + 'skipPostCfReadyApi' => true, + 'authSkipIfNoPasswordDialog' => true, + 'authSelectorTimeoutMs' => 30000, + 'earlyApiIntercept' => true, + ]); + if ($isRetry) { + $antiBot['purgeSession'] = true; + } + } SplitTicketSyncLogger::log('spider', 'whatshub stats node request', [ - 'pageUrl' => $this->pageUrl, - 'api' => self::API_DETAILS, + 'pageUrl' => $this->pageUrl, + 'api' => self::API_DETAILS, + 'isRetry' => $isRetry, ]); $result = $this->requestNode('/api/auth-and-intercept', [ @@ -101,6 +158,7 @@ class WhatshubSpider extends AbstractScrmSpider SplitTicketSyncLogger::log('spider', 'whatshub stats node failed', [ 'error' => $result['error'] ?? '未知', 'code' => $result['code'] ?? null, + 'page' => $result['page'] ?? null, ]); return null; } @@ -109,6 +167,7 @@ class WhatshubSpider extends AbstractScrmSpider if (!isset($intercepted[self::API_DETAILS]['data']) || !is_array($intercepted[self::API_DETAILS]['data'])) { SplitTicketSyncLogger::log('spider', 'whatshub stats node missing payload', [ 'intercepted' => array_keys(is_array($intercepted) ? $intercepted : []), + 'page' => $result['page'] ?? null, ]); return null; } @@ -287,10 +346,10 @@ class WhatshubSpider extends AbstractScrmSpider 'listMethod' => 'POST', 'paginationMode' => self::MODE_UI, 'authActions' => [ - // Element Plus 密码弹窗:仅注入可见 input - ['type' => 'vue_fill', 'selector' => '.el-input__inner', 'value' => $this->password], + // Whatshub 密码 UI 为 vxe-form + Element input,限定在表单内避免误填搜索框 + ['type' => 'vue_fill', 'selector' => self::AUTH_PASSWORD_INPUT, 'value' => $this->password], ['type' => 'wait', 'ms' => 500], - ['type' => 'vue_click', 'selector' => '.vxe-button-group .theme--primary'], + ['type' => 'vue_click', 'selector' => self::AUTH_CONFIRM_BUTTON], ['type' => 'wait', 'ms' => 2200], ], 'antiBot' => AntiBotConfigBuilder::build('whatshub', $this->pageUrl), diff --git a/patches/puppeteer-api/server.js b/patches/puppeteer-api/server.js index 170d849..f75d548 100755 --- a/patches/puppeteer-api/server.js +++ b/patches/puppeteer-api/server.js @@ -61,9 +61,11 @@ const { getSessionDebugInfo, getSessionStats, isSessionLikelyValid, + loadSession, } = require('./lib/session-store'); const { resolveSessionContext, touchSessionIfPresent } = require('./lib/session-context'); const { getProfileStats, profileExists } = require('./lib/profile-dir'); +const { getProfileStats, profileExists } = require('./lib/profile-dir'); const { isHuojianLongLinkUrl, isHuojianShortEntryUrl, @@ -86,6 +88,27 @@ async function runBrowserTask(taskName, handler, profile = 'standard') { return runWithProfileLimit(taskName, profile, handler); } + +/** PHP antiBot.purgeSession:清除磁盘 session 文件(不修改 session-context 模块) */ +function purgeStoredSessionIfRequested(antiBot) { + const sessionKey = antiBot?.sessionKey || ''; + if (!sessionKey || antiBot?.purgeSession !== true) { + return false; + } + const crypto = require('crypto'); + const path = require('path'); + const hash = crypto.createHash('md5').update(sessionKey).digest('hex'); + const filePath = path.join(RUNTIME_DIR, 'sessions', `${hash}.json`); + try { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + console.log(`[Session] purgeSession 已清除 sessionKey=${sessionKey}`); + return true; + } + } catch (_) { /* ignore */ } + return false; +} + function sendTaskError(res, error, extra = {}) { if (error.code === 'QUEUE_TIMEOUT' || error.message === 'POOL_ACQUIRE_TIMEOUT') { return res.status(503).json({ @@ -190,6 +213,30 @@ async function waitForUrlSettled(page, { maxAttempts = 15, stableMs = 500, redir return currentUrl; } +/** PHP → Node 透传的 Whatshub/auth 扩展字段(normalizeAntiBot 不含这些项) */ +const WHATSHUB_AUTH_EXTRA_KEYS = [ + 'authSkipIfNoPasswordDialog', + 'authSelectorTimeoutMs', + 'earlyApiIntercept', + 'skipPostCfReadyApi', + 'purgeSession', + 'postCfReadySelectorOptional', + 'skipShortLinkCfPurgeOnReuse', +]; + +function pickAntiBotExtraFields(rawAntiBot, keys) { + const extras = {}; + if (!rawAntiBot || typeof rawAntiBot !== 'object') { + return extras; + } + for (const key of keys) { + if (rawAntiBot[key] !== undefined) { + extras[key] = rawAntiBot[key]; + } + } + return extras; +} + /** 合并 antiBot 扩展字段(postCfReady* 由 PHP 按工单类型传入,未配置则跳过) */ function resolveAntiBot(rawAntiBot) { const base = normalizeAntiBot(rawAntiBot); @@ -198,6 +245,7 @@ function resolveAntiBot(rawAntiBot) { } return { ...base, + ...pickAntiBotExtraFields(rawAntiBot, WHATSHUB_AUTH_EXTRA_KEYS), postCfReadyUrlContains: rawAntiBot.postCfReadyUrlContains || '', postCfReadySelector: rawAntiBot.postCfReadySelector || '', postCfReadyApiUrl: rawAntiBot.postCfReadyApiUrl || '', @@ -237,11 +285,13 @@ function applyWhatshubPostCfDefaults(antiBot, pageUrl) { return { ...antiBot, postCfReadyUrlContains: 'work-order-sharing', - postCfReadySelector: '.el-input__inner', - postCfReadyApiUrl: '/api/whatshub-counter/workShare/open/statistics', + postCfReadySelector: '.vxe-table, .vxe-grid', + postCfReadySelectorOptional: true, postCfReadyTimeoutMs: antiBot.postCfReadyTimeoutMs || 60000, postCfSettleMs: antiBot.postCfSettleMs || 500, postCfSpaWaitMs: antiBot.postCfSpaWaitMs || 4000, + skipShortLinkCfPurgeOnReuse: antiBot.skipShortLinkCfPurgeOnReuse !== false, + authSkipIfNoPasswordDialog: antiBot.authSkipIfNoPasswordDialog !== false, }; } catch (_) { return antiBot; @@ -392,6 +442,26 @@ async function waitForPostCfSelector(page, selector, timeoutMs) { await page.waitForSelector(selector, { timeout: Math.min(15000, timeoutMs), visible: true }); } +/** 可选 selector:URL 已到业务页时允许降级继续(Whatshub session 复用) */ +async function waitForPostCfSelectorOptional(page, selector, timeoutMs, antiBot) { + const optional = antiBot?.postCfReadySelectorOptional === true; + if (!optional) { + await waitForPostCfSelector(page, selector, timeoutMs); + return; + } + const shortTimeout = Math.min(8000, timeoutMs); + try { + await waitForPostCfSelector(page, selector, shortTimeout); + } catch (err) { + const urlContains = (antiBot.postCfReadyUrlContains || '').trim(); + if (urlContains && page.url().includes(urlContains)) { + console.log(`[CF] 可选 selector "${selector}" 未就绪但 URL 已到业务页,继续`); + return; + } + throw err; + } +} + /** Whatshub 分享短链 /m/xxx/1 */ function isWhatshubShortLinkPath(url) { try { @@ -402,6 +472,116 @@ function isWhatshubShortLinkPath(url) { } } +/** Whatshub 域名判定 */ +function isWhatshubHost(url) { + try { + return new URL(url || '').hostname.includes('whatshub.cc'); + } catch (_) { + return false; + } +} + +/** Whatshub 密码弹窗 DOM 选择器(与 PHP WhatshubSpider authActions 保持一致) */ +const WHATSHUB_PASSWORD_INPUT_SEL = '.vxe-form .el-input__inner[placeholder*="密码"]'; +const WHATSHUB_PASSWORD_CONFIRM_SEL = '.vxe-form .vxe-button-group button.theme--primary[type="submit"]'; + +/** Whatshub 密码 UI 是否可见(必须密码 input + 确认按钮同框,排除搜索区 vxe-form) */ +async function isWhatshubPasswordDialogVisible(page) { + return page.evaluate(() => { + const isVisible = (el) => { + if (!el) return false; + const rect = el.getBoundingClientRect(); + if (rect.width <= 0 || rect.height <= 0) return false; + const style = window.getComputedStyle(el); + return style.display !== 'none' && style.visibility !== 'hidden'; + }; + const isPasswordInput = (input) => { + if (!input || !isVisible(input)) return false; + const placeholder = (input.getAttribute('placeholder') || '').trim(); + if (placeholder.includes('密码')) return true; + return input.getAttribute('maxlength') === '8'; + }; + const rootHasPasswordForm = (root) => { + if (!root || !isVisible(root)) return false; + const pwd = root.querySelector('.el-input__inner[placeholder*="密码"], .el-input__inner[maxlength="8"]'); + if (!isPasswordInput(pwd)) return false; + const confirm = root.querySelector( + '.vxe-button-group button.theme--primary[type="submit"], button.theme--primary[type="submit"]' + ); + return !!(confirm && isVisible(confirm)); + }; + const roots = document.querySelectorAll( + 'form.vxe-form, .vxe-modal--wrapper, .vxe-modal, ' + + '.el-message-box, .el-overlay-message-box, .el-dialog, .el-overlay-dialog' + ); + for (const root of roots) { + if (rootHasPasswordForm(root)) return true; + } + return false; + }).catch(() => false); +} + +/** 业务页表格已就绪且页面上无密码确认按钮 → 视为 session 已登录 */ +async function isWhatshubBusinessLoggedIn(page) { + return page.evaluate(() => { + const isVisible = (el) => { + if (!el) return false; + const rect = el.getBoundingClientRect(); + if (rect.width <= 0 || rect.height <= 0) return false; + const style = window.getComputedStyle(el); + return style.display !== 'none' && style.visibility !== 'hidden'; + }; + const tableReady = !!document.querySelector('.vxe-table, .vxe-grid'); + const pwdDialog = document.querySelector('.vxe-form .el-input__inner[placeholder*="密码"]'); + const confirmBtn = document.querySelector( + '.vxe-form .vxe-button-group button.theme--primary[type="submit"]' + ); + const hasPasswordUi = (pwdDialog && isVisible(pwdDialog)) + && (confirmBtn && isVisible(confirmBtn)); + return tableReady && !hasPasswordUi; + }).catch(() => false); +} + +/** statistics-only 等场景:导航后立即挂 API 拦截器,避免 session 复用时响应早于监听 */ +function shouldUseEarlyApiIntercept(antiBot, apiUrls) { + if (antiBot?.earlyApiIntercept === true) { + return true; + } + if (!Array.isArray(apiUrls) || apiUrls.length === 0) { + return false; + } + return apiUrls.every((u) => String(u).includes('/open/statistics')); +} + +/** statistics-only 拦截时不应在密码前等待业务 API */ +function shouldSkipPostCfReadyApiWait(antiBot, apiUrls = null) { + if (antiBot?.skipPostCfReadyApi) { + return true; + } + if (Array.isArray(apiUrls) && apiUrls.length > 0) { + return apiUrls.every((u) => String(u).includes('/open/statistics')); + } + return false; +} + +/** Whatshub 使用 PHP 配置的 postCfSpaWaitMs;A2C 等仍保底 8s */ +function resolveSpaWaitMs(antiBot, onA2cDirect, pageUrl = '') { + const configured = antiBot?.postCfSpaWaitMs || 0; + if (onA2cDirect) { + return Math.max(8000, configured || 8000); + } + if (isWhatshubShortLinkPath(pageUrl) || isWhatshubHost(pageUrl)) { + return Math.max(2000, configured || 4000); + } + return Math.max(8000, configured || 8000); +} + +/** authActions 选择器超时(Whatshub session 复用时可放宽) */ +function resolveAuthSelectorTimeoutMs(antiBot) { + const ms = antiBot?.authSelectorTimeoutMs; + return typeof ms === 'number' && ms > 0 ? ms : 15000; +} + /** * Whatshub 短链 + Real Browser:不覆盖 UA、不预注入磁盘 cf_clearance * @param {string} pageUrl @@ -766,10 +946,8 @@ async function waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettledFn, return false; } - const spaWaitMs = onA2cDirect - ? Math.max(8000, antiBot.postCfSpaWaitMs || 8000) - : Math.max(8000, antiBot.postCfSpaWaitMs || 8000); - const isWhatshub = isWhatshubShortLinkPath(targetUrl); + const spaWaitMs = resolveSpaWaitMs(antiBot, onA2cDirect, targetUrl); + const isWhatshub = isWhatshubShortLinkPath(targetUrl) || isWhatshubHost(targetUrl); console.log( `[CF] CF 已完成,等待 SPA ${onA2cDirect ? '直链挂载' : '跳转'} settle=${spaWaitMs}ms current=${page.url()}` @@ -1132,11 +1310,22 @@ async function purgeCfClearanceFully(page, pageUrl) { /** * Whatshub 短链首访:清除 Chrome Profile 内陈旧 cf_clearance,确保 Turnstile+SPA 在同一 goto 生命周期 + * session 复用时跳过 purge,保留 clearance 加速二次同步 */ -async function prepareWhatshubShortLinkBeforeNav(page, pageUrl, antiBot) { +async function prepareWhatshubShortLinkBeforeNav(page, pageUrl, antiBot, storedSession = null) { if (!antiBot?.enabled || antiBot.profile !== 'real' || !isWhatshubShortLinkPath(pageUrl)) { return; } + if (antiBot.skipShortLinkCfPurgeOnReuse) { + let session = storedSession; + if (!session && antiBot.sessionKey) { + session = loadSession(antiBot.sessionKey); + } + if (session && isSessionLikelyValid(session)) { + console.log('[Whatshub] 有效 session 复用,跳过短链 cf_clearance 清除'); + return; + } + } const cookies = await page.cookies(); if (!cookies.some((c) => c.name === 'cf_clearance')) { return; @@ -1260,14 +1449,18 @@ async function collectPageDiagnostics(page) { title: document.title || '', bodyText: (document.body && document.body.innerText ? document.body.innerText : '').slice(0, 500), elInputInner: document.querySelectorAll('.el-input__inner').length, + vxeForm: document.querySelectorAll('form.vxe-form').length, + vxePasswordInput: document.querySelectorAll('.vxe-form .el-input__inner[placeholder*="密码"]').length, + vxeConfirmBtn: document.querySelectorAll('.vxe-form .vxe-button-group button.theme--primary[type="submit"]').length, elTable: document.querySelectorAll('.el-table').length, + vxeTable: document.querySelectorAll('.vxe-table, .vxe-grid').length, btnNext: document.querySelectorAll('.btn-next').length, elPagination: document.querySelectorAll('.el-pagination').length, })).catch(() => null); return snapshot || { url, title: '', bodyText: '', elInputInner: 0 }; } -async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, storedSession = null, pageUrl = '') { +async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, storedSession = null, pageUrl = '', apiUrls = null) { if (!antiBot || !antiBot.enabled) { return; } @@ -1278,10 +1471,12 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor } const navUrl = pageUrl || page.url(); + const skipApiWait = shouldSkipPostCfReadyApiWait(antiBot, apiUrls); await ensureCfCompleteBeforeBusinessReady(page, antiBot, waitForUrlSettledFn, storedSession); const onA2cDirect = isA2cDirectSharePage(page, urlContains); + const whatshubHost = isWhatshubHost(navUrl) || isWhatshubHost(page.url()); let skipSpaWait = false; if (onA2cDirect && await isCfTrulyComplete(page, antiBot)) { const domReady = selector @@ -1292,6 +1487,17 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor skipSpaWait = true; } } + // Whatshub session 复用:已在 work-order-sharing 且无密码弹窗时跳过 SPA 固定等待 + if (!skipSpaWait && whatshubHost && urlContains && page.url().includes(urlContains)) { + if (await isCfTrulyComplete(page, antiBot)) { + const pwdVisible = await isWhatshubPasswordDialogVisible(page); + const tableReady = await page.$('.vxe-table, .vxe-grid').catch(() => null); + if (!pwdVisible && (tableReady || (storedSession && isSessionLikelyValid(storedSession)))) { + console.log('[CF] Whatshub 业务页已就绪(session 复用),跳过 SPA 等待'); + skipSpaWait = true; + } + } + } if (!skipSpaWait) { await waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettledFn, navUrl); } @@ -1299,7 +1505,7 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor const timeoutMs = Math.max(5000, antiBot.postCfReadyTimeoutMs || 30000); console.log( `[CF] 等待业务页就绪 current=${page.url()} urlContains="${urlContains}" ` - + `selector="${selector}" timeout=${timeoutMs}ms` + + `selector="${selector}" timeout=${timeoutMs}ms skipApiWait=${skipApiWait}` ); try { @@ -1310,9 +1516,18 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor urlContains ); } - await waitForPostCfListApi(page, antiBot, timeoutMs); + if (!skipApiWait) { + await waitForPostCfListApi(page, antiBot, timeoutMs); + } else if (antiBot?.authSkipIfNoPasswordDialog && isStatisticsOnlyApiUrls(apiUrls)) { + console.log( + '[CF] 跳过 pre-auth statistics 等待' + + '(sid 直进业务页:auth 跳过后 reload 补捕获;需填密码:auth 提交后 waitForApis)' + ); + } else { + console.log('[CF] 跳过 pre-auth 业务 API 等待(statistics 在密码验证后触发)'); + } if (selector !== '') { - await waitForPostCfSelector(page, selector, timeoutMs); + await waitForPostCfSelectorOptional(page, selector, timeoutMs, antiBot); } } catch (err) { const retryMs = Math.min(30000, timeoutMs); @@ -1331,7 +1546,7 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor ); } if (selector !== '') { - await waitForPostCfSelector(page, selector, retryMs); + await waitForPostCfSelectorOptional(page, selector, retryMs, antiBot); } } catch (retryErr) { retryErr.pageDiagnostics = await collectPageDiagnostics(page); @@ -1351,9 +1566,11 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor urlContains ); } - await waitForPostCfListApi(page, antiBot, retryMs); + if (!skipApiWait) { + await waitForPostCfListApi(page, antiBot, retryMs); + } if (selector !== '') { - await waitForPostCfSelector(page, selector, retryMs); + await waitForPostCfSelectorOptional(page, selector, retryMs, antiBot); } } catch (retryErr) { retryErr.pageDiagnostics = await collectPageDiagnostics(page); @@ -1365,9 +1582,11 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor try { await page.waitForNetworkIdle({ idleTime: 500, timeout: 20000 }).catch(() => {}); await softReloadForSpa(page, waitForUrlSettledFn, antiBot, navUrl); - await waitForPostCfListApi(page, antiBot, retryMs); + if (!skipApiWait) { + await waitForPostCfListApi(page, antiBot, retryMs); + } if (selector !== '') { - await waitForPostCfSelector(page, selector, retryMs); + await waitForPostCfSelectorOptional(page, selector, retryMs, antiBot); } } catch (retryErr) { retryErr.pageDiagnostics = await collectPageDiagnostics(page); @@ -1386,24 +1605,191 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor console.log(`[CF] 业务页就绪 url=${page.url()}`); } -async function executeAuthActionsWithDiagnostics(page, authActions) { +async function executeAuthActionsWithDiagnostics(page, authActions, antiBot = null) { try { - await executeAuthActions(page, authActions); + return await executeAuthActions(page, authActions, antiBot); } catch (err) { err.pageDiagnostics = await collectPageDiagnostics(page); throw err; } } -async function executeAuthActions(page, authActions) { - if (!Array.isArray(authActions)) return; +/** 采集 Whatshub auth 决策所需的 DOM 计数(与 collectPageDiagnostics 字段对齐) */ +async function collectWhatshubAuthDomCounts(page) { + return page.evaluate(() => ({ + vxeTable: document.querySelectorAll('.vxe-table, .vxe-grid').length, + vxePasswordInput: document.querySelectorAll( + '.vxe-form .el-input__inner[placeholder*="密码"]' + ).length, + vxeConfirmBtn: document.querySelectorAll( + '.vxe-form .vxe-button-group button.theme--primary[type="submit"]' + ).length, + })).catch(() => ({ vxeTable: 0, vxePasswordInput: 0, vxeConfirmBtn: 0 })); +} + +/** + * Whatshub skip-auth 决策:URL 仅确认路由,登录/业务态靠 DOM + * @returns {Promise<{skip: boolean, reason: string, pwdVisible?: boolean, businessLoggedIn?: boolean, dom?: object, url?: string}>} + */ +async function resolveWhatshubAuthSkipDecision(page, antiBot) { + if (!antiBot?.authSkipIfNoPasswordDialog) { + return { skip: false, reason: 'authSkipIfNoPasswordDialog_disabled' }; + } + const urlContains = (antiBot.postCfReadyUrlContains || 'work-order-sharing').trim(); + let currentUrl = ''; + try { + currentUrl = page.url(); + } catch (_) { + return { skip: false, reason: 'page_url_unavailable' }; + } + const onBusinessRoute = !urlContains || currentUrl.includes(urlContains); + if (!onBusinessRoute) { + return { skip: false, reason: 'not_on_business_route', url: currentUrl }; + } + + const pwdVisible = await isWhatshubPasswordDialogVisible(page); + const businessLoggedIn = await isWhatshubBusinessLoggedIn(page); + const dom = await collectWhatshubAuthDomCounts(page); + + if (pwdVisible) { + return { + skip: false, + reason: 'password_dialog_visible', + pwdVisible, + businessLoggedIn, + dom, + url: currentUrl, + }; + } + const tableReadyNoPassword = dom.vxeTable > 0 + && dom.vxePasswordInput === 0 + && dom.vxeConfirmBtn === 0; + if (businessLoggedIn || tableReadyNoPassword) { + return { + skip: true, + reason: businessLoggedIn ? 'business_logged_in' : 'table_ready_no_password_ui', + pwdVisible, + businessLoggedIn, + dom, + url: currentUrl, + }; + } + return { + skip: true, + reason: 'no_password_dialog_on_business_route', + pwdVisible, + businessLoggedIn, + dom, + url: currentUrl, + }; +} + +function logWhatshubAuthSkipDecision(decision) { + const dom = decision.dom || {}; + console.log( + '[Whatshub] auth skip 决策' + + ` skip=${decision.skip}` + + ` reason=${decision.reason}` + + ` pwdVisible=${decision.pwdVisible}` + + ` businessLoggedIn=${decision.businessLoggedIn}` + + ` vxeTable=${dom.vxeTable ?? '?'}` + + ` vxePasswordInput=${dom.vxePasswordInput ?? '?'}` + + ` vxeConfirmBtn=${dom.vxeConfirmBtn ?? '?'}` + + ` url=${decision.url || ''}` + ); +} + +async function shouldSkipWhatshubAuthActions(page, antiBot) { + const decision = await resolveWhatshubAuthSkipDecision(page, antiBot); + logWhatshubAuthSkipDecision(decision); + return decision.skip; +} + +/** + * Loading 结束后才会出现密码框或业务表格;先等二者之一再决定是否需要填密码 + * @returns {Promise} true=应跳过 authActions + */ +async function waitForWhatshubPasswordFormIfNeeded(page, antiBot) { + if (!antiBot || !(isWhatshubHost(page.url()) || page.url().includes('work-order-sharing'))) { + return false; + } + const initial = await resolveWhatshubAuthSkipDecision(page, antiBot); + logWhatshubAuthSkipDecision({ ...initial, reason: `pre_wait_${initial.reason}` }); + if (initial.skip) { + return true; + } + + const timeout = resolveAuthSelectorTimeoutMs(antiBot); + const surfaceTimeout = Math.min(timeout, 10000); + console.log('[Whatshub] 等待密码框或业务表格渲染(Loading 结束后)...'); + await page.waitForFunction( + () => { + const isVisible = (el) => { + if (!el) return false; + const rect = el.getBoundingClientRect(); + if (rect.width <= 0 || rect.height <= 0) return false; + const style = window.getComputedStyle(el); + return style.display !== 'none' && style.visibility !== 'hidden'; + }; + if (document.querySelector('.vxe-table, .vxe-grid')) { + return true; + } + const roots = document.querySelectorAll( + 'form.vxe-form, .vxe-modal--wrapper, .vxe-modal, .el-dialog, .el-overlay-dialog' + ); + for (const root of roots) { + const pwd = root.querySelector('.el-input__inner[placeholder*="密码"], .el-input__inner[maxlength="8"]'); + const confirm = root.querySelector( + '.vxe-button-group button.theme--primary[type="submit"], button.theme--primary[type="submit"]' + ); + if (isVisible(pwd) && isVisible(confirm)) { + return true; + } + } + return false; + }, + { timeout: surfaceTimeout, polling: 200 } + ).catch(() => null); + + const afterSurface = await resolveWhatshubAuthSkipDecision(page, antiBot); + logWhatshubAuthSkipDecision({ ...afterSurface, reason: `after_surface_${afterSurface.reason}` }); + if (afterSurface.skip) { + return true; + } + + console.log('[Whatshub] 等待 vxe 密码表单出现(password input + confirm)...'); + await page.waitForSelector(WHATSHUB_PASSWORD_INPUT_SEL, { visible: true, timeout }).catch(() => null); + await page.waitForSelector(WHATSHUB_PASSWORD_CONFIRM_SEL, { visible: true, timeout }).catch(() => null); + + const afterWait = await resolveWhatshubAuthSkipDecision(page, antiBot); + logWhatshubAuthSkipDecision({ ...afterWait, reason: `after_password_wait_${afterWait.reason}` }); + return afterWait.skip; +} + +async function executeAuthActions(page, authActions, antiBot = null) { + if (!Array.isArray(authActions)) { + return { skipped: false, ran: false }; + } + + if (await shouldSkipWhatshubAuthActions(page, antiBot)) { + console.log('[Whatshub] Session 已登录且无密码弹窗,跳过 authActions'); + return { skipped: true, ran: false }; + } + + const skipAfterWait = await waitForWhatshubPasswordFormIfNeeded(page, antiBot); + if (skipAfterWait) { + console.log('[Whatshub] 密码等待后复检:业务页已就绪,跳过 authActions'); + return { skipped: true, ran: false }; + } + + const selectorTimeout = resolveAuthSelectorTimeoutMs(antiBot); for (const action of authActions) { if (action.type === 'type') { - await page.waitForSelector(action.selector, { timeout: 15000 }); + await page.waitForSelector(action.selector, { timeout: selectorTimeout }); await page.type(action.selector, action.value, { delay: 100 }); } else if (action.type === 'click') { - await page.waitForSelector(action.selector, { timeout: 15000 }); + await page.waitForSelector(action.selector, { timeout: selectorTimeout }); await page.evaluate((sel) => { const el = document.querySelector(sel); if (el) el.click(); }, action.selector); } else if (action.type === 'press') { await new Promise((r) => setTimeout(r, 200)); @@ -1411,7 +1797,7 @@ async function executeAuthActions(page, authActions) { } else if (action.type === 'wait') { await new Promise((r) => setTimeout(r, action.ms)); } else if (action.type === 'vue_fill') { - await page.waitForSelector(action.selector, { timeout: 15000 }); + await page.waitForSelector(action.selector, { timeout: selectorTimeout }); await page.waitForFunction( (sel) => { const el = document.querySelector(sel); @@ -1436,7 +1822,7 @@ async function executeAuthActions(page, authActions) { } }, action.selector, action.value); } else if (action.type === 'vue_click') { - await page.waitForSelector(action.selector, { timeout: 15000 }); + await page.waitForSelector(action.selector, { timeout: selectorTimeout }); await page.waitForFunction( (sel) => { const el = document.querySelector(sel); @@ -1473,6 +1859,78 @@ async function executeAuthActions(page, authActions) { }, action.selector, action.text); } } + return { skipped: false, ran: true }; +} + +/** 是否 statistics-only 拦截(Whatshub 完成量) */ +function isStatisticsOnlyApiUrls(apiUrls) { + if (!Array.isArray(apiUrls) || apiUrls.length === 0) { + return false; + } + return apiUrls.every((u) => String(u).includes('/open/statistics')); +} + +function areAllApisIntercepted(interceptor, apiUrls) { + if (!interceptor || !Array.isArray(apiUrls)) { + return false; + } + return apiUrls.every((api) => !!interceptor.interceptedApis[api]); +} + +function createApiInterceptorHandle() { + return { interceptor: null, boundPage: null, cleanup: null }; +} + +function attachApiInterceptorToPage(handle, page, apiUrls, timeoutMs, label = '') { + if (handle.interceptor && handle.boundPage === page) { + return handle.interceptor; + } + if (handle.cleanup) { + handle.cleanup(); + handle.cleanup = null; + } + handle.interceptor = createApiInterceptor(page, apiUrls, timeoutMs); + handle.boundPage = page; + handle.cleanup = handle.interceptor.cleanup; + const tag = label ? ` (${label})` : ''; + console.log(`[API] 拦截器已挂载${tag} apis=${apiUrls.join(',')} url=${page.url()}`); + return handle.interceptor; +} + +async function ensureWhatshubStatisticsAfterAuthSkip(page, interceptor, apiUrls, authSkipped, interceptTimeout) { + if (!authSkipped || !isStatisticsOnlyApiUrls(apiUrls)) { + return; + } + if (areAllApisIntercepted(interceptor, apiUrls)) { + console.log('[Whatshub] statistics 已在页面加载时捕获'); + return; + } + + console.log( + '[Whatshub] auth 已跳过但 statistics 未捕获' + + '(可能 CF 换页后漏包或 sid 直进业务页时 pre-auth 未等待),reload 补触发' + ); + + await new Promise((resolve) => setTimeout(resolve, 1500)); + if (areAllApisIntercepted(interceptor, apiUrls)) { + console.log('[Whatshub] statistics 延迟响应已捕获'); + return; + } + + const reloadTimeout = Math.min(60000, interceptTimeout); + try { + await page.reload({ waitUntil: 'domcontentloaded', timeout: reloadTimeout }); + await page.waitForSelector('.vxe-table, .vxe-grid', { timeout: 30000 }).catch(() => null); + await new Promise((resolve) => setTimeout(resolve, 2000)); + } catch (err) { + console.warn(`[Whatshub] reload 触发 statistics 失败: ${err.message}`); + } + + if (areAllApisIntercepted(interceptor, apiUrls)) { + console.log('[Whatshub] reload 后 statistics 已捕获'); + } else { + console.warn('[Whatshub] reload 后 statistics 仍未捕获,继续 waitForApis'); + } } function createApiInterceptor(page, apiUrls, defaultTimeoutMs = API_INTERCEPT_TIMEOUT_MS) { @@ -1702,6 +2160,7 @@ app.post('/api/auth-and-intercept', async (req, res) => { tagBrowserSession(browserSession, profile, antiBot.sessionKey || '', ['--mute-audio']); cfLog.browserReused = !!browserSession.reused; + purgeStoredSessionIfRequested(antiBot); const { storedSession, mergedCookies } = resolveSessionContext(antiBot, pageUrl); const navigationUrl = resolveNavigationUrl(pageUrl, antiBot); @@ -1727,7 +2186,17 @@ app.post('/api/auth-and-intercept', async (req, res) => { await seedShareTokenCookie(page, httpResolvedUrl); } - await prepareWhatshubShortLinkBeforeNav(page, navigationUrl, antiBot); + await prepareWhatshubShortLinkBeforeNav(page, navigationUrl, antiBot, storedSession); + + const apiHandle = createApiInterceptorHandle(); + let interceptor = null; + const useEarlyIntercept = shouldUseEarlyApiIntercept(antiBot, apiUrls); + if (useEarlyIntercept) { + interceptor = attachApiInterceptorToPage( + apiHandle, page, apiUrls, interceptTimeout, 'pre-nav' + ); + } + await navigateToPage(page, navigationUrl); let finalPageUrl = await waitForUrlSettled(page); @@ -1753,7 +2222,12 @@ app.post('/api/auth-and-intercept', async (req, res) => { browserSession = browserCtx.browserSession; page = browserCtx.page; - let interceptor; + if (useEarlyIntercept) { + interceptor = attachApiInterceptorToPage( + apiHandle, page, apiUrls, interceptTimeout, 'post-cf-proxy' + ); + } + if (antiBot.enabled) { const cfResult = await runCloudflareWithBusinessGuard( page, antiBot, waitForUrlSettled, storedSession, pageUrl, browserCtx @@ -1782,7 +2256,7 @@ app.post('/api/auth-and-intercept', async (req, res) => { applyHuojianBusinessHostHint(antiBot, finalPageUrl); touchSessionIfPresent(antiBot.sessionKey); - await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, pageUrl); + await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, pageUrl, apiUrls); } const finalShareToken = resolveShareToken(finalPageUrl); @@ -1791,12 +2265,37 @@ app.post('/api/auth-and-intercept', async (req, res) => { await seedShareTokenCookie(page, finalPageUrl); } - interceptor = createApiInterceptor(page, apiUrls, interceptTimeout); - interceptorCleanup = interceptor.cleanup; + if (useEarlyIntercept) { + interceptor = attachApiInterceptorToPage( + apiHandle, page, apiUrls, interceptTimeout, 'post-cf-ready' + ); + } else { + interceptor = attachApiInterceptorToPage( + apiHandle, page, apiUrls, interceptTimeout, 'pre-auth' + ); + } + interceptorCleanup = apiHandle.cleanup; + await attachHttpIpRewriteInterceptor(page, 'auth-and-intercept', { shareToken }); - await executeAuthActionsWithDiagnostics(page, authActions); + const authResult = await executeAuthActionsWithDiagnostics(page, authActions, antiBot); + await ensureWhatshubStatisticsAfterAuthSkip( + page, + interceptor, + apiUrls, + !!authResult?.skipped, + interceptTimeout + ); + + const capturedBeforeWait = Object.keys(interceptor.interceptedApis).join(',') || 'none'; + console.log( + `[API] 等待 intercept apis=${apiUrls.join(',')} timeout=${interceptTimeout}ms` + + ` already=${capturedBeforeWait}` + ); await interceptor.waitForApis(interceptTimeout); + console.log( + `[API] intercept 完成 captured=${Object.keys(interceptor.interceptedApis).join(',') || 'none'}` + ); const rawCookies = await page.cookies(); const cookiePayload = rawCookies.map((c) => ({ @@ -2017,7 +2516,7 @@ app.post('/api/ui-pagination', async (req, res) => { const shareToken = resolveShareToken(finalPageUrl, pageUrl); await seedShareTokenCookie(page, navigationUrl); - await prepareWhatshubShortLinkBeforeNav(page, navigationUrl, antiBot); + await prepareWhatshubShortLinkBeforeNav(page, navigationUrl, antiBot, storedSession); await navigateToPage(page, navigationUrl); const settledPageUrl = await waitForUrlSettled(page); @@ -2062,7 +2561,7 @@ app.post('/api/ui-pagination', async (req, res) => { } touchSessionIfPresent(antiBot.sessionKey); - await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, navigationUrl); + await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, navigationUrl, apiUrl ? [apiUrl] : null); await attachHttpIpRewriteInterceptor(page, 'ui-pagination', { shareToken }); } else { await attachHttpIpRewriteInterceptor(page, 'ui-pagination', { shareToken }); @@ -2075,7 +2574,7 @@ app.post('/api/ui-pagination', async (req, res) => { waitMs, firstPageData, authActions, - executeAuthActions, + executeAuthActions: (p, actions) => executeAuthActions(p, actions, antiBot), }); res.json({ @@ -2173,7 +2672,7 @@ app.post('/api/auth-intercept-and-paginate', async (req, res) => { let shareToken = resolveShareToken(pageUrl, httpResolvedUrl); await seedShareTokenCookie(page, navigationUrl); - await prepareWhatshubShortLinkBeforeNav(page, navigationUrl, antiBot); + await prepareWhatshubShortLinkBeforeNav(page, navigationUrl, antiBot, storedSession); await navigateToPage(page, navigationUrl); let finalPageUrl = await waitForUrlSettled(page); applyHuojianBusinessHostHint(antiBot, finalPageUrl); @@ -2223,14 +2722,14 @@ app.post('/api/auth-intercept-and-paginate', async (req, res) => { applyHuojianBusinessHostHint(antiBot, finalPageUrl); touchSessionIfPresent(antiBot.sessionKey); - await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, navigationUrl); + await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, navigationUrl, apiUrls); } interceptor = createApiInterceptor(page, apiUrls, interceptTimeout); interceptorCleanup = interceptor.cleanup; await attachHttpIpRewriteInterceptor(page, 'auth-intercept-and-paginate', { shareToken }); - await executeAuthActionsWithDiagnostics(page, authActions); + await executeAuthActionsWithDiagnostics(page, authActions, antiBot); await interceptor.waitForApis(interceptTimeout); const rawCookies = await page.cookies(); @@ -2269,7 +2768,7 @@ app.post('/api/auth-intercept-and-paginate', async (req, res) => { waitMs, firstPageData: effectiveFirstPageData, authActions, - executeAuthActions, + executeAuthActions: (p, actions) => executeAuthActions(p, actions, antiBot), }); extraPages = paginationResult.data; paginationDebug = paginationResult.debugLogs;