feat: 初步测试自动过盾,添加适配器模型管理

This commit is contained in:
foxhui
2025-12-29 03:51:14 +08:00
Unverified
parent 6f4860dbca
commit c82e7d6f06
22 changed files with 526 additions and 238 deletions
+191
View File
@@ -0,0 +1,191 @@
/**
* @fileoverview 浏览器测试适配器
* 提供多种浏览器测试功能,包括 Cloudflare Turnstile 验证、指纹检测等
*
* 模型类型:
* - cloudflare-turnstile: 点击验证后截屏
* - 其他 image 类型: 加载页面后截屏
* - text 类型: 返回页面文本内容
*/
import { sleep } from '../engine/utils.js';
import {
gotoWithCheck,
normalizePageError,
moveMouseAway,
} from '../utils/index.js';
import { clickTurnstile } from '../utils/CloudflareBypass.js';
import { logger } from '../../utils/logger.js';
/**
* 执行 Turnstile 验证并截屏
*/
async function handleTurnstile(page, meta) {
const TARGET_URL = 'https://nopecha.com/captcha/turnstile';
const HOST_SELECTOR = '#example-container5';
logger.info('适配器', '开启 Turnstile 测试...', meta);
await gotoWithCheck(page, TARGET_URL);
// 等待页面加载
await sleep(3000, 4000);
// 使用通用 Cloudflare 验证码点击器
const result = await clickTurnstile(page, HOST_SELECTOR, {
timeout: 10000,
waitAfterClick: 3000,
meta
});
if (!result.success) {
return { error: result.error };
}
// 截屏并返回
logger.info('适配器', '正在截屏...', meta);
const screenshot = await page.screenshot({ type: 'png', fullPage: true });
const base64 = screenshot.toString('base64');
return { image: `data:image/png;base64,${base64}` };
}
/**
* 处理普通 image 类型:加载页面后截屏
*/
async function handleImagePage(page, url, meta) {
logger.info('适配器', `正在加载页面: ${url}`, meta);
await gotoWithCheck(page, url);
// 等待页面加载完成
await sleep(3000, 5000);
// 截屏并返回
logger.info('适配器', '正在截屏...', meta);
const screenshot = await page.screenshot({ type: 'png', fullPage: true });
const base64 = screenshot.toString('base64');
return { image: `data:image/png;base64,${base64}` };
}
/**
* 处理 ping0.cc:检测并处理 Cloudflare 验证后截屏
*/
async function handlePing0(page, url, meta) {
logger.info('适配器', `正在加载页面: ${url}`, meta);
await gotoWithCheck(page, url);
// 等待页面加载
await sleep(2000, 3000);
// 检测是否有 Cloudflare 验证码
const cfElement = await page.$('#captcha-element');
if (cfElement) {
logger.info('适配器', '检测到 Cloudflare 验证码,正在处理...', meta);
const result = await clickTurnstile(page, '#captcha-element', {
timeout: 10000,
waitAfterClick: 5000,
meta
});
if (!result.success) {
logger.warn('适配器', `Cloudflare 验证失败: ${result.error}`, meta);
// 继续截屏,可能验证页面也有价值
}
// 等待页面跳转或刷新
await sleep(3000, 5000);
}
// 截屏并返回
logger.info('适配器', '正在截屏...', meta);
const screenshot = await page.screenshot({ type: 'png', fullPage: true });
const base64 = screenshot.toString('base64');
return { image: `data:image/png;base64,${base64}` };
}
/**
* 处理 text 类型:返回页面文本内容
*/
async function handleTextPage(page, url, meta) {
logger.info('适配器', `正在加载页面: ${url}`, meta);
await gotoWithCheck(page, url);
// 等待页面加载完成
await sleep(1000, 2000);
// 获取页面文本内容
const textContent = await page.evaluate(() => document.body.innerText);
logger.info('适配器', `获取文本内容,长度: ${textContent.length}`, meta);
return { text: textContent.trim() };
}
/**
* 主生成函数
*/
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
const { page } = context;
try {
// 查找模型配置
const modelConfig = manifest.models.find(m => m.id === modelId);
if (!modelConfig) {
return { error: `未找到模型配置: ${modelId}` };
}
const { url, type } = modelConfig;
// 根据模型 ID 和类型分发处理
if (modelId === 'cloudflare-turnstile') {
// Turnstile 验证特殊处理
return await handleTurnstile(page, meta);
} else if (modelId === 'ping0') {
// ping0.cc 需要 Cloudflare 验证
return await handlePing0(page, url, meta);
} else if (type === 'text') {
// text 类型返回页面文本
return await handleTextPage(page, url, meta);
} else {
// 其他 image 类型截屏返回
return await handleImagePage(page, url, meta);
}
} 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: 'test',
displayName: '浏览器检测,仅供调试使用',
description: '包含 Cloudflare Turnstile 验证测试、浏览器指纹检测、IP 纯净度查询等功能,仅供调试使用。',
getTargetUrl(config, workerConfig) {
return 'https://abrahamjuliot.github.io/creepjs/';
},
models: [
{ id: 'cloudflare-turnstile', imagePolicy: 'forbidden', type: 'image', url: 'https://nopecha.com/captcha/turnstile' },
{ id: 'creepjs', imagePolicy: 'forbidden', type: 'image', url: 'https://abrahamjuliot.github.io/creepjs/' },
{ id: 'antibot', imagePolicy: 'forbidden', type: 'image', url: 'https://bot.sannysoft.com/' },
{ id: 'browserleaks-js', imagePolicy: 'forbidden', type: 'image', url: 'https://browserleaks.com/javascript' },
{ id: 'browserleaks-ip', imagePolicy: 'forbidden', type: 'image', url: 'https://browserleaks.com/ip' },
{ id: 'ip', imagePolicy: 'forbidden', type: 'text', url: 'https://api.ip.sb/ip' },
{ id: 'webgl', imagePolicy: 'forbidden', type: 'image', url: 'https://get.webgl.org/' },
{ id: 'ping0', imagePolicy: 'forbidden', type: 'image', url: 'https://ping0.cc/' },
],
navigationHandlers: [],
generate
};
-212
View File
@@ -1,212 +0,0 @@
/**
* @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 '../engine/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 generate(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 (CF人机验证码测试)',
description: '测试适配器,用于验证浏览器能否自动通过 Cloudflare Turnstile 人机验证。仅供调试使用。',
getTargetUrl(config, workerConfig) {
return TARGET_URL;
},
models: [
{ id: 'cloudflare-turnstile', imagePolicy: 'forbidden', type: 'text' }
],
navigationHandlers: [],
generate
};
+4
View File
@@ -34,6 +34,10 @@ export class PoolManager {
// 先加载所有适配器
await registry.loadAll();
// 注入适配器配置(用于模型过滤)
const adapterConfig = this.config.backend?.adapter || {};
registry.setAdapterConfig(adapterConfig);
// 解析登录模式参数
let loginWorkerName = null;
const loginArg = process.argv.find(arg => arg.startsWith('-login'));
+47 -9
View File
@@ -33,9 +33,43 @@ class AdapterRegistry {
constructor() {
/** @type {Map<string, object>} */
this.adapters = new Map();
/** @type {object} 适配器配置(来自 config.yaml */
this.adapterConfig = {};
this.loaded = false;
}
/**
* 设置适配器配置
* @param {object} config - 适配器配置对象
*/
setAdapterConfig(config) {
this.adapterConfig = config || {};
}
/**
* 检查模型是否启用
* @param {string} adapterId - 适配器 ID
* @param {string} modelId - 模型 ID
* @returns {boolean}
*/
isModelEnabled(adapterId, modelId) {
const adapterCfg = this.adapterConfig[adapterId];
if (!adapterCfg?.modelFilter) return true;
const { mode, list } = adapterCfg.modelFilter;
if (!list || !Array.isArray(list)) return true;
const inList = list.includes(modelId);
if (mode === 'whitelist') {
// 白名单模式:只有在列表中的才启用
return inList;
} else {
// 黑名单模式(默认):在列表中的被禁用
return !inList;
}
}
/**
* 扫描并加载所有适配器
*/
@@ -192,14 +226,16 @@ class AdapterRegistry {
return { object: 'list', data: [] };
}
const data = adapter.models.map(m => ({
id: m.id,
object: 'model',
created: Math.floor(Date.now() / 1000),
owned_by: id,
image_policy: m.imagePolicy,
type: m.type || 'image' // Default to image if not specified
}));
const data = adapter.models
.filter(m => this.isModelEnabled(id, m.id))
.map(m => ({
id: m.id,
object: 'model',
created: Math.floor(Date.now() / 1000),
owned_by: id,
image_policy: m.imagePolicy,
type: m.type || 'image'
}));
return { object: 'list', data };
}
@@ -213,7 +249,9 @@ class AdapterRegistry {
supportsModel(adapterId, modelId) {
const adapter = this.getAdapter(adapterId);
if (!adapter?.models) return false;
return adapter.models.some(m => m.id === modelId);
// 检查模型是否存在且未被禁用
const modelExists = adapter.models.some(m => m.id === modelId);
return modelExists && this.isModelEnabled(adapterId, modelId);
}
/**
+164
View File
@@ -0,0 +1,164 @@
/**
* @fileoverview Cloudflare 验证绕过工具
* 提供通用的 Cloudflare Turnstile 验证码点击功能
*/
import { sleep, safeClick } from '../engine/utils.js';
import { logger } from '../../utils/logger.js';
/**
* 递归查找具有 shadowRootUnl 的子元素
* @param {ElementHandle} hostHandle - 宿主元素句柄
* @returns {Promise<ElementHandle|null>}
*/
async function findElementWithShadowRoot(hostHandle) {
return await hostHandle.evaluateHandle(el => {
for (const child of el.querySelectorAll('*')) {
if (child.shadowRootUnl) {
return child;
}
}
return null;
});
}
/**
* 通用 Cloudflare Turnstile 验证码点击器
*
* 支持穿透多层 closed shadow-root 和 iframe 找到并点击 checkbox
*
* @param {Page} page - Playwright page 对象
* @param {string} hostSelector - 宿主元素选择器,如 '#example-container5' 或 '.cf-turnstile'
* @param {object} [options={}] - 配置选项
* @param {number} [options.timeout=10000] - 等待超时时间
* @param {number} [options.waitAfterClick=5000] - 点击后等待时间
* @param {object} [options.meta={}] - 日志元数据
* @returns {Promise<{success: boolean, error?: string}>}
*/
export async function clickTurnstile(page, hostSelector, options = {}) {
const {
timeout = 10000,
waitAfterClick = 5000,
meta = {}
} = options;
try {
// 1. 获取宿主元素
logger.info('人机盾', '正在查找宿主元素...', meta);
const hostLocator = page.locator(hostSelector).first();
await hostLocator.waitFor({ state: 'visible', timeout });
const hostHandle = await hostLocator.elementHandle();
if (!hostHandle) {
return { success: false, error: '无法获取宿主元素句柄' };
}
// 2. 查找有 shadowRootUnl 的子元素
logger.info('人机盾', '正在查找 shadowRootUnl 子元素...', meta);
const childWithShadowHandle = await findElementWithShadowRoot(hostHandle);
const childElement = childWithShadowHandle.asElement();
if (!childElement) {
return { success: false, error: '未找到有 shadowRootUnl 的子元素' };
}
logger.debug('人机盾', '找到 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 { success: false, error: '第一层 shadow-root 内未找到 iframe' };
}
logger.debug('人机盾', '找到 iframe,正在进入...', meta);
// 4. 获取 iframe 的 contentFrame
const frame = await iframeElement.contentFrame();
// 辅助函数:坐标点击
const clickByCoordinates = async () => {
const box = await iframeElement.boundingBox();
if (!box) return false;
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('人机盾', '我是人类! (坐标模式)', meta);
return true;
};
if (!frame) {
logger.warn('人机盾', '无法获取 iframe contentFrame,尝试坐标点击...', meta);
if (await clickByCoordinates()) {
await sleep(waitAfterClick, waitAfterClick + 3000);
return { success: true };
}
return { success: false, error: '无法获取 iframe 边界框' };
}
// 5. 在 iframe 内查找有 shadowRootUnl 的元素
logger.debug('人机盾', '正在查找 iframe 内的 shadow-root...', meta);
await sleep(1000, 2000);
const bodyWithShadowHandle = await frame.evaluateHandle(() => {
if (document.body && document.body.shadowRootUnl) {
return document.body;
}
for (const el of document.querySelectorAll('*')) {
if (el.shadowRootUnl) {
return el;
}
}
return null;
});
const bodyElement = bodyWithShadowHandle.asElement();
if (!bodyElement) {
logger.warn('人机盾', 'iframe 内未找到 shadowRootUnl 元素,尝试坐标点击...', meta);
if (await clickByCoordinates()) {
await sleep(waitAfterClick, waitAfterClick + 3000);
return { success: true };
}
return { success: false, error: 'iframe 内未找到有 shadowRootUnl 的元素' };
}
logger.debug('人机盾', '找到 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;
const checkbox = root.querySelector('input[type="checkbox"]');
if (checkbox) return checkbox;
return root.querySelector('input');
});
const checkboxElement = checkboxHandle.asElement();
if (!checkboxElement) {
logger.warn('人机盾', 'shadow-root 内未找到 checkbox,尝试坐标点击...', meta);
if (await clickByCoordinates()) {
await sleep(waitAfterClick, waitAfterClick + 3000);
return { success: true };
}
return { success: false, error: 'iframe shadow-root 内未找到 checkbox' };
}
// 7. 直接点击 checkbox
logger.info('人机盾', '找到 checkbox,正在点击...', meta);
await safeClick(page, checkboxElement, { bias: 'random' });
logger.info('人机盾', '我是人类!(元素模式)', meta);
await sleep(waitAfterClick, waitAfterClick + 3000);
return { success: true };
} catch (err) {
logger.error('人机盾', `点击失败: ${err.message}`, meta);
return { success: false, error: err.message };
}
}
+3
View File
@@ -371,16 +371,19 @@ export function createAdminRouter(context) {
if (method === 'GET' && pathname === '/adapters') {
const adapters = [];
const adapterIds = registry.getAdapterIds();
const adapterConfig = getAdaptersConfig();
for (const id of adapterIds) {
const adapter = registry.getAdapter(id);
if (adapter) {
const config = adapterConfig[id] || {};
adapters.push({
id: adapter.id,
displayName: adapter.displayName || adapter.id,
description: adapter.description || '',
modelCount: adapter.models?.length || 0,
models: (adapter.models || []).map(m => m.id),
modelFilter: config.modelFilter || { mode: 'blacklist', list: [] },
configSchema: adapter.configSchema || []
});
}