dasdasd
This commit is contained in:
parent
d4cc237ccd
commit
2c2b813052
2
aaa.js
2
aaa.js
@ -14,7 +14,7 @@ console.log('=== 生成卡号验证 ===\n');
|
|||||||
// 生成10张银联卡
|
// 生成10张银联卡
|
||||||
console.log('生成10张银联卡进行验证:\n');
|
console.log('生成10张银联卡进行验证:\n');
|
||||||
|
|
||||||
const cards = generator.generateBatch(10, 'unionpay');
|
const cards = generator.generateBatch(100, 'unionpay');
|
||||||
|
|
||||||
cards.forEach((card, index) => {
|
cards.forEach((card, index) => {
|
||||||
const isValid = luhnCheck(card.number);
|
const isValid = luhnCheck(card.number);
|
||||||
|
|||||||
@ -14,8 +14,10 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as yaml from 'js-yaml';
|
import * as yaml from 'js-yaml';
|
||||||
import { AdsPowerProvider } from '../src/providers/adspower/AdsPowerProvider';
|
import { AdsPowerProvider } from '../src/providers/adspower/AdsPowerProvider';
|
||||||
|
import { PlaywrightStealthProvider } from '../src/providers/playwright-stealth/PlaywrightStealthProvider';
|
||||||
import { WorkflowEngine } from '../src/workflow/WorkflowEngine';
|
import { WorkflowEngine } from '../src/workflow/WorkflowEngine';
|
||||||
import { ISiteAdapter, EmptyAdapter } from '../src/adapters/ISiteAdapter';
|
import { ISiteAdapter, EmptyAdapter } from '../src/adapters/ISiteAdapter';
|
||||||
|
import { IBrowserProvider } from '../src/core/interfaces/IBrowserProvider';
|
||||||
|
|
||||||
interface WorkflowConfig {
|
interface WorkflowConfig {
|
||||||
site: string;
|
site: string;
|
||||||
@ -158,14 +160,23 @@ class AutomationRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initializeProvider(): Promise<AdsPowerProvider> {
|
private async initializeProvider(): Promise<IBrowserProvider> {
|
||||||
console.log('🌐 Initializing AdsPower Provider...');
|
const browserProvider = process.argv[2] || 'adspower';
|
||||||
|
|
||||||
|
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({
|
return new AdsPowerProvider({
|
||||||
profileId: process.env.ADSPOWER_USER_ID,
|
profileId: process.env.ADSPOWER_USER_ID,
|
||||||
siteName: this.siteName.charAt(0).toUpperCase() + this.siteName.slice(1)
|
siteName: this.siteName.charAt(0).toUpperCase() + this.siteName.slice(1)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private buildContext(launchResult: any, config: WorkflowConfig): any {
|
private buildContext(launchResult: any, config: WorkflowConfig): any {
|
||||||
// 创建空context - Adapter负责填充数据
|
// 创建空context - Adapter负责填充数据
|
||||||
@ -207,7 +218,7 @@ class AutomationRunner {
|
|||||||
console.log('='.repeat(60) + '\n');
|
console.log('='.repeat(60) + '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async cleanup(provider: AdsPowerProvider): Promise<void> {
|
private async cleanup(provider: IBrowserProvider): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('\n🔒 Closing browser...');
|
console.log('\n🔒 Closing browser...');
|
||||||
await provider.close();
|
await provider.close();
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export class WindsurfAdapter extends BaseAdapter {
|
|||||||
new AccountGeneratorTool(),
|
new AccountGeneratorTool(),
|
||||||
{
|
{
|
||||||
email: {
|
email: {
|
||||||
domain: 'qichen111.asia'
|
domain: 'qichen.cloud'
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
strategy: 'email',
|
strategy: 'email',
|
||||||
|
|||||||
@ -1,27 +1,27 @@
|
|||||||
# Windsurf 注册自动化配置
|
# Windsurf 注册自动化配置
|
||||||
site:
|
site:
|
||||||
name: Windsurf
|
name: Windsurf
|
||||||
url: https://windsurf.com/account/register
|
url: https://windsurf.com/refer?referral_code=55424ec434
|
||||||
|
|
||||||
# 工作流定义
|
# 工作流定义
|
||||||
workflow:
|
workflow:
|
||||||
# ==================== 步骤 0: 处理邀请链接 ====================
|
# ==================== 步骤 0: 处理邀请链接 ====================
|
||||||
# - action: navigate
|
- action: navigate
|
||||||
# name: "打开邀请链接"
|
name: "打开邀请链接"
|
||||||
# url: "https://windsurf.com/refer?referral_code=55424ec434"
|
url: "https://windsurf.com/refer?referral_code=55424ec434"
|
||||||
# options:
|
options:
|
||||||
# waitUntil: 'networkidle2'
|
waitUntil: 'networkidle2'
|
||||||
# timeout: 30000
|
timeout: 30000
|
||||||
#
|
|
||||||
# - action: click
|
- action: click
|
||||||
# name: "点击接受邀请"
|
name: "点击接受邀请"
|
||||||
# selector:
|
selector:
|
||||||
# - text: 'Sign up to accept referral'
|
- text: 'Sign up to accept referral'
|
||||||
# selector: 'button'
|
selector: 'button'
|
||||||
# - css: 'button.bg-sk-aqua'
|
- css: 'button.bg-sk-aqua'
|
||||||
# - css: 'button:has-text("Sign up to accept referral")'
|
- css: 'button:has-text("Sign up to accept referral")'
|
||||||
# timeout: 30000
|
timeout: 30000
|
||||||
# waitForNavigation: true
|
waitForNavigation: true
|
||||||
|
|
||||||
# 验证跳转到注册页面(带referral_code)
|
# 验证跳转到注册页面(带referral_code)
|
||||||
- action: verify
|
- action: verify
|
||||||
|
|||||||
@ -18,13 +18,16 @@ export * from './factory/BrowserFactory';
|
|||||||
|
|
||||||
// Providers
|
// Providers
|
||||||
export * from './providers/adspower/AdsPowerProvider';
|
export * from './providers/adspower/AdsPowerProvider';
|
||||||
|
export * from './providers/playwright-stealth/PlaywrightStealthProvider';
|
||||||
|
|
||||||
// Register providers
|
// Register providers
|
||||||
import { BrowserFactory } from './factory/BrowserFactory';
|
import { BrowserFactory } from './factory/BrowserFactory';
|
||||||
import { AdsPowerProvider } from './providers/adspower/AdsPowerProvider';
|
import { AdsPowerProvider } from './providers/adspower/AdsPowerProvider';
|
||||||
|
import { PlaywrightStealthProvider } from './providers/playwright-stealth/PlaywrightStealthProvider';
|
||||||
import { BrowserProviderType } from './core/types';
|
import { BrowserProviderType } from './core/types';
|
||||||
|
|
||||||
// Auto-register AdsPower
|
// Auto-register providers
|
||||||
BrowserFactory.register(BrowserProviderType.ADSPOWER, AdsPowerProvider);
|
BrowserFactory.register(BrowserProviderType.ADSPOWER, AdsPowerProvider);
|
||||||
|
BrowserFactory.register(BrowserProviderType.PLAYWRIGHT_STEALTH, PlaywrightStealthProvider);
|
||||||
|
|
||||||
console.log('✅ Browser Automation Framework (TypeScript) initialized');
|
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';
|
readonly name = 'account-generator';
|
||||||
|
|
||||||
// 邮箱域名(与旧框架一致)
|
// 邮箱域名(与旧框架一致)
|
||||||
private emailDomains = ['qichen111.asia'];
|
private emailDomains = ['qichen.cloud'];
|
||||||
|
|
||||||
// 英文名字库(与旧框架一致)
|
// 英文名字库(与旧框架一致)
|
||||||
private firstNames = {
|
private firstNames = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user