修复A2C工单蜘蛛

This commit is contained in:
root
2026-07-01 01:26:27 +08:00
parent 9f2e904fab
commit 54c13c54ef
22 changed files with 1614 additions and 272 deletions
@@ -26,12 +26,16 @@ function normalizeAntiBot(antiBot) {
profile: antiBot.profile === 'real' ? 'real' : 'standard',
turnstile: antiBot.turnstile !== false,
solverFallback: antiBot.solverFallback !== false,
cfClearanceRequired: antiBot.cfClearanceRequired !== false,
sessionKey: antiBot.sessionKey || '',
challengeTimeoutMs: antiBot.challengeTimeoutMs || 60000,
postCfReadyUrlContains: antiBot.postCfReadyUrlContains || '',
postCfReadySelector: antiBot.postCfReadySelector || '',
postCfReadyTimeoutMs: antiBot.postCfReadyTimeoutMs || 0,
postCfSettleMs: antiBot.postCfSettleMs || 0,
postCfSpaWaitMs: antiBot.postCfSpaWaitMs || 0,
businessHostHint: antiBot.businessHostHint || '',
landingUrlHint: antiBot.landingUrlHint || '',
};
}
+31 -12
View File
@@ -1,5 +1,8 @@
/**
* Cloudflare / Turnstile 挑战页检测
*
* antiBot.cfClearanceRequired === falseA2C 业务域):仅真实 CF 中间页算 blocked
* 不因页面引用 turnstile.js 或缺少 cf_clearance cookie 误判。
*/
/**
@@ -17,6 +20,14 @@ function urlIndicatesCfChallenge(url) {
|| url.includes('/cdn-cgi/challenge');
}
/**
* 是否要求 cf_clearance 才视为过盾完成(A2C user.a2c.chat 为 false
* @param {object|null|undefined} antiBot
*/
function isCfClearanceRequired(antiBot) {
return antiBot?.cfClearanceRequired !== false;
}
/**
* @typedef {Object} CloudflareState
* @property {boolean} blocked 是否处于 CF 挑战中
@@ -29,9 +40,10 @@ function urlIndicatesCfChallenge(url) {
/**
* 检测页面是否被 Cloudflare 拦截
* @param {import('puppeteer').Page} page
* @param {object|null} [antiBot]
* @returns {Promise<CloudflareState>}
*/
async function detectCloudflare(page) {
async function detectCloudflare(page, antiBot = null) {
const url = page.url();
const title = await page.title().catch(() => '');
@@ -61,22 +73,28 @@ async function detectCloudflare(page) {
const urlChallenge = urlIndicatesCfChallenge(url);
const realChallenge = urlChallenge
|| domSignals.titleHasJustAMoment
|| domSignals.bodyHasJustAMoment
|| domSignals.hasChallengeRunning
|| title.includes('Just a moment');
const turnstileSignals = domSignals.hasTurnstileInput || domSignals.hasTurnstileWidget;
let challengeType = null;
if (domSignals.hasTurnstileInput || domSignals.hasTurnstileWidget || url.includes('__cf_chl')) {
if (turnstileSignals || url.includes('__cf_chl')) {
challengeType = 'turnstile';
} else if (urlChallenge || domSignals.titleHasJustAMoment || domSignals.bodyHasJustAMoment) {
} else if (realChallenge) {
challengeType = 'js_challenge';
}
const blocked = !hasCfClearance && (
urlChallenge
|| domSignals.titleHasJustAMoment
|| domSignals.bodyHasJustAMoment
|| domSignals.hasTurnstileInput
|| domSignals.hasTurnstileWidget
|| domSignals.hasChallengeRunning
|| title.includes('Just a moment')
);
const requireClearance = isCfClearanceRequired(antiBot);
let blocked;
if (requireClearance) {
blocked = !hasCfClearance && (realChallenge || turnstileSignals);
} else {
blocked = realChallenge;
}
return {
blocked,
@@ -110,4 +128,5 @@ module.exports = {
detectCloudflare,
isTurnstileSolved,
urlIndicatesCfChallenge,
isCfClearanceRequired,
};
+17 -10
View File
@@ -5,7 +5,7 @@
* 否则清除可能过期的 cf_clearance 并重新触发 TurnstileWhatshub 短链 → work-order-sharing 场景)。
*/
const { detectCloudflare } = require('./cf-detector');
const { waitForTurnstile, extractTurnstileSitekey, injectTurnstileToken } = require('./turnstile-handler');
const { waitForTurnstile, extractTurnstileSitekey, injectTurnstileToken, waitForTurnstileSurface } = require('./turnstile-handler');
const { solveTurnstile, isCaptchaConfigured } = require('./captcha-solver');
const { isSessionLikelyValid } = require('./session-store');
@@ -83,14 +83,14 @@ async function tryPostCfRecoverViaReload(page, waitForUrlSettled, timeoutMs, ant
* @param {(page: import('puppeteer').Page) => Promise<string>} [waitForUrlSettled]
* @param {number} timeoutMs
*/
async function invalidateStaleCfAndReload(page, waitForUrlSettled, timeoutMs) {
async function invalidateStaleCfAndReload(page, waitForUrlSettled, timeoutMs, antiBot = null) {
console.log('[CF] 清除 cf_clearance 并 reload,重新触发 Turnstile');
await clearCfClearanceCookies(page);
await page.reload({ waitUntil: 'domcontentloaded', timeout: Math.min(timeoutMs, 45000) });
if (waitForUrlSettled) {
await waitForUrlSettled(page).catch(() => {});
}
return detectCloudflare(page);
return detectCloudflare(page, antiBot);
}
/**
@@ -146,7 +146,7 @@ async function handleCloudflareChallenge(page, antiBot, waitForUrlSettled, store
const started = Date.now();
const timeoutMs = antiBot.challengeTimeoutMs || 60000;
let cfState = await detectCloudflare(page);
let cfState = await detectCloudflare(page, antiBot);
if (!cfState.blocked && cfState.hasCfClearance) {
const fast = await trySessionReuseFastPath(
@@ -155,7 +155,7 @@ async function handleCloudflareChallenge(page, antiBot, waitForUrlSettled, store
if (fast) {
return fast;
}
cfState = await invalidateStaleCfAndReload(page, waitForUrlSettled, timeoutMs);
cfState = await invalidateStaleCfAndReload(page, waitForUrlSettled, timeoutMs, antiBot);
}
if (!cfState.blocked) {
@@ -188,7 +188,7 @@ async function handleCloudflareChallenge(page, antiBot, waitForUrlSettled, store
}
if (cfState.hasCfClearance) {
cfState = await invalidateStaleCfAndReload(page, waitForUrlSettled, timeoutMs);
cfState = await invalidateStaleCfAndReload(page, waitForUrlSettled, timeoutMs, antiBot);
} else {
return {
success: true,
@@ -209,7 +209,7 @@ async function handleCloudflareChallenge(page, antiBot, waitForUrlSettled, store
if (waitForUrlSettled) {
await waitForUrlSettled(page).catch(() => {});
}
cfState = await detectCloudflare(page);
cfState = await detectCloudflare(page, antiBot);
if (!cfState.blocked) {
if (waitForUrlSettled) {
await waitForUrlSettled(page).catch(() => {});
@@ -237,7 +237,7 @@ async function handleCloudflareChallenge(page, antiBot, waitForUrlSettled, store
if (waitForUrlSettled) {
await waitForUrlSettled(page).catch(() => {});
}
cfState = await detectCloudflare(page);
cfState = await detectCloudflare(page, antiBot);
if (!cfState.blocked) {
return {
success: true,
@@ -254,8 +254,15 @@ async function handleCloudflareChallenge(page, antiBot, waitForUrlSettled, store
if (antiBot.solverFallback !== false && isCaptchaConfigured()) {
try {
const sitekey = await extractTurnstileSitekey(page);
if (waitForUrlSettled) {
await waitForUrlSettled(page).catch(() => {});
}
let sitekey = await waitForTurnstileSurface(page, Math.min(timeoutMs, 25000));
if (!sitekey) {
sitekey = await extractTurnstileSitekey(page);
}
const pageUrl = page.url();
console.log(`[CF] Captcha API 求解 pageUrl=${pageUrl} sitekey=${sitekey ? 'ok' : 'missing'}`);
const { token, provider } = await solveTurnstile({ pageUrl, sitekey });
console.log(`[CF] Captcha API (${provider}) 返回 token,注入页面`);
await injectTurnstileToken(page, token);
@@ -267,7 +274,7 @@ async function handleCloudflareChallenge(page, antiBot, waitForUrlSettled, store
await waitForUrlSettled(page).catch(() => {});
}
cfState = await detectCloudflare(page);
cfState = await detectCloudflare(page, antiBot);
if (!cfState.blocked) {
return {
success: true,
+112 -21
View File
@@ -1,5 +1,5 @@
/**
* Turnstile 内置求解:等待 widget 自动完成 + token 轮询
* Turnstile 内置求解:等待 widget 自动完成 + token 轮询 + 多 frame sitekey 提取
*/
const { detectCloudflare, isTurnstileSolved, urlIndicatesCfChallenge } = require('./cf-detector');
@@ -10,6 +10,115 @@ const { detectCloudflare, isTurnstileSolved, urlIndicatesCfChallenge } = require
* @property {number} elapsedMs
*/
/**
* 在单个 frame 内提取 sitekey
* @param {import('puppeteer').Frame} frame
* @returns {Promise<string|null>}
*/
async function extractSitekeyFromFrame(frame) {
return frame.evaluate(() => {
const widget = document.querySelector('.cf-turnstile[data-sitekey], [data-sitekey]');
if (widget) {
const key = widget.getAttribute('data-sitekey');
if (key) {
return key;
}
}
const iframes = document.querySelectorAll('iframe[src*="challenges.cloudflare.com"], iframe[src*="challenge-platform"]');
for (const iframe of iframes) {
const src = iframe.getAttribute('src') || '';
const match = src.match(/[?&]sitekey=([^&]+)/i);
if (match) {
return decodeURIComponent(match[1]);
}
}
const scripts = document.querySelectorAll('script');
for (const script of scripts) {
const text = script.textContent || '';
const match = text.match(/sitekey['"\s:=]+['"]([0-9x_a-zA-Z-]{10,})['"]/i)
|| text.match(/data-sitekey=['"]([0-9x_a-zA-Z-]{10,})['"]/i);
if (match) {
return match[1];
}
}
return null;
}).catch(() => null);
}
/**
* 从主文档及所有子 frame 提取 Turnstile sitekey
* @param {import('puppeteer').Page} page
* @returns {Promise<string|null>}
*/
async function extractTurnstileSitekey(page) {
const mainKey = await extractSitekeyFromFrame(page.mainFrame());
if (mainKey) {
return mainKey;
}
for (const frame of page.frames()) {
if (frame === page.mainFrame()) {
continue;
}
const frameKey = await extractSitekeyFromFrame(frame);
if (frameKey) {
return frameKey;
}
}
return null;
}
/**
* Captcha API 调用前:等待 Turnstile 挑战面出现或 URL 进入 CF 挑战态
*
* @param {import('puppeteer').Page} page
* @param {number} [timeoutMs]
* @returns {Promise<string|null>} 若已提取到 sitekey 则返回,否则 null
*/
async function waitForTurnstileSurface(page, timeoutMs = 20000) {
const started = Date.now();
const pollMs = 400;
while (Date.now() - started < timeoutMs) {
const sitekey = await extractTurnstileSitekey(page);
if (sitekey) {
console.log(`[CF] 已检测到 Turnstile sitekey (${sitekey.slice(0, 8)}...)`);
return sitekey;
}
const url = page.url();
if (urlIndicatesCfChallenge(url)) {
await new Promise((r) => setTimeout(r, pollMs));
continue;
}
const state = await detectCloudflare(page);
if (!state.blocked && state.hasCfClearance) {
return null;
}
const hasSurface = await page.evaluate(() => {
return !!document.querySelector(
'.cf-turnstile, [data-sitekey], [name="cf-turnstile-response"], '
+ 'iframe[src*="challenges.cloudflare.com"], iframe[src*="challenge-platform"]'
);
}).catch(() => false);
if (hasSurface) {
await new Promise((r) => setTimeout(r, pollMs));
continue;
}
await new Promise((r) => setTimeout(r, pollMs));
}
return null;
}
/**
* 等待 Turnstile 通过(内置点击 / 自动跳转)
* @param {import('puppeteer').Page} page
@@ -49,26 +158,6 @@ async function waitForTurnstile(page, options = {}) {
return { success: false, stage: 'timeout', elapsedMs: Date.now() - started };
}
/**
* 从页面提取 Turnstile sitekey
* @param {import('puppeteer').Page} page
* @returns {Promise<string|null>}
*/
async function extractTurnstileSitekey(page) {
return page.evaluate(() => {
const widget = document.querySelector('.cf-turnstile[data-sitekey], [data-sitekey]');
if (widget) {
return widget.getAttribute('data-sitekey');
}
const iframe = document.querySelector('iframe[src*="challenges.cloudflare.com"]');
if (iframe && iframe.src) {
const match = iframe.src.match(/[?&]sitekey=([^&]+)/);
if (match) return decodeURIComponent(match[1]);
}
return null;
}).catch(() => null);
}
/**
* 将 Captcha API 返回的 token 注入页面
* @param {import('puppeteer').Page} page
@@ -91,6 +180,8 @@ async function injectTurnstileToken(page, token) {
module.exports = {
waitForTurnstile,
waitForTurnstileSurface,
extractTurnstileSitekey,
extractSitekeyFromFrame,
injectTurnstileToken,
};
+778 -42
View File
@@ -21,6 +21,7 @@ const {
NAVIGATION_TIMEOUT_MS,
NAVIGATION_WAIT_UNTIL,
NAVIGATION_MAX_RETRIES,
PAGE_DEFAULT_TIMEOUT_MS,
SESSION_TTL_MS,
PERSIST_BROWSER_PROFILE,
POOL_IDLE_MS,
@@ -39,18 +40,22 @@ const {
const { launchStandardBrowser } = require('./lib/launch-standard');
const { isRealBrowserAvailable } = require('./lib/launch-real-browser');
const { handleCloudflareChallenge, isCaptchaConfigured } = require('./lib/cf-handler');
const { detectCloudflare, urlIndicatesCfChallenge } = require('./lib/cf-detector');
const {
extractTurnstileSitekey,
injectTurnstileToken,
waitForTurnstileSurface,
} = require('./lib/turnstile-handler');
const { solveTurnstile } = require('./lib/captcha-solver');
const {
saveSession,
getSessionDebugInfo,
getSessionStats,
isSessionLikelyValid,
} = require('./lib/session-store');
const { resolveSessionContext, touchSessionIfPresent } = require('./lib/session-context');
const { getProfileStats, profileExists } = require('./lib/profile-dir');
const { runUiPagination } = require('./lib/ui-pagination-runner');
const {
awaitPostCfBusinessReady,
executeAuthActionsWithDiagnostics,
} = require('./lib/post-cf-ready');
const app = express();
app.use(express.json({ limit: '50mb' }));
@@ -170,6 +175,719 @@ async function waitForUrlSettled(page, { maxAttempts = 15, stableMs = 500, redir
return currentUrl;
}
/** 合并 antiBot 扩展字段(postCfReady* 由 PHP 按工单类型传入,未配置则跳过) */
function resolveAntiBot(rawAntiBot) {
const base = normalizeAntiBot(rawAntiBot);
if (!rawAntiBot || !base.enabled) {
return base;
}
return {
...base,
postCfReadyUrlContains: rawAntiBot.postCfReadyUrlContains || '',
postCfReadySelector: rawAntiBot.postCfReadySelector || '',
postCfReadyTimeoutMs: rawAntiBot.postCfReadyTimeoutMs || 0,
postCfSettleMs: rawAntiBot.postCfSettleMs || 0,
postCfSpaWaitMs: rawAntiBot.postCfSpaWaitMs || 0,
cfClearanceRequired: rawAntiBot.cfClearanceRequired !== false,
businessHostHint: rawAntiBot.businessHostHint || '',
landingUrlHint: rawAntiBot.landingUrlHint || '',
};
}
/**
* Whatshub 短链场景:PHP 未传 postCfReady* 时 Node 侧自动补全(避免 session_reuse 后 15s auth 超时)
* @param {object} antiBot
* @param {string} pageUrl
*/
function applyWhatshubPostCfDefaults(antiBot, pageUrl) {
if (!antiBot || !antiBot.enabled) {
return antiBot;
}
if ((antiBot.postCfReadyUrlContains || '').trim()) {
return antiBot;
}
try {
const parsed = new URL(pageUrl || '');
if (!parsed.hostname.includes('whatshub.cc') || !parsed.pathname.includes('/m/')) {
return antiBot;
}
console.log('[CF] Whatshub 短链自动启用 postCfReady 默认配置');
return {
...antiBot,
postCfReadyUrlContains: 'work-order-sharing',
postCfReadySelector: '.el-input__inner',
postCfReadyTimeoutMs: antiBot.postCfReadyTimeoutMs || 30000,
postCfSettleMs: antiBot.postCfSettleMs || 500,
postCfSpaWaitMs: antiBot.postCfSpaWaitMs || 8000,
};
} catch (_) {
return antiBot;
}
}
/**
* A2C 短链 → user.a2c.chatPHP 未传 postCfReady* 时 Node 侧自动补全
* @param {object} antiBot
* @param {string} pageUrl
*/
function applyA2cPostCfDefaults(antiBot, pageUrl) {
if (!antiBot || !antiBot.enabled) {
return antiBot;
}
if ((antiBot.postCfReadyUrlContains || '').trim()) {
return antiBot;
}
let shouldApply = false;
const sessionKey = String(antiBot.sessionKey || '').toLowerCase();
if (sessionKey.startsWith('a2c:')) {
shouldApply = true;
}
try {
const parsed = new URL(pageUrl || '');
if (parsed.hostname.includes('a2c.chat')) {
shouldApply = true;
}
} catch (_) {
/* ignore */
}
if (!shouldApply) {
return antiBot;
}
console.log('[CF] A2C 自动启用 postCfReady 默认配置');
return {
...antiBot,
postCfReadyUrlContains: '/visitors/counter/share',
postCfReadySelector: '.btn-next',
postCfReadyTimeoutMs: antiBot.postCfReadyTimeoutMs || 30000,
postCfSettleMs: antiBot.postCfSettleMs || 500,
postCfSpaWaitMs: antiBot.postCfSpaWaitMs || 4000,
cfClearanceRequired: antiBot.cfClearanceRequired === undefined ? false : antiBot.cfClearanceRequired,
solverFallback: antiBot.solverFallback === undefined ? false : antiBot.solverFallback,
};
}
/** 按 pageUrl 合并 antiBot(含 Whatshub / A2C 默认 postCf */
function resolveAntiBotForPage(rawAntiBot, pageUrl) {
return applyA2cPostCfDefaults(
applyWhatshubPostCfDefaults(resolveAntiBot(rawAntiBot), pageUrl),
pageUrl
);
}
function isBusinessUrlReady(page, urlNeedle) {
return !urlNeedle || page.url().includes(urlNeedle);
}
/** Whatshub 分享短链 /m/xxx/1 */
function isWhatshubShortLinkPath(url) {
try {
const parsed = new URL(url || '');
return parsed.hostname.includes('whatshub.cc') && parsed.pathname.includes('/m/');
} catch (_) {
return false;
}
}
/**
* Whatshub 短链 + Real Browser:不覆盖 UA、不预注入磁盘 cf_clearance
* @param {string} pageUrl
* @param {object} antiBot
* @param {object[]} mergedCookies
* @param {object} extraOpts
*/
function buildManagedPageOptions(pageUrl, antiBot, mergedCookies, extraOpts = {}) {
const opts = { ...extraOpts };
const isWhatshubShort = isWhatshubShortLinkPath(pageUrl);
const isRealProfile = antiBot?.profile === 'real';
if (isWhatshubShort && isRealProfile && antiBot?.enabled) {
opts.skipUserAgent = true;
const filtered = (mergedCookies || []).filter((c) => c.name !== 'cf_clearance');
if (filtered.length < (mergedCookies || []).length) {
console.log('[Whatshub] 短链首访:跳过磁盘 cf_clearance 预注入,避免 SPA 路由未触发');
}
opts.cookies = filtered;
return opts;
}
opts.cookies = mergedCookies;
if (!opts.skipUserAgent && !opts.userAgent) {
opts.userAgent = DEFAULT_UA;
}
return opts;
}
/**
* 任务页创建(含 Whatshub skipUserAgent);browser-factory 无写权限时在 server 侧实现
*/
async function createTaskPage(browser, existingPage, pageUrl, antiBot, mergedCookies, extraOpts = {}) {
const opts = buildManagedPageOptions(pageUrl, antiBot, mergedCookies, extraOpts);
const page = existingPage || await browser.newPage();
page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT_MS);
page.setDefaultTimeout(PAGE_DEFAULT_TIMEOUT_MS);
if (!opts.skipUserAgent) {
await page.setUserAgent(opts.userAgent || DEFAULT_UA);
}
if (opts.viewport) {
await page.setViewport(opts.viewport);
}
if (opts.cookies && opts.cookies.length > 0) {
await page.setCookie(...opts.cookies);
}
page.on('error', (err) => console.error('[Page Error]', err.message));
page.on('pageerror', (err) => console.error('[Page JS Error]', err.message));
return page;
}
/** CF 已过但仍在短链、未到业务页(Whatshub / A2C yyk.ink 等) */
function isOnShortLinkNotBusiness(page, urlNeedle) {
const url = page.url();
if (urlNeedle && url.includes(urlNeedle)) {
return false;
}
if (isWhatshubShortLinkPath(url)) {
return true;
}
if (urlNeedle && urlNeedle.includes('/visitors/counter/share')) {
try {
return !new URL(url).hostname.includes('a2c.chat');
} catch (_) {
return false;
}
}
return false;
}
/**
* CF 完成后等待 Vue SPA 从短链跳转到 work-order-sharing(对齐 debug 脚本 goto+8s
* Whatshub:二次 goto 短链(保留 clearance),禁止 reload(会重新触发 __cf_chl_rt_tk
*/
async function waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettledFn, pageUrl = '') {
const urlNeedle = (antiBot.postCfReadyUrlContains || '').trim();
const targetUrl = pageUrl || page.url();
if (!urlNeedle || !isOnShortLinkNotBusiness(page, urlNeedle)) {
return isBusinessUrlReady(page, urlNeedle);
}
if (!(await isCfTrulyComplete(page, antiBot))) {
return false;
}
const spaWaitMs = Math.max(8000, antiBot.postCfSpaWaitMs || 8000);
const isWhatshub = isWhatshubShortLinkPath(targetUrl);
console.log(
`[CF] CF 已完成,等待 SPA 跳转 settle=${spaWaitMs}ms current=${page.url()}`
);
await new Promise((resolve) => setTimeout(resolve, spaWaitMs));
if (typeof waitForUrlSettledFn === 'function') {
await waitForUrlSettledFn(page).catch(() => {});
}
await page.waitForNetworkIdle({ idleTime: 500, timeout: 15000 }).catch(() => {});
if (isBusinessUrlReady(page, urlNeedle)) {
console.log(`[CF] SPA 已跳转至业务页 url=${page.url()}`);
return true;
}
if (isWhatshub && isWhatshubShortLinkPath(targetUrl)) {
console.log(`[CF] Whatshub 二次 goto 触发 SPA 路由(保留 clearanceurl=${targetUrl}`);
await navigateToPage(page, targetUrl);
if (typeof waitForUrlSettledFn === 'function') {
await waitForUrlSettledFn(page).catch(() => {});
}
if (!(await isCfTrulyComplete(page, antiBot))) {
console.log('[CF] 二次 goto 后 CF 未完成,等待过盾...');
const cfWait = await waitForCfTrulyComplete(page, antiBot.challengeTimeoutMs || 60000, 500, antiBot);
if (!cfWait.success) {
console.log(`[CF] SPA 等待后仍未到业务页 url=${page.url()}`);
return false;
}
}
console.log(`[CF] Whatshub 二次 goto 后 SPA 等待 settle=${spaWaitMs}ms current=${page.url()}`);
await new Promise((resolve) => setTimeout(resolve, spaWaitMs));
await page.waitForNetworkIdle({ idleTime: 500, timeout: 15000 }).catch(() => {});
if (isBusinessUrlReady(page, urlNeedle)) {
console.log(`[CF] 二次 goto 后 SPA 已跳转 url=${page.url()}`);
return true;
}
}
console.log(`[CF] SPA 等待后仍未到业务页 url=${page.url()}`);
return false;
}
/** 非 Whatshub 场景:软 reloadWhatshub 短链改用二次 goto */
async function softReloadForSpa(page, waitForUrlSettledFn, antiBot, pageUrl = '') {
const targetUrl = pageUrl || page.url();
if (isWhatshubShortLinkPath(targetUrl)) {
console.log(`[CF] Whatshub 跳过 soft reload,改用二次 goto url=${targetUrl}`);
return waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettledFn, targetUrl);
}
console.log(`[CF] 尝试软 reload(保留 clearanceurl=${page.url()}`);
await page.reload({ waitUntil: 'domcontentloaded', timeout: NAVIGATION_TIMEOUT_MS }).catch(() => {});
if (typeof waitForUrlSettledFn === 'function') {
await waitForUrlSettledFn(page).catch(() => {});
}
return waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettledFn, targetUrl);
}
/** CF 是否真正完成:默认需 cf_clearanceA2C 等 cfClearanceRequired=false 时仅看真实挑战 */
async function isCfTrulyComplete(page, antiBot = null) {
if (urlIndicatesCfChallenge(page.url())) {
return false;
}
if (antiBot && antiBot.cfClearanceRequired === false) {
const state = await detectCloudflare(page, antiBot);
return !state.blocked;
}
const cookies = await page.cookies();
if (!cookies.some((c) => c.name === 'cf_clearance')) {
return false;
}
const state = await detectCloudflare(page, antiBot);
return !state.blocked;
}
/** 轮询直到 CF 真正完成(不依赖 turnstile-handler 的 early return */
async function waitForCfTrulyComplete(page, timeoutMs, pollMs = 500, antiBot = null) {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
if (await isCfTrulyComplete(page, antiBot)) {
return { success: true, elapsedMs: Date.now() - started };
}
await new Promise((resolve) => setTimeout(resolve, pollMs));
}
return { success: false, elapsedMs: Date.now() - started };
}
/** 校验 cfResult,防止 handleCloudflareChallenge 假 success */
async function finalizeCfResult(page, cfResult, antiBot = null) {
if (!cfResult || !cfResult.success) {
return cfResult;
}
if (await isCfTrulyComplete(page, antiBot)) {
return cfResult;
}
console.log(`[CF] 过盾返回 success 但 CF 未真正完成 url=${page.url()}`);
return {
success: false,
code: 'CF_TURNSTILE_FAILED',
challengeType: cfResult.challengeType || 'turnstile',
stage: 'incomplete',
solverUsed: cfResult.solverUsed || false,
elapsedMs: cfResult.elapsedMs || 0,
cfDetected: true,
};
}
/**
* 完整过盾:内置 Real Browser Turnstile 轮询 → Captcha API 兜底
*/
async function runCfChallengeFlow(page, antiBot, waitForUrlSettled, pageUrl = '') {
const started = Date.now();
const timeoutMs = Math.min(Math.max(Number(antiBot.challengeTimeoutMs) || 60000, 5000), 120000);
const navUrl = pageUrl || page.url();
console.log(`[CF] 开始过盾 url=${page.url()}`);
if (waitForUrlSettled) {
await waitForUrlSettled(page).catch(() => {});
}
if (antiBot.cfClearanceRequired === false) {
const preState = await detectCloudflare(page, antiBot);
if (!preState.blocked) {
console.log('[CF] 免 clearance 模式:无真实 CF 挑战,跳过 Turnstile/CapSolver');
await waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettled, navUrl);
return {
success: true,
code: null,
challengeType: null,
stage: 'no_clearance_required',
solverUsed: false,
elapsedMs: Date.now() - started,
cfDetected: false,
};
}
console.log(`[CF] 免 clearance 模式但检测到真实 CF 挑战 type=${preState.challengeType},继续过盾`);
}
if (antiBot.turnstile !== false) {
console.log(`[CF] 内置 Turnstile 轮询 timeout=${timeoutMs}ms`);
const builtIn = await waitForCfTrulyComplete(page, timeoutMs, 500, antiBot);
if (builtIn.success) {
if (waitForUrlSettled) {
await waitForUrlSettled(page).catch(() => {});
}
await waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettled, navUrl);
return {
success: true,
code: null,
challengeType: 'turnstile',
stage: 'built_in',
solverUsed: false,
elapsedMs: Date.now() - started,
cfDetected: true,
};
}
console.warn(`[CF] 内置 Turnstile 未在 ${builtIn.elapsedMs}ms 内完成,尝试 Captcha API`);
}
if (antiBot.solverFallback !== false && isCaptchaConfigured()) {
try {
if (waitForUrlSettled) {
await waitForUrlSettled(page).catch(() => {});
}
let sitekey = await waitForTurnstileSurface(page, Math.min(timeoutMs, 25000));
if (!sitekey) {
sitekey = await extractTurnstileSitekey(page);
}
const pageUrlForSolver = page.url();
console.log(`[CF] Captcha API 求解 pageUrl=${pageUrlForSolver} sitekey=${sitekey ? 'ok' : 'missing'}`);
const { token, provider } = await solveTurnstile({ pageUrl: pageUrlForSolver, sitekey });
console.log(`[CF] Captcha API (${provider}) 返回 token,注入页面`);
await injectTurnstileToken(page, token);
const apiWait = await waitForCfTrulyComplete(page, Math.min(timeoutMs, 45000), 500, antiBot);
if (apiWait.success) {
if (waitForUrlSettled) {
await waitForUrlSettled(page).catch(() => {});
}
await waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettled, navUrl);
return {
success: true,
code: null,
challengeType: 'turnstile',
stage: 'api',
solverUsed: true,
elapsedMs: Date.now() - started,
cfDetected: true,
};
}
console.warn('[CF] Captcha API token 注入后仍未获得 cf_clearance');
} catch (apiErr) {
console.error('[CF] Captcha API 兜底失败:', apiErr.message);
return {
success: false,
code: 'CF_TURNSTILE_FAILED',
challengeType: 'turnstile',
stage: 'api',
solverUsed: true,
elapsedMs: Date.now() - started,
cfDetected: true,
};
}
}
return {
success: false,
code: 'CF_TURNSTILE_FAILED',
challengeType: 'turnstile',
stage: antiBot.solverFallback !== false && !isCaptchaConfigured() ? 'built_in' : 'timeout',
solverUsed: false,
elapsedMs: Date.now() - started,
cfDetected: true,
};
}
/** 安全 CF 处理:未真正完成则走完整过盾,禁止 cf-handler 假过关 */
async function handleCloudflareChallengeSafe(page, antiBot, waitForUrlSettled, storedSession) {
if (!(await isCfTrulyComplete(page, antiBot))) {
return finalizeCfResult(page, await runCfChallengeFlow(page, antiBot, waitForUrlSettled), antiBot);
}
const result = await handleCloudflareChallenge(page, antiBot, waitForUrlSettled, storedSession);
return finalizeCfResult(page, result, antiBot);
}
/** 是否应 purge 陈旧 clearance(短链 SPA 未跳转时禁止 purge) */
function shouldPurgeStaleClearance(page, cfState, urlNeedle = '') {
if (urlIndicatesCfChallenge(page.url())) {
return false;
}
if (urlNeedle && isOnShortLinkNotBusiness(page, urlNeedle) && cfState.hasCfClearance) {
return false;
}
return cfState.hasCfClearance && !cfState.blocked;
}
/** 业务页等待前确保 CF 已完成 */
async function ensureCfCompleteBeforeBusinessReady(page, antiBot, waitForUrlSettledFn, storedSession) {
if (await isCfTrulyComplete(page, antiBot)) {
return;
}
const cfState = await detectCloudflare(page, antiBot);
console.log(`[CF] 业务页等待前 CF 未完成 blocked=${cfState.blocked} url=${page.url()}`);
const cfResult = await handleCloudflareChallengeSafe(page, antiBot, waitForUrlSettledFn, storedSession);
if (!cfResult.success) {
const err = new Error('Cloudflare Turnstile 验证失败(业务页等待前)');
err.code = 'CF_TURNSTILE_FAILED';
throw err;
}
if (typeof waitForUrlSettledFn === 'function') {
await waitForUrlSettledFn(page).catch(() => {});
}
}
/** 清除 cf_clearance(含 Chrome Profile / CDP 层) */
async function clearCfClearanceCookies(page) {
const cookies = await page.cookies();
for (const c of cookies) {
if (c.name !== 'cf_clearance') {
continue;
}
const host = (c.domain || '').replace(/^\./, '');
if (host) {
await page.deleteCookie({ name: c.name, url: `https://${host}/` }).catch(() => {});
}
await page.deleteCookie({
name: c.name,
domain: c.domain,
path: c.path || '/',
}).catch(() => {});
}
}
async function purgeCfClearanceFully(page, pageUrl) {
await clearCfClearanceCookies(page);
let hostname = '';
try {
hostname = new URL(pageUrl).hostname;
} catch (_) {
return;
}
try {
const cdp = await page.createCDPSession();
const domains = new Set([hostname, `.${hostname}`]);
const cookies = await page.cookies();
for (const c of cookies) {
if (c.domain) {
domains.add(c.domain);
}
}
for (const domain of domains) {
await cdp.send('Network.deleteCookies', { name: 'cf_clearance', domain }).catch(() => {});
}
} catch (err) {
console.warn('[CF] CDP 清除 cf_clearance 失败:', err.message);
}
}
/**
* Whatshub 短链首访:清除 Chrome Profile 内陈旧 cf_clearance,确保 Turnstile+SPA 在同一 goto 生命周期
*/
async function prepareWhatshubShortLinkBeforeNav(page, pageUrl, antiBot) {
if (!antiBot?.enabled || antiBot.profile !== 'real' || !isWhatshubShortLinkPath(pageUrl)) {
return;
}
const cookies = await page.cookies();
if (!cookies.some((c) => c.name === 'cf_clearance')) {
return;
}
console.log('[Whatshub] 短链首访:清除 Profile/页面内陈旧 cf_clearance');
await purgeCfClearanceFully(page, pageUrl);
}
/**
* CF 处理 + 业务 URL 守卫:磁盘 session 陈旧 clearance 时 purge__cf_chl 中间态只过盾不 purge
*/
async function runCloudflareWithBusinessGuard(page, antiBot, waitForUrlSettled, storedSession, pageUrl) {
const urlNeedle = (antiBot.postCfReadyUrlContains || '').trim();
const hasStoredSession = !!(storedSession && isSessionLikelyValid(storedSession));
async function reNavigateWithCfPurge(label) {
if (urlIndicatesCfChallenge(page.url())) {
console.log(`[CF] ${label}: URL 含 __cf_chl,跳过 purge,直接过盾`);
return handleCloudflareChallengeSafe(page, antiBot, waitForUrlSettled, storedSession);
}
console.log(`[CF] ${label}: 清除陈旧 CF 并重新 goto ${pageUrl} (current=${page.url()})`);
await purgeCfClearanceFully(page, pageUrl);
await navigateToPage(page, pageUrl);
if (waitForUrlSettled) {
await waitForUrlSettled(page).catch(() => {});
}
return handleCloudflareChallengeSafe(page, antiBot, waitForUrlSettled, storedSession);
}
let cfResult;
if (urlNeedle && !isBusinessUrlReady(page, urlNeedle) && hasStoredSession) {
const pre = await detectCloudflare(page, antiBot);
if (shouldPurgeStaleClearance(page, pre, urlNeedle)) {
cfResult = await reNavigateWithCfPurge('预检(磁盘会话-陈旧clearance)');
} else if (await isCfTrulyComplete(page, antiBot)) {
await waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettled, pageUrl);
}
}
if (!cfResult) {
cfResult = await handleCloudflareChallengeSafe(page, antiBot, waitForUrlSettled, storedSession);
}
// 短链跨域落地(如 yyk.ink → user.a2c.chat)后,业务域可能仍有 CF
if (cfResult.success && !(await isCfTrulyComplete(page, antiBot))) {
const crossState = await detectCloudflare(page, antiBot);
if (crossState.blocked || urlIndicatesCfChallenge(page.url())) {
console.log(`[CF] 跨域落地后 CF 未完成,二次过盾 url=${page.url()}`);
cfResult = await finalizeCfResult(
page,
await runCfChallengeFlow(page, antiBot, waitForUrlSettled, page.url()),
antiBot
);
}
}
if (cfResult.success && urlNeedle && !isBusinessUrlReady(page, urlNeedle)) {
await waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettled, pageUrl);
}
let retries = 0;
while (
cfResult.success
&& urlNeedle
&& !isBusinessUrlReady(page, urlNeedle)
&& retries < 2
) {
retries += 1;
if (!(await isCfTrulyComplete(page, antiBot))) {
console.log(`[CF] 业务页未达且 CF 未完成,完整过盾 重试#${retries}`);
cfResult = await finalizeCfResult(
page,
await runCfChallengeFlow(page, antiBot, waitForUrlSettled, pageUrl),
antiBot
);
} else {
console.log(`[CF] CF 已完成但未到业务页,等待 SPA 跳转 重试#${retries}`);
const spaOk = await waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettled, pageUrl);
if (!spaOk && retries >= 2) {
await softReloadForSpa(page, waitForUrlSettled, antiBot, pageUrl);
}
if (isBusinessUrlReady(page, urlNeedle)) {
break;
}
}
if (!cfResult.success) {
break;
}
}
return finalizeCfResult(page, cfResult, antiBot);
}
async function collectPageDiagnostics(page) {
const url = page.url();
const snapshot = await page.evaluate(() => ({
url: window.location.href,
title: document.title || '',
bodyText: (document.body && document.body.innerText ? document.body.innerText : '').slice(0, 500),
elInputInner: document.querySelectorAll('.el-input__inner').length,
})).catch(() => null);
return snapshot || { url, title: '', bodyText: '', elInputInner: 0 };
}
async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, storedSession = null, pageUrl = '') {
if (!antiBot || !antiBot.enabled) {
return;
}
const urlContains = (antiBot.postCfReadyUrlContains || '').trim();
const selector = (antiBot.postCfReadySelector || '').trim();
if (urlContains === '' && selector === '') {
return;
}
const navUrl = pageUrl || page.url();
await ensureCfCompleteBeforeBusinessReady(page, antiBot, waitForUrlSettledFn, storedSession);
await waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettledFn, navUrl);
const timeoutMs = Math.max(5000, antiBot.postCfReadyTimeoutMs || 30000);
console.log(
`[CF] 等待业务页就绪 current=${page.url()} urlContains="${urlContains}" `
+ `selector="${selector}" timeout=${timeoutMs}ms`
);
try {
if (urlContains !== '') {
await page.waitForFunction(
(needle) => window.location.href.includes(needle),
{ timeout: timeoutMs, polling: 200 },
urlContains
);
}
if (selector !== '') {
await page.waitForSelector(selector, { timeout: timeoutMs, visible: true });
}
} catch (err) {
const retryMs = Math.min(15000, timeoutMs);
const needsRetry = urlContains !== '' && !page.url().includes(urlContains);
if (needsRetry) {
if (!(await isCfTrulyComplete(page, antiBot))) {
console.warn(`[CF] 业务页等待失败且 CF 未完成 url=${page.url()},完整过盾后重试`);
try {
await ensureCfCompleteBeforeBusinessReady(page, antiBot, waitForUrlSettledFn, storedSession);
if (urlContains !== '') {
await page.waitForFunction(
(needle) => window.location.href.includes(needle),
{ timeout: retryMs, polling: 200 },
urlContains
);
}
if (selector !== '') {
await page.waitForSelector(selector, { timeout: retryMs, visible: true });
}
} catch (retryErr) {
retryErr.pageDiagnostics = await collectPageDiagnostics(page);
throw retryErr;
}
} else {
console.warn(`[CF] 业务页等待失败 url=${page.url()}SPA 等待 + 二次 goto`);
try {
await waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettledFn, navUrl);
if (!page.url().includes(urlContains)) {
await softReloadForSpa(page, waitForUrlSettledFn, antiBot, navUrl);
}
if (urlContains !== '') {
await page.waitForFunction(
(needle) => window.location.href.includes(needle),
{ timeout: retryMs, polling: 200 },
urlContains
);
}
if (selector !== '') {
await page.waitForSelector(selector, { timeout: retryMs, visible: true });
}
} catch (retryErr) {
retryErr.pageDiagnostics = await collectPageDiagnostics(page);
throw retryErr;
}
}
} else {
err.pageDiagnostics = await collectPageDiagnostics(page);
throw err;
}
}
const settleMs = antiBot.postCfSettleMs || 0;
if (settleMs > 0) {
await new Promise((resolve) => setTimeout(resolve, settleMs));
}
console.log(`[CF] 业务页就绪 url=${page.url()}`);
}
async function executeAuthActionsWithDiagnostics(page, authActions) {
try {
await executeAuthActions(page, authActions);
} catch (err) {
err.pageDiagnostics = await collectPageDiagnostics(page);
throw err;
}
}
async function executeAuthActions(page, authActions) {
if (!Array.isArray(authActions)) return;
@@ -455,7 +1173,7 @@ app.post('/api/auth-and-intercept', async (req, res) => {
return res.status(400).json({ success: false, error: '参数错误' });
}
const antiBot = normalizeAntiBot(rawAntiBot);
const antiBot = resolveAntiBotForPage(rawAntiBot, pageUrl);
const profile = antiBot.profile || 'standard';
const interceptTimeout = resolveInterceptTimeout(antiBot);
const cfLog = { profile, cfDetected: false, turnstileStage: null, solverUsed: false, elapsedMs: 0, browserReused: false };
@@ -477,26 +1195,26 @@ app.post('/api/auth-and-intercept', async (req, res) => {
const { storedSession, mergedCookies } = resolveSessionContext(antiBot, pageUrl);
const page = await createManagedPage(browserSession.browser, browserSession.page, {
userAgent: DEFAULT_UA,
cookies: mergedCookies,
});
const page = await createTaskPage(
browserSession.browser,
browserSession.page,
pageUrl,
antiBot,
mergedCookies
);
const httpResolvedUrl = await resolveHttpRedirects(pageUrl).catch(() => pageUrl);
if (httpResolvedUrl !== pageUrl) {
console.log(`[URL解析] HTTP 重定向: ${pageUrl} -> ${httpResolvedUrl}`);
}
const interceptor = createApiInterceptor(page, apiUrls, interceptTimeout);
interceptorCleanup = interceptor.cleanup;
let shareToken = resolveShareToken(pageUrl, httpResolvedUrl);
await seedShareTokenCookie(page, pageUrl);
if (httpResolvedUrl !== pageUrl) {
await seedShareTokenCookie(page, httpResolvedUrl);
}
await attachHttpIpRewriteInterceptor(page, 'auth-and-intercept', { shareToken });
await prepareWhatshubShortLinkBeforeNav(page, pageUrl, antiBot);
await navigateToPage(page, pageUrl);
let finalPageUrl = await waitForUrlSettled(page);
@@ -504,8 +1222,11 @@ app.post('/api/auth-and-intercept', async (req, res) => {
console.log(`[URL解析] 浏览器落地: ${pageUrl} -> ${finalPageUrl}`);
}
let interceptor;
if (antiBot.enabled) {
const cfResult = await handleCloudflareChallenge(page, antiBot, waitForUrlSettled, storedSession);
const cfResult = await runCloudflareWithBusinessGuard(
page, antiBot, waitForUrlSettled, storedSession, pageUrl
);
cfLog.cfDetected = cfResult.cfDetected;
cfLog.turnstileStage = cfResult.stage;
cfLog.solverUsed = cfResult.solverUsed;
@@ -526,6 +1247,8 @@ app.post('/api/auth-and-intercept', async (req, res) => {
finalPageUrl = page.url();
touchSessionIfPresent(antiBot.sessionKey);
await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, pageUrl);
}
const finalShareToken = resolveShareToken(finalPageUrl);
@@ -534,11 +1257,11 @@ app.post('/api/auth-and-intercept', async (req, res) => {
await seedShareTokenCookie(page, finalPageUrl);
}
if (antiBot.enabled) {
await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled);
}
interceptor = createApiInterceptor(page, apiUrls, interceptTimeout);
interceptorCleanup = interceptor.cleanup;
await attachHttpIpRewriteInterceptor(page, 'auth-and-intercept', { shareToken });
await executeAuthActionsWithDiagnostics(page, authActions, executeAuthActions);
await executeAuthActionsWithDiagnostics(page, authActions);
await interceptor.waitForApis(interceptTimeout);
const rawCookies = await page.cookies();
@@ -715,7 +1438,7 @@ app.post('/api/ui-pagination', async (req, res) => {
antiBot: rawAntiBot,
} = req.body;
const navigationUrl = finalPageUrl || pageUrl;
const antiBot = normalizeAntiBot(rawAntiBot);
const antiBot = resolveAntiBotForPage(rawAntiBot, pageUrl || navigationUrl);
const profile = antiBot.profile || 'standard';
try {
@@ -733,10 +1456,14 @@ app.post('/api/ui-pagination', async (req, res) => {
const { storedSession, mergedCookies } = resolveSessionContext(antiBot, navigationUrl, cookies || []);
const page = await createManagedPage(browserSession.browser, browserSession.page, {
viewport: { width: 1920, height: 1080 },
cookies: mergedCookies,
});
const page = await createTaskPage(
browserSession.browser,
browserSession.page,
navigationUrl,
antiBot,
mergedCookies,
{ viewport: { width: 1920, height: 1080 } }
);
const log = (msg) => { console.log(msg); debugLogs.push(msg); };
log(`[启动] 准备访问页面: ${navigationUrl}`);
@@ -746,8 +1473,8 @@ app.post('/api/ui-pagination', async (req, res) => {
const shareToken = resolveShareToken(finalPageUrl, pageUrl);
await seedShareTokenCookie(page, navigationUrl);
await attachHttpIpRewriteInterceptor(page, 'ui-pagination', { shareToken });
await prepareWhatshubShortLinkBeforeNav(page, navigationUrl, antiBot);
await navigateToPage(page, navigationUrl);
const settledPageUrl = await waitForUrlSettled(page);
@@ -756,7 +1483,9 @@ app.post('/api/ui-pagination', async (req, res) => {
}
if (antiBot.enabled) {
const cfResult = await handleCloudflareChallenge(page, antiBot, waitForUrlSettled, storedSession);
const cfResult = await runCloudflareWithBusinessGuard(
page, antiBot, waitForUrlSettled, storedSession, navigationUrl
);
if (!cfResult.success) {
cfDestroyBrowser = true;
res.status(422).json({
@@ -770,10 +1499,11 @@ app.post('/api/ui-pagination', async (req, res) => {
return;
}
touchSessionIfPresent(antiBot.sessionKey);
}
if (antiBot.enabled) {
await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled);
await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, navigationUrl);
await attachHttpIpRewriteInterceptor(page, 'ui-pagination', { shareToken });
} else {
await attachHttpIpRewriteInterceptor(page, 'ui-pagination', { shareToken });
}
const paginationResult = await runUiPagination(page, {
@@ -830,7 +1560,7 @@ app.post('/api/auth-intercept-and-paginate', async (req, res) => {
firstPageData,
} = pagination;
const antiBot = normalizeAntiBot(rawAntiBot);
const antiBot = resolveAntiBotForPage(rawAntiBot, pageUrl);
const profile = antiBot.profile || 'standard';
const interceptTimeout = resolveInterceptTimeout(antiBot);
const cfLog = { profile, cfDetected: false, turnstileStage: null, solverUsed: false, elapsedMs: 0, browserReused: false };
@@ -852,25 +1582,29 @@ app.post('/api/auth-intercept-and-paginate', async (req, res) => {
const { storedSession, mergedCookies } = resolveSessionContext(antiBot, pageUrl);
const page = await createManagedPage(browserSession.browser, browserSession.page, {
userAgent: DEFAULT_UA,
viewport: { width: 1920, height: 1080 },
cookies: mergedCookies,
});
const page = await createTaskPage(
browserSession.browser,
browserSession.page,
pageUrl,
antiBot,
mergedCookies,
{ viewport: { width: 1920, height: 1080 } }
);
const httpResolvedUrl = await resolveHttpRedirects(pageUrl).catch(() => pageUrl);
const interceptor = createApiInterceptor(page, apiUrls, interceptTimeout);
interceptorCleanup = interceptor.cleanup;
let shareToken = resolveShareToken(pageUrl, httpResolvedUrl);
await seedShareTokenCookie(page, pageUrl);
await attachHttpIpRewriteInterceptor(page, 'auth-intercept-and-paginate', { shareToken });
await prepareWhatshubShortLinkBeforeNav(page, pageUrl, antiBot);
await navigateToPage(page, pageUrl);
let finalPageUrl = await waitForUrlSettled(page);
let interceptor;
if (antiBot.enabled) {
const cfResult = await handleCloudflareChallenge(page, antiBot, waitForUrlSettled, storedSession);
const cfResult = await runCloudflareWithBusinessGuard(
page, antiBot, waitForUrlSettled, storedSession, pageUrl
);
cfLog.cfDetected = cfResult.cfDetected;
cfLog.turnstileStage = cfResult.stage;
cfLog.solverUsed = cfResult.solverUsed;
@@ -890,13 +1624,15 @@ app.post('/api/auth-intercept-and-paginate', async (req, res) => {
}
finalPageUrl = page.url();
touchSessionIfPresent(antiBot.sessionKey);
await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, pageUrl);
}
if (antiBot.enabled) {
await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled);
}
interceptor = createApiInterceptor(page, apiUrls, interceptTimeout);
interceptorCleanup = interceptor.cleanup;
await attachHttpIpRewriteInterceptor(page, 'auth-intercept-and-paginate', { shareToken });
await executeAuthActionsWithDiagnostics(page, authActions, executeAuthActions);
await executeAuthActionsWithDiagnostics(page, authActions);
await interceptor.waitForApis(interceptTimeout);
const rawCookies = await page.cookies();