dasdasd
This commit is contained in:
parent
a288aa01aa
commit
acceddac7b
@ -22,17 +22,51 @@ class ClickAction extends BaseAction {
|
|||||||
throw new Error(`无法找到元素: ${JSON.stringify(selector)}`);
|
throw new Error(`无法找到元素: ${JSON.stringify(selector)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 滚动到可视区域
|
// 等待元素变为可点击状态(参考旧框架)
|
||||||
await element.evaluate((el) => {
|
const waitForEnabled = this.config.waitForEnabled !== false; // 默认等待
|
||||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
if (waitForEnabled) {
|
||||||
});
|
// 传入 selector 配置,在循环中重新查找元素
|
||||||
|
await this.waitForClickable(element, this.config.timeout || 30000, selector);
|
||||||
|
}
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
// 记录找到的元素信息(总是显示,便于调试)
|
||||||
|
try {
|
||||||
|
const info = await element.evaluate((el) => {
|
||||||
|
const tag = el.tagName;
|
||||||
|
const id = el.id ? `#${el.id}` : '';
|
||||||
|
const cls = el.className ? `.${el.className.split(' ')[0]}` : '';
|
||||||
|
const text = (el.textContent || '').trim().substring(0, 30);
|
||||||
|
const disabled = el.disabled ? ' [DISABLED]' : '';
|
||||||
|
return `${tag}${id}${cls} "${text}"${disabled}`;
|
||||||
|
});
|
||||||
|
this.log('info', `→ 找到元素: ${info}`);
|
||||||
|
} catch (e) {
|
||||||
|
this.log('warn', `无法获取元素信息: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
// 点击
|
// 滚动到可视区域(带容错)
|
||||||
await element.click();
|
try {
|
||||||
|
await element.evaluate((el) => {
|
||||||
|
if (el && typeof el.scrollIntoView === 'function') {
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.log('debug', '✓ 点击完成');
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
} catch (error) {
|
||||||
|
this.log('warn', `滚动失败,继续尝试点击: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击(支持人类行为模拟)
|
||||||
|
const humanLike = this.config.humanLike !== false; // 默认使用人类行为
|
||||||
|
if (humanLike) {
|
||||||
|
// 重新查找元素并点击(参考旧框架,避免元素失效)
|
||||||
|
await this.humanClickWithSelector(selector);
|
||||||
|
} else {
|
||||||
|
await element.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log('info', '✓ 点击完成');
|
||||||
|
|
||||||
// 验证点击后的变化(新元素出现 / 旧元素消失)
|
// 验证点击后的变化(新元素出现 / 旧元素消失)
|
||||||
if (this.config.verifyAfter) {
|
if (this.config.verifyAfter) {
|
||||||
@ -52,6 +86,150 @@ class ClickAction extends BaseAction {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待元素变为可点击状态(参考旧框架)
|
||||||
|
* @param {ElementHandle} element - 元素句柄(可选,如果不传则在循环中重新查找)
|
||||||
|
* @param {number} timeout - 超时时间
|
||||||
|
* @param {Object} selectorConfig - 选择器配置(用于重新查找)
|
||||||
|
*/
|
||||||
|
async waitForClickable(element, timeout, selectorConfig = null) {
|
||||||
|
this.log('info', '→ 等待元素可点击...');
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
let lastLogTime = 0;
|
||||||
|
|
||||||
|
while (Date.now() - startTime < timeout) {
|
||||||
|
try {
|
||||||
|
// 如果提供了 selectorConfig,每次重新查找元素(参考旧框架)
|
||||||
|
let currentElement = element;
|
||||||
|
if (selectorConfig) {
|
||||||
|
const smartSelector = SmartSelector.fromConfig(selectorConfig, this.page);
|
||||||
|
currentElement = await smartSelector.find(1000);
|
||||||
|
if (!currentElement) {
|
||||||
|
// 元素不存在,继续等待
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isClickable = await currentElement.evaluate((el) => {
|
||||||
|
// 更严格的检查:
|
||||||
|
// 1. 必须可见
|
||||||
|
if (el.offsetParent === null) return false;
|
||||||
|
// 2. 如果是 button/input,检查 disabled 属性
|
||||||
|
if (el.tagName === 'BUTTON' || el.tagName === 'INPUT') {
|
||||||
|
if (el.disabled) return false;
|
||||||
|
}
|
||||||
|
// 3. 检查是否被遮挡(可选)
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
if (rect.width === 0 || rect.height === 0) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isClickable) {
|
||||||
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||||
|
this.log('info', `✓ 元素已可点击 (耗时: ${elapsed}秒)`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 元素可能被重新渲染,继续等待
|
||||||
|
this.log('debug', `元素检查失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每5秒输出一次进度
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
if (elapsed - lastLogTime >= 5000) {
|
||||||
|
this.log('info', `→ 等待元素可点击中... 已用时 ${(elapsed/1000).toFixed(0)}秒`);
|
||||||
|
lastLogTime = elapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log('warn', '⚠️ 等待元素可点击超时,将尝试点击');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人类行为模拟点击 - 使用选择器(参考旧框架,每次重新查找元素)
|
||||||
|
*/
|
||||||
|
async humanClickWithSelector(selectorConfig) {
|
||||||
|
this.log('info', '→ 使用人类行为模拟点击...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 重新查找元素(避免元素失效)
|
||||||
|
const smartSelector = SmartSelector.fromConfig(selectorConfig, this.page);
|
||||||
|
const element = await smartSelector.find(5000);
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
throw new Error(`重新查找元素失败: ${JSON.stringify(selectorConfig)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log('debug', '✓ 已重新定位元素');
|
||||||
|
|
||||||
|
// 获取元素的边界框
|
||||||
|
const box = await element.boundingBox();
|
||||||
|
if (!box) {
|
||||||
|
this.log('warn', '⚠️ 无法获取元素边界框,使用直接点击');
|
||||||
|
await element.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log('debug', `元素位置: x=${box.x.toFixed(0)}, y=${box.y.toFixed(0)}, w=${box.width.toFixed(0)}, h=${box.height.toFixed(0)}`);
|
||||||
|
|
||||||
|
// 计算随机点击位置(在元素范围内)
|
||||||
|
const targetX = box.x + box.width / 2 + this.randomInt(-box.width * 0.3, box.width * 0.3);
|
||||||
|
const targetY = box.y + box.height / 2 + this.randomInt(-box.height * 0.3, box.height * 0.3);
|
||||||
|
|
||||||
|
// 第一段移动:先移动到附近(模拟人眼定位)
|
||||||
|
const nearX = targetX + this.randomInt(-50, 50);
|
||||||
|
const nearY = targetY + this.randomInt(-50, 50);
|
||||||
|
const steps1 = this.randomInt(10, 20);
|
||||||
|
|
||||||
|
this.log('debug', `移动鼠标到附近: (${nearX.toFixed(0)}, ${nearY.toFixed(0)})`);
|
||||||
|
await this.page.mouse.move(nearX, nearY, { steps: Math.floor(steps1 / 2) });
|
||||||
|
await this.randomDelay(50, 150);
|
||||||
|
|
||||||
|
// 第二段移动:移动到目标位置
|
||||||
|
this.log('debug', `移动鼠标到目标: (${targetX.toFixed(0)}, ${targetY.toFixed(0)})`);
|
||||||
|
await this.page.mouse.move(targetX, targetY, { steps: Math.floor(steps1 / 2) });
|
||||||
|
|
||||||
|
// 短暂停顿(模拟人类反应)
|
||||||
|
await this.randomDelay(50, 200);
|
||||||
|
|
||||||
|
// 点击(使用 down + up,而不是 click)
|
||||||
|
this.log('debug', '执行点击 (mouse down + up)...');
|
||||||
|
await this.page.mouse.down();
|
||||||
|
await this.randomDelay(50, 120);
|
||||||
|
await this.page.mouse.up();
|
||||||
|
|
||||||
|
// 点击后延迟(参考旧框架)
|
||||||
|
await this.randomDelay(1000, 2000);
|
||||||
|
|
||||||
|
this.log('info', '✓ 人类行为点击完成');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.log('error', `⚠️ 人类行为点击失败: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 随机整数
|
||||||
|
*/
|
||||||
|
randomInt(min, max) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 随机延迟
|
||||||
|
*/
|
||||||
|
async randomDelay(min, max) {
|
||||||
|
const delay = this.randomInt(min, max);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证点击后的变化
|
* 验证点击后的变化
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -11,35 +11,66 @@ class NavigateAction extends BaseAction {
|
|||||||
timeout: 30000
|
timeout: 30000
|
||||||
};
|
};
|
||||||
|
|
||||||
this.log('info', `导航到: ${url}`);
|
// 重试配置
|
||||||
|
const maxRetries = this.config.maxRetries || 5;
|
||||||
|
const retryDelay = this.config.retryDelay || 3000;
|
||||||
|
const totalTimeout = this.config.totalTimeout || 180000; // 默认3分钟
|
||||||
|
|
||||||
try {
|
const startTime = Date.now();
|
||||||
await this.page.goto(url, options);
|
let lastError = null;
|
||||||
|
|
||||||
// 验证页面URL是否正确(避免重定向到登录页等)
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
const currentUrl = this.page.url();
|
// 检查总超时
|
||||||
if (this.config.verifyUrl && !currentUrl.includes(this.config.verifyUrl)) {
|
if (Date.now() - startTime > totalTimeout) {
|
||||||
throw new Error(`页面跳转异常: 期望包含 "${this.config.verifyUrl}", 实际为 "${currentUrl}"`);
|
this.log('error', `总超时 ${totalTimeout}ms,停止重试`);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证关键元素存在(确保页面加载正确)
|
try {
|
||||||
if (this.config.verifyElements) {
|
if (attempt > 0) {
|
||||||
await this.verifyElements(this.config.verifyElements);
|
this.log('info', `第 ${attempt + 1} 次尝试导航...`);
|
||||||
|
} else {
|
||||||
|
this.log('info', `导航到: ${url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试导航
|
||||||
|
await this.page.goto(url, options);
|
||||||
|
|
||||||
|
// 验证页面URL是否正确(避免重定向到会员中心等)
|
||||||
|
const currentUrl = this.page.url();
|
||||||
|
if (this.config.verifyUrl && !currentUrl.includes(this.config.verifyUrl)) {
|
||||||
|
throw new Error(`页面跳转异常: 期望包含 "${this.config.verifyUrl}", 实际为 "${currentUrl}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证关键元素存在(确保页面加载正确)
|
||||||
|
if (this.config.verifyElements) {
|
||||||
|
await this.verifyElements(this.config.verifyElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可选的等待时间
|
||||||
|
if (this.config.waitAfter) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, this.config.waitAfter));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log('info', `✓ 页面加载完成${attempt > 0 ? ` (尝试 ${attempt + 1} 次)` : ''}`);
|
||||||
|
|
||||||
|
return { success: true, url: currentUrl };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
this.log('warn', `导航失败 (尝试 ${attempt + 1}/${maxRetries}): ${error.message}`);
|
||||||
|
|
||||||
|
// 如果不是最后一次尝试,等待后重试
|
||||||
|
if (attempt < maxRetries - 1) {
|
||||||
|
this.log('debug', `等待 ${retryDelay}ms 后重试...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 可选的等待时间
|
|
||||||
if (this.config.waitAfter) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, this.config.waitAfter));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log('info', `✓ 页面加载完成`);
|
|
||||||
|
|
||||||
return { success: true, url: currentUrl };
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
this.log('error', `导航失败: ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 所有重试都失败
|
||||||
|
this.log('error', `导航失败: ${lastError.message}`);
|
||||||
|
throw lastError;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -12,7 +12,13 @@ workflow:
|
|||||||
options:
|
options:
|
||||||
waitUntil: 'networkidle2'
|
waitUntil: 'networkidle2'
|
||||||
timeout: 30000
|
timeout: 30000
|
||||||
# 验证关键元素存在(SPA应用验证页面加载)
|
# 重试配置(参考旧框架)
|
||||||
|
maxRetries: 5
|
||||||
|
retryDelay: 3000
|
||||||
|
totalTimeout: 180000 # 3分钟总超时
|
||||||
|
# URL验证(确保没有跳转到会员中心)
|
||||||
|
verifyUrl: "/account/register"
|
||||||
|
# 验证关键元素(确保页面完整加载)
|
||||||
verifyElements:
|
verifyElements:
|
||||||
- '#firstName'
|
- '#firstName'
|
||||||
- '#lastName'
|
- '#lastName'
|
||||||
@ -52,8 +58,9 @@ workflow:
|
|||||||
- action: click
|
- action: click
|
||||||
name: "点击 Continue (基本信息)"
|
name: "点击 Continue (基本信息)"
|
||||||
selector:
|
selector:
|
||||||
- css: 'button[type="submit"]'
|
|
||||||
- text: 'Continue'
|
- text: 'Continue'
|
||||||
|
selector: 'button, a' # 明确指定查找范围:按钮或链接
|
||||||
|
timeout: 30000
|
||||||
# 验证点击后密码页面出现
|
# 验证点击后密码页面出现
|
||||||
verifyAfter:
|
verifyAfter:
|
||||||
appears:
|
appears:
|
||||||
@ -88,8 +95,9 @@ workflow:
|
|||||||
- action: click
|
- action: click
|
||||||
name: "提交密码"
|
name: "提交密码"
|
||||||
selector:
|
selector:
|
||||||
- css: 'button[type="submit"]'
|
|
||||||
- text: 'Continue'
|
- text: 'Continue'
|
||||||
|
selector: 'button, a'
|
||||||
|
timeout: 30000
|
||||||
waitAfter: 2000
|
waitAfter: 2000
|
||||||
|
|
||||||
# ==================== 步骤 2.5: Cloudflare Turnstile 验证 ====================
|
# ==================== 步骤 2.5: Cloudflare Turnstile 验证 ====================
|
||||||
@ -118,17 +126,22 @@ workflow:
|
|||||||
- action: click
|
- action: click
|
||||||
name: "跳过问卷"
|
name: "跳过问卷"
|
||||||
selector:
|
selector:
|
||||||
- text: "Skip this step"
|
- text: 'Skip this step'
|
||||||
- text: "skip"
|
selector: 'button, a'
|
||||||
|
- text: 'skip'
|
||||||
|
selector: 'button, a'
|
||||||
|
exact: false
|
||||||
waitAfter: 2000
|
waitAfter: 2000
|
||||||
|
|
||||||
# ==================== 步骤 5: 选择计划 ====================
|
# ==================== 步骤 5: 选择计划 ====================
|
||||||
- action: click
|
- action: click
|
||||||
name: "选择计划"
|
name: "选择计划"
|
||||||
selector:
|
selector:
|
||||||
- text: "Select plan"
|
- text: 'Select'
|
||||||
- text: "Continue"
|
selector: 'button, a'
|
||||||
- text: "Get started"
|
- text: 'Continue'
|
||||||
|
selector: 'button, a'
|
||||||
|
timeout: 30000
|
||||||
waitAfter: 2000
|
waitAfter: 2000
|
||||||
|
|
||||||
# ==================== 步骤 6: 填写支付信息 ====================
|
# ==================== 步骤 6: 填写支付信息 ====================
|
||||||
@ -199,9 +212,9 @@ workflow:
|
|||||||
- action: click
|
- action: click
|
||||||
name: "点击提交支付"
|
name: "点击提交支付"
|
||||||
selector:
|
selector:
|
||||||
- css: 'button[type="submit"]'
|
|
||||||
- text: '订阅'
|
|
||||||
- text: 'Subscribe'
|
- text: 'Subscribe'
|
||||||
|
- text: '订阅'
|
||||||
|
timeout: 15000
|
||||||
waitAfter: 2000
|
waitAfter: 2000
|
||||||
|
|
||||||
# 验证支付结果(轮询检测成功或失败)
|
# 验证支付结果(轮询检测成功或失败)
|
||||||
|
|||||||
@ -45,7 +45,7 @@ class SiteAdapter {
|
|||||||
async beforeWorkflow() {
|
async beforeWorkflow() {
|
||||||
this.log('info', `开始执行 ${this.siteName} 工作流`);
|
this.log('info', `开始执行 ${this.siteName} 工作流`);
|
||||||
|
|
||||||
// 清除浏览器状态(Cookie、缓存、localStorage)
|
// 清除浏览器状态(Cookie、缓存)
|
||||||
await this.clearBrowserState();
|
await this.clearBrowserState();
|
||||||
|
|
||||||
this.log('debug', '执行 beforeWorkflow 钩子');
|
this.log('debug', '执行 beforeWorkflow 钩子');
|
||||||
@ -53,44 +53,69 @@ class SiteAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除浏览器状态
|
* 清除浏览器状态(模拟真实用户操作:Ctrl+Shift+Delete)
|
||||||
*/
|
*/
|
||||||
async clearBrowserState() {
|
async clearBrowserState() {
|
||||||
this.log('info', '清除浏览器状态...');
|
this.log('info', '清除浏览器状态...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 清除所有 Cookie
|
// 模拟真实用户操作:Ctrl+Shift+Delete
|
||||||
const cookies = await this.page.cookies();
|
this.log('debug', '模拟 Ctrl+Shift+Delete 打开清除对话框...');
|
||||||
if (cookies.length > 0) {
|
|
||||||
await this.page.deleteCookie(...cookies);
|
// 按下快捷键打开清除浏览数据对话框
|
||||||
this.log('info', `✓ 已清除 ${cookies.length} 个 Cookie`);
|
await this.page.keyboard.down('Control');
|
||||||
|
await this.page.keyboard.down('Shift');
|
||||||
|
await this.page.keyboard.press('Delete');
|
||||||
|
await this.page.keyboard.up('Shift');
|
||||||
|
await this.page.keyboard.up('Control');
|
||||||
|
|
||||||
|
// 等待对话框出现
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
|
this.log('debug', '选择清除选项...');
|
||||||
|
|
||||||
|
// 按 Tab 键导航并选择所有选项(通用方法)
|
||||||
|
// 不同浏览器可能有不同的界面,这里用快捷键方式更通用
|
||||||
|
|
||||||
|
// 全选时间范围(通常是第一个下拉框,选择"所有时间")
|
||||||
|
await this.page.keyboard.press('Tab'); // 移到时间范围
|
||||||
|
await this.page.keyboard.press('Space'); // 打开下拉
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
await this.page.keyboard.press('End'); // 选择最后一项(通常是"所有时间")
|
||||||
|
await this.page.keyboard.press('Enter'); // 确认
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
|
||||||
|
// 勾选所有清除选项(通过 Tab + Space)
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
await this.page.keyboard.press('Tab');
|
||||||
|
await this.page.keyboard.press('Space'); // 勾选
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除 localStorage 和 sessionStorage(仅在有效页面)
|
this.log('debug', '点击清除数据按钮...');
|
||||||
const currentUrl = this.page.url();
|
|
||||||
if (currentUrl && currentUrl.startsWith('http')) {
|
// 找到并点击"清除数据"按钮(通常可以用 Enter 或继续 Tab 到按钮)
|
||||||
await this.page.evaluate(() => {
|
await this.page.keyboard.press('Enter');
|
||||||
try {
|
|
||||||
localStorage.clear();
|
// 等待清除完成
|
||||||
sessionStorage.clear();
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
} catch (e) {
|
|
||||||
// 某些页面可能无法访问 storage
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.log('info', '✓ 已清除 localStorage 和 sessionStorage');
|
|
||||||
} else {
|
|
||||||
this.log('debug', '跳过 storage 清理(页面未加载)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除缓存(通过 CDP)
|
|
||||||
const client = await this.page.target().createCDPSession();
|
|
||||||
await client.send('Network.clearBrowserCookies');
|
|
||||||
await client.send('Network.clearBrowserCache');
|
|
||||||
await client.detach();
|
|
||||||
this.log('info', '✓ 已清除浏览器缓存');
|
this.log('info', '✓ 已清除浏览器缓存');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log('warn', `清除浏览器状态失败: ${error.message}`);
|
this.log('warn', `⚠ WARNING: 清除浏览器状态失败: ${error.message}`);
|
||||||
|
this.log('debug', '尝试备用清理方法...');
|
||||||
|
|
||||||
|
// 备用方案:直接删除 Cookie(不使用 CDP)
|
||||||
|
try {
|
||||||
|
const cookies = await this.page.cookies();
|
||||||
|
if (cookies.length > 0) {
|
||||||
|
await this.page.deleteCookie(...cookies);
|
||||||
|
this.log('info', '✓ 已使用备用方法清除 Cookie');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.log('warn', `备用清理也失败: ${e.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,16 @@ class SmartSelector {
|
|||||||
config.forEach(strategy => {
|
config.forEach(strategy => {
|
||||||
if (strategy.css) selector.css(strategy.css);
|
if (strategy.css) selector.css(strategy.css);
|
||||||
if (strategy.xpath) selector.xpath(strategy.xpath);
|
if (strategy.xpath) selector.xpath(strategy.xpath);
|
||||||
if (strategy.text) selector.text(strategy.text);
|
if (strategy.text) {
|
||||||
|
const textOptions = {
|
||||||
|
exact: strategy.exact,
|
||||||
|
caseInsensitive: strategy.caseInsensitive,
|
||||||
|
selector: strategy.selector,
|
||||||
|
filterDisabled: strategy.filterDisabled,
|
||||||
|
filterHidden: strategy.filterHidden
|
||||||
|
};
|
||||||
|
selector.text(strategy.text, textOptions);
|
||||||
|
}
|
||||||
if (strategy.placeholder) selector.placeholder(strategy.placeholder);
|
if (strategy.placeholder) selector.placeholder(strategy.placeholder);
|
||||||
if (strategy.label) selector.label(strategy.label);
|
if (strategy.label) selector.label(strategy.label);
|
||||||
if (strategy.type) selector.type(strategy.type);
|
if (strategy.type) selector.type(strategy.type);
|
||||||
@ -36,7 +45,16 @@ class SmartSelector {
|
|||||||
// 单个策略对象
|
// 单个策略对象
|
||||||
if (config.css) selector.css(config.css);
|
if (config.css) selector.css(config.css);
|
||||||
if (config.xpath) selector.xpath(config.xpath);
|
if (config.xpath) selector.xpath(config.xpath);
|
||||||
if (config.text) selector.text(config.text);
|
if (config.text) {
|
||||||
|
const textOptions = {
|
||||||
|
exact: config.exact,
|
||||||
|
caseInsensitive: config.caseInsensitive,
|
||||||
|
selector: config.selector,
|
||||||
|
filterDisabled: config.filterDisabled,
|
||||||
|
filterHidden: config.filterHidden
|
||||||
|
};
|
||||||
|
selector.text(config.text, textOptions);
|
||||||
|
}
|
||||||
if (config.placeholder) selector.placeholder(config.placeholder);
|
if (config.placeholder) selector.placeholder(config.placeholder);
|
||||||
if (config.label) selector.label(config.label);
|
if (config.label) selector.label(config.label);
|
||||||
if (config.type) selector.type(config.type);
|
if (config.type) selector.type(config.type);
|
||||||
@ -67,31 +85,84 @@ class SmartSelector {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
text(text) {
|
/**
|
||||||
|
* 文本选择器(通用方法 - 框架级)
|
||||||
|
* 根据文本内容查找元素,不限定元素类型
|
||||||
|
* 用户可通过 selector 参数指定查找范围
|
||||||
|
*/
|
||||||
|
text(text, options = {}) {
|
||||||
|
const {
|
||||||
|
exact = true, // 默认精确匹配(更安全)
|
||||||
|
caseInsensitive = true,
|
||||||
|
selector = '*', // 默认查找所有元素(通用)
|
||||||
|
filterDisabled = false,
|
||||||
|
filterHidden = true
|
||||||
|
} = options;
|
||||||
|
|
||||||
this.strategies.push({
|
this.strategies.push({
|
||||||
type: 'text',
|
type: 'text',
|
||||||
find: async () => {
|
find: async () => {
|
||||||
return await this.page.evaluateHandle((searchText) => {
|
return await this._findByText(text, selector, { exact, caseInsensitive, filterDisabled, filterHidden });
|
||||||
const walker = document.createTreeWalker(
|
|
||||||
document.body,
|
|
||||||
NodeFilter.SHOW_TEXT,
|
|
||||||
null,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
let node;
|
|
||||||
while (node = walker.nextNode()) {
|
|
||||||
if (node.textContent.trim() === searchText.trim()) {
|
|
||||||
return node.parentElement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, text);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部方法:按文本查找元素(使用指定的 CSS 选择器)
|
||||||
|
*/
|
||||||
|
async _findByText(searchText, cssSelector, options) {
|
||||||
|
const { exact, caseInsensitive, filterDisabled, filterHidden } = options;
|
||||||
|
|
||||||
|
const element = await this.page.evaluateHandle(
|
||||||
|
({ searchText, exact, caseInsensitive, cssSelector, filterDisabled, filterHidden }) => {
|
||||||
|
const elements = Array.from(document.querySelectorAll(cssSelector));
|
||||||
|
|
||||||
|
for (const el of elements) {
|
||||||
|
// 过滤 disabled
|
||||||
|
if (filterDisabled && (el.tagName === 'BUTTON' || el.tagName === 'INPUT') && el.disabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤隐藏元素
|
||||||
|
if (filterHidden && el.offsetParent === null) continue;
|
||||||
|
|
||||||
|
// 获取文本
|
||||||
|
const text = (el.textContent || '').trim();
|
||||||
|
if (!text) continue;
|
||||||
|
|
||||||
|
// 标准化空格
|
||||||
|
const normalizedText = text.replace(/\s+/g, ' ');
|
||||||
|
const normalizedSearch = searchText.replace(/\s+/g, ' ');
|
||||||
|
|
||||||
|
// 匹配
|
||||||
|
let matches = false;
|
||||||
|
if (exact) {
|
||||||
|
matches = caseInsensitive
|
||||||
|
? normalizedText.toLowerCase() === normalizedSearch.toLowerCase()
|
||||||
|
: normalizedText === normalizedSearch;
|
||||||
|
} else {
|
||||||
|
matches = caseInsensitive
|
||||||
|
? normalizedText.toLowerCase().includes(normalizedSearch.toLowerCase())
|
||||||
|
: normalizedText.includes(normalizedSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches) return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
{ searchText, exact, caseInsensitive, cssSelector, filterDisabled, filterHidden }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
const elementHandle = element.asElement();
|
||||||
|
if (elementHandle) return elementHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
placeholder(placeholder) {
|
placeholder(placeholder) {
|
||||||
this.strategies.push({
|
this.strategies.push({
|
||||||
type: 'placeholder',
|
type: 'placeholder',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user