mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 为将来自动过 CloudFlare 验证码做准备
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
Reference in New Issue
Block a user