A2C工单修复

This commit is contained in:
root
2026-07-03 20:12:20 +08:00
parent c3223f026e
commit 0f3eb53afe
21 changed files with 1561 additions and 172 deletions
+431 -52
View File
@@ -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);
}