A2C工单修复
This commit is contained in:
+431
-52
@@ -1,5 +1,12 @@
|
||||
require('dotenv').config({ path: require('path').join(__dirname, '.env') });
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('[FATAL] uncaughtException:', err);
|
||||
});
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
console.error('[FATAL] unhandledRejection:', reason);
|
||||
});
|
||||
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const {
|
||||
@@ -41,12 +48,14 @@ 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 { runCaptchaApiFallback, shouldPreferManagedCloudflareSolver } = require('./lib/cf-api-fallback');
|
||||
const {
|
||||
extractTurnstileSitekey,
|
||||
injectTurnstileToken,
|
||||
waitForTurnstileSurface,
|
||||
} = require('./lib/turnstile-handler');
|
||||
const { solveTurnstile } = require('./lib/captcha-solver');
|
||||
getCaptchaProxyChromeArgs,
|
||||
applyCaptchaProxyToPage,
|
||||
parseCaptchaProxy,
|
||||
hasCaptchaProxy,
|
||||
} = require('./lib/captcha-proxy');
|
||||
const { attachSitekeyCapture } = require('./lib/sitekey-capture');
|
||||
const {
|
||||
saveSession,
|
||||
getSessionDebugInfo,
|
||||
@@ -78,7 +87,7 @@ async function runBrowserTask(taskName, handler, profile = 'standard') {
|
||||
}
|
||||
|
||||
function sendTaskError(res, error, extra = {}) {
|
||||
if (error.code === 'QUEUE_TIMEOUT') {
|
||||
if (error.code === 'QUEUE_TIMEOUT' || error.message === 'POOL_ACQUIRE_TIMEOUT') {
|
||||
return res.status(503).json({
|
||||
success: false,
|
||||
error: '服务繁忙,排队超时,请稍后重试',
|
||||
@@ -124,7 +133,8 @@ function classifyPuppeteerError(err) {
|
||||
if (err?.name === 'TimeoutError' || msg.includes('timeout')) return 'timeout';
|
||||
if (msg.includes('net::') || msg.includes('network') || msg.includes('err_connection')
|
||||
|| msg.includes('err_address_unreachable') || msg.includes('err_name_not_resolved')) return 'network';
|
||||
if (msg.includes('navigation') || msg.includes('frame was detached')) return 'navigation';
|
||||
if (msg.includes('navigation') || msg.includes('frame was detached')
|
||||
|| msg.includes('detached frame') || msg.includes('attempted to use detached')) return 'navigation';
|
||||
if (msg.includes('target closed') || msg.includes('session closed')) return 'target_closed';
|
||||
return 'unknown';
|
||||
}
|
||||
@@ -190,12 +200,16 @@ function resolveAntiBot(rawAntiBot) {
|
||||
...base,
|
||||
postCfReadyUrlContains: rawAntiBot.postCfReadyUrlContains || '',
|
||||
postCfReadySelector: rawAntiBot.postCfReadySelector || '',
|
||||
postCfReadyApiUrl: rawAntiBot.postCfReadyApiUrl || '',
|
||||
postCfReadyTimeoutMs: rawAntiBot.postCfReadyTimeoutMs || 0,
|
||||
postCfSettleMs: rawAntiBot.postCfSettleMs || 0,
|
||||
postCfSpaWaitMs: rawAntiBot.postCfSpaWaitMs || 0,
|
||||
cfClearanceRequired: rawAntiBot.cfClearanceRequired !== false,
|
||||
businessHostHint: rawAntiBot.businessHostHint || '',
|
||||
landingUrlHint: rawAntiBot.landingUrlHint || '',
|
||||
turnstileSitekey: rawAntiBot.turnstileSitekey || '',
|
||||
cfCloudflareSolver: rawAntiBot.cfCloudflareSolver !== false,
|
||||
cfChallengeMode: rawAntiBot.cfChallengeMode || '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -265,12 +279,15 @@ function applyA2cPostCfDefaults(antiBot, pageUrl) {
|
||||
return {
|
||||
...antiBot,
|
||||
postCfReadyUrlContains: '/visitors/counter/share',
|
||||
postCfReadySelector: '.btn-next',
|
||||
postCfReadyTimeoutMs: antiBot.postCfReadyTimeoutMs || 30000,
|
||||
postCfReadySelector: '.el-table',
|
||||
postCfReadyApiUrl: antiBot.postCfReadyApiUrl || '/api/talk/counter/share/record/list',
|
||||
postCfReadyTimeoutMs: antiBot.postCfReadyTimeoutMs || 60000,
|
||||
postCfSettleMs: antiBot.postCfSettleMs || 500,
|
||||
postCfSpaWaitMs: antiBot.postCfSpaWaitMs || 4000,
|
||||
postCfSpaWaitMs: antiBot.postCfSpaWaitMs || 8000,
|
||||
cfClearanceRequired: antiBot.cfClearanceRequired === undefined ? false : antiBot.cfClearanceRequired,
|
||||
solverFallback: antiBot.solverFallback === undefined ? false : antiBot.solverFallback,
|
||||
solverFallback: antiBot.solverFallback === undefined ? true : antiBot.solverFallback,
|
||||
cfCloudflareSolver: antiBot.cfCloudflareSolver !== false,
|
||||
cfChallengeMode: antiBot.cfChallengeMode || 'managed',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -326,6 +343,51 @@ function isBusinessUrlReady(page, urlNeedle) {
|
||||
return !urlNeedle || page.url().includes(urlNeedle);
|
||||
}
|
||||
|
||||
/** A2C 已直达 user.a2c.chat 分享页(非 yyk.ink 短链跳转场景) */
|
||||
function isA2cDirectSharePage(page, urlNeedle = '') {
|
||||
const needle = (urlNeedle || '').trim();
|
||||
if (!needle.includes('/visitors/counter/share')) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(page.url() || '');
|
||||
return parsed.hostname.includes('a2c.chat') && parsed.pathname.includes('/visitors/counter/share');
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 过盾后等待业务 list API 返回(A2C Vue 挂载信号) */
|
||||
async function waitForPostCfListApi(page, antiBot, timeoutMs) {
|
||||
const apiUrl = (antiBot.postCfReadyApiUrl || '').trim();
|
||||
if (!apiUrl) {
|
||||
return false;
|
||||
}
|
||||
console.log(`[CF] 等待业务 API 响应 api=${apiUrl} timeout=${timeoutMs}ms`);
|
||||
const matched = await page.waitForResponse(
|
||||
(resp) => {
|
||||
const req = resp.request();
|
||||
if (req.method() === 'OPTIONS') {
|
||||
return false;
|
||||
}
|
||||
return resp.url().includes(apiUrl) && resp.status() >= 200 && resp.status() < 400;
|
||||
},
|
||||
{ timeout: timeoutMs }
|
||||
).catch(() => null);
|
||||
if (matched) {
|
||||
console.log(`[CF] 业务 API 已响应 status=${matched.status()} url=${matched.url()}`);
|
||||
return true;
|
||||
}
|
||||
console.warn('[CF] 业务 API 等待超时,降级为 DOM selector 检测');
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 等待业务 DOM:先 attached 再 visible,避免 Vue 渲染中途误判 */
|
||||
async function waitForPostCfSelector(page, selector, timeoutMs) {
|
||||
await page.waitForSelector(selector, { timeout: timeoutMs, visible: false });
|
||||
await page.waitForSelector(selector, { timeout: Math.min(15000, timeoutMs), visible: true });
|
||||
}
|
||||
|
||||
/** Whatshub 分享短链 /m/xxx/1 */
|
||||
function isWhatshubShortLinkPath(url) {
|
||||
try {
|
||||
@@ -366,11 +428,141 @@ function buildManagedPageOptions(pageUrl, antiBot, mergedCookies, extraOpts = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务页创建(含 Whatshub skipUserAgent);browser-factory 无写权限时在 server 侧实现
|
||||
* A2C:短链入口时优先导航至 PHP 预解析的长链,减少 yyk.ink 跳转竞态
|
||||
* @param {string} pageUrl
|
||||
* @param {object} antiBot
|
||||
*/
|
||||
async function createTaskPage(browser, existingPage, pageUrl, antiBot, mergedCookies, extraOpts = {}) {
|
||||
function resolveNavigationUrl(pageUrl, antiBot) {
|
||||
const hint = String(antiBot?.landingUrlHint || '').trim();
|
||||
if (hint === '' || !hint.includes('/visitors/counter/share')) {
|
||||
return pageUrl;
|
||||
}
|
||||
try {
|
||||
const pageHost = new URL(pageUrl).hostname;
|
||||
const hintHost = new URL(hint).hostname;
|
||||
if (hintHost.includes('a2c.chat') && pageHost !== hintHost) {
|
||||
console.log(`[A2C] 短链入口,直接导航至预解析长链: ${hint}`);
|
||||
return hint;
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
return pageUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chrome 启动参数:仅当 useCaptchaProxy=true 时注入 CAPTCHA_PROXY(与 CapSolver 同源 IP)
|
||||
* @param {object|null|undefined} antiBot
|
||||
* @param {string[]} [baseArgs]
|
||||
* @param {boolean} [useCaptchaProxy]
|
||||
*/
|
||||
function buildBrowserExtraArgs(antiBot, baseArgs = [], useCaptchaProxy = false) {
|
||||
const args = [...baseArgs];
|
||||
if (!useCaptchaProxy || !hasCaptchaProxy()) {
|
||||
return args;
|
||||
}
|
||||
const proxyArgs = getCaptchaProxyChromeArgs();
|
||||
if (proxyArgs.length > 0) {
|
||||
const parsed = parseCaptchaProxy();
|
||||
console.log(
|
||||
`[Proxy] Chrome 使用 CAPTCHA_PROXY ${parsed.host}:${parsed.port}(与 CapSolver 同源 IP)`
|
||||
);
|
||||
args.push(...proxyArgs);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Managed A2C 等:仅在检测到真实 CF 挑战时切换为 CAPTCHA_PROXY Browser(无盾则直连)
|
||||
* 会原地更新 browserCtx.browserSession / browserCtx.page
|
||||
* @param {object} browserCtx
|
||||
*/
|
||||
async function ensureCaptchaProxyBrowserIfNeeded(browserCtx) {
|
||||
const {
|
||||
antiBot,
|
||||
profile,
|
||||
navigationUrl,
|
||||
mergedCookies,
|
||||
baseExtraArgs = ['--mute-audio'],
|
||||
pageExtraOpts = {},
|
||||
} = browserCtx;
|
||||
let { browserSession, page } = browserCtx;
|
||||
|
||||
if (!antiBot?.enabled || browserSession?.captchaProxyActive) {
|
||||
return;
|
||||
}
|
||||
if (!shouldPreferManagedCloudflareSolver(antiBot) || !hasCaptchaProxy()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cfState = await detectCloudflare(page, antiBot);
|
||||
const needsProxy = cfState.blocked || urlIndicatesCfChallenge(page.url());
|
||||
if (!needsProxy) {
|
||||
console.log('[Proxy] 未检测到 CF 挑战,Chrome 直连(不使用 CAPTCHA_PROXY)');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Proxy] 检测到 CF 挑战,切换 Browser 为 CAPTCHA_PROXY(与 CapSolver 同源 IP)');
|
||||
|
||||
const snapshotCookies = await page.cookies().catch(() => []);
|
||||
await closeTaskPage(page);
|
||||
await browserSession.release(true);
|
||||
|
||||
const extraArgs = buildBrowserExtraArgs(antiBot, baseExtraArgs, true);
|
||||
browserSession = await acquireBrowserSession({
|
||||
profile,
|
||||
extraArgs,
|
||||
sessionKey: antiBot.sessionKey || '',
|
||||
});
|
||||
browserSession.captchaProxyActive = true;
|
||||
|
||||
page = await createTaskPage(
|
||||
browserSession.browser,
|
||||
browserSession.reused ? null : browserSession.page,
|
||||
navigationUrl,
|
||||
antiBot,
|
||||
mergedCookies.length > 0 ? mergedCookies : snapshotCookies,
|
||||
{ ...pageExtraOpts, useCaptchaProxy: true }
|
||||
);
|
||||
|
||||
if (antiBot?.enabled) {
|
||||
attachSitekeyCapture(page, browserSession.browser);
|
||||
}
|
||||
|
||||
await prepareWhatshubShortLinkBeforeNav(page, navigationUrl, antiBot);
|
||||
await navigateToPage(page, navigationUrl);
|
||||
await waitForUrlSettled(page).catch(() => {});
|
||||
|
||||
if (typeof browserCtx.afterRelaunchNav === 'function') {
|
||||
await browserCtx.afterRelaunchNav(page);
|
||||
}
|
||||
|
||||
browserCtx.browserSession = browserSession;
|
||||
browserCtx.page = page;
|
||||
}
|
||||
|
||||
/** 任务结束关闭本次 Tab(温池复用 Browser 时保留 Browser 实例) */
|
||||
async function closeTaskPage(page) {
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (typeof page.isClosed === 'function' && !page.isClosed()) {
|
||||
await page.close();
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务页创建(含 Whatshub skipUserAgent)
|
||||
* 温池复用 Browser 时每次 newPage,冷启动 Real Browser 可使用 launch 返回的首个 Tab
|
||||
*/
|
||||
async function createTaskPage(browser, warmPage, pageUrl, antiBot, mergedCookies, extraOpts = {}) {
|
||||
const opts = buildManagedPageOptions(pageUrl, antiBot, mergedCookies, extraOpts);
|
||||
const page = existingPage || await browser.newPage();
|
||||
const useWarmPage = !!(warmPage && extraOpts.useWarmPage !== false && !extraOpts.forceNewPage);
|
||||
const page = useWarmPage ? warmPage : await browser.newPage();
|
||||
page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT_MS);
|
||||
page.setDefaultTimeout(PAGE_DEFAULT_TIMEOUT_MS);
|
||||
|
||||
@@ -384,9 +576,17 @@ async function createTaskPage(browser, existingPage, pageUrl, antiBot, mergedCoo
|
||||
await page.setCookie(...opts.cookies);
|
||||
}
|
||||
|
||||
if (extraOpts.useCaptchaProxy && hasCaptchaProxy()) {
|
||||
await applyCaptchaProxyToPage(page);
|
||||
}
|
||||
|
||||
page.on('error', (err) => console.error('[Page Error]', err.message));
|
||||
page.on('pageerror', (err) => console.error('[Page JS Error]', err.message));
|
||||
|
||||
if (antiBot?.enabled) {
|
||||
attachSitekeyCapture(page, browser);
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
@@ -419,25 +619,32 @@ function isOnShortLinkNotBusiness(page, urlNeedle) {
|
||||
async function waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettledFn, pageUrl = '') {
|
||||
const urlNeedle = (antiBot.postCfReadyUrlContains || '').trim();
|
||||
const targetUrl = pageUrl || page.url();
|
||||
if (!urlNeedle || !isOnShortLinkNotBusiness(page, urlNeedle)) {
|
||||
const onA2cDirect = isA2cDirectSharePage(page, urlNeedle);
|
||||
if (!urlNeedle || (!isOnShortLinkNotBusiness(page, urlNeedle) && !onA2cDirect)) {
|
||||
return isBusinessUrlReady(page, urlNeedle);
|
||||
}
|
||||
if (!(await isCfTrulyComplete(page, antiBot))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const spaWaitMs = Math.max(8000, antiBot.postCfSpaWaitMs || 8000);
|
||||
const spaWaitMs = onA2cDirect
|
||||
? Math.max(8000, antiBot.postCfSpaWaitMs || 8000)
|
||||
: Math.max(8000, antiBot.postCfSpaWaitMs || 8000);
|
||||
const isWhatshub = isWhatshubShortLinkPath(targetUrl);
|
||||
|
||||
console.log(
|
||||
`[CF] CF 已完成,等待 SPA 跳转 settle=${spaWaitMs}ms current=${page.url()}`
|
||||
`[CF] CF 已完成,等待 SPA ${onA2cDirect ? '直链挂载' : '跳转'} 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(() => {});
|
||||
await page.waitForNetworkIdle({ idleTime: 500, timeout: 20000 }).catch(() => {});
|
||||
|
||||
if (onA2cDirect) {
|
||||
await waitForPostCfListApi(page, antiBot, Math.max(15000, antiBot.postCfReadyTimeoutMs || 60000)).catch(() => {});
|
||||
}
|
||||
|
||||
if (isBusinessUrlReady(page, urlNeedle)) {
|
||||
console.log(`[CF] SPA 已跳转至业务页 url=${page.url()}`);
|
||||
@@ -503,13 +710,18 @@ async function isCfTrulyComplete(page, antiBot = null) {
|
||||
return !state.blocked;
|
||||
}
|
||||
|
||||
/** 轮询直到 CF 真正完成(不依赖 turnstile-handler 的 early return) */
|
||||
/** 轮询直到 CF 真正完成;并行扫描 sitekey 供 CapSolver 提前使用 */
|
||||
async function waitForCfTrulyComplete(page, timeoutMs, pollMs = 500, antiBot = null) {
|
||||
const { getSitekeyCapture } = require('./lib/sitekey-capture');
|
||||
const capture = getSitekeyCapture(page);
|
||||
const started = Date.now();
|
||||
while (Date.now() - started < timeoutMs) {
|
||||
if (await isCfTrulyComplete(page, antiBot)) {
|
||||
return { success: true, elapsedMs: Date.now() - started };
|
||||
}
|
||||
if (capture) {
|
||||
await capture.scanNow(page, antiBot).catch(() => {});
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
||||
}
|
||||
return { success: false, elapsedMs: Date.now() - started };
|
||||
@@ -566,6 +778,62 @@ async function runCfChallengeFlow(page, antiBot, waitForUrlSettled, pageUrl = ''
|
||||
console.log(`[CF] 免 clearance 模式但检测到真实 CF 挑战 type=${preState.challengeType},继续过盾`);
|
||||
}
|
||||
|
||||
const preferManaged = shouldPreferManagedCloudflareSolver(antiBot);
|
||||
|
||||
if (preferManaged && antiBot.solverFallback !== false && isCaptchaConfigured()) {
|
||||
console.log('[CF] Managed Challenge:跳过内置 60s 轮询,直接使用 CapSolver AntiCloudflareTask');
|
||||
try {
|
||||
if (waitForUrlSettled) {
|
||||
await waitForUrlSettled(page).catch(() => {});
|
||||
}
|
||||
await runCaptchaApiFallback(page, {
|
||||
antiBot,
|
||||
navUrl,
|
||||
waitForUrlSettled,
|
||||
timeoutMs,
|
||||
});
|
||||
|
||||
const apiWait = await waitForCfTrulyComplete(page, Math.min(timeoutMs, 60000), 500, antiBot);
|
||||
if (apiWait.success) {
|
||||
if (waitForUrlSettled) {
|
||||
await waitForUrlSettled(page).catch(() => {});
|
||||
}
|
||||
await waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettled, navUrl);
|
||||
return {
|
||||
success: true,
|
||||
code: null,
|
||||
challengeType: 'managed',
|
||||
stage: 'api_managed',
|
||||
solverUsed: true,
|
||||
elapsedMs: Date.now() - started,
|
||||
cfDetected: true,
|
||||
};
|
||||
}
|
||||
console.warn('[CF] Managed CapSolver 注入后仍未获得有效 cf_clearance');
|
||||
} catch (apiErr) {
|
||||
console.error('[CF] Managed Challenge CapSolver 失败:', apiErr.message);
|
||||
return {
|
||||
success: false,
|
||||
code: 'CF_TURNSTILE_FAILED',
|
||||
challengeType: 'managed',
|
||||
stage: 'api_managed',
|
||||
solverUsed: true,
|
||||
elapsedMs: Date.now() - started,
|
||||
cfDetected: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
code: 'CF_TURNSTILE_FAILED',
|
||||
challengeType: 'managed',
|
||||
stage: 'api_managed',
|
||||
solverUsed: true,
|
||||
elapsedMs: Date.now() - started,
|
||||
cfDetected: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (antiBot.turnstile !== false) {
|
||||
console.log(`[CF] 内置 Turnstile 轮询 timeout=${timeoutMs}ms`);
|
||||
const builtIn = await waitForCfTrulyComplete(page, timeoutMs, 500, antiBot);
|
||||
@@ -592,15 +860,12 @@ async function runCfChallengeFlow(page, antiBot, waitForUrlSettled, pageUrl = ''
|
||||
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);
|
||||
await runCaptchaApiFallback(page, {
|
||||
antiBot,
|
||||
navUrl,
|
||||
waitForUrlSettled,
|
||||
timeoutMs,
|
||||
});
|
||||
|
||||
const apiWait = await waitForCfTrulyComplete(page, Math.min(timeoutMs, 45000), 500, antiBot);
|
||||
if (apiWait.success) {
|
||||
@@ -744,10 +1009,19 @@ async function prepareWhatshubShortLinkBeforeNav(page, pageUrl, antiBot) {
|
||||
/**
|
||||
* CF 处理 + 业务 URL 守卫:磁盘 session 陈旧 clearance 时 purge;__cf_chl 中间态只过盾不 purge
|
||||
*/
|
||||
async function runCloudflareWithBusinessGuard(page, antiBot, waitForUrlSettled, storedSession, pageUrl) {
|
||||
async function runCloudflareWithBusinessGuard(page, antiBot, waitForUrlSettled, storedSession, pageUrl, browserCtx = null) {
|
||||
const urlNeedle = (antiBot.postCfReadyUrlContains || '').trim();
|
||||
const hasStoredSession = !!(storedSession && isSessionLikelyValid(storedSession));
|
||||
|
||||
async function maybeEnsureProxy(currentPage) {
|
||||
if (!browserCtx) {
|
||||
return currentPage;
|
||||
}
|
||||
browserCtx.page = currentPage;
|
||||
await ensureCaptchaProxyBrowserIfNeeded(browserCtx);
|
||||
return browserCtx.page;
|
||||
}
|
||||
|
||||
async function reNavigateWithCfPurge(label) {
|
||||
if (urlIndicatesCfChallenge(page.url())) {
|
||||
console.log(`[CF] ${label}: URL 含 __cf_chl,跳过 purge,直接过盾`);
|
||||
@@ -759,6 +1033,7 @@ async function runCloudflareWithBusinessGuard(page, antiBot, waitForUrlSettled,
|
||||
if (waitForUrlSettled) {
|
||||
await waitForUrlSettled(page).catch(() => {});
|
||||
}
|
||||
page = await maybeEnsureProxy(page);
|
||||
return handleCloudflareChallengeSafe(page, antiBot, waitForUrlSettled, storedSession);
|
||||
}
|
||||
|
||||
@@ -773,6 +1048,7 @@ async function runCloudflareWithBusinessGuard(page, antiBot, waitForUrlSettled,
|
||||
}
|
||||
|
||||
if (!cfResult) {
|
||||
page = await maybeEnsureProxy(page);
|
||||
cfResult = await handleCloudflareChallengeSafe(page, antiBot, waitForUrlSettled, storedSession);
|
||||
}
|
||||
|
||||
@@ -781,6 +1057,7 @@ async function runCloudflareWithBusinessGuard(page, antiBot, waitForUrlSettled,
|
||||
const crossState = await detectCloudflare(page, antiBot);
|
||||
if (crossState.blocked || urlIndicatesCfChallenge(page.url())) {
|
||||
console.log(`[CF] 跨域落地后 CF 未完成,二次过盾 url=${page.url()}`);
|
||||
page = await maybeEnsureProxy(page);
|
||||
cfResult = await finalizeCfResult(
|
||||
page,
|
||||
await runCfChallengeFlow(page, antiBot, waitForUrlSettled, page.url()),
|
||||
@@ -803,6 +1080,7 @@ async function runCloudflareWithBusinessGuard(page, antiBot, waitForUrlSettled,
|
||||
retries += 1;
|
||||
if (!(await isCfTrulyComplete(page, antiBot))) {
|
||||
console.log(`[CF] 业务页未达且 CF 未完成,完整过盾 重试#${retries}`);
|
||||
page = await maybeEnsureProxy(page);
|
||||
cfResult = await finalizeCfResult(
|
||||
page,
|
||||
await runCfChallengeFlow(page, antiBot, waitForUrlSettled, pageUrl),
|
||||
@@ -823,6 +1101,10 @@ async function runCloudflareWithBusinessGuard(page, antiBot, waitForUrlSettled,
|
||||
}
|
||||
}
|
||||
|
||||
if (browserCtx) {
|
||||
browserCtx.page = page;
|
||||
}
|
||||
|
||||
return finalizeCfResult(page, cfResult, antiBot);
|
||||
}
|
||||
|
||||
@@ -833,6 +1115,9 @@ 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,
|
||||
elTable: document.querySelectorAll('.el-table').length,
|
||||
btnNext: document.querySelectorAll('.btn-next').length,
|
||||
elPagination: document.querySelectorAll('.el-pagination').length,
|
||||
})).catch(() => null);
|
||||
return snapshot || { url, title: '', bodyText: '', elInputInner: 0 };
|
||||
}
|
||||
@@ -851,7 +1136,20 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor
|
||||
|
||||
await ensureCfCompleteBeforeBusinessReady(page, antiBot, waitForUrlSettledFn, storedSession);
|
||||
|
||||
await waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettledFn, navUrl);
|
||||
const onA2cDirect = isA2cDirectSharePage(page, urlContains);
|
||||
let skipSpaWait = false;
|
||||
if (onA2cDirect && await isCfTrulyComplete(page, antiBot)) {
|
||||
const domReady = selector
|
||||
? await page.$(selector).catch(() => null)
|
||||
: true;
|
||||
if (domReady) {
|
||||
console.log('[CF] A2C 业务 DOM 已就绪,跳过重复 SPA 等待');
|
||||
skipSpaWait = true;
|
||||
}
|
||||
}
|
||||
if (!skipSpaWait) {
|
||||
await waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettledFn, navUrl);
|
||||
}
|
||||
|
||||
const timeoutMs = Math.max(5000, antiBot.postCfReadyTimeoutMs || 30000);
|
||||
console.log(
|
||||
@@ -867,12 +1165,14 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor
|
||||
urlContains
|
||||
);
|
||||
}
|
||||
await waitForPostCfListApi(page, antiBot, timeoutMs);
|
||||
if (selector !== '') {
|
||||
await page.waitForSelector(selector, { timeout: timeoutMs, visible: true });
|
||||
await waitForPostCfSelector(page, selector, timeoutMs);
|
||||
}
|
||||
} catch (err) {
|
||||
const retryMs = Math.min(15000, timeoutMs);
|
||||
const retryMs = Math.min(30000, timeoutMs);
|
||||
const needsRetry = urlContains !== '' && !page.url().includes(urlContains);
|
||||
const needsDomRetry = urlContains !== '' && page.url().includes(urlContains) && isA2cDirectSharePage(page, urlContains);
|
||||
if (needsRetry) {
|
||||
if (!(await isCfTrulyComplete(page, antiBot))) {
|
||||
console.warn(`[CF] 业务页等待失败且 CF 未完成 url=${page.url()},完整过盾后重试`);
|
||||
@@ -886,7 +1186,7 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor
|
||||
);
|
||||
}
|
||||
if (selector !== '') {
|
||||
await page.waitForSelector(selector, { timeout: retryMs, visible: true });
|
||||
await waitForPostCfSelector(page, selector, retryMs);
|
||||
}
|
||||
} catch (retryErr) {
|
||||
retryErr.pageDiagnostics = await collectPageDiagnostics(page);
|
||||
@@ -906,14 +1206,28 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor
|
||||
urlContains
|
||||
);
|
||||
}
|
||||
await waitForPostCfListApi(page, antiBot, retryMs);
|
||||
if (selector !== '') {
|
||||
await page.waitForSelector(selector, { timeout: retryMs, visible: true });
|
||||
await waitForPostCfSelector(page, selector, retryMs);
|
||||
}
|
||||
} catch (retryErr) {
|
||||
retryErr.pageDiagnostics = await collectPageDiagnostics(page);
|
||||
throw retryErr;
|
||||
}
|
||||
}
|
||||
} else if (needsDomRetry) {
|
||||
console.warn(`[CF] A2C URL 已就绪但 DOM 未挂载 url=${page.url()},soft reload 后重试`);
|
||||
try {
|
||||
await page.waitForNetworkIdle({ idleTime: 500, timeout: 20000 }).catch(() => {});
|
||||
await softReloadForSpa(page, waitForUrlSettledFn, antiBot, navUrl);
|
||||
await waitForPostCfListApi(page, antiBot, retryMs);
|
||||
if (selector !== '') {
|
||||
await waitForPostCfSelector(page, selector, retryMs);
|
||||
}
|
||||
} catch (retryErr) {
|
||||
retryErr.pageDiagnostics = await collectPageDiagnostics(page);
|
||||
throw retryErr;
|
||||
}
|
||||
} else {
|
||||
err.pageDiagnostics = await collectPageDiagnostics(page);
|
||||
throw err;
|
||||
@@ -1230,6 +1544,7 @@ app.post('/api/auth-and-intercept', async (req, res) => {
|
||||
await runBrowserTask('auth-and-intercept', async () => {
|
||||
let browserSession = null;
|
||||
let interceptorCleanup = null;
|
||||
let page = null;
|
||||
const taskStarted = Date.now();
|
||||
let cfDestroyBrowser = false;
|
||||
|
||||
@@ -1242,11 +1557,12 @@ app.post('/api/auth-and-intercept', async (req, res) => {
|
||||
cfLog.browserReused = !!browserSession.reused;
|
||||
|
||||
const { storedSession, mergedCookies } = resolveSessionContext(antiBot, pageUrl);
|
||||
const navigationUrl = resolveNavigationUrl(pageUrl, antiBot);
|
||||
|
||||
const page = await createTaskPage(
|
||||
page = await createTaskPage(
|
||||
browserSession.browser,
|
||||
browserSession.page,
|
||||
pageUrl,
|
||||
browserSession.reused ? null : browserSession.page,
|
||||
navigationUrl,
|
||||
antiBot,
|
||||
mergedCookies
|
||||
);
|
||||
@@ -1257,13 +1573,13 @@ app.post('/api/auth-and-intercept', async (req, res) => {
|
||||
}
|
||||
|
||||
let shareToken = resolveShareToken(pageUrl, httpResolvedUrl);
|
||||
await seedShareTokenCookie(page, pageUrl);
|
||||
await seedShareTokenCookie(page, navigationUrl);
|
||||
if (httpResolvedUrl !== pageUrl) {
|
||||
await seedShareTokenCookie(page, httpResolvedUrl);
|
||||
}
|
||||
|
||||
await prepareWhatshubShortLinkBeforeNav(page, pageUrl, antiBot);
|
||||
await navigateToPage(page, pageUrl);
|
||||
await prepareWhatshubShortLinkBeforeNav(page, navigationUrl, antiBot);
|
||||
await navigateToPage(page, navigationUrl);
|
||||
|
||||
let finalPageUrl = await waitForUrlSettled(page);
|
||||
applyHuojianBusinessHostHint(antiBot, finalPageUrl);
|
||||
@@ -1271,11 +1587,30 @@ app.post('/api/auth-and-intercept', async (req, res) => {
|
||||
console.log(`[URL解析] 浏览器落地: ${pageUrl} -> ${finalPageUrl}`);
|
||||
}
|
||||
|
||||
const browserCtx = {
|
||||
browserSession,
|
||||
page,
|
||||
antiBot,
|
||||
profile,
|
||||
navigationUrl,
|
||||
mergedCookies,
|
||||
baseExtraArgs: ['--mute-audio'],
|
||||
pageExtraOpts: {},
|
||||
afterRelaunchNav: async (p) => {
|
||||
await seedShareTokenCookie(p, navigationUrl);
|
||||
},
|
||||
};
|
||||
await ensureCaptchaProxyBrowserIfNeeded(browserCtx);
|
||||
browserSession = browserCtx.browserSession;
|
||||
page = browserCtx.page;
|
||||
|
||||
let interceptor;
|
||||
if (antiBot.enabled) {
|
||||
const cfResult = await runCloudflareWithBusinessGuard(
|
||||
page, antiBot, waitForUrlSettled, storedSession, pageUrl
|
||||
page, antiBot, waitForUrlSettled, storedSession, pageUrl, browserCtx
|
||||
);
|
||||
page = browserCtx.page;
|
||||
browserSession = browserCtx.browserSession;
|
||||
cfLog.cfDetected = cfResult.cfDetected;
|
||||
cfLog.turnstileStage = cfResult.stage;
|
||||
cfLog.solverUsed = cfResult.solverUsed;
|
||||
@@ -1346,6 +1681,7 @@ app.post('/api/auth-and-intercept', async (req, res) => {
|
||||
});
|
||||
} finally {
|
||||
if (interceptorCleanup) interceptorCleanup();
|
||||
await closeTaskPage(page);
|
||||
if (browserSession) {
|
||||
await browserSession.release(cfDestroyBrowser);
|
||||
}
|
||||
@@ -1495,6 +1831,7 @@ app.post('/api/ui-pagination', async (req, res) => {
|
||||
await runBrowserTask('ui-pagination', async () => {
|
||||
let browserSession = null;
|
||||
const debugLogs = [];
|
||||
let page = null;
|
||||
let cfDestroyBrowser = false;
|
||||
|
||||
try {
|
||||
@@ -1506,9 +1843,9 @@ app.post('/api/ui-pagination', async (req, res) => {
|
||||
|
||||
const { storedSession, mergedCookies } = resolveSessionContext(antiBot, navigationUrl, cookies || []);
|
||||
|
||||
const page = await createTaskPage(
|
||||
page = await createTaskPage(
|
||||
browserSession.browser,
|
||||
browserSession.page,
|
||||
browserSession.reused ? null : browserSession.page,
|
||||
navigationUrl,
|
||||
antiBot,
|
||||
mergedCookies,
|
||||
@@ -1532,10 +1869,29 @@ app.post('/api/ui-pagination', async (req, res) => {
|
||||
log(`[URL解析] 翻页阶段落地: ${navigationUrl} -> ${settledPageUrl}`);
|
||||
}
|
||||
|
||||
const browserCtx = {
|
||||
browserSession,
|
||||
page,
|
||||
antiBot,
|
||||
profile,
|
||||
navigationUrl,
|
||||
mergedCookies,
|
||||
baseExtraArgs: ['--window-size=1920,1080'],
|
||||
pageExtraOpts: { viewport: { width: 1920, height: 1080 } },
|
||||
afterRelaunchNav: async (p) => {
|
||||
await seedShareTokenCookie(p, navigationUrl);
|
||||
},
|
||||
};
|
||||
await ensureCaptchaProxyBrowserIfNeeded(browserCtx);
|
||||
browserSession = browserCtx.browserSession;
|
||||
page = browserCtx.page;
|
||||
|
||||
if (antiBot.enabled) {
|
||||
const cfResult = await runCloudflareWithBusinessGuard(
|
||||
page, antiBot, waitForUrlSettled, storedSession, navigationUrl
|
||||
page, antiBot, waitForUrlSettled, storedSession, navigationUrl, browserCtx
|
||||
);
|
||||
page = browserCtx.page;
|
||||
browserSession = browserCtx.browserSession;
|
||||
if (!cfResult.success) {
|
||||
cfDestroyBrowser = true;
|
||||
res.status(422).json({
|
||||
@@ -1578,6 +1934,7 @@ app.post('/api/ui-pagination', async (req, res) => {
|
||||
sendTaskError(res, error, { debug: debugLogs, profile });
|
||||
}
|
||||
} finally {
|
||||
await closeTaskPage(page);
|
||||
if (browserSession) {
|
||||
await browserSession.release(cfDestroyBrowser);
|
||||
}
|
||||
@@ -1619,6 +1976,7 @@ app.post('/api/auth-intercept-and-paginate', async (req, res) => {
|
||||
await runBrowserTask('auth-intercept-and-paginate', async () => {
|
||||
let browserSession = null;
|
||||
let interceptorCleanup = null;
|
||||
let page = null;
|
||||
const taskStarted = Date.now();
|
||||
let cfDestroyBrowser = false;
|
||||
|
||||
@@ -1631,11 +1989,12 @@ app.post('/api/auth-intercept-and-paginate', async (req, res) => {
|
||||
cfLog.browserReused = !!browserSession.reused;
|
||||
|
||||
const { storedSession, mergedCookies } = resolveSessionContext(antiBot, pageUrl);
|
||||
const navigationUrl = resolveNavigationUrl(pageUrl, antiBot);
|
||||
|
||||
const page = await createTaskPage(
|
||||
page = await createTaskPage(
|
||||
browserSession.browser,
|
||||
browserSession.page,
|
||||
pageUrl,
|
||||
browserSession.reused ? null : browserSession.page,
|
||||
navigationUrl,
|
||||
antiBot,
|
||||
mergedCookies,
|
||||
{ viewport: { width: 1920, height: 1080 } }
|
||||
@@ -1644,18 +2003,37 @@ app.post('/api/auth-intercept-and-paginate', async (req, res) => {
|
||||
const httpResolvedUrl = await resolveHttpRedirects(pageUrl).catch(() => pageUrl);
|
||||
|
||||
let shareToken = resolveShareToken(pageUrl, httpResolvedUrl);
|
||||
await seedShareTokenCookie(page, pageUrl);
|
||||
await seedShareTokenCookie(page, navigationUrl);
|
||||
|
||||
await prepareWhatshubShortLinkBeforeNav(page, pageUrl, antiBot);
|
||||
await navigateToPage(page, pageUrl);
|
||||
await prepareWhatshubShortLinkBeforeNav(page, navigationUrl, antiBot);
|
||||
await navigateToPage(page, navigationUrl);
|
||||
let finalPageUrl = await waitForUrlSettled(page);
|
||||
applyHuojianBusinessHostHint(antiBot, finalPageUrl);
|
||||
|
||||
const browserCtx = {
|
||||
browserSession,
|
||||
page,
|
||||
antiBot,
|
||||
profile,
|
||||
navigationUrl,
|
||||
mergedCookies,
|
||||
baseExtraArgs: ['--mute-audio', '--window-size=1920,1080'],
|
||||
pageExtraOpts: { viewport: { width: 1920, height: 1080 } },
|
||||
afterRelaunchNav: async (p) => {
|
||||
await seedShareTokenCookie(p, navigationUrl);
|
||||
},
|
||||
};
|
||||
await ensureCaptchaProxyBrowserIfNeeded(browserCtx);
|
||||
browserSession = browserCtx.browserSession;
|
||||
page = browserCtx.page;
|
||||
|
||||
let interceptor;
|
||||
if (antiBot.enabled) {
|
||||
const cfResult = await runCloudflareWithBusinessGuard(
|
||||
page, antiBot, waitForUrlSettled, storedSession, pageUrl
|
||||
page, antiBot, waitForUrlSettled, storedSession, navigationUrl, browserCtx
|
||||
);
|
||||
page = browserCtx.page;
|
||||
browserSession = browserCtx.browserSession;
|
||||
cfLog.cfDetected = cfResult.cfDetected;
|
||||
cfLog.turnstileStage = cfResult.stage;
|
||||
cfLog.solverUsed = cfResult.solverUsed;
|
||||
@@ -1677,7 +2055,7 @@ app.post('/api/auth-intercept-and-paginate', async (req, res) => {
|
||||
applyHuojianBusinessHostHint(antiBot, finalPageUrl);
|
||||
touchSessionIfPresent(antiBot.sessionKey);
|
||||
|
||||
await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, pageUrl);
|
||||
await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, navigationUrl);
|
||||
}
|
||||
|
||||
interceptor = createApiInterceptor(page, apiUrls, interceptTimeout);
|
||||
@@ -1744,6 +2122,7 @@ app.post('/api/auth-intercept-and-paginate', async (req, res) => {
|
||||
});
|
||||
} finally {
|
||||
if (interceptorCleanup) interceptorCleanup();
|
||||
await closeTaskPage(page);
|
||||
if (browserSession) {
|
||||
await browserSession.release(cfDestroyBrowser);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user