dasdasd
This commit is contained in:
parent
d4cc237ccd
commit
2c2b813052
2
aaa.js
2
aaa.js
@ -14,7 +14,7 @@ console.log('=== 生成卡号验证 ===\n');
|
||||
// 生成10张银联卡
|
||||
console.log('生成10张银联卡进行验证:\n');
|
||||
|
||||
const cards = generator.generateBatch(10, 'unionpay');
|
||||
const cards = generator.generateBatch(100, 'unionpay');
|
||||
|
||||
cards.forEach((card, index) => {
|
||||
const isValid = luhnCheck(card.number);
|
||||
|
||||
@ -14,8 +14,10 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { AdsPowerProvider } from '../src/providers/adspower/AdsPowerProvider';
|
||||
import { PlaywrightStealthProvider } from '../src/providers/playwright-stealth/PlaywrightStealthProvider';
|
||||
import { WorkflowEngine } from '../src/workflow/WorkflowEngine';
|
||||
import { ISiteAdapter, EmptyAdapter } from '../src/adapters/ISiteAdapter';
|
||||
import { IBrowserProvider } from '../src/core/interfaces/IBrowserProvider';
|
||||
|
||||
interface WorkflowConfig {
|
||||
site: string;
|
||||
@ -158,13 +160,22 @@ class AutomationRunner {
|
||||
}
|
||||
}
|
||||
|
||||
private async initializeProvider(): Promise<AdsPowerProvider> {
|
||||
console.log('🌐 Initializing AdsPower Provider...');
|
||||
private async initializeProvider(): Promise<IBrowserProvider> {
|
||||
const browserProvider = process.argv[2] || 'adspower';
|
||||
|
||||
return new AdsPowerProvider({
|
||||
profileId: process.env.ADSPOWER_USER_ID,
|
||||
siteName: this.siteName.charAt(0).toUpperCase() + this.siteName.slice(1)
|
||||
});
|
||||
console.log(`🌐 Initializing ${browserProvider} Provider...`);
|
||||
|
||||
if (browserProvider === 'playwright' || browserProvider === 'playwright-stealth') {
|
||||
return new PlaywrightStealthProvider({
|
||||
headless: false,
|
||||
viewport: { width: 1920, height: 1080 }
|
||||
});
|
||||
} else {
|
||||
return new AdsPowerProvider({
|
||||
profileId: process.env.ADSPOWER_USER_ID,
|
||||
siteName: this.siteName.charAt(0).toUpperCase() + this.siteName.slice(1)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private buildContext(launchResult: any, config: WorkflowConfig): any {
|
||||
@ -207,7 +218,7 @@ class AutomationRunner {
|
||||
console.log('='.repeat(60) + '\n');
|
||||
}
|
||||
|
||||
private async cleanup(provider: AdsPowerProvider): Promise<void> {
|
||||
private async cleanup(provider: IBrowserProvider): Promise<void> {
|
||||
try {
|
||||
console.log('\n🔒 Closing browser...');
|
||||
await provider.close();
|
||||
|
||||
@ -28,7 +28,7 @@ export class WindsurfAdapter extends BaseAdapter {
|
||||
new AccountGeneratorTool(),
|
||||
{
|
||||
email: {
|
||||
domain: 'qichen111.asia'
|
||||
domain: 'qichen.cloud'
|
||||
},
|
||||
password: {
|
||||
strategy: 'email',
|
||||
|
||||
@ -1,27 +1,27 @@
|
||||
# Windsurf 注册自动化配置
|
||||
site:
|
||||
name: Windsurf
|
||||
url: https://windsurf.com/account/register
|
||||
url: https://windsurf.com/refer?referral_code=55424ec434
|
||||
|
||||
# 工作流定义
|
||||
workflow:
|
||||
# ==================== 步骤 0: 处理邀请链接 ====================
|
||||
# - action: navigate
|
||||
# name: "打开邀请链接"
|
||||
# url: "https://windsurf.com/refer?referral_code=55424ec434"
|
||||
# options:
|
||||
# waitUntil: 'networkidle2'
|
||||
# timeout: 30000
|
||||
#
|
||||
# - action: click
|
||||
# name: "点击接受邀请"
|
||||
# selector:
|
||||
# - text: 'Sign up to accept referral'
|
||||
# selector: 'button'
|
||||
# - css: 'button.bg-sk-aqua'
|
||||
# - css: 'button:has-text("Sign up to accept referral")'
|
||||
# timeout: 30000
|
||||
# waitForNavigation: true
|
||||
- action: navigate
|
||||
name: "打开邀请链接"
|
||||
url: "https://windsurf.com/refer?referral_code=55424ec434"
|
||||
options:
|
||||
waitUntil: 'networkidle2'
|
||||
timeout: 30000
|
||||
|
||||
- action: click
|
||||
name: "点击接受邀请"
|
||||
selector:
|
||||
- text: 'Sign up to accept referral'
|
||||
selector: 'button'
|
||||
- css: 'button.bg-sk-aqua'
|
||||
- css: 'button:has-text("Sign up to accept referral")'
|
||||
timeout: 30000
|
||||
waitForNavigation: true
|
||||
|
||||
# 验证跳转到注册页面(带referral_code)
|
||||
- action: verify
|
||||
|
||||
@ -18,13 +18,16 @@ export * from './factory/BrowserFactory';
|
||||
|
||||
// Providers
|
||||
export * from './providers/adspower/AdsPowerProvider';
|
||||
export * from './providers/playwright-stealth/PlaywrightStealthProvider';
|
||||
|
||||
// Register providers
|
||||
import { BrowserFactory } from './factory/BrowserFactory';
|
||||
import { AdsPowerProvider } from './providers/adspower/AdsPowerProvider';
|
||||
import { PlaywrightStealthProvider } from './providers/playwright-stealth/PlaywrightStealthProvider';
|
||||
import { BrowserProviderType } from './core/types';
|
||||
|
||||
// Auto-register AdsPower
|
||||
// Auto-register providers
|
||||
BrowserFactory.register(BrowserProviderType.ADSPOWER, AdsPowerProvider);
|
||||
BrowserFactory.register(BrowserProviderType.PLAYWRIGHT_STEALTH, PlaywrightStealthProvider);
|
||||
|
||||
console.log('✅ Browser Automation Framework (TypeScript) initialized');
|
||||
|
||||
@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Playwright Stealth Provider - 开源指纹浏览器实现
|
||||
*/
|
||||
|
||||
import { BaseBrowserProvider } from '../../core/base/BaseBrowserProvider';
|
||||
import { IBrowserCapabilities, ILaunchOptions, ILaunchResult } from '../../core/types';
|
||||
import { PlaywrightStealthActionFactory } from './core/ActionFactory';
|
||||
import { chromium, Browser, Page, BrowserContext } from 'playwright';
|
||||
|
||||
export interface IPlaywrightStealthConfig {
|
||||
headless?: boolean;
|
||||
proxy?: {
|
||||
server: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
userAgent?: string;
|
||||
viewport?: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
locale?: string;
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
export class PlaywrightStealthProvider extends BaseBrowserProvider {
|
||||
private context: BrowserContext | null = null;
|
||||
private launchOptions: IPlaywrightStealthConfig;
|
||||
|
||||
constructor(config: IPlaywrightStealthConfig = {}) {
|
||||
super(config);
|
||||
this.launchOptions = {
|
||||
headless: config.headless ?? false,
|
||||
locale: config.locale ?? 'zh-CN',
|
||||
timezone: config.timezone ?? 'Asia/Shanghai',
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return 'Playwright Stealth';
|
||||
}
|
||||
|
||||
getVersion(): string {
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
isFree(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
getCapabilities(): IBrowserCapabilities {
|
||||
return {
|
||||
stealth: true,
|
||||
fingerprint: true,
|
||||
proxy: true,
|
||||
incognito: true,
|
||||
profiles: false,
|
||||
cloudflareBypass: true,
|
||||
stripeCompatible: true
|
||||
};
|
||||
}
|
||||
|
||||
async validateConfig(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async launch(options?: ILaunchOptions): Promise<ILaunchResult> {
|
||||
console.log('[Playwright Stealth] Launching browser...');
|
||||
|
||||
try {
|
||||
const mergedOptions = { ...this.launchOptions, ...options };
|
||||
|
||||
this.browser = await chromium.launch({
|
||||
headless: mergedOptions.headless,
|
||||
args: [
|
||||
'--disable-blink-features=AutomationControlled',
|
||||
'--disable-dev-shm-usage',
|
||||
'--no-sandbox'
|
||||
]
|
||||
});
|
||||
|
||||
this.context = await this.browser.newContext({
|
||||
viewport: mergedOptions.viewport || { width: 1920, height: 1080 },
|
||||
userAgent: mergedOptions.userAgent,
|
||||
locale: mergedOptions.locale,
|
||||
timezoneId: mergedOptions.timezone,
|
||||
proxy: mergedOptions.proxy
|
||||
});
|
||||
|
||||
await this.applyStealthScripts(this.context);
|
||||
this.page = await this.context.newPage();
|
||||
|
||||
console.log('[Playwright Stealth] ✅ Browser launched');
|
||||
|
||||
return {
|
||||
browser: this.browser,
|
||||
page: this.page
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`[Playwright Stealth] ❌ Failed: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (!this.browser) return;
|
||||
|
||||
try {
|
||||
if (this.page) await this.page.close();
|
||||
if (this.context) await this.context.close();
|
||||
await this.browser.close();
|
||||
|
||||
this.page = null;
|
||||
this.context = null;
|
||||
this.browser = null;
|
||||
|
||||
console.log('[Playwright Stealth] ✅ Browser closed');
|
||||
} catch (error: any) {
|
||||
console.error(`[Playwright Stealth] Error: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getActionFactory(): PlaywrightStealthActionFactory {
|
||||
return new PlaywrightStealthActionFactory();
|
||||
}
|
||||
|
||||
async clearCache(): Promise<void> {
|
||||
if (this.context) {
|
||||
await this.context.clearCookies();
|
||||
}
|
||||
}
|
||||
|
||||
private async applyStealthScripts(context: BrowserContext): Promise<void> {
|
||||
await context.addInitScript(() => {
|
||||
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
||||
(window as any).chrome = { runtime: {} };
|
||||
Object.defineProperty(navigator, 'plugins', {
|
||||
get: () => [{ name: 'Chrome PDF Plugin' }]
|
||||
});
|
||||
Object.defineProperty(navigator, 'languages', {
|
||||
get: () => ['zh-CN', 'zh', 'en']
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,237 @@
|
||||
import BaseAction from '../core/BaseAction';
|
||||
import SmartSelector from '../../../core/selectors/SmartSelector';
|
||||
|
||||
class ClickAction extends BaseAction {
|
||||
async execute(): Promise<any> {
|
||||
const selector = this.config.selector || this.config.find;
|
||||
|
||||
if (!selector) {
|
||||
throw new Error('缺少选择器配置');
|
||||
}
|
||||
|
||||
this.log('info', '执行点击');
|
||||
|
||||
const smartSelector = SmartSelector.fromConfig(selector, this.page);
|
||||
const element = await smartSelector.find(this.config.timeout || 10000);
|
||||
|
||||
if (!element) {
|
||||
throw new Error(`元素未找到: ${JSON.stringify(selector)}`);
|
||||
}
|
||||
|
||||
const waitForEnabled = this.config.waitForEnabled !== false;
|
||||
if (waitForEnabled) {
|
||||
await this.waitForClickable(selector, this.config.timeout || 30000);
|
||||
}
|
||||
|
||||
try {
|
||||
const info = await element.evaluate((el: any) => {
|
||||
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: any) {
|
||||
this.log('warn', `无法获取元素信息: ${e.message}`);
|
||||
}
|
||||
|
||||
await element.scrollIntoViewIfNeeded();
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
await this.pauseDelay();
|
||||
|
||||
const humanLike = this.config.humanLike !== false;
|
||||
if (humanLike) {
|
||||
await this.humanClick(selector);
|
||||
} else {
|
||||
await element.click();
|
||||
}
|
||||
|
||||
this.log('info', '✓ 点击完成');
|
||||
await this.pauseDelay();
|
||||
|
||||
if (this.config.verifyAfter) {
|
||||
await this.verifyAfterClick(this.config.verifyAfter);
|
||||
}
|
||||
|
||||
if (this.config.waitAfter) {
|
||||
await new Promise(resolve => setTimeout(resolve, this.config.waitAfter));
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async waitForClickable(selectorConfig: any, timeout: number): Promise<boolean> {
|
||||
this.log('info', '→ 等待元素可点击...');
|
||||
|
||||
const startTime = Date.now();
|
||||
let lastLogTime = 0;
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
try {
|
||||
const smartSelector = SmartSelector.fromConfig(selectorConfig, this.page);
|
||||
const element = await smartSelector.find(1000);
|
||||
|
||||
if (!element) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
continue;
|
||||
}
|
||||
|
||||
const isClickable = await element.evaluate((el: any) => {
|
||||
if (el.offsetParent === null) return false;
|
||||
if (el.tagName === 'BUTTON' || el.tagName === 'INPUT') {
|
||||
if (el.disabled) return false;
|
||||
}
|
||||
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: any) {
|
||||
this.log('debug', `元素检查失败: ${error.message}`);
|
||||
}
|
||||
|
||||
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 humanClick(selectorConfig: any): Promise<void> {
|
||||
this.log('info', '→ 使用人类行为模拟点击...');
|
||||
|
||||
try {
|
||||
const smartSelector = SmartSelector.fromConfig(selectorConfig, this.page);
|
||||
const element = await smartSelector.find(5000);
|
||||
|
||||
if (!element) {
|
||||
throw new Error('重新定位失败');
|
||||
}
|
||||
|
||||
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;
|
||||
const targetY = box.y + box.height / 2;
|
||||
|
||||
const nearX = targetX + this.randomInt(-50, 50);
|
||||
const nearY = targetY + this.randomInt(-50, 50);
|
||||
const steps1 = this.randomInt(15, 30);
|
||||
|
||||
this.log('debug', `移动鼠标到附近: (${nearX.toFixed(0)}, ${nearY.toFixed(0)})`);
|
||||
await this.page.mouse.move(nearX, nearY, { steps: steps1 });
|
||||
await this.randomDelay(150, 400);
|
||||
|
||||
this.log('debug', `移动鼠标到目标: (${targetX.toFixed(0)}, ${targetY.toFixed(0)})`);
|
||||
await this.page.mouse.move(targetX, targetY, { steps: this.randomInt(10, 20) });
|
||||
await this.randomDelay(200, 500);
|
||||
|
||||
this.log('debug', '执行点击 (mouse down + up)...');
|
||||
await this.page.mouse.down();
|
||||
await this.randomDelay(80, 180);
|
||||
await this.page.mouse.up();
|
||||
await this.randomDelay(1200, 2500);
|
||||
|
||||
this.log('info', '✓ 人类行为点击完成');
|
||||
|
||||
} catch (error: any) {
|
||||
this.log('error', `⚠️ 人类行为点击失败: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
randomInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
async verifyAfterClick(config: any): Promise<void> {
|
||||
const { appears, disappears, checked, timeout = 10000 } = config;
|
||||
|
||||
if (appears) {
|
||||
this.log('debug', '验证新元素出现...');
|
||||
for (const selector of (Array.isArray(appears) ? appears : [appears])) {
|
||||
try {
|
||||
await this.page.waitForSelector(selector, { timeout, state: 'visible' });
|
||||
this.log('debug', `✓ 新元素已出现: ${selector}`);
|
||||
} catch (error: any) {
|
||||
throw new Error(`点击后验证失败: 元素未出现 ${selector}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (disappears) {
|
||||
this.log('debug', '验证旧元素消失...');
|
||||
for (const selector of (Array.isArray(disappears) ? disappears : [disappears])) {
|
||||
try {
|
||||
await this.page.waitForSelector(selector, { timeout, state: 'hidden' });
|
||||
this.log('debug', `✓ 旧元素已消失: ${selector}`);
|
||||
} catch (error: any) {
|
||||
throw new Error(`点击后验证失败: 元素未消失 ${selector}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (checked !== undefined) {
|
||||
this.log('debug', `验证 checked 状态: ${checked}...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
const selectorConfig = this.config.selector;
|
||||
let cssSelector = null;
|
||||
|
||||
if (typeof selectorConfig === 'string') {
|
||||
cssSelector = selectorConfig;
|
||||
} else if (Array.isArray(selectorConfig)) {
|
||||
for (const sel of selectorConfig) {
|
||||
if (typeof sel === 'string') {
|
||||
cssSelector = sel;
|
||||
break;
|
||||
} else if (sel.css) {
|
||||
cssSelector = sel.css;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (selectorConfig.css) {
|
||||
cssSelector = selectorConfig.css;
|
||||
}
|
||||
|
||||
if (!cssSelector) {
|
||||
throw new Error('无法从选择器配置中提取 CSS 选择器');
|
||||
}
|
||||
|
||||
const isChecked = await this.page.evaluate((sel: string) => {
|
||||
const element = document.querySelector(sel) as any;
|
||||
return element && element.checked === true;
|
||||
}, cssSelector);
|
||||
|
||||
const expectedState = checked === true;
|
||||
if (isChecked !== expectedState) {
|
||||
throw new Error(`点击后验证失败: checked 状态不符 (期望: ${expectedState}, 实际: ${isChecked})`);
|
||||
}
|
||||
|
||||
this.log('debug', `✓ checked 状态验证通过: ${isChecked}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ClickAction;
|
||||
@ -0,0 +1,41 @@
|
||||
import BaseAction from '../core/BaseAction';
|
||||
|
||||
class CustomAction extends BaseAction {
|
||||
async execute(): Promise<any> {
|
||||
const handler = this.config.handler;
|
||||
const params = this.config.params || {};
|
||||
const timeout = this.config.timeout || 300000;
|
||||
|
||||
if (!handler) throw new Error('缺少处理函数名称');
|
||||
|
||||
this.log('info', `执行自定义函数: ${handler}`);
|
||||
|
||||
if (typeof this.context.adapter[handler] !== 'function') {
|
||||
throw new Error(`自定义处理函数不存在: ${handler}`);
|
||||
}
|
||||
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error(`自定义函数超时: ${handler} (${timeout}ms)`));
|
||||
}, timeout);
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await Promise.race([
|
||||
this.context.adapter[handler](params),
|
||||
timeoutPromise
|
||||
]);
|
||||
|
||||
this.log('debug', '✓ 自定义函数执行完成');
|
||||
return result;
|
||||
|
||||
} catch (error: any) {
|
||||
if (error.message.includes('超时')) {
|
||||
this.log('error', `⚠️ ${error.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomAction;
|
||||
@ -0,0 +1,107 @@
|
||||
import BaseAction from '../core/BaseAction';
|
||||
|
||||
class ExtractAction extends BaseAction {
|
||||
async execute(): Promise<any> {
|
||||
const { selector, extractType = 'text', regex, saveTo, contextKey, filter, multiple = false, required = true } = this.config;
|
||||
|
||||
if (!selector) throw new Error('Extract action 需要 selector 参数');
|
||||
|
||||
this.log('debug', `提取数据: ${selector}`);
|
||||
|
||||
try {
|
||||
const extractedData = await this.page.evaluate((config: any) => {
|
||||
const { selector, extractType, filter, multiple } = config;
|
||||
|
||||
let elements = Array.from(document.querySelectorAll(selector));
|
||||
|
||||
if (filter) {
|
||||
if (filter.contains) {
|
||||
elements = elements.filter(el => el.textContent!.includes(filter.contains));
|
||||
}
|
||||
if (filter.notContains) {
|
||||
elements = elements.filter(el => !el.textContent!.includes(filter.notContains));
|
||||
}
|
||||
}
|
||||
|
||||
if (elements.length === 0) return null;
|
||||
|
||||
const extractFrom = (element: any) => {
|
||||
switch (extractType) {
|
||||
case 'text':
|
||||
return element.textContent.trim();
|
||||
case 'html':
|
||||
return element.innerHTML;
|
||||
case 'attribute':
|
||||
return element.getAttribute(config.attribute);
|
||||
case 'value':
|
||||
return element.value;
|
||||
default:
|
||||
return element.textContent.trim();
|
||||
}
|
||||
};
|
||||
|
||||
if (multiple) {
|
||||
return elements.map(extractFrom);
|
||||
} else {
|
||||
return extractFrom(elements[0]);
|
||||
}
|
||||
}, { selector, extractType, filter, multiple, attribute: this.config.attribute });
|
||||
|
||||
if (extractedData === null) {
|
||||
if (required) {
|
||||
throw new Error(`未找到匹配的元素: ${selector}`);
|
||||
} else {
|
||||
this.log('warn', `未找到元素: ${selector},跳过提取`);
|
||||
return { success: true, data: null };
|
||||
}
|
||||
}
|
||||
|
||||
this.log('debug', `提取到原始数据: ${JSON.stringify(extractedData)}`);
|
||||
|
||||
let processedData = extractedData;
|
||||
if (regex && typeof extractedData === 'string') {
|
||||
const regexObj = new RegExp(regex);
|
||||
const match = extractedData.match(regexObj);
|
||||
|
||||
if (match) {
|
||||
if (saveTo && typeof saveTo === 'object') {
|
||||
processedData = {};
|
||||
for (const [key, value] of Object.entries(saveTo)) {
|
||||
if (typeof value === 'string' && value.startsWith('$')) {
|
||||
const groupIndex = parseInt(value.substring(1));
|
||||
(processedData as any)[key] = match[groupIndex] || null;
|
||||
} else {
|
||||
(processedData as any)[key] = value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
processedData = match[1] || match[0];
|
||||
}
|
||||
} else if (required) {
|
||||
throw new Error(`正则表达式不匹配: ${regex}`);
|
||||
} else {
|
||||
this.log('warn', `正则表达式不匹配: ${regex}`);
|
||||
processedData = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (contextKey && processedData !== null) {
|
||||
if (!this.context.data) {
|
||||
this.context.data = {};
|
||||
}
|
||||
this.context.data[contextKey] = processedData;
|
||||
this.log('info', `✓ 数据已保存到 context.${contextKey}`);
|
||||
}
|
||||
|
||||
this.log('debug', `处理后的数据: ${JSON.stringify(processedData)}`);
|
||||
|
||||
return { success: true, data: processedData };
|
||||
|
||||
} catch (error: any) {
|
||||
this.log('error', `数据提取失败: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ExtractAction;
|
||||
@ -0,0 +1,135 @@
|
||||
import BaseAction from '../core/BaseAction';
|
||||
import SmartSelector from '../../../core/selectors/SmartSelector';
|
||||
|
||||
class FillFormAction extends BaseAction {
|
||||
async execute(): Promise<any> {
|
||||
const fields = this.config.fields;
|
||||
const humanLike = this.config.humanLike !== false;
|
||||
|
||||
if (!fields || typeof fields !== 'object') {
|
||||
throw new Error('表单字段配置无效');
|
||||
}
|
||||
|
||||
this.log('info', `填写表单,共 ${Object.keys(fields).length} 个字段`);
|
||||
|
||||
const fieldEntries = Object.entries(fields);
|
||||
for (let i = 0; i < fieldEntries.length; i++) {
|
||||
const [key, fieldConfig] = fieldEntries[i];
|
||||
await this.fillField(key, fieldConfig, humanLike);
|
||||
|
||||
if (i < fieldEntries.length - 1) {
|
||||
await this.pauseDelay();
|
||||
}
|
||||
}
|
||||
|
||||
this.log('info', '✓ 表单填写完成');
|
||||
await this.thinkDelay();
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async fillField(key: string, fieldConfig: any, humanLike: boolean): Promise<void> {
|
||||
let selector, value, fieldType;
|
||||
|
||||
if (typeof fieldConfig === 'object' && fieldConfig.find) {
|
||||
selector = fieldConfig.find;
|
||||
value = this.replaceVariables(fieldConfig.value);
|
||||
fieldType = fieldConfig.type;
|
||||
} else if (typeof fieldConfig === 'string') {
|
||||
selector = [
|
||||
{ css: `#${key}` },
|
||||
{ name: key },
|
||||
{ css: `input[name="${key}"]` },
|
||||
{ css: `select[name="${key}"]` },
|
||||
{ css: `textarea[name="${key}"]` }
|
||||
];
|
||||
value = this.replaceVariables(fieldConfig);
|
||||
fieldType = 'input';
|
||||
} else {
|
||||
selector = key;
|
||||
value = this.replaceVariables(fieldConfig);
|
||||
fieldType = 'input';
|
||||
}
|
||||
|
||||
const smartSelector = SmartSelector.fromConfig(selector, this.page);
|
||||
const element = await smartSelector.find(10000);
|
||||
|
||||
if (!element) {
|
||||
throw new Error(`元素未找到: ${key}`);
|
||||
}
|
||||
|
||||
this.log('debug', ` → 填写字段: ${key}`);
|
||||
|
||||
if (!fieldType) {
|
||||
fieldType = fieldConfig.type || 'input';
|
||||
}
|
||||
|
||||
if (fieldType === 'select') {
|
||||
const cssSelector = this.getCssSelector(selector);
|
||||
await this.page.selectOption(cssSelector, value);
|
||||
this.log('debug', ` → 已选择: ${value}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await element.click({ clickCount: 3 });
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const clearTimes = fieldConfig.clearTimes || this.config.clearTimes || 25;
|
||||
for (let i = 0; i < clearTimes; i++) {
|
||||
await this.page.keyboard.press('Backspace');
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
if (humanLike) {
|
||||
await this.typeHumanLike(element, value);
|
||||
} else {
|
||||
await element.type(value, { delay: 100 });
|
||||
}
|
||||
|
||||
await element.evaluate((el: any) => {
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
}
|
||||
|
||||
getCssSelector(selector: any): string {
|
||||
if (typeof selector === 'string') {
|
||||
return selector;
|
||||
}
|
||||
|
||||
if (Array.isArray(selector)) {
|
||||
for (const sel of selector) {
|
||||
if (typeof sel === 'string') {
|
||||
return sel;
|
||||
} else if (sel.css) {
|
||||
return sel.css;
|
||||
}
|
||||
}
|
||||
return selector[0];
|
||||
}
|
||||
|
||||
if (selector.css) {
|
||||
return selector.css;
|
||||
}
|
||||
|
||||
throw new Error('无法从选择器配置中提取 CSS 选择器');
|
||||
}
|
||||
|
||||
async typeHumanLike(element: any, text: string): Promise<void> {
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
|
||||
await element.type(char, {
|
||||
delay: Math.random() * 150 + 100
|
||||
});
|
||||
|
||||
if (i > 0 && i % (Math.floor(Math.random() * 3) + 3) === 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, Math.random() * 800 + 300));
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, Math.random() * 500 + 300));
|
||||
}
|
||||
}
|
||||
|
||||
export default FillFormAction;
|
||||
@ -0,0 +1,88 @@
|
||||
import BaseAction from '../core/BaseAction';
|
||||
|
||||
class NavigateAction extends BaseAction {
|
||||
async execute(): Promise<any> {
|
||||
const url = this.replaceVariables(this.config.url);
|
||||
|
||||
// 转换 Puppeteer 的 waitUntil 为 Playwright 兼容的值
|
||||
let waitUntil = this.config.options?.waitUntil || 'networkidle';
|
||||
if (waitUntil === 'networkidle2' || waitUntil === 'networkidle0') {
|
||||
waitUntil = 'networkidle';
|
||||
}
|
||||
|
||||
const options = {
|
||||
waitUntil: waitUntil as 'load' | 'domcontentloaded' | 'networkidle' | 'commit',
|
||||
timeout: this.config.options?.timeout || 30000
|
||||
};
|
||||
|
||||
const maxRetries = this.config.maxRetries || 5;
|
||||
const retryDelay = this.config.retryDelay || 3000;
|
||||
const totalTimeout = this.config.totalTimeout || 180000;
|
||||
|
||||
const startTime = Date.now();
|
||||
let lastError: any = null;
|
||||
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
if (Date.now() - startTime > totalTimeout) {
|
||||
this.log('error', `总超时 ${totalTimeout}ms`);
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
if (attempt > 0) {
|
||||
this.log('info', `第 ${attempt + 1} 次尝试导航...`);
|
||||
} else {
|
||||
this.log('info', `导航到: ${url}`);
|
||||
}
|
||||
|
||||
await this.page.goto(url, options);
|
||||
|
||||
const currentUrl = this.page.url();
|
||||
if (this.config.verifyUrl && !currentUrl.includes(this.config.verifyUrl)) {
|
||||
throw new Error(`页面跳转异常`);
|
||||
}
|
||||
|
||||
if (this.config.verifyElements) {
|
||||
await this.verifyElements(this.config.verifyElements);
|
||||
}
|
||||
|
||||
this.log('info', `✓ 页面加载完成${attempt > 0 ? ` (尝试 ${attempt + 1} 次)` : ''}`);
|
||||
await this.readPageDelay();
|
||||
|
||||
if (this.config.waitAfter) {
|
||||
await new Promise(resolve => setTimeout(resolve, this.config.waitAfter));
|
||||
}
|
||||
|
||||
return { success: true, url: currentUrl };
|
||||
|
||||
} catch (error: any) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.log('error', `导航失败: ${lastError.message}`);
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async verifyElements(selectors: string[]): Promise<void> {
|
||||
this.log('debug', '验证页面元素...');
|
||||
|
||||
for (const selector of selectors) {
|
||||
try {
|
||||
await this.page.waitForSelector(selector, { timeout: 10000 });
|
||||
} catch (error: any) {
|
||||
throw new Error(`元素未找到: ${selector}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.log('debug', `✓ 已验证 ${selectors.length} 个关键元素`);
|
||||
}
|
||||
}
|
||||
|
||||
export default NavigateAction;
|
||||
@ -0,0 +1,106 @@
|
||||
import BaseAction from '../core/BaseAction';
|
||||
|
||||
class RetryBlockAction extends BaseAction {
|
||||
async execute(): Promise<any> {
|
||||
const { steps = [], maxRetries = 3, retryDelay = 1000, totalTimeout = 600000, onRetryBefore = [], onRetryAfter = [] } = this.config;
|
||||
|
||||
const blockName = this.config.name || 'RetryBlock';
|
||||
|
||||
if (!steps || steps.length === 0) {
|
||||
throw new Error('RetryBlock 必须包含至少一个步骤');
|
||||
}
|
||||
|
||||
let lastError: any = null;
|
||||
const startTime = Date.now();
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
const elapsed = Date.now() - startTime;
|
||||
if (elapsed > totalTimeout) {
|
||||
throw new Error(`${blockName} 整体超时 (${totalTimeout}ms)`);
|
||||
}
|
||||
|
||||
try {
|
||||
if (attempt > 0) {
|
||||
this.log('info', `${blockName} - 第 ${attempt + 1} 次重试...`);
|
||||
|
||||
if (onRetryBefore.length > 0) {
|
||||
this.log('debug', '执行重试前钩子...');
|
||||
await this.executeHooks(onRetryBefore);
|
||||
}
|
||||
|
||||
if (retryDelay > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||
}
|
||||
}
|
||||
|
||||
this.log('debug', `执行 ${steps.length} 个步骤...`);
|
||||
await this.executeSteps(steps);
|
||||
|
||||
if (attempt > 0 && onRetryAfter.length > 0) {
|
||||
this.log('debug', '执行重试后钩子...');
|
||||
await this.executeHooks(onRetryAfter);
|
||||
}
|
||||
|
||||
if (attempt > 0) {
|
||||
this.log('success', `✓ ${blockName} 在第 ${attempt + 1} 次尝试后成功`);
|
||||
}
|
||||
|
||||
return { success: true, attempts: attempt + 1 };
|
||||
|
||||
} catch (error: any) {
|
||||
lastError = error;
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
this.log('warn', `${blockName} 执行失败: ${error.message}`);
|
||||
this.log('info', `准备重试 (${attempt + 1}/${maxRetries})...`);
|
||||
} else {
|
||||
this.log('error', `${blockName} 在 ${maxRetries + 1} 次尝试后仍然失败`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`${blockName} 重试耗尽: ${lastError.message}`);
|
||||
}
|
||||
|
||||
async executeHooks(hooks: any[]): Promise<void> {
|
||||
for (const hookConfig of hooks) {
|
||||
await this.executeStep(hookConfig);
|
||||
}
|
||||
}
|
||||
|
||||
async executeSteps(steps: any[]): Promise<void> {
|
||||
for (const stepConfig of steps) {
|
||||
await this.executeStep(stepConfig);
|
||||
}
|
||||
}
|
||||
|
||||
async executeStep(stepConfig: any): Promise<any> {
|
||||
const actionType = stepConfig.action;
|
||||
const ActionClass = this.getActionClass(actionType);
|
||||
const action = new ActionClass(this.context, stepConfig);
|
||||
return await action.execute();
|
||||
}
|
||||
|
||||
getActionClass(actionType: string): any {
|
||||
const actionMap: any = {
|
||||
navigate: require('./NavigateAction').default,
|
||||
fillForm: require('./FillFormAction').default,
|
||||
click: require('./ClickAction').default,
|
||||
wait: require('./WaitAction').default,
|
||||
custom: require('./CustomAction').default,
|
||||
scroll: require('./ScrollAction').default,
|
||||
verify: require('./VerifyAction').default,
|
||||
extract: require('./ExtractAction').default
|
||||
};
|
||||
|
||||
const ActionClass = actionMap[actionType];
|
||||
|
||||
if (!ActionClass) {
|
||||
throw new Error(`未知的 action 类型: ${actionType}`);
|
||||
}
|
||||
|
||||
return ActionClass;
|
||||
}
|
||||
}
|
||||
|
||||
export default RetryBlockAction;
|
||||
@ -0,0 +1,45 @@
|
||||
import BaseAction from '../core/BaseAction';
|
||||
|
||||
class ScrollAction extends BaseAction {
|
||||
async execute(): Promise<any> {
|
||||
const { type = 'bottom', selector, x = 0, y = 0, behavior = 'smooth' } = this.config;
|
||||
|
||||
this.log('debug', `执行滚动: ${type}`);
|
||||
|
||||
switch (type) {
|
||||
case 'bottom':
|
||||
await this.page.evaluate((b: string) => {
|
||||
window.scrollTo({ top: document.body.scrollHeight, left: 0, behavior: b as ScrollBehavior });
|
||||
}, behavior);
|
||||
break;
|
||||
case 'top':
|
||||
await this.page.evaluate((b: string) => {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: b as ScrollBehavior });
|
||||
}, behavior);
|
||||
break;
|
||||
case 'element':
|
||||
if (!selector) throw new Error('滚动到元素需要提供 selector');
|
||||
const element = await this.page.$(selector);
|
||||
if (!element) throw new Error(`元素不存在: ${selector}`);
|
||||
await element.evaluate((el: any, b: string) => {
|
||||
el.scrollIntoView({ behavior: b as ScrollBehavior, block: 'center' });
|
||||
}, behavior);
|
||||
break;
|
||||
case 'distance':
|
||||
await this.page.evaluate(({ dx, dy, b }: any) => {
|
||||
window.scrollBy({ top: dy, left: dx, behavior: b as ScrollBehavior });
|
||||
}, { dx: x, dy: y, b: behavior });
|
||||
break;
|
||||
default:
|
||||
throw new Error(`不支持的滚动类型: ${type}`);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
this.log('debug', '✓ 滚动完成');
|
||||
await this.pauseDelay();
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
export default ScrollAction;
|
||||
@ -0,0 +1,108 @@
|
||||
import BaseAction from '../core/BaseAction';
|
||||
|
||||
class VerifyAction extends BaseAction {
|
||||
async execute(): Promise<any> {
|
||||
const { conditions, timeout = 10000, pollInterval = 500, onSuccess = 'continue', onFailure = 'throw', onTimeout = 'throw' } = this.config;
|
||||
|
||||
if (!conditions) throw new Error('Verify action 需要 conditions 参数');
|
||||
|
||||
this.log('debug', '开始验证...');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
if (conditions.success) {
|
||||
const successResult = await this.checkConditions(conditions.success);
|
||||
if (successResult.matched) {
|
||||
this.log('success', `✓ 验证成功: ${successResult.reason}`);
|
||||
return this.handleResult('success', onSuccess);
|
||||
}
|
||||
}
|
||||
|
||||
if (conditions.failure) {
|
||||
const failureResult = await this.checkConditions(conditions.failure);
|
||||
if (failureResult.matched) {
|
||||
this.log('error', `✗ 验证失败: ${failureResult.reason}`);
|
||||
return this.handleResult('failure', onFailure, failureResult.reason);
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
}
|
||||
|
||||
this.log('warn', `⚠ 验证超时(${timeout}ms)`);
|
||||
return this.handleResult('timeout', onTimeout, '验证超时');
|
||||
}
|
||||
|
||||
async checkConditions(conditionList: any): Promise<{matched: boolean; reason?: string | null}> {
|
||||
if (!Array.isArray(conditionList)) conditionList = [conditionList];
|
||||
|
||||
for (const condition of conditionList) {
|
||||
const result = await this.checkSingleCondition(condition);
|
||||
if (result.matched) return result;
|
||||
}
|
||||
|
||||
return { matched: false };
|
||||
}
|
||||
|
||||
async checkSingleCondition(condition: any): Promise<{matched: boolean; reason?: string | null}> {
|
||||
if (condition.urlContains !== undefined) {
|
||||
const currentUrl = this.page.url();
|
||||
const matched = currentUrl.includes(condition.urlContains);
|
||||
return { matched, reason: matched ? `URL 包含 "${condition.urlContains}"` : null };
|
||||
}
|
||||
|
||||
if (condition.urlNotContains !== undefined) {
|
||||
const currentUrl = this.page.url();
|
||||
const matched = !currentUrl.includes(condition.urlNotContains);
|
||||
return { matched, reason: matched ? `URL 不包含 "${condition.urlNotContains}"` : null };
|
||||
}
|
||||
|
||||
if (condition.urlEquals !== undefined) {
|
||||
const currentUrl = this.page.url();
|
||||
const matched = currentUrl === condition.urlEquals;
|
||||
return { matched, reason: matched ? `URL 等于 "${condition.urlEquals}"` : null };
|
||||
}
|
||||
|
||||
if (condition.elementExists !== undefined) {
|
||||
const element = await this.page.$(condition.elementExists);
|
||||
const matched = !!element;
|
||||
return { matched, reason: matched ? `元素存在: ${condition.elementExists}` : null };
|
||||
}
|
||||
|
||||
if (condition.elementNotExists !== undefined) {
|
||||
const element = await this.page.$(condition.elementNotExists);
|
||||
const matched = !element;
|
||||
return { matched, reason: matched ? `元素不存在: ${condition.elementNotExists}` : null };
|
||||
}
|
||||
|
||||
if (condition.textContains !== undefined) {
|
||||
const hasText = await this.page.evaluate((text: string) => {
|
||||
return document.body.textContent!.includes(text);
|
||||
}, condition.textContains);
|
||||
return { matched: hasText, reason: hasText ? `页面包含文本: "${condition.textContains}"` : null };
|
||||
}
|
||||
|
||||
if (condition.custom !== undefined) {
|
||||
const matched = await this.page.evaluate(condition.custom);
|
||||
return { matched: !!matched, reason: matched ? '自定义条件满足' : null };
|
||||
}
|
||||
|
||||
return { matched: false };
|
||||
}
|
||||
|
||||
handleResult(resultType: string, action: string, reason: string | null = ''): any {
|
||||
switch (action) {
|
||||
case 'continue':
|
||||
return { success: true, result: resultType };
|
||||
case 'throw':
|
||||
throw new Error(`验证${resultType}: ${reason}`);
|
||||
case 'return':
|
||||
return { success: resultType === 'success', result: resultType, reason };
|
||||
default:
|
||||
return { success: true, result: resultType };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default VerifyAction;
|
||||
@ -0,0 +1,135 @@
|
||||
import BaseAction from '../core/BaseAction';
|
||||
import SmartSelector from '../../../core/selectors/SmartSelector';
|
||||
|
||||
class WaitAction extends BaseAction {
|
||||
async execute(): Promise<any> {
|
||||
const type = this.config.type || 'delay';
|
||||
|
||||
switch (type) {
|
||||
case 'delay':
|
||||
return await this.waitDelay();
|
||||
case 'element':
|
||||
return await this.waitForElement();
|
||||
case 'navigation':
|
||||
return await this.waitForNavigation();
|
||||
case 'condition':
|
||||
return await this.waitForCondition();
|
||||
case 'url':
|
||||
return await this.waitForUrl();
|
||||
default:
|
||||
throw new Error(`未知的等待类型: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
async waitDelay(): Promise<{success: boolean}> {
|
||||
const duration = this.config.duration || this.config.ms || 1000;
|
||||
this.log('debug', `等待 ${duration}ms`);
|
||||
await new Promise(resolve => setTimeout(resolve, duration));
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async waitForElement(): Promise<{success: boolean}> {
|
||||
const selector = this.config.selector || this.config.find;
|
||||
const timeout = this.config.timeout || 10000;
|
||||
|
||||
if (!selector) {
|
||||
throw new Error('缺少选择器配置');
|
||||
}
|
||||
|
||||
this.log('debug', '等待元素出现');
|
||||
|
||||
const smartSelector = SmartSelector.fromConfig(selector, this.page);
|
||||
const element = await smartSelector.find(timeout);
|
||||
|
||||
if (!element) {
|
||||
throw new Error(`元素未找到: ${JSON.stringify(selector)}`);
|
||||
}
|
||||
|
||||
this.log('debug', '✓ 元素已出现');
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async waitForNavigation(): Promise<{success: boolean}> {
|
||||
const timeout = this.config.timeout || 30000;
|
||||
|
||||
this.log('debug', '等待页面导航');
|
||||
|
||||
let waitUntil = this.config.waitUntil || 'networkidle';
|
||||
if (waitUntil === 'networkidle2' || waitUntil === 'networkidle0') {
|
||||
waitUntil = 'networkidle';
|
||||
}
|
||||
|
||||
await this.page.waitForLoadState(waitUntil as any, { timeout });
|
||||
|
||||
this.log('debug', '✓ 导航完成');
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async waitForCondition(): Promise<{success: boolean}> {
|
||||
const handler = this.config.handler;
|
||||
const timeout = this.config.timeout || 10000;
|
||||
|
||||
if (!handler) {
|
||||
throw new Error('缺少条件处理函数');
|
||||
}
|
||||
|
||||
this.log('debug', '等待自定义条件');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
if (typeof this.context.adapter[handler] === 'function') {
|
||||
const result = await this.context.adapter[handler]();
|
||||
if (result) {
|
||||
this.log('debug', '✓ 条件满足');
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
throw new Error(`waitForCondition 超时: ${handler}`);
|
||||
}
|
||||
|
||||
async waitForUrl(): Promise<{success: boolean}> {
|
||||
const timeout = this.config.timeout || 20000;
|
||||
const urlContains = this.config.urlContains;
|
||||
const urlNotContains = this.config.urlNotContains;
|
||||
const urlEquals = this.config.urlEquals;
|
||||
|
||||
if (!urlContains && !urlNotContains && !urlEquals) {
|
||||
throw new Error('需要指定 urlContains、urlNotContains 或 urlEquals');
|
||||
}
|
||||
|
||||
this.log('debug', '等待 URL 变化');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
const currentUrl = this.page.url();
|
||||
|
||||
if (urlContains && currentUrl.includes(urlContains)) {
|
||||
this.log('debug', `✓ URL 包含 "${urlContains}": ${currentUrl}`);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
if (urlNotContains && !currentUrl.includes(urlNotContains)) {
|
||||
this.log('debug', `✓ URL 不包含 "${urlNotContains}": ${currentUrl}`);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
if (urlEquals && currentUrl === urlEquals) {
|
||||
this.log('debug', `✓ URL 等于 "${urlEquals}"`);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
const finalUrl = this.page.url();
|
||||
throw new Error(`waitForUrl 超时: 实际URL ${finalUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default WaitAction;
|
||||
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Playwright Stealth Action Factory
|
||||
*/
|
||||
|
||||
import { IActionFactory } from '../../../core/interfaces/IAction';
|
||||
import ClickAction from '../actions/ClickAction';
|
||||
import WaitAction from '../actions/WaitAction';
|
||||
import NavigateAction from '../actions/NavigateAction';
|
||||
import CustomAction from '../actions/CustomAction';
|
||||
import VerifyAction from '../actions/VerifyAction';
|
||||
import FillFormAction from '../actions/FillFormAction';
|
||||
import ScrollAction from '../actions/ScrollAction';
|
||||
import ExtractAction from '../actions/ExtractAction';
|
||||
import RetryBlockAction from '../actions/RetryBlockAction';
|
||||
|
||||
export class PlaywrightStealthActionFactory implements IActionFactory {
|
||||
private actions: Map<string, any>;
|
||||
|
||||
constructor() {
|
||||
this.actions = new Map();
|
||||
this.registerDefaultActions();
|
||||
}
|
||||
|
||||
private registerDefaultActions(): void {
|
||||
this.actions.set('click', ClickAction);
|
||||
this.actions.set('wait', WaitAction);
|
||||
this.actions.set('navigate', NavigateAction);
|
||||
this.actions.set('custom', CustomAction);
|
||||
this.actions.set('verify', VerifyAction);
|
||||
this.actions.set('fillForm', FillFormAction);
|
||||
this.actions.set('scroll', ScrollAction);
|
||||
this.actions.set('extract', ExtractAction);
|
||||
this.actions.set('retryBlock', RetryBlockAction);
|
||||
}
|
||||
|
||||
getAction(actionName: string): any {
|
||||
const ActionClass = this.actions.get(actionName);
|
||||
|
||||
if (!ActionClass) {
|
||||
throw new Error(`Unknown action: ${actionName}`);
|
||||
}
|
||||
|
||||
return ActionClass;
|
||||
}
|
||||
|
||||
hasAction(actionName: string): boolean {
|
||||
return this.actions.has(actionName);
|
||||
}
|
||||
|
||||
registerAction(name: string, ActionClass: any): void {
|
||||
this.actions.set(name, ActionClass);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Playwright Stealth Provider 的 BaseAction
|
||||
*/
|
||||
|
||||
import { Page } from 'playwright';
|
||||
|
||||
export interface ActionContext {
|
||||
page: Page;
|
||||
browser: any;
|
||||
logger?: any;
|
||||
data?: any;
|
||||
siteConfig?: any;
|
||||
config?: any;
|
||||
siteName?: string;
|
||||
adapter?: any;
|
||||
}
|
||||
|
||||
export abstract class BaseAction {
|
||||
protected context: ActionContext;
|
||||
protected config: any;
|
||||
protected page: Page;
|
||||
protected logger: any;
|
||||
|
||||
constructor(context: ActionContext, config: any) {
|
||||
this.context = context;
|
||||
this.config = config;
|
||||
this.page = context.page;
|
||||
this.logger = context.logger;
|
||||
}
|
||||
|
||||
abstract execute(): Promise<any>;
|
||||
|
||||
replaceVariables(value: any): any {
|
||||
if (typeof value !== 'string') return value;
|
||||
|
||||
return value.replace(/\{\{(.+?)\}\}/g, (match, expression) => {
|
||||
const [path, defaultValue] = expression.split('|').map((s: string) => s.trim());
|
||||
const result = this.resolveVariablePath(path);
|
||||
|
||||
if (result !== undefined && result !== null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (defaultValue !== undefined) {
|
||||
this.log('debug', `变量 "${path}" 不存在,使用默认值: "${defaultValue}"`);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
this.log('warn', `⚠️ 变量 "${path}" 不存在`);
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
resolveVariablePath(path: string): any {
|
||||
const keys = path.split('.');
|
||||
const rootKey = keys[0];
|
||||
|
||||
let dataSource: any;
|
||||
let startIndex = 1;
|
||||
|
||||
switch (rootKey) {
|
||||
case 'site':
|
||||
dataSource = this.context.siteConfig;
|
||||
break;
|
||||
case 'config':
|
||||
dataSource = this.context.config;
|
||||
break;
|
||||
case 'env':
|
||||
dataSource = process.env;
|
||||
break;
|
||||
default:
|
||||
dataSource = this.context.data;
|
||||
startIndex = 0;
|
||||
}
|
||||
|
||||
if (!dataSource) return undefined;
|
||||
|
||||
let result = dataSource;
|
||||
for (let i = startIndex; i < keys.length; i++) {
|
||||
if (result && typeof result === 'object') {
|
||||
result = result[keys[i]];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
log(level: string, message: string): void {
|
||||
if (this.logger && this.logger[level]) {
|
||||
this.logger[level](this.context.siteName || 'Automation', message);
|
||||
} else {
|
||||
console.log(`[${level.toUpperCase()}] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async randomDelay(min: number, max: number): Promise<void> {
|
||||
const delay = min + Math.random() * (max - min);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
async readPageDelay(): Promise<void> {
|
||||
await this.randomDelay(2000, 5000);
|
||||
}
|
||||
|
||||
async thinkDelay(): Promise<void> {
|
||||
await this.randomDelay(1000, 2500);
|
||||
}
|
||||
|
||||
async pauseDelay(): Promise<void> {
|
||||
await this.randomDelay(300, 800);
|
||||
}
|
||||
|
||||
async stepDelay(): Promise<void> {
|
||||
await this.randomDelay(1500, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseAction;
|
||||
@ -44,7 +44,7 @@ export class AccountGeneratorTool extends BaseTool<AccountGeneratorConfig> {
|
||||
readonly name = 'account-generator';
|
||||
|
||||
// 邮箱域名(与旧框架一致)
|
||||
private emailDomains = ['qichen111.asia'];
|
||||
private emailDomains = ['qichen.cloud'];
|
||||
|
||||
// 英文名字库(与旧框架一致)
|
||||
private firstNames = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user