修复A2C工单蜘蛛
This commit is contained in:
+778
-42
@@ -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.chat:PHP 未传 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 路由(保留 clearance)url=${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 场景:软 reload;Whatshub 短链改用二次 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(保留 clearance)url=${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_clearance;A2C 等 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();
|
||||
|
||||
Reference in New Issue
Block a user