From c6aa3cf8c4e5bb5da84d957b94edab811441435a Mon Sep 17 00:00:00 2001 From: foxhui Date: Wed, 17 Dec 2025 03:28:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BA=E5=B0=86=E6=9D=A5=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E8=BF=87=20CloudFlare=20=E9=AA=8C=E8=AF=81=E7=A0=81?= =?UTF-8?q?=E5=81=9A=E5=87=86=E5=A4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 11 ++ src/backend/adapter/turnstile_test.js | 216 ++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 src/backend/adapter/turnstile_test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index f05d695..871e8e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/backend/adapter/turnstile_test.js b/src/backend/adapter/turnstile_test.js new file mode 100644 index 0000000..a72468f --- /dev/null +++ b/src/backend/adapter/turnstile_test.js @@ -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 +};