修复A2C工单蜘蛛
This commit is contained in:
@@ -26,12 +26,16 @@ function normalizeAntiBot(antiBot) {
|
||||
profile: antiBot.profile === 'real' ? 'real' : 'standard',
|
||||
turnstile: antiBot.turnstile !== false,
|
||||
solverFallback: antiBot.solverFallback !== false,
|
||||
cfClearanceRequired: antiBot.cfClearanceRequired !== false,
|
||||
sessionKey: antiBot.sessionKey || '',
|
||||
challengeTimeoutMs: antiBot.challengeTimeoutMs || 60000,
|
||||
postCfReadyUrlContains: antiBot.postCfReadyUrlContains || '',
|
||||
postCfReadySelector: antiBot.postCfReadySelector || '',
|
||||
postCfReadyTimeoutMs: antiBot.postCfReadyTimeoutMs || 0,
|
||||
postCfSettleMs: antiBot.postCfSettleMs || 0,
|
||||
postCfSpaWaitMs: antiBot.postCfSpaWaitMs || 0,
|
||||
businessHostHint: antiBot.businessHostHint || '',
|
||||
landingUrlHint: antiBot.landingUrlHint || '',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/**
|
||||
* Cloudflare / Turnstile 挑战页检测
|
||||
*
|
||||
* antiBot.cfClearanceRequired === false(A2C 业务域):仅真实 CF 中间页算 blocked,
|
||||
* 不因页面引用 turnstile.js 或缺少 cf_clearance cookie 误判。
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -17,6 +20,14 @@ function urlIndicatesCfChallenge(url) {
|
||||
|| url.includes('/cdn-cgi/challenge');
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否要求 cf_clearance 才视为过盾完成(A2C user.a2c.chat 为 false)
|
||||
* @param {object|null|undefined} antiBot
|
||||
*/
|
||||
function isCfClearanceRequired(antiBot) {
|
||||
return antiBot?.cfClearanceRequired !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} CloudflareState
|
||||
* @property {boolean} blocked 是否处于 CF 挑战中
|
||||
@@ -29,9 +40,10 @@ function urlIndicatesCfChallenge(url) {
|
||||
/**
|
||||
* 检测页面是否被 Cloudflare 拦截
|
||||
* @param {import('puppeteer').Page} page
|
||||
* @param {object|null} [antiBot]
|
||||
* @returns {Promise<CloudflareState>}
|
||||
*/
|
||||
async function detectCloudflare(page) {
|
||||
async function detectCloudflare(page, antiBot = null) {
|
||||
const url = page.url();
|
||||
const title = await page.title().catch(() => '');
|
||||
|
||||
@@ -61,22 +73,28 @@ async function detectCloudflare(page) {
|
||||
|
||||
const urlChallenge = urlIndicatesCfChallenge(url);
|
||||
|
||||
const realChallenge = urlChallenge
|
||||
|| domSignals.titleHasJustAMoment
|
||||
|| domSignals.bodyHasJustAMoment
|
||||
|| domSignals.hasChallengeRunning
|
||||
|| title.includes('Just a moment');
|
||||
|
||||
const turnstileSignals = domSignals.hasTurnstileInput || domSignals.hasTurnstileWidget;
|
||||
|
||||
let challengeType = null;
|
||||
if (domSignals.hasTurnstileInput || domSignals.hasTurnstileWidget || url.includes('__cf_chl')) {
|
||||
if (turnstileSignals || url.includes('__cf_chl')) {
|
||||
challengeType = 'turnstile';
|
||||
} else if (urlChallenge || domSignals.titleHasJustAMoment || domSignals.bodyHasJustAMoment) {
|
||||
} else if (realChallenge) {
|
||||
challengeType = 'js_challenge';
|
||||
}
|
||||
|
||||
const blocked = !hasCfClearance && (
|
||||
urlChallenge
|
||||
|| domSignals.titleHasJustAMoment
|
||||
|| domSignals.bodyHasJustAMoment
|
||||
|| domSignals.hasTurnstileInput
|
||||
|| domSignals.hasTurnstileWidget
|
||||
|| domSignals.hasChallengeRunning
|
||||
|| title.includes('Just a moment')
|
||||
);
|
||||
const requireClearance = isCfClearanceRequired(antiBot);
|
||||
let blocked;
|
||||
if (requireClearance) {
|
||||
blocked = !hasCfClearance && (realChallenge || turnstileSignals);
|
||||
} else {
|
||||
blocked = realChallenge;
|
||||
}
|
||||
|
||||
return {
|
||||
blocked,
|
||||
@@ -110,4 +128,5 @@ module.exports = {
|
||||
detectCloudflare,
|
||||
isTurnstileSolved,
|
||||
urlIndicatesCfChallenge,
|
||||
isCfClearanceRequired,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 否则清除可能过期的 cf_clearance 并重新触发 Turnstile(Whatshub 短链 → work-order-sharing 场景)。
|
||||
*/
|
||||
const { detectCloudflare } = require('./cf-detector');
|
||||
const { waitForTurnstile, extractTurnstileSitekey, injectTurnstileToken } = require('./turnstile-handler');
|
||||
const { waitForTurnstile, extractTurnstileSitekey, injectTurnstileToken, waitForTurnstileSurface } = require('./turnstile-handler');
|
||||
const { solveTurnstile, isCaptchaConfigured } = require('./captcha-solver');
|
||||
const { isSessionLikelyValid } = require('./session-store');
|
||||
|
||||
@@ -83,14 +83,14 @@ async function tryPostCfRecoverViaReload(page, waitForUrlSettled, timeoutMs, ant
|
||||
* @param {(page: import('puppeteer').Page) => Promise<string>} [waitForUrlSettled]
|
||||
* @param {number} timeoutMs
|
||||
*/
|
||||
async function invalidateStaleCfAndReload(page, waitForUrlSettled, timeoutMs) {
|
||||
async function invalidateStaleCfAndReload(page, waitForUrlSettled, timeoutMs, antiBot = null) {
|
||||
console.log('[CF] 清除 cf_clearance 并 reload,重新触发 Turnstile');
|
||||
await clearCfClearanceCookies(page);
|
||||
await page.reload({ waitUntil: 'domcontentloaded', timeout: Math.min(timeoutMs, 45000) });
|
||||
if (waitForUrlSettled) {
|
||||
await waitForUrlSettled(page).catch(() => {});
|
||||
}
|
||||
return detectCloudflare(page);
|
||||
return detectCloudflare(page, antiBot);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,7 +146,7 @@ async function handleCloudflareChallenge(page, antiBot, waitForUrlSettled, store
|
||||
const started = Date.now();
|
||||
const timeoutMs = antiBot.challengeTimeoutMs || 60000;
|
||||
|
||||
let cfState = await detectCloudflare(page);
|
||||
let cfState = await detectCloudflare(page, antiBot);
|
||||
|
||||
if (!cfState.blocked && cfState.hasCfClearance) {
|
||||
const fast = await trySessionReuseFastPath(
|
||||
@@ -155,7 +155,7 @@ async function handleCloudflareChallenge(page, antiBot, waitForUrlSettled, store
|
||||
if (fast) {
|
||||
return fast;
|
||||
}
|
||||
cfState = await invalidateStaleCfAndReload(page, waitForUrlSettled, timeoutMs);
|
||||
cfState = await invalidateStaleCfAndReload(page, waitForUrlSettled, timeoutMs, antiBot);
|
||||
}
|
||||
|
||||
if (!cfState.blocked) {
|
||||
@@ -188,7 +188,7 @@ async function handleCloudflareChallenge(page, antiBot, waitForUrlSettled, store
|
||||
}
|
||||
|
||||
if (cfState.hasCfClearance) {
|
||||
cfState = await invalidateStaleCfAndReload(page, waitForUrlSettled, timeoutMs);
|
||||
cfState = await invalidateStaleCfAndReload(page, waitForUrlSettled, timeoutMs, antiBot);
|
||||
} else {
|
||||
return {
|
||||
success: true,
|
||||
@@ -209,7 +209,7 @@ async function handleCloudflareChallenge(page, antiBot, waitForUrlSettled, store
|
||||
if (waitForUrlSettled) {
|
||||
await waitForUrlSettled(page).catch(() => {});
|
||||
}
|
||||
cfState = await detectCloudflare(page);
|
||||
cfState = await detectCloudflare(page, antiBot);
|
||||
if (!cfState.blocked) {
|
||||
if (waitForUrlSettled) {
|
||||
await waitForUrlSettled(page).catch(() => {});
|
||||
@@ -237,7 +237,7 @@ async function handleCloudflareChallenge(page, antiBot, waitForUrlSettled, store
|
||||
if (waitForUrlSettled) {
|
||||
await waitForUrlSettled(page).catch(() => {});
|
||||
}
|
||||
cfState = await detectCloudflare(page);
|
||||
cfState = await detectCloudflare(page, antiBot);
|
||||
if (!cfState.blocked) {
|
||||
return {
|
||||
success: true,
|
||||
@@ -254,8 +254,15 @@ async function handleCloudflareChallenge(page, antiBot, waitForUrlSettled, store
|
||||
|
||||
if (antiBot.solverFallback !== false && isCaptchaConfigured()) {
|
||||
try {
|
||||
const sitekey = await extractTurnstileSitekey(page);
|
||||
if (waitForUrlSettled) {
|
||||
await waitForUrlSettled(page).catch(() => {});
|
||||
}
|
||||
let sitekey = await waitForTurnstileSurface(page, Math.min(timeoutMs, 25000));
|
||||
if (!sitekey) {
|
||||
sitekey = await extractTurnstileSitekey(page);
|
||||
}
|
||||
const pageUrl = page.url();
|
||||
console.log(`[CF] Captcha API 求解 pageUrl=${pageUrl} sitekey=${sitekey ? 'ok' : 'missing'}`);
|
||||
const { token, provider } = await solveTurnstile({ pageUrl, sitekey });
|
||||
console.log(`[CF] Captcha API (${provider}) 返回 token,注入页面`);
|
||||
await injectTurnstileToken(page, token);
|
||||
@@ -267,7 +274,7 @@ async function handleCloudflareChallenge(page, antiBot, waitForUrlSettled, store
|
||||
await waitForUrlSettled(page).catch(() => {});
|
||||
}
|
||||
|
||||
cfState = await detectCloudflare(page);
|
||||
cfState = await detectCloudflare(page, antiBot);
|
||||
if (!cfState.blocked) {
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Turnstile 内置求解:等待 widget 自动完成 + token 轮询
|
||||
* Turnstile 内置求解:等待 widget 自动完成 + token 轮询 + 多 frame sitekey 提取
|
||||
*/
|
||||
const { detectCloudflare, isTurnstileSolved, urlIndicatesCfChallenge } = require('./cf-detector');
|
||||
|
||||
@@ -10,6 +10,115 @@ const { detectCloudflare, isTurnstileSolved, urlIndicatesCfChallenge } = require
|
||||
* @property {number} elapsedMs
|
||||
*/
|
||||
|
||||
/**
|
||||
* 在单个 frame 内提取 sitekey
|
||||
* @param {import('puppeteer').Frame} frame
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
async function extractSitekeyFromFrame(frame) {
|
||||
return frame.evaluate(() => {
|
||||
const widget = document.querySelector('.cf-turnstile[data-sitekey], [data-sitekey]');
|
||||
if (widget) {
|
||||
const key = widget.getAttribute('data-sitekey');
|
||||
if (key) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
const iframes = document.querySelectorAll('iframe[src*="challenges.cloudflare.com"], iframe[src*="challenge-platform"]');
|
||||
for (const iframe of iframes) {
|
||||
const src = iframe.getAttribute('src') || '';
|
||||
const match = src.match(/[?&]sitekey=([^&]+)/i);
|
||||
if (match) {
|
||||
return decodeURIComponent(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
const scripts = document.querySelectorAll('script');
|
||||
for (const script of scripts) {
|
||||
const text = script.textContent || '';
|
||||
const match = text.match(/sitekey['"\s:=]+['"]([0-9x_a-zA-Z-]{10,})['"]/i)
|
||||
|| text.match(/data-sitekey=['"]([0-9x_a-zA-Z-]{10,})['"]/i);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}).catch(() => null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从主文档及所有子 frame 提取 Turnstile sitekey
|
||||
* @param {import('puppeteer').Page} page
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
async function extractTurnstileSitekey(page) {
|
||||
const mainKey = await extractSitekeyFromFrame(page.mainFrame());
|
||||
if (mainKey) {
|
||||
return mainKey;
|
||||
}
|
||||
|
||||
for (const frame of page.frames()) {
|
||||
if (frame === page.mainFrame()) {
|
||||
continue;
|
||||
}
|
||||
const frameKey = await extractSitekeyFromFrame(frame);
|
||||
if (frameKey) {
|
||||
return frameKey;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Captcha API 调用前:等待 Turnstile 挑战面出现或 URL 进入 CF 挑战态
|
||||
*
|
||||
* @param {import('puppeteer').Page} page
|
||||
* @param {number} [timeoutMs]
|
||||
* @returns {Promise<string|null>} 若已提取到 sitekey 则返回,否则 null
|
||||
*/
|
||||
async function waitForTurnstileSurface(page, timeoutMs = 20000) {
|
||||
const started = Date.now();
|
||||
const pollMs = 400;
|
||||
|
||||
while (Date.now() - started < timeoutMs) {
|
||||
const sitekey = await extractTurnstileSitekey(page);
|
||||
if (sitekey) {
|
||||
console.log(`[CF] 已检测到 Turnstile sitekey (${sitekey.slice(0, 8)}...)`);
|
||||
return sitekey;
|
||||
}
|
||||
|
||||
const url = page.url();
|
||||
if (urlIndicatesCfChallenge(url)) {
|
||||
await new Promise((r) => setTimeout(r, pollMs));
|
||||
continue;
|
||||
}
|
||||
|
||||
const state = await detectCloudflare(page);
|
||||
if (!state.blocked && state.hasCfClearance) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasSurface = await page.evaluate(() => {
|
||||
return !!document.querySelector(
|
||||
'.cf-turnstile, [data-sitekey], [name="cf-turnstile-response"], '
|
||||
+ 'iframe[src*="challenges.cloudflare.com"], iframe[src*="challenge-platform"]'
|
||||
);
|
||||
}).catch(() => false);
|
||||
|
||||
if (hasSurface) {
|
||||
await new Promise((r) => setTimeout(r, pollMs));
|
||||
continue;
|
||||
}
|
||||
|
||||
await new Promise((r) => setTimeout(r, pollMs));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待 Turnstile 通过(内置点击 / 自动跳转)
|
||||
* @param {import('puppeteer').Page} page
|
||||
@@ -49,26 +158,6 @@ async function waitForTurnstile(page, options = {}) {
|
||||
return { success: false, stage: 'timeout', elapsedMs: Date.now() - started };
|
||||
}
|
||||
|
||||
/**
|
||||
* 从页面提取 Turnstile sitekey
|
||||
* @param {import('puppeteer').Page} page
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
async function extractTurnstileSitekey(page) {
|
||||
return page.evaluate(() => {
|
||||
const widget = document.querySelector('.cf-turnstile[data-sitekey], [data-sitekey]');
|
||||
if (widget) {
|
||||
return widget.getAttribute('data-sitekey');
|
||||
}
|
||||
const iframe = document.querySelector('iframe[src*="challenges.cloudflare.com"]');
|
||||
if (iframe && iframe.src) {
|
||||
const match = iframe.src.match(/[?&]sitekey=([^&]+)/);
|
||||
if (match) return decodeURIComponent(match[1]);
|
||||
}
|
||||
return null;
|
||||
}).catch(() => null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Captcha API 返回的 token 注入页面
|
||||
* @param {import('puppeteer').Page} page
|
||||
@@ -91,6 +180,8 @@ async function injectTurnstileToken(page, token) {
|
||||
|
||||
module.exports = {
|
||||
waitForTurnstile,
|
||||
waitForTurnstileSurface,
|
||||
extractTurnstileSitekey,
|
||||
extractSitekeyFromFrame,
|
||||
injectTurnstileToken,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user