307 lines
9.4 KiB
TypeScript
307 lines
9.4 KiB
TypeScript
/**
|
||
* 通用自动化执行工具
|
||
* 根据YAML配置自动化执行任意网站的操作流程
|
||
*
|
||
* 用法:
|
||
* npm run run -- windsurf # 执行Windsurf自动化
|
||
* npm run run -- stripe # 执行Stripe自动化
|
||
* npm run run -- <网站名> # 执行任意网站自动化
|
||
*
|
||
* 配置文件位置:configs/sites/<网站名>.yaml
|
||
*/
|
||
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import * as yaml from 'js-yaml';
|
||
import { AdsPowerProvider } from '../src/providers/adspower/AdsPowerProvider';
|
||
import { PlaywrightStealthProvider } from '../src/providers/playwright-stealth/PlaywrightStealthProvider';
|
||
import { WorkflowEngine } from '../src/workflow/WorkflowEngine';
|
||
import { ISiteAdapter, EmptyAdapter } from '../src/adapters/ISiteAdapter';
|
||
import { IBrowserProvider } from '../src/core/interfaces/IBrowserProvider';
|
||
|
||
interface WorkflowConfig {
|
||
site: string;
|
||
workflow: any[];
|
||
errorHandling?: any;
|
||
variables?: any;
|
||
url?: string;
|
||
siteConfig?: {
|
||
url?: string;
|
||
name?: string;
|
||
[key: string]: any;
|
||
};
|
||
}
|
||
|
||
class AutomationRunner {
|
||
private siteName: string;
|
||
private configPath: string;
|
||
private adapterPath: string;
|
||
private adapter: ISiteAdapter | null = null;
|
||
|
||
constructor(siteName: string) {
|
||
this.siteName = siteName;
|
||
this.configPath = path.join(__dirname, '../configs/sites', `${siteName}.yaml`);
|
||
this.adapterPath = path.join(__dirname, '../configs/sites', `${siteName}-adapter.ts`);
|
||
}
|
||
|
||
async run() {
|
||
console.log('🚀 Browser Automation Executor\n');
|
||
console.log(`Site: ${this.siteName}`);
|
||
console.log(`Config: ${this.configPath}\n`);
|
||
|
||
// 1. 检查配置文件
|
||
if (!fs.existsSync(this.configPath)) {
|
||
console.error(`❌ Config file not found: ${this.configPath}`);
|
||
console.log('\n💡 Available configs:');
|
||
this.listAvailableConfigs();
|
||
process.exit(1);
|
||
}
|
||
|
||
// 2. 加载配置
|
||
const config = this.loadConfig();
|
||
console.log(`✅ Loaded workflow with ${config.workflow.length} steps\n`);
|
||
|
||
// 2.5. 加载Adapter(如果存在)
|
||
await this.loadAdapter();
|
||
|
||
// 3. 初始化Provider
|
||
const provider = await this.initializeProvider();
|
||
|
||
try {
|
||
// 4. 启动浏览器
|
||
const result = await provider.launch();
|
||
console.log('✅ Browser launched successfully\n');
|
||
|
||
// 5. 准备Context
|
||
const context = this.buildContext(result, config);
|
||
|
||
// 5.5. 初始化Adapter
|
||
if (this.adapter) {
|
||
await this.adapter.initialize(context);
|
||
if (this.adapter.beforeWorkflow) {
|
||
await this.adapter.beforeWorkflow(context);
|
||
}
|
||
}
|
||
|
||
// 6. 创建并执行WorkflowEngine
|
||
const engine = new WorkflowEngine(
|
||
config.workflow,
|
||
context,
|
||
provider.getActionFactory()
|
||
);
|
||
|
||
console.log('▶️ Starting workflow execution...\n');
|
||
const workflowResult = await engine.execute();
|
||
|
||
// 7. 显示结果
|
||
this.displayResults(workflowResult, config.workflow.length);
|
||
|
||
// 7.5. Adapter后处理
|
||
if (this.adapter && this.adapter.afterWorkflow) {
|
||
await this.adapter.afterWorkflow(context, workflowResult);
|
||
}
|
||
|
||
// 8. 等待查看
|
||
console.log('⏸️ Waiting 5 seconds before closing...');
|
||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||
|
||
} catch (error: any) {
|
||
console.error('\n❌ Fatal error:', error.message);
|
||
console.error(error.stack);
|
||
} finally {
|
||
await this.cleanup(provider);
|
||
}
|
||
}
|
||
|
||
private loadConfig(): WorkflowConfig {
|
||
const content = fs.readFileSync(this.configPath, 'utf8');
|
||
return yaml.load(content) as WorkflowConfig;
|
||
}
|
||
|
||
/**
|
||
* 动态加载网站适配器
|
||
*/
|
||
private async loadAdapter(): Promise<void> {
|
||
// 检查是否存在adapter文件(支持.ts和.js)
|
||
const tsPath = this.adapterPath;
|
||
const jsPath = this.adapterPath.replace('.ts', '.js');
|
||
|
||
let adapterPath: string | null = null;
|
||
if (fs.existsSync(tsPath)) {
|
||
adapterPath = tsPath;
|
||
} else if (fs.existsSync(jsPath)) {
|
||
adapterPath = jsPath;
|
||
}
|
||
|
||
if (!adapterPath) {
|
||
console.log(`⚠️ No adapter found for ${this.siteName}, using empty adapter`);
|
||
this.adapter = new EmptyAdapter();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
console.log(`🔌 Loading adapter: ${path.basename(adapterPath)}`);
|
||
|
||
// 动态导入adapter
|
||
const adapterModule = await import(adapterPath);
|
||
const AdapterClass = adapterModule.default;
|
||
|
||
if (!AdapterClass) {
|
||
throw new Error('Adapter must have a default export');
|
||
}
|
||
|
||
this.adapter = new AdapterClass();
|
||
console.log(`✅ Adapter loaded: ${this.adapter!.name}\n`);
|
||
|
||
} catch (error: any) {
|
||
console.error(`❌ Failed to load adapter: ${error.message}`);
|
||
console.log('Using empty adapter as fallback\n');
|
||
this.adapter = new EmptyAdapter();
|
||
}
|
||
}
|
||
|
||
private async initializeProvider(): Promise<IBrowserProvider> {
|
||
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({
|
||
profileId: process.env.ADSPOWER_USER_ID,
|
||
siteName: this.siteName.charAt(0).toUpperCase() + this.siteName.slice(1)
|
||
});
|
||
}
|
||
}
|
||
|
||
private buildContext(launchResult: any, config: WorkflowConfig): any {
|
||
// 创建空context - Adapter负责填充数据
|
||
return {
|
||
page: launchResult.page,
|
||
browser: launchResult.browser,
|
||
logger: console,
|
||
data: {
|
||
account: {}, // Adapter会在beforeWorkflow中填充
|
||
...config.variables
|
||
},
|
||
siteConfig: config.siteConfig || {
|
||
url: config.url || '',
|
||
name: config.site || this.siteName
|
||
},
|
||
config: config,
|
||
siteName: this.siteName,
|
||
// 注入adapter的handlers
|
||
adapter: this.adapter ? this.adapter.getHandlers() : {}
|
||
};
|
||
}
|
||
|
||
private displayResults(result: any, totalSteps: number): void {
|
||
console.log('\n' + '='.repeat(60));
|
||
console.log('📊 Workflow Execution Summary');
|
||
console.log('='.repeat(60));
|
||
console.log(`Site: ${this.siteName}`);
|
||
console.log(`Status: ${result.success ? '✅ SUCCESS' : '❌ FAILED'}`);
|
||
console.log(`Steps Completed: ${result.steps}/${totalSteps}`);
|
||
console.log(`Duration: ${(result.duration / 1000).toFixed(2)}s`);
|
||
console.log(`Errors: ${result.errors.length}`);
|
||
|
||
if (result.errors.length > 0) {
|
||
console.log('\n❌ Errors:');
|
||
result.errors.forEach((err: any, i: number) => {
|
||
console.log(` ${i + 1}. Step ${err.step} (${err.name}): ${err.error}`);
|
||
});
|
||
}
|
||
console.log('='.repeat(60) + '\n');
|
||
}
|
||
|
||
private async cleanup(provider: IBrowserProvider): Promise<void> {
|
||
try {
|
||
console.log('\n🔒 Closing browser...');
|
||
await provider.close();
|
||
console.log('✅ Browser closed successfully');
|
||
|
||
// 清理adapter资源
|
||
if (this.adapter && this.adapter.cleanup) {
|
||
await this.adapter.cleanup();
|
||
}
|
||
} catch (e: any) {
|
||
console.error('⚠️ Error closing browser:', e.message);
|
||
}
|
||
}
|
||
|
||
private listAvailableConfigs(): void {
|
||
const configsDir = path.join(__dirname, '../configs/sites');
|
||
|
||
if (!fs.existsSync(configsDir)) {
|
||
console.log(' (No configs directory found)');
|
||
return;
|
||
}
|
||
|
||
const files = fs.readdirSync(configsDir)
|
||
.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
|
||
.map(f => f.replace(/\.(yaml|yml)$/, ''));
|
||
|
||
if (files.length === 0) {
|
||
console.log(' (No config files found)');
|
||
} else {
|
||
files.forEach(name => console.log(` - ${name}`));
|
||
}
|
||
}
|
||
}
|
||
|
||
// 主程序
|
||
async function main() {
|
||
const args = process.argv.slice(2);
|
||
|
||
// 参数格式:node cli/run.js <browser-provider> <browser-params> <site>
|
||
// 示例:node cli/run.js adspower k1728p8l windsurf
|
||
|
||
if (args.length < 3) {
|
||
console.error('❌ Usage: node cli/run.js <browser-provider> <browser-params> <site>');
|
||
console.error('\nArguments:');
|
||
console.error(' browser-provider 浏览器提供商 (adspower, playwright, etc)');
|
||
console.error(' browser-params 浏览器参数 (AdsPower的profileId, 或 - 表示默认)');
|
||
console.error(' site 网站名称 (windsurf, stripe, etc)\n');
|
||
console.error('Examples:');
|
||
console.error(' node cli/run.js adspower k1728p8l windsurf');
|
||
console.error(' node cli/run.js playwright - windsurf');
|
||
console.error(' node cli/run.js adspower j9abc123 stripe\n');
|
||
process.exit(1);
|
||
}
|
||
|
||
const browserProvider = args[0];
|
||
const browserParams = args[1];
|
||
const siteName = args[2];
|
||
|
||
console.log('🚀 Browser Automation Executor\n');
|
||
console.log(`Browser: ${browserProvider}`);
|
||
console.log(`Params: ${browserParams}`);
|
||
console.log(`Site: ${siteName}\n`);
|
||
|
||
// 设置浏览器参数
|
||
if (browserProvider === 'adspower') {
|
||
if (browserParams !== '-') {
|
||
process.env.ADSPOWER_USER_ID = browserParams;
|
||
console.log(`📍 AdsPower Profile: ${browserParams}`);
|
||
}
|
||
}
|
||
|
||
const runner = new AutomationRunner(siteName);
|
||
|
||
try {
|
||
await runner.run();
|
||
console.log('\n✅ Automation completed successfully!');
|
||
process.exit(0);
|
||
} catch (error: any) {
|
||
console.error('\n❌ Automation failed:', error.message);
|
||
console.error(error.stack);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
main();
|