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
+147 -19
View File
@@ -1,22 +1,52 @@
/**
* Turnstile 内置求解:等待 widget 自动完成 + token 轮询 + 多 frame sitekey 提取
*
* 支持新版 challenge-platform iframesitekey 在 URL 路径 /rch/{id}/{sitekey}/ 中
*/
const { detectCloudflare, isTurnstileSolved, urlIndicatesCfChallenge } = require('./cf-detector');
/**
* @typedef {Object} TurnstileResult
* @property {boolean} success
* @property {string} stage built_in|timeout|already_clear
* @property {number} elapsedMs
* 从 challenge-platform / Turnstile URL 解析 sitekeyNode 侧,可读 frame.url()
* @param {string} url
* @returns {string|null}
*/
function extractSitekeyFromChallengeUrl(url) {
if (!url || typeof url !== 'string') {
return null;
}
const pathMatch = url.match(/\/rch\/[^/]+\/(0x4[A-Za-z0-9_-]+)\//i);
if (pathMatch) {
return pathMatch[1];
}
const queryMatch = url.match(/[?&]sitekey=([^&]+)/i);
if (queryMatch) {
return decodeURIComponent(queryMatch[1]);
}
const looseMatch = url.match(/\/(0x4[A-Za-z0-9_-]{10,})\//i);
if (looseMatch) {
return looseMatch[1];
}
return null;
}
/**
* 在单个 frame 内提取 sitekey
* 在单个 frame 内提取 sitekey(主文档 DOM + iframe src 属性)
* @param {import('puppeteer').Frame} frame
* @returns {Promise<string|null>}
*/
async function extractSitekeyFromFrame(frame) {
return frame.evaluate(() => {
const extractFromSrc = (src) => {
if (!src) return null;
const pathMatch = src.match(/\/rch\/[^/]+\/(0x4[A-Za-z0-9_-]+)\//i);
if (pathMatch) return pathMatch[1];
const queryMatch = src.match(/[?&]sitekey=([^&]+)/i);
if (queryMatch) return decodeURIComponent(queryMatch[1]);
const looseMatch = src.match(/\/(0x4[A-Za-z0-9_-]{10,})\//i);
if (looseMatch) return looseMatch[1];
return null;
};
const widget = document.querySelector('.cf-turnstile[data-sitekey], [data-sitekey]');
if (widget) {
const key = widget.getAttribute('data-sitekey');
@@ -25,12 +55,13 @@ async function extractSitekeyFromFrame(frame) {
}
}
const iframes = document.querySelectorAll('iframe[src*="challenges.cloudflare.com"], iframe[src*="challenge-platform"]');
const iframes = document.querySelectorAll(
'iframe[src*="challenges.cloudflare.com"], iframe[src*="challenge-platform"], iframe#cf-chl-widget'
);
for (const iframe of iframes) {
const src = iframe.getAttribute('src') || '';
const match = src.match(/[?&]sitekey=([^&]+)/i);
if (match) {
return decodeURIComponent(match[1]);
const fromSrc = extractFromSrc(iframe.getAttribute('src') || '');
if (fromSrc) {
return fromSrc;
}
}
@@ -38,7 +69,8 @@ async function extractSitekeyFromFrame(frame) {
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);
|| text.match(/data-sitekey=['"]([0-9x_a-zA-Z-]{10,})['"]/i)
|| text.match(/\/(0x4[A-Za-z0-9_-]{10,})\//i);
if (match) {
return match[1];
}
@@ -48,6 +80,34 @@ async function extractSitekeyFromFrame(frame) {
}).catch(() => null);
}
/**
* 从整页 HTML 文本提取 sitekey(应对 OOPIF 无法 enumerate 的场景)
* @param {string} html
* @returns {string|null}
*/
function extractSitekeyFromHtml(html) {
if (!html || typeof html !== 'string') {
return null;
}
const pathMatch = html.match(/\/rch\/[^/]+\/(0x4[A-Za-z0-9_-]+)\//i);
if (pathMatch) {
return pathMatch[1];
}
const dataMatch = html.match(/data-sitekey=["']([^"']+)["']/i);
if (dataMatch) {
return dataMatch[1];
}
const queryMatch = html.match(/[?&]sitekey=([^&"']+)/i);
if (queryMatch) {
return decodeURIComponent(queryMatch[1]);
}
const looseMatch = html.match(/\/(0x4[A-Za-z0-9_-]{10,})\//i);
if (looseMatch) {
return looseMatch[1];
}
return null;
}
/**
* 从主文档及所有子 frame 提取 Turnstile sitekey
* @param {import('puppeteer').Page} page
@@ -60,6 +120,10 @@ async function extractTurnstileSitekey(page) {
}
for (const frame of page.frames()) {
const fromFrameUrl = extractSitekeyFromChallengeUrl(frame.url());
if (fromFrameUrl) {
return fromFrameUrl;
}
if (frame === page.mainFrame()) {
continue;
}
@@ -69,15 +133,30 @@ async function extractTurnstileSitekey(page) {
}
}
const html = await page.content().catch(() => '');
const fromHtml = extractSitekeyFromHtml(html);
if (fromHtml) {
return fromHtml;
}
const browser = page.browser();
if (browser) {
for (const target of browser.targets()) {
const fromTarget = extractSitekeyFromChallengeUrl(target.url());
if (fromTarget) {
return fromTarget;
}
}
}
return null;
}
/**
* Captcha API 调用前:等待 Turnstile 挑战面出现或 URL 进入 CF 挑战态
*
* @param {import('puppeteer').Page} page
* @param {number} [timeoutMs]
* @returns {Promise<string|null>} 若已提取到 sitekey 则返回,否则 null
* @returns {Promise<string|null>}
*/
async function waitForTurnstileSurface(page, timeoutMs = 20000) {
const started = Date.now();
@@ -86,7 +165,7 @@ async function waitForTurnstileSurface(page, timeoutMs = 20000) {
while (Date.now() - started < timeoutMs) {
const sitekey = await extractTurnstileSitekey(page);
if (sitekey) {
console.log(`[CF] 已检测到 Turnstile sitekey (${sitekey.slice(0, 8)}...)`);
console.log(`[CF] 已检测到 Turnstile sitekey (${sitekey.slice(0, 12)}...)`);
return sitekey;
}
@@ -104,7 +183,8 @@ async function waitForTurnstileSurface(page, timeoutMs = 20000) {
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"]'
+ 'iframe[src*="challenges.cloudflare.com"], iframe[src*="challenge-platform"], '
+ 'iframe[id^="cf-chl-widget"]'
);
}).catch(() => false);
@@ -122,8 +202,7 @@ async function waitForTurnstileSurface(page, timeoutMs = 20000) {
/**
* 等待 Turnstile 通过(内置点击 / 自动跳转)
* @param {import('puppeteer').Page} page
* @param {{ timeoutMs?: number, pollMs?: number, useBuiltInClick?: boolean }} [options]
* @returns {Promise<TurnstileResult>}
* @param {{ timeoutMs?: number, pollMs?: number }} [options]
*/
async function waitForTurnstile(page, options = {}) {
const timeoutMs = options.timeoutMs ?? 60000;
@@ -148,7 +227,7 @@ async function waitForTurnstile(page, options = {}) {
}
const state = await detectCloudflare(page);
if (!state.blocked && !urlIndicatesCfChallenge(page.url()) && state.hasCfClearance) {
if (!state.blocked && !urlIndicatesCfChallenge(page.url())) {
return { success: true, stage: 'built_in', elapsedMs: Date.now() - started };
}
@@ -159,7 +238,27 @@ async function waitForTurnstile(page, options = {}) {
}
/**
* 将 Captcha API 返回的 token 注入页面
* 等待页面离开 CF 挑战 URL / 挑战 DOM
* @param {import('puppeteer').Page} page
* @param {number} timeoutMs
* @param {number} pollMs
*/
async function waitForChallengeUrlClear(page, timeoutMs = 45000, pollMs = 500) {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
if (!urlIndicatesCfChallenge(page.url())) {
const state = await detectCloudflare(page);
if (!state.blocked) {
return true;
}
}
await new Promise((r) => setTimeout(r, pollMs));
}
return false;
}
/**
* 将 Captcha API 返回的 token 注入页面并尝试触发提交
* @param {import('puppeteer').Page} page
* @param {string} token
*/
@@ -178,10 +277,39 @@ async function injectTurnstileToken(page, token) {
}, token);
}
/**
* token 注入后等待过盾;若仍停留在挑战页则 reload 干净业务 URL
* @param {import('puppeteer').Page} page
* @param {string} cleanUrl
* @param {(page: import('puppeteer').Page) => Promise<string>} [waitForUrlSettled]
* @param {number} [timeoutMs]
*/
async function recoverAfterTokenInject(page, cleanUrl, waitForUrlSettled, timeoutMs = 45000) {
if (await waitForChallengeUrlClear(page, Math.min(timeoutMs, 12000))) {
return true;
}
if (!cleanUrl) {
return false;
}
console.log(`[CF] token 注入后仍处挑战页,reload 干净 URL: ${cleanUrl}`);
await page.goto(cleanUrl, { waitUntil: 'domcontentloaded', timeout: Math.min(timeoutMs, 60000) }).catch(() => {});
if (typeof waitForUrlSettled === 'function') {
await waitForUrlSettled(page).catch(() => {});
}
return waitForChallengeUrlClear(page, timeoutMs);
}
module.exports = {
waitForTurnstile,
waitForTurnstileSurface,
extractTurnstileSitekey,
extractSitekeyFromFrame,
extractSitekeyFromChallengeUrl,
extractSitekeyFromHtml,
injectTurnstileToken,
waitForChallengeUrlClear,
recoverAfterTokenInject,
};