feat: 为将来自动过 CloudFlare 验证码做准备

This commit is contained in:
foxhui
2025-12-17 03:28:33 +08:00
Unverified
parent ae8629686c
commit c6aa3cf8c4
2 changed files with 227 additions and 0 deletions
+11
View File
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.1.0] - 2025-12-17
### ✨ Added
- **支持文本模型**
- 添加专门的文本模型适配器(目前仅支持 LMArena 和 Gemini Busineess
- 支持网络搜索模型,例如 gemini-3-pro-grounding、grok-4-1-fast-search
- **图片调度**
- 若有适配器同时支持同一个模型,但是图片策略不同,将会优先将带图片的请求分发给支持图片的适配器
- **为自动通过验证码做准备**
- 新增测试适配器 turnstile_test ,为将来需要自动过 CloudFlare 验证码做准备
## [3.0.1] - 2025-12-16
### ✨ Added
+216
View File
@@ -0,0 +1,216 @@
/**
* @fileoverview Cloudflare Turnstile 测试适配器
* 使用 shadowRootUnl 访问 closed shadow-root 内的元素
*
* HTML 结构:
* #example-container5 > div > closed shadow-root > iframe
* iframe body > closed shadow-root > ... > input[type="checkbox"]
*/
import {
sleep,
safeClick
} from '../../browser/utils.js';
import {
gotoWithCheck,
normalizePageError,
moveMouseAway,
} from '../utils/index.js';
import { logger } from '../../utils/logger.js';
// --- 配置常量 ---
//const TARGET_URL = 'https://nopecha.com/captcha/turnstile';
const TARGET_URL = 'https://nowsecure.nl/';
/**
* 递归查找具有 shadowRootUnl 的子元素
*/
async function findElementWithShadowRoot(hostHandle) {
return await hostHandle.evaluateHandle(el => {
for (const child of el.querySelectorAll('*')) {
if (child.shadowRootUnl) {
return child;
}
}
return null;
});
}
/**
* 执行 Turnstile 验证任务
*/
async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
const { page } = context;
try {
logger.info('适配器', '开启 Turnstile 测试...', meta);
await gotoWithCheck(page, TARGET_URL);
// 等待页面加载
await sleep(3000, 4000);
// 1. 获取宿主元素
logger.info('适配器', '正在查找宿主元素...', meta);
//const hostLocator = page.locator('#example-container5');
const hostLocator = page.locator('.cf-turnstile').first();
await hostLocator.waitFor({ state: 'visible', timeout: 10000 });
const hostHandle = await hostLocator.elementHandle();
if (!hostHandle) {
return { error: '无法获取宿主元素句柄' };
}
// 2. 查找有 shadowRootUnl 的子元素
logger.info('适配器', '正在查找有 shadowRootUnl 的子元素...', meta);
const childWithShadowHandle = await findElementWithShadowRoot(hostHandle);
const childElement = childWithShadowHandle.asElement();
if (!childElement) {
return { error: '未找到有 shadowRootUnl 的子元素' };
}
logger.info('适配器', '找到有 shadowRootUnl 的子元素', meta);
// 3. 获取第一层 shadow-root 并找到 iframe
const shadowRootHandle = await childElement.evaluateHandle(el => el.shadowRootUnl);
const iframeHandle = await shadowRootHandle.evaluateHandle(root => root?.querySelector('iframe'));
const iframeElement = iframeHandle.asElement();
if (!iframeElement) {
return { error: '第一层 shadow-root 内未找到 iframe' };
}
logger.info('适配器', '找到 iframe,正在进入 iframe 内部...', meta);
// 4. 获取 iframe 的 contentDocument (使用 contentFrame)
const frame = await iframeElement.contentFrame();
if (!frame) {
logger.warn('适配器', '无法获取 iframe 的 contentFrame,尝试坐标点击...', meta);
// 降级方案:坐标点击
const box = await iframeElement.boundingBox();
if (box) {
const checkboxX = box.x + 28;
const checkboxY = box.y + box.height / 2;
await page.mouse.move(checkboxX, checkboxY, { steps: 10 });
await sleep(300, 500);
await page.mouse.click(checkboxX, checkboxY);
logger.info('适配器', '已点击 checkbox(坐标模式)', meta);
await sleep(5000, 8000);
return { text: 'Turnstile 验证已点击(坐标模式)' };
}
return { error: '无法获取 iframe 边界框' };
}
// 5. 在 iframe 内查找 body 或有 shadowRootUnl 的元素
logger.info('适配器', '正在查找 iframe 内的 shadow-root...', meta);
// 等待 iframe 内容加载
await sleep(1000, 2000);
// 尝试获取 iframe 内 body 的 shadowRootUnl
const bodyWithShadowHandle = await frame.evaluateHandle(() => {
// 先检查 body 本身
if (document.body && document.body.shadowRootUnl) {
return document.body;
}
// 遍历所有元素查找有 shadowRootUnl 的
for (const el of document.querySelectorAll('*')) {
if (el.shadowRootUnl) {
return el;
}
}
return null;
});
const bodyElement = bodyWithShadowHandle.asElement();
if (!bodyElement) {
logger.warn('适配器', 'iframe 内未找到有 shadowRootUnl 的元素,尝试坐标点击...', meta);
const box = await iframeElement.boundingBox();
if (box) {
const checkboxX = box.x + 28;
const checkboxY = box.y + box.height / 2;
await page.mouse.move(checkboxX, checkboxY, { steps: 10 });
await sleep(300, 500);
await page.mouse.click(checkboxX, checkboxY);
logger.info('适配器', '已点击 checkbox(坐标模式)', meta);
await sleep(5000, 8000);
return { text: 'Turnstile 验证已点击(坐标模式)' };
}
return { error: 'iframe 内未找到有 shadowRootUnl 的元素' };
}
logger.info('适配器', '找到 iframe 内的 shadowRootUnl 宿主', meta);
// 6. 获取 iframe 内部的 shadow-root 并查找 checkbox
const innerShadowRootHandle = await bodyElement.evaluateHandle(el => el.shadowRootUnl);
const checkboxHandle = await innerShadowRootHandle.evaluateHandle(root => {
if (!root) return null;
// 查找 input[type="checkbox"]
const checkbox = root.querySelector('input[type="checkbox"]');
if (checkbox) return checkbox;
// 备用:查找任何 input
return root.querySelector('input');
});
const checkboxElement = checkboxHandle.asElement();
if (!checkboxElement) {
logger.warn('适配器', 'iframe shadow-root 内未找到 checkbox,尝试坐标点击...', meta);
const box = await iframeElement.boundingBox();
if (box) {
const checkboxX = box.x + 28;
const checkboxY = box.y + box.height / 2;
await page.mouse.move(checkboxX, checkboxY, { steps: 10 });
await sleep(300, 500);
await page.mouse.click(checkboxX, checkboxY);
logger.info('适配器', '已点击 checkbox(坐标模式)', meta);
await sleep(5000, 8000);
return { text: 'Turnstile 验证已点击(坐标模式)' };
}
return { error: 'iframe shadow-root 内未找到 checkbox' };
}
logger.info('适配器', '找到 checkbox,正在点击...', meta);
// 7. 点击 checkbox
await safeClick(page, checkboxElement, { bias: 'random' });
logger.info('适配器', '已点击 checkbox(直接模式)', meta);
await sleep(5000, 8000);
return { text: 'Turnstile 验证已点击(直接模式)' };
} catch (err) {
const pageError = normalizePageError(err, meta);
if (pageError) return pageError;
logger.error('适配器', '任务失败', { ...meta, error: err.message });
return { error: `任务失败: ${err.message}` };
} finally {
await moveMouseAway(page);
}
}
/**
* 适配器 manifest
*/
export const manifest = {
id: 'turnstile_test',
displayName: 'Cloudflare Turnstile Test',
getTargetUrl(config, workerConfig) {
return TARGET_URL;
},
models: [
{ id: 'cloudflare-turnstile', imagePolicy: 'forbidden', type: 'text' }
],
resolveModelId(modelKey) {
const model = this.models.find(m => m.id === modelKey);
return model ? model.id : null;
},
navigationHandlers: [],
generateImage
};