auto-account-machine/browser-automation-ts/cli/run.ts
2025-11-21 17:59:49 +08:00

296 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 通用自动化执行工具
* 根据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 { WorkflowEngine } from '../src/workflow/WorkflowEngine';
import { ISiteAdapter, EmptyAdapter } from '../src/adapters/ISiteAdapter';
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<AdsPowerProvider> {
console.log('🌐 Initializing AdsPower Provider...');
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: AdsPowerProvider): 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();