mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 初步测试自动过盾,添加适配器模型管理
This commit is contained in:
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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 || []
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user