This commit is contained in:
dengqichen 2025-12-01 13:21:06 +08:00
parent d4cc237ccd
commit 2c2b813052
18 changed files with 1365 additions and 28 deletions

2
aaa.js
View File

@ -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);

View File

@ -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,13 +160,22 @@ class AutomationRunner {
} }
} }
private async initializeProvider(): Promise<AdsPowerProvider> { private async initializeProvider(): Promise<IBrowserProvider> {
console.log('🌐 Initializing AdsPower Provider...'); const browserProvider = process.argv[2] || 'adspower';
return new AdsPowerProvider({ console.log(`🌐 Initializing ${browserProvider} Provider...`);
profileId: process.env.ADSPOWER_USER_ID,
siteName: this.siteName.charAt(0).toUpperCase() + this.siteName.slice(1) 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 { private buildContext(launchResult: any, config: WorkflowConfig): any {
@ -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();

View File

@ -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',

View File

@ -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

View File

@ -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');

View File

@ -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']
});
});
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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 = {