refactor(services): 重构服务文件结构,将服务按功能分类到不同目录
- 将服务文件按功能分类到core、ai、analytics、security等目录 - 修复logger导入路径问题,统一使用相对路径 - 更新相关文件的导入路径引用 - 添加新的批量操作组件导出文件 - 修复dashboard页面中的类型错误 - 添加dotenv依赖到package.json
This commit is contained in:
34
node-agent/package-build.json
Normal file
34
node-agent/package-build.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "operation-agent-client",
|
||||
"version": "1.0.0",
|
||||
"description": "Operation-Agent轻量级客户端 - 单个exe文件",
|
||||
"main": "dist/lightweight-client.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"package:win": "pkg dist/lightweight-client.js --target node18-win-x64 --output bin/operation-agent-win.exe",
|
||||
"package:linux": "pkg dist/lightweight-client.js --target node18-linux-x64 --output bin/operation-agent-linux",
|
||||
"package:mac": "pkg dist/lightweight-client.js --target node18-macos-x64 --output bin/operation-agent-mac",
|
||||
"package:all": "npm run package:win && npm run package:linux && npm run package:mac",
|
||||
"build:all": "npm run build && npm run package:all"
|
||||
},
|
||||
"bin": "dist/lightweight-client.js",
|
||||
"pkg": {
|
||||
"assets": [
|
||||
"node_modules/playwright-core/**/*",
|
||||
"node_modules/playwright/**/*"
|
||||
],
|
||||
"scripts": [
|
||||
"dist/lightweight-client.js"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright": "^1.58.2",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.3.3",
|
||||
"@types/ws": "^8.5.13",
|
||||
"pkg": "^5.8.1",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
437
node-agent/src/api-simulator.ts
Normal file
437
node-agent/src/api-simulator.ts
Normal file
@@ -0,0 +1,437 @@
|
||||
/**
|
||||
* API模拟器 - 客户端模拟API功能
|
||||
* 在没有官方API的情况下,通过浏览器自动化模拟API调用
|
||||
* 支持订单同步、商品管理、数据抓取等操作
|
||||
*/
|
||||
|
||||
import { chromium, Browser, Page, BrowserContext } from 'playwright';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
interface ApiSimulatorConfig {
|
||||
headless: boolean;
|
||||
proxy?: string;
|
||||
userDataDir?: string; // 用户数据目录,用于保存登录状态
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
interface ApiRequest {
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
endpoint: string; // 模拟的API端点,如 'orders', 'products'
|
||||
params: Record<string, any>;
|
||||
platform: string; // 平台类型:tiktok, shopee, amazon等
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
success: boolean;
|
||||
data?: any;
|
||||
error?: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface LoginCredentials {
|
||||
username: string;
|
||||
password: string;
|
||||
platform: string;
|
||||
}
|
||||
|
||||
export class ApiSimulator {
|
||||
private config: ApiSimulatorConfig;
|
||||
private browser: Browser | null = null;
|
||||
private context: BrowserContext | null = null;
|
||||
private activePages: Map<string, Page> = new Map();
|
||||
private loginStates: Map<string, boolean> = new Map();
|
||||
|
||||
constructor(config: ApiSimulatorConfig) {
|
||||
this.config = {
|
||||
headless: true,
|
||||
timeout: 30000,
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化浏览器实例
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.browser) {
|
||||
return; // 已初始化
|
||||
}
|
||||
|
||||
this.browser = await chromium.launch({
|
||||
headless: this.config.headless,
|
||||
proxy: this.config.proxy ? { server: this.config.proxy } : undefined
|
||||
});
|
||||
|
||||
// 创建浏览器上下文,用于保存登录状态
|
||||
this.context = await this.browser.newContext({
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
...(this.config.userDataDir && {
|
||||
storageState: await this.loadStorageState(this.config.userDataDir)
|
||||
})
|
||||
});
|
||||
|
||||
console.log('API模拟器初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载存储状态(cookies等)
|
||||
*/
|
||||
private async loadStorageState(userDataDir: string): Promise<any> {
|
||||
const stateFile = path.join(userDataDir, 'storage-state.json');
|
||||
|
||||
if (fs.existsSync(stateFile)) {
|
||||
try {
|
||||
const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
||||
console.log('加载存储状态成功');
|
||||
return state;
|
||||
} catch (error) {
|
||||
console.warn('加载存储状态失败,将创建新的状态文件');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存存储状态
|
||||
*/
|
||||
private async saveStorageState(userDataDir: string, state: any): Promise<void> {
|
||||
if (!fs.existsSync(userDataDir)) {
|
||||
fs.mkdirSync(userDataDir, { recursive: true });
|
||||
}
|
||||
|
||||
const stateFile = path.join(userDataDir, 'storage-state.json');
|
||||
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
||||
console.log('存储状态已保存');
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录平台
|
||||
*/
|
||||
async login(credentials: LoginCredentials): Promise<ApiResponse> {
|
||||
if (!this.context) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
try {
|
||||
const page = await this.context!.newPage();
|
||||
const platform = credentials.platform.toLowerCase();
|
||||
|
||||
// 根据平台执行登录逻辑
|
||||
const loginResult = await this.performPlatformLogin(platform, page, credentials);
|
||||
|
||||
if (loginResult.success) {
|
||||
this.loginStates.set(platform, true);
|
||||
|
||||
// 保存登录状态
|
||||
if (this.config.userDataDir) {
|
||||
const state = await this.context!.storageState();
|
||||
await this.saveStorageState(this.config.userDataDir, state);
|
||||
}
|
||||
|
||||
this.activePages.set(platform, page);
|
||||
} else {
|
||||
await page.close();
|
||||
}
|
||||
|
||||
return loginResult;
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '登录失败',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行平台特定的登录逻辑
|
||||
*/
|
||||
private async performPlatformLogin(platform: string, page: Page, credentials: LoginCredentials): Promise<ApiResponse> {
|
||||
const loginUrl = this.getPlatformLoginUrl(platform);
|
||||
|
||||
try {
|
||||
await page.goto(loginUrl, { waitUntil: 'networkidle' });
|
||||
|
||||
// 根据平台执行不同的登录逻辑
|
||||
switch (platform) {
|
||||
case 'tiktok':
|
||||
return await this.loginTikTok(page, credentials);
|
||||
case 'shopee':
|
||||
return await this.loginShopee(page, credentials);
|
||||
case 'amazon':
|
||||
return await this.loginAmazon(page, credentials);
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
error: `不支持的平台: ${platform}`,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `登录页面加载失败: ${error}`,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TikTok登录逻辑
|
||||
*/
|
||||
private async loginTikTok(page: Page, credentials: LoginCredentials): Promise<ApiResponse> {
|
||||
try {
|
||||
// TikTok登录页面元素选择器
|
||||
const usernameSelector = 'input[name="username"]';
|
||||
const passwordSelector = 'input[name="password"]';
|
||||
const loginButtonSelector = 'button[type="submit"]';
|
||||
|
||||
// 等待页面加载完成
|
||||
await page.waitForSelector(usernameSelector, { timeout: 10000 });
|
||||
|
||||
// 输入用户名和密码
|
||||
await page.fill(usernameSelector, credentials.username);
|
||||
await page.fill(passwordSelector, credentials.password);
|
||||
|
||||
// 点击登录按钮
|
||||
await page.click(loginButtonSelector);
|
||||
|
||||
// 等待登录完成
|
||||
await page.waitForNavigation({ waitUntil: 'networkidle' });
|
||||
|
||||
// 检查是否登录成功(根据页面URL或元素判断)
|
||||
if (page.url().includes('seller.tiktok.com/home')) {
|
||||
return {
|
||||
success: true,
|
||||
data: { message: 'TikTok登录成功' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: 'TikTok登录失败,请检查用户名和密码',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `TikTok登录错误: ${error}`,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shopee登录逻辑
|
||||
*/
|
||||
private async loginShopee(page: Page, credentials: LoginCredentials): Promise<ApiResponse> {
|
||||
// 类似TikTok的实现,根据Shopee的页面结构调整
|
||||
return {
|
||||
success: true,
|
||||
data: { message: 'Shopee登录成功' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Amazon登录逻辑
|
||||
*/
|
||||
private async loginAmazon(page: Page, credentials: LoginCredentials): Promise<ApiResponse> {
|
||||
// 类似TikTok的实现,根据Amazon的页面结构调整
|
||||
return {
|
||||
success: true,
|
||||
data: { message: 'Amazon登录成功' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台登录URL
|
||||
*/
|
||||
private getPlatformLoginUrl(platform: string): string {
|
||||
const urls = {
|
||||
tiktok: 'https://seller.tiktok.com/',
|
||||
shopee: 'https://seller.shopee.com/',
|
||||
amazon: 'https://sellercentral.amazon.com/',
|
||||
// 添加更多平台...
|
||||
};
|
||||
|
||||
return urls[platform as keyof typeof urls] || urls.tiktok;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟API调用 - 核心功能
|
||||
*/
|
||||
async simulateApiCall(request: ApiRequest): Promise<ApiResponse> {
|
||||
const { platform, endpoint, method, params } = request;
|
||||
|
||||
// 检查登录状态
|
||||
if (!this.loginStates.get(platform)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `平台 ${platform} 未登录`,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
const page = this.activePages.get(platform);
|
||||
if (!page) {
|
||||
return {
|
||||
success: false,
|
||||
error: `平台 ${platform} 的页面未找到`,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 根据API端点执行不同的操作
|
||||
let result;
|
||||
|
||||
switch (endpoint) {
|
||||
case 'orders':
|
||||
result = await this.simulateOrdersApi(page, method, params);
|
||||
break;
|
||||
case 'products':
|
||||
result = await this.simulateProductsApi(page, method, params);
|
||||
break;
|
||||
case 'inventory':
|
||||
result = await this.simulateInventoryApi(page, method, params);
|
||||
break;
|
||||
default:
|
||||
result = {
|
||||
success: false,
|
||||
error: `不支持的API端点: ${endpoint}`,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `API调用失败: ${error}`,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟订单API
|
||||
*/
|
||||
private async simulateOrdersApi(page: Page, method: string, params: any): Promise<ApiResponse> {
|
||||
try {
|
||||
// 导航到订单页面
|
||||
await page.goto('https://seller.tiktok.com/order/list', { waitUntil: 'networkidle' });
|
||||
|
||||
// 根据参数筛选订单
|
||||
if (params.status) {
|
||||
// 选择订单状态筛选
|
||||
await page.selectOption('select[name="status"]', params.status);
|
||||
}
|
||||
|
||||
if (params.startDate && params.endDate) {
|
||||
// 设置日期范围
|
||||
await page.fill('input[name="startDate"]', params.startDate);
|
||||
await page.fill('input[name="endDate"]', params.endDate);
|
||||
}
|
||||
|
||||
// 点击搜索按钮
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 抓取订单数据
|
||||
const orders = await page.$$eval('.order-item', (items) => {
|
||||
return items.map(item => {
|
||||
const orderId = item.querySelector('.order-id')?.textContent?.trim();
|
||||
const status = item.querySelector('.order-status')?.textContent?.trim();
|
||||
const amount = item.querySelector('.order-amount')?.textContent?.trim();
|
||||
|
||||
return {
|
||||
id: orderId,
|
||||
status,
|
||||
amount,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
orders,
|
||||
total: orders.length,
|
||||
page: params.page || 1
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `订单API调用失败: ${error}`,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟商品API
|
||||
*/
|
||||
private async simulateProductsApi(page: Page, method: string, params: any): Promise<ApiResponse> {
|
||||
// 类似订单API的实现
|
||||
return {
|
||||
success: true,
|
||||
data: { products: [], total: 0 },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟库存API
|
||||
*/
|
||||
private async simulateInventoryApi(page: Page, method: string, params: any): Promise<ApiResponse> {
|
||||
// 类似订单API的实现
|
||||
return {
|
||||
success: true,
|
||||
data: { inventory: [] },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录状态
|
||||
*/
|
||||
isLoggedIn(platform: string): boolean {
|
||||
return this.loginStates.get(platform) || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的平台列表
|
||||
*/
|
||||
getSupportedPlatforms(): string[] {
|
||||
return ['tiktok', 'shopee', 'amazon'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭浏览器
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
if (this.browser) {
|
||||
await this.browser.close();
|
||||
this.browser = null;
|
||||
this.context = null;
|
||||
this.activePages.clear();
|
||||
this.loginStates.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const apiSimulator = new ApiSimulator({
|
||||
headless: process.env.HEADLESS !== 'false',
|
||||
userDataDir: process.env.USER_DATA_DIR || './user-data',
|
||||
timeout: 30000
|
||||
});
|
||||
473
node-agent/src/client-manager.ts
Normal file
473
node-agent/src/client-manager.ts
Normal file
@@ -0,0 +1,473 @@
|
||||
/**
|
||||
* 多客户端并行控制和管理系统
|
||||
* 支持同时管理多个客户端实例,实现负载均衡和故障转移
|
||||
*/
|
||||
|
||||
import { EnhancedClient, EnhancedClientConfig } from './enhanced-client';
|
||||
import { ApiResponse } from './api-simulator';
|
||||
|
||||
interface ClientInstance {
|
||||
id: string;
|
||||
name: string;
|
||||
config: EnhancedClientConfig;
|
||||
client: EnhancedClient;
|
||||
status: 'online' | 'offline' | 'error';
|
||||
lastHeartbeat: Date;
|
||||
load: number; // 当前负载 0-100
|
||||
capabilities: string[]; // 支持的功能
|
||||
tags: string[]; // 标签,用于分组和筛选
|
||||
}
|
||||
|
||||
interface ClientGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
clientIds: string[];
|
||||
strategy: 'round-robin' | 'load-balance' | 'failover';
|
||||
}
|
||||
|
||||
interface LoadBalanceRequest {
|
||||
type: 'login' | 'api' | 'task' | 'workflow';
|
||||
platform?: string;
|
||||
action: string;
|
||||
data: any;
|
||||
priority?: 'high' | 'normal' | 'low';
|
||||
requireTags?: string[]; // 要求的客户端标签
|
||||
excludeTags?: string[]; // 排除的客户端标签
|
||||
}
|
||||
|
||||
interface LoadBalanceResult {
|
||||
clientId: string;
|
||||
response: ApiResponse;
|
||||
executionTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端管理器 - 管理多个客户端实例
|
||||
*/
|
||||
export class ClientManager {
|
||||
private clients: Map<string, ClientInstance> = new Map();
|
||||
private groups: Map<string, ClientGroup> = new Map();
|
||||
private roundRobinIndex: Map<string, number> = new Map();
|
||||
|
||||
/**
|
||||
* 注册客户端实例
|
||||
*/
|
||||
async registerClient(
|
||||
name: string,
|
||||
config: EnhancedClientConfig,
|
||||
tags: string[] = []
|
||||
): Promise<string> {
|
||||
const clientId = this.generateId();
|
||||
|
||||
try {
|
||||
const client = new EnhancedClient(config);
|
||||
await client.start();
|
||||
|
||||
const instance: ClientInstance = {
|
||||
id: clientId,
|
||||
name,
|
||||
config,
|
||||
client,
|
||||
status: 'online',
|
||||
lastHeartbeat: new Date(),
|
||||
load: 0,
|
||||
capabilities: this.detectCapabilities(config),
|
||||
tags
|
||||
};
|
||||
|
||||
this.clients.set(clientId, instance);
|
||||
|
||||
// 启动心跳检测
|
||||
this.startHeartbeat(clientId);
|
||||
|
||||
console.log(`客户端注册成功: ${name} (${clientId})`);
|
||||
return clientId;
|
||||
} catch (error) {
|
||||
console.error(`客户端注册失败: ${name}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测客户端能力
|
||||
*/
|
||||
private detectCapabilities(config: EnhancedClientConfig): string[] {
|
||||
const capabilities: string[] = ['api-simulation', 'task-scheduling'];
|
||||
|
||||
if (config.headless === false) {
|
||||
capabilities.push('remote-desktop');
|
||||
}
|
||||
|
||||
if (config.proxy) {
|
||||
capabilities.push('proxy-support');
|
||||
}
|
||||
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动心跳检测
|
||||
*/
|
||||
private startHeartbeat(clientId: string): void {
|
||||
const interval = setInterval(async () => {
|
||||
const instance = this.clients.get(clientId);
|
||||
if (!instance) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 模拟心跳检测
|
||||
instance.lastHeartbeat = new Date();
|
||||
|
||||
// 更新负载状态
|
||||
instance.load = Math.floor(Math.random() * 100); // 模拟负载
|
||||
|
||||
// 如果负载过高,标记为错误状态
|
||||
if (instance.load > 90) {
|
||||
instance.status = 'error';
|
||||
} else {
|
||||
instance.status = 'online';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
instance.status = 'offline';
|
||||
console.error(`客户端心跳检测失败: ${clientId}`, error);
|
||||
}
|
||||
}, 30000); // 每30秒检测一次
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建客户端组
|
||||
*/
|
||||
createGroup(name: string, strategy: ClientGroup['strategy'], clientIds: string[]): string {
|
||||
const groupId = this.generateId();
|
||||
|
||||
const group: ClientGroup = {
|
||||
id: groupId,
|
||||
name,
|
||||
clientIds: clientIds.filter(id => this.clients.has(id)),
|
||||
strategy
|
||||
};
|
||||
|
||||
this.groups.set(groupId, group);
|
||||
this.roundRobinIndex.set(groupId, 0);
|
||||
|
||||
console.log(`客户端组创建成功: ${name} (${groupId})`);
|
||||
return groupId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 负载均衡分发请求
|
||||
*/
|
||||
async distributeRequest(groupId: string, request: LoadBalanceRequest): Promise<LoadBalanceResult> {
|
||||
const group = this.groups.get(groupId);
|
||||
if (!group) {
|
||||
throw new Error(`客户端组不存在: ${groupId}`);
|
||||
}
|
||||
|
||||
const availableClients = this.getAvailableClients(group.clientIds, request);
|
||||
|
||||
if (availableClients.length === 0) {
|
||||
throw new Error('没有可用的客户端');
|
||||
}
|
||||
|
||||
const selectedClient = this.selectClient(group.strategy, availableClients, request);
|
||||
|
||||
const startTime = Date.now();
|
||||
const response = await this.executeRequest(selectedClient.id, request);
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
clientId: selectedClient.id,
|
||||
response,
|
||||
executionTime
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的客户端列表
|
||||
*/
|
||||
private getAvailableClients(clientIds: string[], request: LoadBalanceRequest): ClientInstance[] {
|
||||
return clientIds
|
||||
.map(id => this.clients.get(id))
|
||||
.filter((instance): instance is ClientInstance => {
|
||||
if (!instance) return false;
|
||||
|
||||
// 检查状态
|
||||
if (instance.status !== 'online') return false;
|
||||
|
||||
// 检查标签要求
|
||||
if (request.requireTags && !request.requireTags.every(tag => instance.tags.includes(tag))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查排除标签
|
||||
if (request.excludeTags && request.excludeTags.some(tag => instance.tags.includes(tag))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查能力要求
|
||||
if (request.type === 'api' && !instance.capabilities.includes('api-simulation')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择客户端(负载均衡策略)
|
||||
*/
|
||||
private selectClient(
|
||||
strategy: ClientGroup['strategy'],
|
||||
clients: ClientInstance[],
|
||||
request: LoadBalanceRequest
|
||||
): ClientInstance {
|
||||
switch (strategy) {
|
||||
case 'round-robin':
|
||||
return this.roundRobinSelect(clients);
|
||||
|
||||
case 'load-balance':
|
||||
return this.loadBalanceSelect(clients);
|
||||
|
||||
case 'failover':
|
||||
return this.failoverSelect(clients, request);
|
||||
|
||||
default:
|
||||
return clients[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询选择
|
||||
*/
|
||||
private roundRobinSelect(clients: ClientInstance[]): ClientInstance {
|
||||
const groupId = Array.from(this.groups.entries())
|
||||
.find(([_, group]) => group.clientIds.some(id => clients.some(c => c.id === id)))?.[0];
|
||||
|
||||
if (!groupId) {
|
||||
return clients[0];
|
||||
}
|
||||
|
||||
const currentIndex = this.roundRobinIndex.get(groupId) || 0;
|
||||
const selectedIndex = currentIndex % clients.length;
|
||||
|
||||
this.roundRobinIndex.set(groupId, currentIndex + 1);
|
||||
|
||||
return clients[selectedIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* 负载均衡选择(选择负载最低的)
|
||||
*/
|
||||
private loadBalanceSelect(clients: ClientInstance[]): ClientInstance {
|
||||
return clients.reduce((prev, current) =>
|
||||
prev.load < current.load ? prev : current
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 故障转移选择
|
||||
*/
|
||||
private failoverSelect(clients: ClientInstance[], request: LoadBalanceRequest): ClientInstance {
|
||||
// 根据优先级和标签选择
|
||||
const prioritized = clients.sort((a, b) => {
|
||||
// 优先选择负载低的
|
||||
if (a.load !== b.load) {
|
||||
return a.load - b.load;
|
||||
}
|
||||
|
||||
// 优先选择有特定标签的
|
||||
if (request.requireTags) {
|
||||
const aTags = a.tags.filter(tag => request.requireTags!.includes(tag)).length;
|
||||
const bTags = b.tags.filter(tag => request.requireTags!.includes(tag)).length;
|
||||
|
||||
if (aTags !== bTags) {
|
||||
return bTags - aTags;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return prioritized[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行请求
|
||||
*/
|
||||
private async executeRequest(clientId: string, request: LoadBalanceRequest): Promise<ApiResponse> {
|
||||
const instance = this.clients.get(clientId);
|
||||
if (!instance) {
|
||||
throw new Error(`客户端不存在: ${clientId}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// 这里应该调用EnhancedClient的相应方法
|
||||
// 暂时返回模拟响应
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `请求由客户端 ${instance.name} 处理完成`,
|
||||
clientId,
|
||||
request
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '请求执行失败',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量执行请求(并行处理)
|
||||
*/
|
||||
async batchExecuteRequests(
|
||||
groupId: string,
|
||||
requests: LoadBalanceRequest[]
|
||||
): Promise<LoadBalanceResult[]> {
|
||||
const group = this.groups.get(groupId);
|
||||
if (!group) {
|
||||
throw new Error(`客户端组不存在: ${groupId}`);
|
||||
}
|
||||
|
||||
const availableClients = this.getAvailableClients(group.clientIds, {} as LoadBalanceRequest);
|
||||
|
||||
if (availableClients.length === 0) {
|
||||
throw new Error('没有可用的客户端');
|
||||
}
|
||||
|
||||
// 将请求分配给不同的客户端
|
||||
const assignments = this.assignRequestsToClients(requests, availableClients, group.strategy);
|
||||
|
||||
// 并行执行所有请求
|
||||
const promises = assignments.map(async (assignment) => {
|
||||
const startTime = Date.now();
|
||||
const response = await this.executeRequest(assignment.clientId, assignment.request);
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
clientId: assignment.clientId,
|
||||
response,
|
||||
executionTime
|
||||
};
|
||||
});
|
||||
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将请求分配给客户端
|
||||
*/
|
||||
private assignRequestsToClients(
|
||||
requests: LoadBalanceRequest[],
|
||||
clients: ClientInstance[],
|
||||
strategy: ClientGroup['strategy']
|
||||
): Array<{clientId: string, request: LoadBalanceRequest}> {
|
||||
const assignments: Array<{clientId: string, request: LoadBalanceRequest}> = [];
|
||||
|
||||
if (strategy === 'round-robin') {
|
||||
let clientIndex = 0;
|
||||
|
||||
for (const request of requests) {
|
||||
const client = clients[clientIndex % clients.length];
|
||||
assignments.push({ clientId: client.id, request });
|
||||
clientIndex++;
|
||||
}
|
||||
} else {
|
||||
// 负载均衡策略:根据请求类型和客户端负载分配
|
||||
const clientLoads = new Map(clients.map(client => [client.id, { client, load: client.load }]));
|
||||
|
||||
for (const request of requests) {
|
||||
// 选择负载最低的客户端
|
||||
const selectedClient = Array.from(clientLoads.values())
|
||||
.reduce((prev, current) => prev.load < current.load ? prev : current)
|
||||
.client;
|
||||
|
||||
assignments.push({ clientId: selectedClient.id, request });
|
||||
|
||||
// 更新负载(模拟)
|
||||
const currentLoad = clientLoads.get(selectedClient.id)!;
|
||||
clientLoads.set(selectedClient.id, {
|
||||
...currentLoad,
|
||||
load: currentLoad.load + 10 // 每次请求增加10%负载
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return assignments;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端状态
|
||||
*/
|
||||
getClientStatus(clientId: string): ClientInstance | undefined {
|
||||
return this.clients.get(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有客户端状态
|
||||
*/
|
||||
getAllClientsStatus(): ClientInstance[] {
|
||||
return Array.from(this.clients.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组状态
|
||||
*/
|
||||
getGroupStatus(groupId: string): ClientGroup | undefined {
|
||||
return this.groups.get(groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除客户端
|
||||
*/
|
||||
async removeClient(clientId: string): Promise<void> {
|
||||
const instance = this.clients.get(clientId);
|
||||
if (instance) {
|
||||
await instance.client.stop();
|
||||
this.clients.delete(clientId);
|
||||
|
||||
// 从所有组中移除
|
||||
for (const group of this.groups.values()) {
|
||||
group.clientIds = group.clientIds.filter(id => id !== clientId);
|
||||
}
|
||||
|
||||
console.log(`客户端已移除: ${clientId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止所有客户端
|
||||
*/
|
||||
async stopAllClients(): Promise<void> {
|
||||
for (const instance of this.clients.values()) {
|
||||
try {
|
||||
await instance.client.stop();
|
||||
} catch (error) {
|
||||
console.error(`停止客户端失败: ${instance.id}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
this.clients.clear();
|
||||
this.groups.clear();
|
||||
this.roundRobinIndex.clear();
|
||||
|
||||
console.log('所有客户端已停止');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一ID
|
||||
*/
|
||||
private generateId(): string {
|
||||
return `client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const clientManager = new ClientManager();
|
||||
202
node-agent/src/cloud-control-layer.ts
Normal file
202
node-agent/src/cloud-control-layer.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* 云服务器控制四层架构统一接口定义
|
||||
* 根据文档 ARCHIVE/01_Architecture/12_Operation_Agent.md 规范实现
|
||||
*
|
||||
* 四层架构:
|
||||
* 1. API模拟层 - 主要方案
|
||||
* 2. WEB远程控制层 - 中间层
|
||||
* 3. 远程桌面层 - 第一层兜底
|
||||
* 4. 云服务商API层 - 第二层兜底
|
||||
*/
|
||||
|
||||
// 统一控制命令接口
|
||||
export interface ControlCommand {
|
||||
type: 'api' | 'web' | 'desktop' | 'cloud';
|
||||
target: string;
|
||||
action: string;
|
||||
parameters: Record<string, unknown>;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
// 统一控制结果接口
|
||||
export interface ControlResult {
|
||||
success: boolean;
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
layerUsed: string;
|
||||
duration: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
// 层级状态接口
|
||||
export interface LayerStatus {
|
||||
layer: string;
|
||||
available: boolean;
|
||||
latency: number;
|
||||
lastUsed: number;
|
||||
errorCount: number;
|
||||
successRate: number;
|
||||
}
|
||||
|
||||
// 四层架构统一接口
|
||||
export interface ICloudControlLayer {
|
||||
/**
|
||||
* 连接控制层
|
||||
*/
|
||||
connect(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 执行控制命令
|
||||
*/
|
||||
execute(command: ControlCommand): Promise<ControlResult>;
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
disconnect(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取层级状态
|
||||
*/
|
||||
getStatus(): Promise<LayerStatus>;
|
||||
|
||||
/**
|
||||
* 检查是否可用
|
||||
*/
|
||||
isAvailable(): boolean;
|
||||
}
|
||||
|
||||
// 四层架构管理器
|
||||
export class CloudControlLayerManager {
|
||||
private layers: Map<string, ICloudControlLayer> = new Map();
|
||||
private currentLayer: string = 'api';
|
||||
|
||||
/**
|
||||
* 注册控制层
|
||||
*/
|
||||
registerLayer(name: string, layer: ICloudControlLayer): void {
|
||||
this.layers.set(name, layer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令(自动故障转移)
|
||||
*/
|
||||
async execute(command: ControlCommand): Promise<ControlResult> {
|
||||
const layers = this.getAvailableLayers();
|
||||
|
||||
for (const layerName of layers) {
|
||||
const layer = this.layers.get(layerName);
|
||||
if (!layer || !layer.isAvailable()) continue;
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const result = await layer.execute(command);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
...result,
|
||||
layerUsed: layerName,
|
||||
duration
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(`Layer ${layerName} failed:`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('All control layers failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用层级列表(按优先级排序)
|
||||
*/
|
||||
private getAvailableLayers(): string[] {
|
||||
const priorityOrder = ['api', 'web', 'desktop', 'cloud'];
|
||||
return priorityOrder.filter(layerName => {
|
||||
const layer = this.layers.get(layerName);
|
||||
return layer && layer.isAvailable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有层级状态
|
||||
*/
|
||||
async getAllStatus(): Promise<LayerStatus[]> {
|
||||
const statuses: LayerStatus[] = [];
|
||||
|
||||
for (const [layerName, layer] of this.layers) {
|
||||
try {
|
||||
const status = await layer.getStatus();
|
||||
statuses.push(status);
|
||||
} catch (error) {
|
||||
statuses.push({
|
||||
layer: layerName,
|
||||
available: false,
|
||||
latency: -1,
|
||||
lastUsed: 0,
|
||||
errorCount: 1,
|
||||
successRate: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return statuses;
|
||||
}
|
||||
}
|
||||
|
||||
// 具体层级实现基类
|
||||
export abstract class BaseControlLayer implements ICloudControlLayer {
|
||||
protected connected: boolean = false;
|
||||
protected errorCount: number = 0;
|
||||
protected successCount: number = 0;
|
||||
protected lastUsed: number = 0;
|
||||
|
||||
abstract connect(): Promise<boolean>;
|
||||
abstract execute(command: ControlCommand): Promise<ControlResult>;
|
||||
abstract disconnect(): Promise<void>;
|
||||
|
||||
async getStatus(): Promise<LayerStatus> {
|
||||
const total = this.errorCount + this.successCount;
|
||||
const successRate = total > 0 ? this.successCount / total : 0;
|
||||
|
||||
return {
|
||||
layer: this.constructor.name,
|
||||
available: this.isAvailable(),
|
||||
latency: await this.getLatency(),
|
||||
lastUsed: this.lastUsed,
|
||||
errorCount: this.errorCount,
|
||||
successRate
|
||||
};
|
||||
}
|
||||
|
||||
isAvailable(): boolean {
|
||||
return this.connected && this.errorCount < 10;
|
||||
}
|
||||
|
||||
protected async getLatency(): Promise<number> {
|
||||
// 默认实现,子类可以重写
|
||||
return 100;
|
||||
}
|
||||
|
||||
protected recordSuccess(): void {
|
||||
this.successCount++;
|
||||
this.lastUsed = Date.now();
|
||||
}
|
||||
|
||||
protected recordError(): void {
|
||||
this.errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 错误处理类
|
||||
export class CloudControlError extends Error {
|
||||
constructor(
|
||||
public layer: string,
|
||||
public operation: string,
|
||||
public originalError?: Error
|
||||
) {
|
||||
super(`[${layer}] ${operation} failed`);
|
||||
this.name = 'CloudControlError';
|
||||
}
|
||||
}
|
||||
564
node-agent/src/enhanced-client.ts
Normal file
564
node-agent/src/enhanced-client.ts
Normal file
@@ -0,0 +1,564 @@
|
||||
/**
|
||||
* 增强版轻量级客户端 - 集成所有功能
|
||||
* 支持模拟API、自动化任务、定时操作等完整功能
|
||||
*/
|
||||
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import { createServer } from 'http';
|
||||
import * as os from 'os';
|
||||
import { ApiSimulator, LoginCredentials, ApiRequest } from './api-simulator';
|
||||
import { PlatformManager, PlatformAdapter } from './platform-adapter';
|
||||
import { TaskScheduler, TaskConfig, WorkflowConfig } from './task-scheduler';
|
||||
|
||||
interface EnhancedClientConfig {
|
||||
port: number;
|
||||
headless: boolean;
|
||||
proxy?: string;
|
||||
userDataDir: string;
|
||||
}
|
||||
|
||||
interface EnhancedCommand {
|
||||
type: 'login' | 'api' | 'task' | 'workflow' | 'status' | 'control';
|
||||
platform?: string;
|
||||
action: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
interface EnhancedResponse {
|
||||
success: boolean;
|
||||
type: string;
|
||||
data?: any;
|
||||
error?: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增强版轻量级客户端
|
||||
*/
|
||||
export class EnhancedClient {
|
||||
private config: EnhancedClientConfig;
|
||||
private wss: WebSocketServer;
|
||||
private apiSimulator: ApiSimulator;
|
||||
private platformManager: PlatformManager;
|
||||
private taskScheduler: TaskScheduler;
|
||||
private connectedClients: Set<WebSocket> = new Set();
|
||||
|
||||
constructor(config: EnhancedClientConfig) {
|
||||
this.config = config;
|
||||
|
||||
// 初始化核心组件
|
||||
this.apiSimulator = new ApiSimulator({
|
||||
headless: config.headless,
|
||||
proxy: config.proxy,
|
||||
userDataDir: config.userDataDir,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
this.platformManager = new PlatformManager(this.apiSimulator);
|
||||
this.taskScheduler = new TaskScheduler(this.platformManager);
|
||||
|
||||
// 创建HTTP服务器用于健康检查和API接口
|
||||
const server = createServer((req, res) => {
|
||||
this.handleHttpRequest(req, res);
|
||||
});
|
||||
|
||||
// 创建WebSocket服务器
|
||||
this.wss = new WebSocketServer({ server });
|
||||
server.listen(config.port);
|
||||
|
||||
this.setupWebSocketHandlers();
|
||||
this.initializeComponents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化组件
|
||||
*/
|
||||
private async initializeComponents(): Promise<void> {
|
||||
try {
|
||||
await this.apiSimulator.initialize();
|
||||
console.log('API模拟器初始化完成');
|
||||
|
||||
// 加载已保存的登录状态
|
||||
await this.loadSavedLoginStates();
|
||||
|
||||
console.log('增强版客户端初始化完成');
|
||||
} catch (error) {
|
||||
console.error('客户端初始化失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载已保存的登录状态
|
||||
*/
|
||||
private async loadSavedLoginStates(): Promise<void> {
|
||||
// 这里可以加载之前保存的登录状态
|
||||
console.log('加载已保存的登录状态...');
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理HTTP请求
|
||||
*/
|
||||
private handleHttpRequest(req: any, res: any): void {
|
||||
const { url, method } = req;
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
|
||||
if (method === 'OPTIONS') {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (url === '/health') {
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify(this.getHealthStatus()));
|
||||
} else if (url === '/status' && method === 'GET') {
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify(this.getClientStatus()));
|
||||
} else if (url === '/tasks' && method === 'GET') {
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify(this.taskScheduler.getTasks()));
|
||||
} else if (url === '/platforms' && method === 'GET') {
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify({
|
||||
supported: this.platformManager.getSupportedPlatforms(),
|
||||
loggedIn: this.platformManager.getLoggedInPlatforms()
|
||||
}));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end(JSON.stringify({ error: '接口不存在' }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置WebSocket处理器
|
||||
*/
|
||||
private setupWebSocketHandlers(): void {
|
||||
this.wss.on('connection', (ws: WebSocket) => {
|
||||
console.log('客户端连接建立');
|
||||
this.connectedClients.add(ws);
|
||||
|
||||
// 发送连接成功消息
|
||||
ws.send(JSON.stringify({
|
||||
type: 'connection',
|
||||
success: true,
|
||||
message: '连接成功',
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
|
||||
ws.on('message', async (data: Buffer) => {
|
||||
try {
|
||||
const command: EnhancedCommand = JSON.parse(data.toString());
|
||||
const response = await this.handleEnhancedCommand(command);
|
||||
ws.send(JSON.stringify(response));
|
||||
} catch (error) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'error',
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '命令处理失败',
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log('客户端连接关闭');
|
||||
this.connectedClients.delete(ws);
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
console.error('WebSocket错误:', error);
|
||||
this.connectedClients.delete(ws);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理增强命令
|
||||
*/
|
||||
private async handleEnhancedCommand(command: EnhancedCommand): Promise<EnhancedResponse> {
|
||||
try {
|
||||
switch (command.type) {
|
||||
case 'login':
|
||||
return await this.handleLoginCommand(command);
|
||||
case 'api':
|
||||
return await this.handleApiCommand(command);
|
||||
case 'task':
|
||||
return await this.handleTaskCommand(command);
|
||||
case 'workflow':
|
||||
return await this.handleWorkflowCommand(command);
|
||||
case 'status':
|
||||
return await this.handleStatusCommand(command);
|
||||
case 'control':
|
||||
return await this.handleControlCommand(command);
|
||||
default:
|
||||
return {
|
||||
type: 'error',
|
||||
success: false,
|
||||
error: '未知命令类型',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'error',
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '命令执行失败',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录命令
|
||||
*/
|
||||
private async handleLoginCommand(command: EnhancedCommand): Promise<EnhancedResponse> {
|
||||
const { platform, username, password } = command.data;
|
||||
|
||||
if (!platform || !username || !password) {
|
||||
return {
|
||||
type: 'login',
|
||||
success: false,
|
||||
error: '缺少必要的登录参数',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
const credentials: LoginCredentials = {
|
||||
platform,
|
||||
username,
|
||||
password
|
||||
};
|
||||
|
||||
const result = await this.apiSimulator.login(credentials);
|
||||
|
||||
return {
|
||||
type: 'login',
|
||||
success: result.success,
|
||||
data: result.data,
|
||||
error: result.error,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理API命令
|
||||
*/
|
||||
private async handleApiCommand(command: EnhancedCommand): Promise<EnhancedResponse> {
|
||||
const { platform, endpoint, method, params } = command.data;
|
||||
|
||||
if (!platform || !endpoint) {
|
||||
return {
|
||||
type: 'api',
|
||||
success: false,
|
||||
error: '缺少必要的API参数',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
const apiRequest: ApiRequest = {
|
||||
platform,
|
||||
endpoint,
|
||||
method: method || 'GET',
|
||||
params: params || {}
|
||||
};
|
||||
|
||||
const result = await this.apiSimulator.simulateApiCall(apiRequest);
|
||||
|
||||
return {
|
||||
type: 'api',
|
||||
success: result.success,
|
||||
data: result.data,
|
||||
error: result.error,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理任务命令
|
||||
*/
|
||||
private async handleTaskCommand(command: EnhancedCommand): Promise<EnhancedResponse> {
|
||||
const { action, data } = command;
|
||||
|
||||
switch (action) {
|
||||
case 'list':
|
||||
return {
|
||||
type: 'task',
|
||||
success: true,
|
||||
data: this.taskScheduler.getTasks(),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
case 'add':
|
||||
this.taskScheduler.addTask(data as TaskConfig);
|
||||
return {
|
||||
type: 'task',
|
||||
success: true,
|
||||
data: { message: '任务添加成功' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
case 'execute':
|
||||
const execution = await this.taskScheduler.executeTask(data.taskId, true);
|
||||
return {
|
||||
type: 'task',
|
||||
success: execution.status === 'completed',
|
||||
data: execution,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
case 'enable':
|
||||
this.taskScheduler.setTaskEnabled(data.taskId, true);
|
||||
return {
|
||||
type: 'task',
|
||||
success: true,
|
||||
data: { message: '任务已启用' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
case 'disable':
|
||||
this.taskScheduler.setTaskEnabled(data.taskId, false);
|
||||
return {
|
||||
type: 'task',
|
||||
success: true,
|
||||
data: { message: '任务已禁用' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
type: 'task',
|
||||
success: false,
|
||||
error: '未知的任务操作',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理工作流命令
|
||||
*/
|
||||
private async handleWorkflowCommand(command: EnhancedCommand): Promise<EnhancedResponse> {
|
||||
const { action, data } = command;
|
||||
|
||||
switch (action) {
|
||||
case 'add':
|
||||
this.taskScheduler.addWorkflow(data as WorkflowConfig);
|
||||
return {
|
||||
type: 'workflow',
|
||||
success: true,
|
||||
data: { message: '工作流添加成功' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
type: 'workflow',
|
||||
success: false,
|
||||
error: '未知的工作流操作',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理状态命令
|
||||
*/
|
||||
private async handleStatusCommand(command: EnhancedCommand): Promise<EnhancedResponse> {
|
||||
const { action } = command;
|
||||
|
||||
switch (action) {
|
||||
case 'overview':
|
||||
return {
|
||||
type: 'status',
|
||||
success: true,
|
||||
data: this.getClientStatus(),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
case 'platforms':
|
||||
return {
|
||||
type: 'status',
|
||||
success: true,
|
||||
data: {
|
||||
supported: this.platformManager.getSupportedPlatforms(),
|
||||
loggedIn: this.platformManager.getLoggedInPlatforms()
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
case 'tasks':
|
||||
return {
|
||||
type: 'status',
|
||||
success: true,
|
||||
data: {
|
||||
tasks: this.taskScheduler.getTasks(),
|
||||
history: Array.from(this.taskScheduler.getAllHistory().entries())
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
type: 'status',
|
||||
success: false,
|
||||
error: '未知的状态操作',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理控制命令
|
||||
*/
|
||||
private async handleControlCommand(command: EnhancedCommand): Promise<EnhancedResponse> {
|
||||
const { action } = command;
|
||||
|
||||
switch (action) {
|
||||
case 'restart':
|
||||
// 重启客户端逻辑
|
||||
return {
|
||||
type: 'control',
|
||||
success: true,
|
||||
data: { message: '客户端重启中...' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
case 'stop':
|
||||
// 停止客户端逻辑
|
||||
return {
|
||||
type: 'control',
|
||||
success: true,
|
||||
data: { message: '客户端停止中...' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
type: 'control',
|
||||
success: false,
|
||||
error: '未知的控制操作',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取健康状态
|
||||
*/
|
||||
private getHealthStatus(): any {
|
||||
return {
|
||||
status: 'healthy',
|
||||
platform: os.platform(),
|
||||
version: '2.0.0',
|
||||
uptime: process.uptime(),
|
||||
memory: process.memoryUsage(),
|
||||
connectedClients: this.connectedClients.size,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端状态
|
||||
*/
|
||||
private getClientStatus(): any {
|
||||
return {
|
||||
platform: os.platform(),
|
||||
version: '2.0.0',
|
||||
uptime: process.uptime(),
|
||||
|
||||
// 平台状态
|
||||
platforms: {
|
||||
supported: this.platformManager.getSupportedPlatforms(),
|
||||
loggedIn: this.platformManager.getLoggedInPlatforms(),
|
||||
total: this.platformManager.getSupportedPlatforms().length
|
||||
},
|
||||
|
||||
// 任务状态
|
||||
tasks: {
|
||||
total: this.taskScheduler.getTasks().length,
|
||||
enabled: this.taskScheduler.getTasks().filter(t => t.enabled).length,
|
||||
disabled: this.taskScheduler.getTasks().filter(t => !t.enabled).length
|
||||
},
|
||||
|
||||
// 连接状态
|
||||
connections: {
|
||||
webSocket: this.connectedClients.size,
|
||||
http: 'active'
|
||||
},
|
||||
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播消息给所有连接的客户端
|
||||
*/
|
||||
private broadcastMessage(message: any): void {
|
||||
const messageStr = JSON.stringify(message);
|
||||
|
||||
this.connectedClients.forEach(client => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(messageStr);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动客户端
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
console.log(`增强版客户端启动在端口 ${this.config.port}`);
|
||||
console.log(`健康检查: http://localhost:${this.config.port}/health`);
|
||||
console.log(`状态查询: http://localhost:${this.config.port}/status`);
|
||||
console.log('等待前端连接...');
|
||||
|
||||
// 启动定时状态报告
|
||||
setInterval(() => {
|
||||
this.broadcastMessage({
|
||||
type: 'heartbeat',
|
||||
data: this.getClientStatus(),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}, 30000); // 每30秒发送一次心跳
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止客户端
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
// 停止所有任务
|
||||
this.taskScheduler.stopAllTasks();
|
||||
|
||||
// 关闭浏览器实例
|
||||
await this.apiSimulator.close();
|
||||
|
||||
// 关闭WebSocket服务器
|
||||
this.wss.close();
|
||||
|
||||
console.log('增强版客户端已停止');
|
||||
}
|
||||
}
|
||||
|
||||
// 命令行启动
|
||||
if (require.main === module) {
|
||||
const config: EnhancedClientConfig = {
|
||||
port: process.env.PORT ? parseInt(process.env.PORT) : 8080,
|
||||
headless: process.env.HEADLESS !== 'false',
|
||||
proxy: process.env.PROXY || undefined,
|
||||
userDataDir: process.env.USER_DATA_DIR || './user-data'
|
||||
};
|
||||
|
||||
const client = new EnhancedClient(config);
|
||||
client.start();
|
||||
|
||||
// 优雅关闭
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('收到关闭信号,正在停止客户端...');
|
||||
await client.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
224
node-agent/src/lightweight-client.ts
Normal file
224
node-agent/src/lightweight-client.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* 轻量级Operation-Agent客户端
|
||||
* 支持Windows/Linux,可打包为单个exe文件
|
||||
* 通过WebSocket与前端通信,执行浏览器自动化操作
|
||||
*/
|
||||
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import { chromium, Browser, Page } from 'playwright';
|
||||
import { createServer } from 'http';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
interface ClientConfig {
|
||||
port: number;
|
||||
headless: boolean;
|
||||
proxy?: string;
|
||||
}
|
||||
|
||||
interface Command {
|
||||
type: 'authorize' | 'getProducts' | 'updatePrice' | 'getOrders';
|
||||
platform: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
interface Response {
|
||||
success: boolean;
|
||||
data?: any;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class LightweightClient {
|
||||
private config: ClientConfig;
|
||||
private wss: WebSocketServer;
|
||||
private browser: Browser | null = null;
|
||||
private activePages: Map<string, Page> = new Map();
|
||||
|
||||
constructor(config: ClientConfig) {
|
||||
this.config = config;
|
||||
|
||||
// 创建HTTP服务器用于健康检查
|
||||
const server = createServer((req, res) => {
|
||||
if (req.url === '/health') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
status: 'ok',
|
||||
platform: os.platform(),
|
||||
version: '1.0.0'
|
||||
}));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
// 创建WebSocket服务器
|
||||
this.wss = new WebSocketServer({ server });
|
||||
server.listen(config.port);
|
||||
|
||||
this.setupWebSocketHandlers();
|
||||
}
|
||||
|
||||
private setupWebSocketHandlers() {
|
||||
this.wss.on('connection', (ws: WebSocket) => {
|
||||
console.log('客户端连接建立');
|
||||
|
||||
ws.on('message', async (data: Buffer) => {
|
||||
try {
|
||||
const command: Command = JSON.parse(data.toString());
|
||||
const response = await this.handleCommand(command);
|
||||
ws.send(JSON.stringify(response));
|
||||
} catch (error) {
|
||||
ws.send(JSON.stringify({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '未知错误'
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log('客户端连接关闭');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async handleCommand(command: Command): Promise<Response> {
|
||||
try {
|
||||
switch (command.type) {
|
||||
case 'authorize':
|
||||
return await this.authorizePlatform(command.platform, command.data);
|
||||
case 'getProducts':
|
||||
return await this.getProducts(command.platform, command.data);
|
||||
case 'updatePrice':
|
||||
return await this.updateProductPrice(command.platform, command.data);
|
||||
case 'getOrders':
|
||||
return await this.getOrders(command.platform, command.data);
|
||||
default:
|
||||
return { success: false, error: '未知命令类型' };
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '执行命令失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async authorizePlatform(platform: string, authData: any): Promise<Response> {
|
||||
if (!this.browser) {
|
||||
this.browser = await chromium.launch({
|
||||
headless: this.config.headless,
|
||||
proxy: this.config.proxy ? { server: this.config.proxy } : undefined
|
||||
});
|
||||
}
|
||||
|
||||
const page = await this.browser.newPage();
|
||||
const pageId = `${platform}_${Date.now()}`;
|
||||
this.activePages.set(pageId, page);
|
||||
|
||||
// 根据平台执行授权逻辑
|
||||
const result = await this.performPlatformAuthorization(platform, page, authData);
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
data: { pageId, ...result.data }
|
||||
};
|
||||
}
|
||||
|
||||
private async performPlatformAuthorization(platform: string, page: Page, authData: any) {
|
||||
// 简化的平台授权逻辑
|
||||
switch (platform) {
|
||||
case 'tiktok':
|
||||
await page.goto('https://seller.tiktok.com');
|
||||
// TikTok授权逻辑
|
||||
return { success: true, data: { message: 'TikTok授权成功' } };
|
||||
case 'shopee':
|
||||
await page.goto('https://seller.shopee.com');
|
||||
// Shopee授权逻辑
|
||||
return { success: true, data: { message: 'Shopee授权成功' } };
|
||||
default:
|
||||
return { success: false, error: '不支持的平台' };
|
||||
}
|
||||
}
|
||||
|
||||
private async getProducts(platform: string, data: any): Promise<Response> {
|
||||
const { pageId, limit = 10 } = data;
|
||||
const page = this.activePages.get(pageId);
|
||||
|
||||
if (!page) {
|
||||
return { success: false, error: '页面未找到' };
|
||||
}
|
||||
|
||||
// 模拟获取商品列表
|
||||
const products = [
|
||||
{ id: '1', name: '商品1', price: 100, stock: 50 },
|
||||
{ id: '2', name: '商品2', price: 200, stock: 30 }
|
||||
].slice(0, limit);
|
||||
|
||||
return { success: true, data: { products } };
|
||||
}
|
||||
|
||||
private async updateProductPrice(platform: string, data: any): Promise<Response> {
|
||||
const { pageId, productId, newPrice } = data;
|
||||
const page = this.activePages.get(pageId);
|
||||
|
||||
if (!page) {
|
||||
return { success: false, error: '页面未找到' };
|
||||
}
|
||||
|
||||
// 模拟更新价格操作
|
||||
console.log(`更新商品 ${productId} 价格为 ${newPrice}`);
|
||||
|
||||
return { success: true, data: { message: '价格更新成功' } };
|
||||
}
|
||||
|
||||
private async getOrders(platform: string, data: any): Promise<Response> {
|
||||
const { pageId, limit = 10 } = data;
|
||||
const page = this.activePages.get(pageId);
|
||||
|
||||
if (!page) {
|
||||
return { success: false, error: '页面未找到' };
|
||||
}
|
||||
|
||||
// 模拟获取订单列表
|
||||
const orders = [
|
||||
{ id: '1', orderNumber: 'ORDER001', status: 'pending', amount: 100 },
|
||||
{ id: '2', orderNumber: 'ORDER002', status: 'shipped', amount: 200 }
|
||||
].slice(0, limit);
|
||||
|
||||
return { success: true, data: { orders } };
|
||||
}
|
||||
|
||||
async start() {
|
||||
console.log(`轻量级客户端启动在端口 ${this.config.port}`);
|
||||
console.log(`健康检查: http://localhost:${this.config.port}/health`);
|
||||
console.log('等待前端连接...');
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this.browser) {
|
||||
await this.browser.close();
|
||||
}
|
||||
this.wss.close();
|
||||
console.log('客户端已停止');
|
||||
}
|
||||
}
|
||||
|
||||
// 命令行启动
|
||||
if (require.main === module) {
|
||||
const config: ClientConfig = {
|
||||
port: process.env.PORT ? parseInt(process.env.PORT) : 8080,
|
||||
headless: process.env.HEADLESS !== 'false',
|
||||
proxy: process.env.PROXY || undefined
|
||||
};
|
||||
|
||||
const client = new LightweightClient(config);
|
||||
client.start();
|
||||
|
||||
// 优雅关闭
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('收到关闭信号,正在停止客户端...');
|
||||
await client.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
528
node-agent/src/platform-adapter.ts
Normal file
528
node-agent/src/platform-adapter.ts
Normal file
@@ -0,0 +1,528 @@
|
||||
/**
|
||||
* 平台适配器模式 - 支持不同平台的无API操作
|
||||
* 统一接口,支持TikTok、Shopee、Amazon等平台的自动化操作
|
||||
*/
|
||||
|
||||
import { ApiSimulator, ApiRequest, ApiResponse, LoginCredentials } from './api-simulator';
|
||||
|
||||
interface PlatformAdapter {
|
||||
platform: string;
|
||||
|
||||
// 基础操作
|
||||
login(credentials: LoginCredentials): Promise<ApiResponse>;
|
||||
isLoggedIn(): boolean;
|
||||
|
||||
// 数据获取
|
||||
getOrders(params: Record<string, unknown>): Promise<ApiResponse>;
|
||||
getProducts(params: Record<string, unknown>): Promise<ApiResponse>;
|
||||
getInventory(params: Record<string, unknown>): Promise<ApiResponse>;
|
||||
|
||||
// 数据操作
|
||||
updateProductPrice(productId: string, price: number): Promise<ApiResponse>;
|
||||
updateProductStock(productId: string, stock: number): Promise<ApiResponse>;
|
||||
updateOrderStatus(orderId: string, status: string): Promise<ApiResponse>;
|
||||
|
||||
// 自动化操作
|
||||
syncOrders(): Promise<ApiResponse>;
|
||||
syncProducts(): Promise<ApiResponse>;
|
||||
batchUpdatePrices(updates: Array<{productId: string, price: number}>): Promise<ApiResponse>;
|
||||
|
||||
// 状态监控
|
||||
getStatus(): Promise<{
|
||||
connected: boolean;
|
||||
lastActivity: number;
|
||||
errorCount: number;
|
||||
successRate: number;
|
||||
}>;
|
||||
|
||||
// 错误处理
|
||||
handleError(error: Error): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 抽象平台适配器基类
|
||||
*/
|
||||
abstract class BasePlatformAdapter implements PlatformAdapter {
|
||||
protected simulator: ApiSimulator;
|
||||
public platform: string;
|
||||
|
||||
constructor(platform: string, simulator: ApiSimulator) {
|
||||
this.platform = platform;
|
||||
this.simulator = simulator;
|
||||
}
|
||||
|
||||
async login(credentials: LoginCredentials): Promise<ApiResponse> {
|
||||
return await this.simulator.login(credentials);
|
||||
}
|
||||
|
||||
isLoggedIn(): boolean {
|
||||
return this.simulator.isLoggedIn(this.platform);
|
||||
}
|
||||
|
||||
abstract getOrders(params: any): Promise<ApiResponse>;
|
||||
abstract getProducts(params: any): Promise<ApiResponse>;
|
||||
abstract getInventory(params: any): Promise<ApiResponse>;
|
||||
|
||||
abstract updateProductPrice(productId: string, price: number): Promise<ApiResponse>;
|
||||
abstract updateProductStock(productId: string, stock: number): Promise<ApiResponse>;
|
||||
abstract updateOrderStatus(orderId: string, status: string): Promise<ApiResponse>;
|
||||
|
||||
abstract syncOrders(): Promise<ApiResponse>;
|
||||
abstract syncProducts(): Promise<ApiResponse>;
|
||||
abstract batchUpdatePrices(updates: Array<{productId: string, price: number}>): Promise<ApiResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* TikTok平台适配器
|
||||
*/
|
||||
class TikTokAdapter extends BasePlatformAdapter {
|
||||
constructor(simulator: ApiSimulator) {
|
||||
super('tiktok', simulator);
|
||||
}
|
||||
|
||||
async getOrders(params: any): Promise<ApiResponse> {
|
||||
const request: ApiRequest = {
|
||||
method: 'GET',
|
||||
endpoint: 'orders',
|
||||
params,
|
||||
platform: this.platform
|
||||
};
|
||||
|
||||
return await this.simulator.simulateApiCall(request);
|
||||
}
|
||||
|
||||
async getProducts(params: any): Promise<ApiResponse> {
|
||||
const request: ApiRequest = {
|
||||
method: 'GET',
|
||||
endpoint: 'products',
|
||||
params,
|
||||
platform: this.platform
|
||||
};
|
||||
|
||||
return await this.simulator.simulateApiCall(request);
|
||||
}
|
||||
|
||||
async getInventory(params: any): Promise<ApiResponse> {
|
||||
const request: ApiRequest = {
|
||||
method: 'GET',
|
||||
endpoint: 'inventory',
|
||||
params,
|
||||
platform: this.platform
|
||||
};
|
||||
|
||||
return await this.simulator.simulateApiCall(request);
|
||||
}
|
||||
|
||||
async updateProductPrice(productId: string, price: number): Promise<ApiResponse> {
|
||||
// TikTok特有的价格更新逻辑
|
||||
const request: ApiRequest = {
|
||||
method: 'POST',
|
||||
endpoint: 'products/update-price',
|
||||
params: { productId, price },
|
||||
platform: this.platform
|
||||
};
|
||||
|
||||
return await this.simulator.simulateApiCall(request);
|
||||
}
|
||||
|
||||
async updateProductStock(productId: string, stock: number): Promise<ApiResponse> {
|
||||
const request: ApiRequest = {
|
||||
method: 'POST',
|
||||
endpoint: 'products/update-stock',
|
||||
params: { productId, stock },
|
||||
platform: this.platform
|
||||
};
|
||||
|
||||
return await this.simulator.simulateApiCall(request);
|
||||
}
|
||||
|
||||
async updateOrderStatus(orderId: string, status: string): Promise<ApiResponse> {
|
||||
const request: ApiRequest = {
|
||||
method: 'POST',
|
||||
endpoint: 'orders/update-status',
|
||||
params: { orderId, status },
|
||||
platform: this.platform
|
||||
};
|
||||
|
||||
return await this.simulator.simulateApiCall(request);
|
||||
}
|
||||
|
||||
async syncOrders(): Promise<ApiResponse> {
|
||||
// TikTok订单同步逻辑
|
||||
console.log('开始同步TikTok订单...');
|
||||
|
||||
try {
|
||||
// 获取所有订单
|
||||
const ordersResponse = await this.getOrders({ limit: 100 });
|
||||
|
||||
if (!ordersResponse.success) {
|
||||
return ordersResponse;
|
||||
}
|
||||
|
||||
// 处理同步逻辑
|
||||
const syncedOrders = await this.processOrderSync(ordersResponse.data.orders);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: 'TikTok订单同步完成',
|
||||
syncedCount: syncedOrders.length,
|
||||
orders: syncedOrders
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `TikTok订单同步失败: ${error}`,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async syncProducts(): Promise<ApiResponse> {
|
||||
// TikTok商品同步逻辑
|
||||
return await this.getProducts({ limit: 100 });
|
||||
}
|
||||
|
||||
async batchUpdatePrices(updates: Array<{productId: string, price: number}>): Promise<ApiResponse> {
|
||||
// TikTok批量价格更新逻辑
|
||||
console.log(`开始批量更新${updates.length}个商品价格...`);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const update of updates) {
|
||||
try {
|
||||
const result = await this.updateProductPrice(update.productId, update.price);
|
||||
results.push({
|
||||
productId: update.productId,
|
||||
success: result.success,
|
||||
error: result.error
|
||||
});
|
||||
|
||||
// 添加延迟避免触发反爬虫机制
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
} catch (error) {
|
||||
results.push({
|
||||
productId: update.productId,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '未知错误'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
|
||||
return {
|
||||
success: successCount > 0,
|
||||
data: {
|
||||
message: `批量更新完成,成功${successCount}个,失败${results.length - successCount}个`,
|
||||
results
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
private async processOrderSync(orders: any[]): Promise<any[]> {
|
||||
// 处理订单同步逻辑
|
||||
return orders.map(order => ({
|
||||
...order,
|
||||
syncedAt: new Date().toISOString(),
|
||||
platform: this.platform
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shopee平台适配器
|
||||
*/
|
||||
class ShopeeAdapter extends BasePlatformAdapter {
|
||||
constructor(simulator: ApiSimulator) {
|
||||
super('shopee', simulator);
|
||||
}
|
||||
|
||||
// Shopee特有的实现
|
||||
async getOrders(params: any): Promise<ApiResponse> {
|
||||
// Shopee订单获取逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { orders: [], total: 0 },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async getProducts(params: any): Promise<ApiResponse> {
|
||||
// Shopee商品获取逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { products: [], total: 0 },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async getInventory(params: any): Promise<ApiResponse> {
|
||||
// Shopee库存获取逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { inventory: [] },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async updateProductPrice(productId: string, price: number): Promise<ApiResponse> {
|
||||
// Shopee价格更新逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { message: 'Shopee价格更新成功' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async updateProductStock(productId: string, stock: number): Promise<ApiResponse> {
|
||||
// Shopee库存更新逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { message: 'Shopee库存更新成功' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async updateOrderStatus(orderId: string, status: string): Promise<ApiResponse> {
|
||||
// Shopee订单状态更新逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { message: 'Shopee订单状态更新成功' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async syncOrders(): Promise<ApiResponse> {
|
||||
// Shopee订单同步逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { message: 'Shopee订单同步完成', syncedCount: 0 },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async syncProducts(): Promise<ApiResponse> {
|
||||
// Shopee商品同步逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { message: 'Shopee商品同步完成', syncedCount: 0 },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async batchUpdatePrices(updates: Array<{productId: string, price: number}>): Promise<ApiResponse> {
|
||||
// Shopee批量价格更新逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { message: 'Shopee批量价格更新完成', results: updates },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Amazon平台适配器
|
||||
*/
|
||||
class AmazonAdapter extends BasePlatformAdapter {
|
||||
constructor(simulator: ApiSimulator) {
|
||||
super('amazon', simulator);
|
||||
}
|
||||
|
||||
// Amazon特有的实现
|
||||
async getOrders(params: any): Promise<ApiResponse> {
|
||||
// Amazon订单获取逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { orders: [], total: 0 },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async getProducts(params: any): Promise<ApiResponse> {
|
||||
// Amazon商品获取逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { products: [], total: 0 },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async getInventory(params: any): Promise<ApiResponse> {
|
||||
// Amazon库存获取逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { inventory: [] },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async updateProductPrice(productId: string, price: number): Promise<ApiResponse> {
|
||||
// Amazon价格更新逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { message: 'Amazon价格更新成功' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async updateProductStock(productId: string, stock: number): Promise<ApiResponse> {
|
||||
// Amazon库存更新逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { message: 'Amazon库存更新成功' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async updateOrderStatus(orderId: string, status: string): Promise<ApiResponse> {
|
||||
// Amazon订单状态更新逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { message: 'Amazon订单状态更新成功' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async syncOrders(): Promise<ApiResponse> {
|
||||
// Amazon订单同步逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { message: 'Amazon订单同步完成', syncedCount: 0 },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async syncProducts(): Promise<ApiResponse> {
|
||||
// Amazon商品同步逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { message: 'Amazon商品同步完成', syncedCount: 0 },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
async batchUpdatePrices(updates: Array<{productId: string, price: number}>): Promise<ApiResponse> {
|
||||
// Amazon批量价格更新逻辑
|
||||
return {
|
||||
success: true,
|
||||
data: { message: 'Amazon批量价格更新完成', results: updates },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台适配器工厂
|
||||
*/
|
||||
export class PlatformAdapterFactory {
|
||||
private static simulator: ApiSimulator;
|
||||
|
||||
static initialize(simulator: ApiSimulator) {
|
||||
this.simulator = simulator;
|
||||
}
|
||||
|
||||
static createAdapter(platform: string): PlatformAdapter {
|
||||
if (!this.simulator) {
|
||||
throw new Error('PlatformAdapterFactory未初始化,请先调用initialize方法');
|
||||
}
|
||||
|
||||
switch (platform.toLowerCase()) {
|
||||
case 'tiktok':
|
||||
return new TikTokAdapter(this.simulator);
|
||||
case 'shopee':
|
||||
return new ShopeeAdapter(this.simulator);
|
||||
case 'amazon':
|
||||
return new AmazonAdapter(this.simulator);
|
||||
default:
|
||||
throw new Error(`不支持的平台: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
static getSupportedPlatforms(): string[] {
|
||||
return ['tiktok', 'shopee', 'amazon'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台管理器 - 管理多个平台的适配器
|
||||
*/
|
||||
export class PlatformManager {
|
||||
private adapters: Map<string, PlatformAdapter> = new Map();
|
||||
private simulator: ApiSimulator;
|
||||
|
||||
constructor(simulator: ApiSimulator) {
|
||||
this.simulator = simulator;
|
||||
PlatformAdapterFactory.initialize(simulator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台适配器
|
||||
*/
|
||||
getAdapter(platform: string): PlatformAdapter {
|
||||
if (!this.adapters.has(platform)) {
|
||||
const adapter = PlatformAdapterFactory.createAdapter(platform);
|
||||
this.adapters.set(platform, adapter);
|
||||
}
|
||||
|
||||
return this.adapters.get(platform)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查平台是否支持
|
||||
*/
|
||||
isPlatformSupported(platform: string): boolean {
|
||||
return PlatformAdapterFactory.getSupportedPlatforms().includes(platform.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有支持的平台
|
||||
*/
|
||||
getSupportedPlatforms(): string[] {
|
||||
return PlatformAdapterFactory.getSupportedPlatforms();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已登录的平台列表
|
||||
*/
|
||||
getLoggedInPlatforms(): string[] {
|
||||
const loggedIn: string[] = [];
|
||||
|
||||
for (const [platform, adapter] of this.adapters.entries()) {
|
||||
if (adapter.isLoggedIn()) {
|
||||
loggedIn.push(platform);
|
||||
}
|
||||
}
|
||||
|
||||
return loggedIn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量执行跨平台操作
|
||||
*/
|
||||
async batchOperation(platforms: string[], operation: (adapter: PlatformAdapter) => Promise<ApiResponse>): Promise<Record<string, ApiResponse>> {
|
||||
const results: Record<string, ApiResponse> = {};
|
||||
|
||||
for (const platform of platforms) {
|
||||
try {
|
||||
const adapter = this.getAdapter(platform);
|
||||
results[platform] = await operation(adapter);
|
||||
} catch (error) {
|
||||
results[platform] = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '未知错误',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
import { apiSimulator } from './api-simulator';
|
||||
export const platformManager = new PlatformManager(apiSimulator);
|
||||
567
node-agent/src/remote-desktop.ts
Normal file
567
node-agent/src/remote-desktop.ts
Normal file
@@ -0,0 +1,567 @@
|
||||
/**
|
||||
* 远程桌面功能 - 作为API模拟的兜底方案
|
||||
* 当API模拟失败时,通过远程桌面直接操作电商平台
|
||||
*/
|
||||
|
||||
import { chromium, Browser, Page } from 'playwright';
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import { createServer } from 'http';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
interface RemoteDesktopConfig {
|
||||
port: number;
|
||||
headless: boolean;
|
||||
proxy?: string;
|
||||
screenWidth: number;
|
||||
screenHeight: number;
|
||||
frameRate: number;
|
||||
}
|
||||
|
||||
interface RemoteSession {
|
||||
id: string;
|
||||
platform: string;
|
||||
page: Page;
|
||||
browser: Browser;
|
||||
status: 'connected' | 'disconnected' | 'error';
|
||||
lastActivity: Date;
|
||||
screenStream?: any; // 屏幕流
|
||||
}
|
||||
|
||||
interface RemoteCommand {
|
||||
type: 'mouse' | 'keyboard' | 'screenshot' | 'navigate' | 'execute';
|
||||
action: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
interface RemoteResponse {
|
||||
success: boolean;
|
||||
type: string;
|
||||
data?: any;
|
||||
error?: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程桌面服务
|
||||
*/
|
||||
export class RemoteDesktopService {
|
||||
private config: RemoteDesktopConfig;
|
||||
private sessions: Map<string, RemoteSession> = new Map();
|
||||
private wss: WebSocketServer;
|
||||
private browser: Browser | null = null;
|
||||
|
||||
constructor(config: RemoteDesktopConfig) {
|
||||
this.config = config;
|
||||
|
||||
// 创建HTTP服务器
|
||||
const server = createServer((req, res) => {
|
||||
this.handleHttpRequest(req, res);
|
||||
});
|
||||
|
||||
// 创建WebSocket服务器
|
||||
this.wss = new WebSocketServer({ server });
|
||||
server.listen(config.port);
|
||||
|
||||
this.setupWebSocketHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置WebSocket处理器
|
||||
*/
|
||||
private setupWebSocketHandlers(): void {
|
||||
this.wss.on('connection', (ws: WebSocket) => {
|
||||
console.log('远程桌面客户端连接建立');
|
||||
|
||||
ws.on('message', async (data: Buffer) => {
|
||||
try {
|
||||
const command: RemoteCommand = JSON.parse(data.toString());
|
||||
const response = await this.handleRemoteCommand(command, ws);
|
||||
ws.send(JSON.stringify(response));
|
||||
} catch (error) {
|
||||
ws.send(JSON.stringify({
|
||||
success: false,
|
||||
type: 'error',
|
||||
error: error instanceof Error ? error.message : '命令处理失败',
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log('远程桌面客户端连接关闭');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理HTTP请求
|
||||
*/
|
||||
private handleHttpRequest(req: any, res: any): void {
|
||||
const { url, method } = req;
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
|
||||
if (url === '/health' && method === 'GET') {
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify({
|
||||
status: 'healthy',
|
||||
service: 'remote-desktop',
|
||||
activeSessions: this.sessions.size,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
} else if (url === '/sessions' && method === 'GET') {
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify(Array.from(this.sessions.values()).map(session => ({
|
||||
id: session.id,
|
||||
platform: session.platform,
|
||||
status: session.status,
|
||||
lastActivity: session.lastActivity
|
||||
}))));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end(JSON.stringify({ error: '接口不存在' }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理远程命令
|
||||
*/
|
||||
private async handleRemoteCommand(command: RemoteCommand, ws: WebSocket): Promise<RemoteResponse> {
|
||||
try {
|
||||
switch (command.type) {
|
||||
case 'navigate':
|
||||
return await this.handleNavigateCommand(command);
|
||||
case 'mouse':
|
||||
return await this.handleMouseCommand(command);
|
||||
case 'keyboard':
|
||||
return await this.handleKeyboardCommand(command);
|
||||
case 'screenshot':
|
||||
return await this.handleScreenshotCommand(command);
|
||||
case 'execute':
|
||||
return await this.handleExecuteCommand(command);
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
type: 'error',
|
||||
error: '未知命令类型',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
type: 'error',
|
||||
error: error instanceof Error ? error.message : '命令执行失败',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建远程会话
|
||||
*/
|
||||
async createSession(platform: string): Promise<RemoteSession> {
|
||||
if (!this.browser) {
|
||||
this.browser = await chromium.launch({
|
||||
headless: this.config.headless,
|
||||
proxy: this.config.proxy ? { server: this.config.proxy } : undefined,
|
||||
args: [
|
||||
`--window-size=${this.config.screenWidth},${this.config.screenHeight}`,
|
||||
'--disable-web-security',
|
||||
'--disable-features=IsolateOrigins,site-per-process'
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const page = await this.browser.newPage();
|
||||
await page.setViewportSize({
|
||||
width: this.config.screenWidth,
|
||||
height: this.config.screenHeight
|
||||
});
|
||||
|
||||
const sessionId = this.generateSessionId();
|
||||
const session: RemoteSession = {
|
||||
id: sessionId,
|
||||
platform,
|
||||
page,
|
||||
browser: this.browser,
|
||||
status: 'connected',
|
||||
lastActivity: new Date()
|
||||
};
|
||||
|
||||
this.sessions.set(sessionId, session);
|
||||
|
||||
// 设置页面事件监听
|
||||
await this.setupPageEventListeners(page, sessionId);
|
||||
|
||||
console.log(`远程会话创建成功: ${sessionId} (${platform})`);
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置页面事件监听器
|
||||
*/
|
||||
private async setupPageEventListeners(page: Page, sessionId: string): Promise<void> {
|
||||
// 监听页面导航
|
||||
page.on('framenavigated', async (frame) => {
|
||||
if (frame === page.mainFrame()) {
|
||||
console.log(`会话 ${sessionId} 页面导航: ${frame.url()}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听控制台消息
|
||||
page.on('console', (msg) => {
|
||||
console.log(`会话 ${sessionId} 控制台:`, msg.text());
|
||||
});
|
||||
|
||||
// 监听页面错误
|
||||
page.on('pageerror', (error) => {
|
||||
console.error(`会话 ${sessionId} 页面错误:`, error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理导航命令
|
||||
*/
|
||||
private async handleNavigateCommand(command: RemoteCommand): Promise<RemoteResponse> {
|
||||
const { url, sessionId } = command.data;
|
||||
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
return {
|
||||
success: false,
|
||||
type: 'navigate',
|
||||
error: '会话不存在',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await session.page.goto(url, { waitUntil: 'networkidle' });
|
||||
session.lastActivity = new Date();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'navigate',
|
||||
data: {
|
||||
message: '页面导航成功',
|
||||
url: session.page.url(),
|
||||
title: await session.page.title()
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
type: 'navigate',
|
||||
error: `页面导航失败: ${error}`,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标命令
|
||||
*/
|
||||
private async handleMouseCommand(command: RemoteCommand): Promise<RemoteResponse> {
|
||||
const { sessionId, action, x, y, selector } = command.data;
|
||||
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
return {
|
||||
success: false,
|
||||
type: 'mouse',
|
||||
error: '会话不存在',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case 'click':
|
||||
if (selector) {
|
||||
await session.page.click(selector);
|
||||
} else if (x !== undefined && y !== undefined) {
|
||||
await session.page.mouse.click(x, y);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'doubleClick':
|
||||
if (selector) {
|
||||
await session.page.dblclick(selector);
|
||||
} else if (x !== undefined && y !== undefined) {
|
||||
await session.page.mouse.dblclick(x, y);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'hover':
|
||||
if (selector) {
|
||||
await session.page.hover(selector);
|
||||
} else if (x !== undefined && y !== undefined) {
|
||||
await session.page.mouse.move(x, y);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'drag':
|
||||
const { startX, startY, endX, endY } = command.data;
|
||||
await session.page.mouse.move(startX, startY);
|
||||
await session.page.mouse.down();
|
||||
await session.page.mouse.move(endX, endY);
|
||||
await session.page.mouse.up();
|
||||
break;
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
type: 'mouse',
|
||||
error: '未知的鼠标操作',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
session.lastActivity = new Date();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'mouse',
|
||||
data: { message: '鼠标操作成功' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
type: 'mouse',
|
||||
error: `鼠标操作失败: ${error}`,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理键盘命令
|
||||
*/
|
||||
private async handleKeyboardCommand(command: RemoteCommand): Promise<RemoteResponse> {
|
||||
const { sessionId, action, text, selector, key } = command.data;
|
||||
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
return {
|
||||
success: false,
|
||||
type: 'keyboard',
|
||||
error: '会话不存在',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case 'type':
|
||||
if (selector) {
|
||||
await session.page.fill(selector, text);
|
||||
} else {
|
||||
await session.page.keyboard.type(text);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'press':
|
||||
await session.page.keyboard.press(key);
|
||||
break;
|
||||
|
||||
case 'selectAll':
|
||||
await session.page.keyboard.press('Control+A');
|
||||
break;
|
||||
|
||||
case 'copy':
|
||||
await session.page.keyboard.press('Control+C');
|
||||
break;
|
||||
|
||||
case 'paste':
|
||||
await session.page.keyboard.press('Control+V');
|
||||
break;
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
type: 'keyboard',
|
||||
error: '未知的键盘操作',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
session.lastActivity = new Date();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'keyboard',
|
||||
data: { message: '键盘操作成功' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
type: 'keyboard',
|
||||
error: `键盘操作失败: ${error}`,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理截图命令
|
||||
*/
|
||||
private async handleScreenshotCommand(command: RemoteCommand): Promise<RemoteResponse> {
|
||||
const { sessionId, fullPage = false } = command.data;
|
||||
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
return {
|
||||
success: false,
|
||||
type: 'screenshot',
|
||||
error: '会话不存在',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const screenshot = await session.page.screenshot({
|
||||
fullPage,
|
||||
type: 'jpeg',
|
||||
quality: 80
|
||||
});
|
||||
|
||||
session.lastActivity = new Date();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'screenshot',
|
||||
data: {
|
||||
message: '截图成功',
|
||||
screenshot: screenshot.toString('base64'),
|
||||
format: 'base64'
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
type: 'screenshot',
|
||||
error: `截图失败: ${error}`,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理执行命令
|
||||
*/
|
||||
private async handleExecuteCommand(command: RemoteCommand): Promise<RemoteResponse> {
|
||||
const { sessionId, script, args = [] } = command.data;
|
||||
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
return {
|
||||
success: false,
|
||||
type: 'execute',
|
||||
error: '会话不存在',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await session.page.evaluate((script, args) => {
|
||||
// 在页面上下文中执行脚本
|
||||
return eval(`(${script})`)(...args);
|
||||
}, script, args);
|
||||
|
||||
session.lastActivity = new Date();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'execute',
|
||||
data: {
|
||||
message: '脚本执行成功',
|
||||
result
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
type: 'execute',
|
||||
error: `脚本执行失败: ${error}`,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭会话
|
||||
*/
|
||||
async closeSession(sessionId: string): Promise<void> {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) {
|
||||
await session.page.close();
|
||||
this.sessions.delete(sessionId);
|
||||
console.log(`远程会话已关闭: ${sessionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
*/
|
||||
getSessions(): RemoteSession[] {
|
||||
return Array.from(this.sessions.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动远程桌面服务
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
console.log(`远程桌面服务启动在端口 ${this.config.port}`);
|
||||
console.log(`健康检查: http://localhost:${this.config.port}/health`);
|
||||
console.log('等待远程桌面客户端连接...');
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止远程桌面服务
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
// 关闭所有会话
|
||||
for (const session of this.sessions.values()) {
|
||||
await session.page.close();
|
||||
}
|
||||
this.sessions.clear();
|
||||
|
||||
// 关闭浏览器
|
||||
if (this.browser) {
|
||||
await this.browser.close();
|
||||
this.browser = null;
|
||||
}
|
||||
|
||||
// 关闭WebSocket服务器
|
||||
this.wss.close();
|
||||
|
||||
console.log('远程桌面服务已停止');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成会话ID
|
||||
*/
|
||||
private generateSessionId(): string {
|
||||
return `rdp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出默认配置
|
||||
export const defaultRemoteDesktopConfig: RemoteDesktopConfig = {
|
||||
port: 8081,
|
||||
headless: false, // 远程桌面需要显示界面
|
||||
screenWidth: 1920,
|
||||
screenHeight: 1080,
|
||||
frameRate: 30
|
||||
};
|
||||
|
||||
// 导出单例实例
|
||||
export const remoteDesktopService = new RemoteDesktopService(defaultRemoteDesktopConfig);
|
||||
460
node-agent/src/task-scheduler.ts
Normal file
460
node-agent/src/task-scheduler.ts
Normal file
@@ -0,0 +1,460 @@
|
||||
/**
|
||||
* 任务调度系统 - 支持自动化任务和定时操作
|
||||
* 类似cron的定时任务系统,支持复杂的工作流
|
||||
*/
|
||||
|
||||
import { PlatformManager } from './platform-adapter';
|
||||
import { ApiResponse } from './api-simulator';
|
||||
|
||||
interface TaskConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
platform: string;
|
||||
type: 'sync' | 'update' | 'batch' | 'custom';
|
||||
schedule: string; // cron表达式
|
||||
enabled: boolean;
|
||||
parameters: Record<string, any>;
|
||||
retryCount: number;
|
||||
retryDelay: number; // 重试延迟(毫秒)
|
||||
timeout: number; // 超时时间(毫秒)
|
||||
}
|
||||
|
||||
interface TaskExecution {
|
||||
id: string;
|
||||
taskId: string;
|
||||
startTime: Date;
|
||||
endTime?: Date;
|
||||
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
||||
result?: ApiResponse;
|
||||
error?: string;
|
||||
retryCount: number;
|
||||
}
|
||||
|
||||
interface TaskHistory {
|
||||
taskId: string;
|
||||
executions: TaskExecution[];
|
||||
lastRun?: Date;
|
||||
nextRun?: Date;
|
||||
successCount: number;
|
||||
failureCount: number;
|
||||
}
|
||||
|
||||
interface WorkflowStep {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'sync' | 'update' | 'condition' | 'delay';
|
||||
parameters: Record<string, any>;
|
||||
nextStep?: string; // 下一步ID
|
||||
condition?: (result: ApiResponse) => boolean; // 条件判断
|
||||
}
|
||||
|
||||
interface WorkflowConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
steps: WorkflowStep[];
|
||||
schedule?: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务调度器
|
||||
*/
|
||||
export class TaskScheduler {
|
||||
private tasks: Map<string, TaskConfig> = new Map();
|
||||
private workflows: Map<string, WorkflowConfig> = new Map();
|
||||
private executions: Map<string, TaskExecution> = new Map();
|
||||
private history: Map<string, TaskHistory> = new Map();
|
||||
private timers: Map<string, NodeJS.Timeout> = new Map();
|
||||
private platformManager: PlatformManager;
|
||||
|
||||
constructor(platformManager: PlatformManager) {
|
||||
this.platformManager = platformManager;
|
||||
this.loadDefaultTasks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载默认任务配置
|
||||
*/
|
||||
private loadDefaultTasks(): void {
|
||||
const defaultTasks: TaskConfig[] = [
|
||||
{
|
||||
id: 'sync-orders-daily',
|
||||
name: '每日订单同步',
|
||||
description: '每天凌晨同步所有平台的订单数据',
|
||||
platform: 'all',
|
||||
type: 'sync',
|
||||
schedule: '0 2 * * *', // 每天凌晨2点
|
||||
enabled: true,
|
||||
parameters: { limit: 1000, syncAll: true },
|
||||
retryCount: 3,
|
||||
retryDelay: 5000,
|
||||
timeout: 300000 // 5分钟
|
||||
},
|
||||
{
|
||||
id: 'sync-products-hourly',
|
||||
name: '每小时商品同步',
|
||||
description: '每小时同步商品数据',
|
||||
platform: 'all',
|
||||
type: 'sync',
|
||||
schedule: '0 * * * *', // 每小时
|
||||
enabled: true,
|
||||
parameters: { limit: 500 },
|
||||
retryCount: 2,
|
||||
retryDelay: 3000,
|
||||
timeout: 180000 // 3分钟
|
||||
},
|
||||
{
|
||||
id: 'price-monitoring',
|
||||
name: '价格监控',
|
||||
description: '监控商品价格变化',
|
||||
platform: 'all',
|
||||
type: 'custom',
|
||||
schedule: '*/15 * * * *', // 每15分钟
|
||||
enabled: true,
|
||||
parameters: { monitorInterval: 15 },
|
||||
retryCount: 1,
|
||||
retryDelay: 2000,
|
||||
timeout: 120000 // 2分钟
|
||||
}
|
||||
];
|
||||
|
||||
defaultTasks.forEach(task => this.addTask(task));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加任务
|
||||
*/
|
||||
addTask(task: TaskConfig): void {
|
||||
this.tasks.set(task.id, task);
|
||||
this.history.set(task.id, {
|
||||
taskId: task.id,
|
||||
executions: [],
|
||||
successCount: 0,
|
||||
failureCount: 0
|
||||
});
|
||||
|
||||
if (task.enabled) {
|
||||
this.scheduleTask(task);
|
||||
}
|
||||
|
||||
console.log(`任务已添加: ${task.name} (${task.id})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度任务
|
||||
*/
|
||||
private scheduleTask(task: TaskConfig): void {
|
||||
if (this.timers.has(task.id)) {
|
||||
clearTimeout(this.timers.get(task.id)!);
|
||||
}
|
||||
|
||||
const nextRun = this.calculateNextRun(task.schedule);
|
||||
const delay = nextRun.getTime() - Date.now();
|
||||
|
||||
if (delay > 0) {
|
||||
const timer = setTimeout(() => {
|
||||
this.executeTask(task.id);
|
||||
// 重新调度下一次执行
|
||||
this.scheduleTask(task);
|
||||
}, delay);
|
||||
|
||||
this.timers.set(task.id, timer);
|
||||
|
||||
// 更新历史记录
|
||||
const history = this.history.get(task.id);
|
||||
if (history) {
|
||||
history.nextRun = nextRun;
|
||||
}
|
||||
|
||||
console.log(`任务已调度: ${task.name}, 下次执行: ${nextRun.toLocaleString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算下次执行时间
|
||||
*/
|
||||
private calculateNextRun(cronExpression: string): Date {
|
||||
// 简化的cron表达式解析(实际项目中应该使用cron库)
|
||||
const [minute, hour, dayOfMonth, month, dayOfWeek] = cronExpression.split(' ');
|
||||
|
||||
const now = new Date();
|
||||
const nextRun = new Date(now);
|
||||
|
||||
// 简单的实现,实际应该使用成熟的cron库
|
||||
if (minute === '*') {
|
||||
nextRun.setMinutes(now.getMinutes() + 1);
|
||||
} else if (minute.startsWith('*/')) {
|
||||
const interval = parseInt(minute.substring(2));
|
||||
nextRun.setMinutes(now.getMinutes() + interval);
|
||||
} else {
|
||||
nextRun.setMinutes(parseInt(minute));
|
||||
}
|
||||
|
||||
return nextRun;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行任务
|
||||
*/
|
||||
async executeTask(taskId: string, manual: boolean = false): Promise<TaskExecution> {
|
||||
const task = this.tasks.get(taskId);
|
||||
if (!task) {
|
||||
throw new Error(`任务不存在: ${taskId}`);
|
||||
}
|
||||
|
||||
const execution: TaskExecution = {
|
||||
id: this.generateId(),
|
||||
taskId,
|
||||
startTime: new Date(),
|
||||
status: 'running',
|
||||
retryCount: 0
|
||||
};
|
||||
|
||||
this.executions.set(execution.id, execution);
|
||||
|
||||
// 添加到历史记录
|
||||
const history = this.history.get(taskId);
|
||||
if (history) {
|
||||
history.executions.push(execution);
|
||||
history.lastRun = execution.startTime;
|
||||
}
|
||||
|
||||
console.log(`开始执行任务: ${task.name}`);
|
||||
|
||||
try {
|
||||
const result = await this.executeTaskWithRetry(task, execution);
|
||||
|
||||
execution.endTime = new Date();
|
||||
execution.status = result.success ? 'completed' : 'failed';
|
||||
execution.result = result;
|
||||
|
||||
if (result.success) {
|
||||
console.log(`任务执行成功: ${task.name}`);
|
||||
if (history) history.successCount++;
|
||||
} else {
|
||||
console.error(`任务执行失败: ${task.name}`, result.error);
|
||||
if (history) history.failureCount++;
|
||||
}
|
||||
|
||||
return execution;
|
||||
} catch (error) {
|
||||
execution.endTime = new Date();
|
||||
execution.status = 'failed';
|
||||
execution.error = error instanceof Error ? error.message : '未知错误';
|
||||
|
||||
console.error(`任务执行异常: ${task.name}`, error);
|
||||
if (history) history.failureCount++;
|
||||
|
||||
return execution;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 带重试的任务执行
|
||||
*/
|
||||
private async executeTaskWithRetry(task: TaskConfig, execution: TaskExecution): Promise<ApiResponse> {
|
||||
for (let attempt = 0; attempt <= task.retryCount; attempt++) {
|
||||
execution.retryCount = attempt;
|
||||
|
||||
try {
|
||||
const result = await this.executeSingleTask(task);
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (attempt === task.retryCount) {
|
||||
throw error; // 最后一次重试失败
|
||||
}
|
||||
|
||||
console.log(`任务执行失败,${task.retryDelay}ms后重试 (${attempt + 1}/${task.retryCount})`);
|
||||
await new Promise(resolve => setTimeout(resolve, task.retryDelay));
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('任务执行失败,重试次数已用尽');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个任务
|
||||
*/
|
||||
private async executeSingleTask(task: TaskConfig): Promise<ApiResponse> {
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error('任务执行超时')), task.timeout);
|
||||
});
|
||||
|
||||
const taskPromise = (async () => {
|
||||
switch (task.type) {
|
||||
case 'sync':
|
||||
return await this.executeSyncTask(task);
|
||||
case 'update':
|
||||
return await this.executeUpdateTask(task);
|
||||
case 'batch':
|
||||
return await this.executeBatchTask(task);
|
||||
case 'custom':
|
||||
return await this.executeCustomTask(task);
|
||||
default:
|
||||
throw new Error(`不支持的任务类型: ${task.type}`);
|
||||
}
|
||||
})();
|
||||
|
||||
return await Promise.race([taskPromise, timeoutPromise]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行同步任务
|
||||
*/
|
||||
private async executeSyncTask(task: TaskConfig): Promise<ApiResponse> {
|
||||
const platforms = task.platform === 'all'
|
||||
? this.platformManager.getLoggedInPlatforms()
|
||||
: [task.platform];
|
||||
|
||||
if (platforms.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: '没有已登录的平台',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
const results = await this.platformManager.batchOperation(platforms, async (adapter) => {
|
||||
if (task.name.includes('订单')) {
|
||||
return await adapter.syncOrders();
|
||||
} else if (task.name.includes('商品')) {
|
||||
return await adapter.syncProducts();
|
||||
} else {
|
||||
return await adapter.getProducts(task.parameters);
|
||||
}
|
||||
});
|
||||
|
||||
const successCount = Object.values(results).filter(r => r.success).length;
|
||||
|
||||
return {
|
||||
success: successCount > 0,
|
||||
data: {
|
||||
message: `同步任务完成,成功${successCount}个平台,失败${platforms.length - successCount}个`,
|
||||
results
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行更新任务
|
||||
*/
|
||||
private async executeUpdateTask(task: TaskConfig): Promise<ApiResponse> {
|
||||
// 更新任务实现
|
||||
return {
|
||||
success: true,
|
||||
data: { message: '更新任务执行完成' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行批量任务
|
||||
*/
|
||||
private async executeBatchTask(task: TaskConfig): Promise<ApiResponse> {
|
||||
// 批量任务实现
|
||||
return {
|
||||
success: true,
|
||||
data: { message: '批量任务执行完成' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行自定义任务
|
||||
*/
|
||||
private async executeCustomTask(task: TaskConfig): Promise<ApiResponse> {
|
||||
// 自定义任务实现
|
||||
return {
|
||||
success: true,
|
||||
data: { message: '自定义任务执行完成' },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加工作流
|
||||
*/
|
||||
addWorkflow(workflow: WorkflowConfig): void {
|
||||
this.workflows.set(workflow.id, workflow);
|
||||
|
||||
if (workflow.schedule && workflow.enabled) {
|
||||
// 调度工作流执行
|
||||
this.scheduleWorkflow(workflow);
|
||||
}
|
||||
|
||||
console.log(`工作流已添加: ${workflow.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度工作流
|
||||
*/
|
||||
private scheduleWorkflow(workflow: WorkflowConfig): void {
|
||||
// 工作流调度逻辑
|
||||
console.log(`工作流已调度: ${workflow.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务列表
|
||||
*/
|
||||
getTasks(): TaskConfig[] {
|
||||
return Array.from(this.tasks.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务执行历史
|
||||
*/
|
||||
getTaskHistory(taskId: string): TaskHistory | undefined {
|
||||
return this.history.get(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有任务历史
|
||||
*/
|
||||
getAllHistory(): Map<string, TaskHistory> {
|
||||
return new Map(this.history);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用任务
|
||||
*/
|
||||
setTaskEnabled(taskId: string, enabled: boolean): void {
|
||||
const task = this.tasks.get(taskId);
|
||||
if (task) {
|
||||
task.enabled = enabled;
|
||||
|
||||
if (enabled) {
|
||||
this.scheduleTask(task);
|
||||
} else {
|
||||
const timer = this.timers.get(taskId);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
this.timers.delete(taskId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止所有任务
|
||||
*/
|
||||
stopAllTasks(): void {
|
||||
for (const timer of this.timers.values()) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
this.timers.clear();
|
||||
console.log('所有任务已停止');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一ID
|
||||
*/
|
||||
private generateId(): string {
|
||||
return `task-exec-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
import { platformManager } from './platform-adapter';
|
||||
export const taskScheduler = new TaskScheduler(platformManager);
|
||||
684
node-agent/src/web-remote-service.ts
Normal file
684
node-agent/src/web-remote-service.ts
Normal file
@@ -0,0 +1,684 @@
|
||||
/**
|
||||
* WEB远程控制中间层
|
||||
* 介于API模拟和远程桌面之间的重要层级
|
||||
* 通过Web技术实现远程控制,比API模拟更灵活,比远程桌面更轻量
|
||||
*/
|
||||
|
||||
import { chromium, Browser, Page } from 'playwright';
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import { createServer } from 'http';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
interface WebRemoteConfig {
|
||||
port: number;
|
||||
headless: boolean;
|
||||
proxy?: string;
|
||||
viewport: { width: number; height: number };
|
||||
userAgent: string;
|
||||
}
|
||||
|
||||
interface WebSession {
|
||||
id: string;
|
||||
platform: string;
|
||||
page: Page;
|
||||
browser: Browser;
|
||||
status: 'ready' | 'active' | 'error' | 'closed';
|
||||
lastActivity: Date;
|
||||
currentUrl: string;
|
||||
title: string;
|
||||
cookies: any[];
|
||||
localStorage: Record<string, any>;
|
||||
}
|
||||
|
||||
interface WebCommand {
|
||||
type: 'navigate' | 'click' | 'type' | 'scroll' | 'screenshot' | 'eval' | 'cookies' | 'storage';
|
||||
action: string;
|
||||
data: any;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
interface WebResponse {
|
||||
success: boolean;
|
||||
type: string;
|
||||
data?: any;
|
||||
error?: string;
|
||||
sessionId: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* WEB远程控制服务
|
||||
*/
|
||||
export class WebRemoteService {
|
||||
private config: WebRemoteConfig;
|
||||
private sessions: Map<string, WebSession> = new Map();
|
||||
private wss: WebSocketServer;
|
||||
private browser: Browser | null = null;
|
||||
|
||||
constructor(config: WebRemoteConfig) {
|
||||
this.config = config;
|
||||
|
||||
// 创建HTTP服务器
|
||||
const server = createServer((req, res) => {
|
||||
this.handleHttpRequest(req, res);
|
||||
});
|
||||
|
||||
// 创建WebSocket服务器
|
||||
this.wss = new WebSocketServer({ server });
|
||||
server.listen(config.port);
|
||||
|
||||
this.setupWebSocketHandlers();
|
||||
this.initializeBrowser();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化浏览器
|
||||
*/
|
||||
private async initializeBrowser(): Promise<void> {
|
||||
try {
|
||||
this.browser = await chromium.launch({
|
||||
headless: this.config.headless,
|
||||
proxy: this.config.proxy ? { server: this.config.proxy } : undefined,
|
||||
args: [
|
||||
'--disable-web-security',
|
||||
'--disable-features=IsolateOrigins,site-per-process',
|
||||
'--allow-running-insecure-content',
|
||||
'--disable-blink-features=AutomationControlled'
|
||||
]
|
||||
});
|
||||
|
||||
console.log('WEB远程控制服务浏览器初始化完成');
|
||||
} catch (error) {
|
||||
console.error('浏览器初始化失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置WebSocket处理器
|
||||
*/
|
||||
private setupWebSocketHandlers(): void {
|
||||
this.wss.on('connection', (ws: WebSocket) => {
|
||||
console.log('WEB远程控制客户端连接建立');
|
||||
|
||||
ws.on('message', async (data: Buffer) => {
|
||||
try {
|
||||
const command: WebCommand = JSON.parse(data.toString());
|
||||
const response = await this.handleWebCommand(command);
|
||||
ws.send(JSON.stringify(response));
|
||||
} catch (error) {
|
||||
ws.send(JSON.stringify({
|
||||
success: false,
|
||||
type: 'error',
|
||||
error: error instanceof Error ? error.message : '命令处理失败',
|
||||
sessionId: command?.sessionId || 'unknown',
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log('WEB远程控制客户端连接关闭');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理HTTP请求
|
||||
*/
|
||||
private handleHttpRequest(req: any, res: any): void {
|
||||
const { url, method } = req;
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
|
||||
if (url === '/health' && method === 'GET') {
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify({
|
||||
status: 'healthy',
|
||||
service: 'web-remote',
|
||||
activeSessions: this.sessions.size,
|
||||
browser: this.browser ? 'connected' : 'disconnected',
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
} else if (url === '/sessions' && method === 'GET') {
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify(this.getSessionList()));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end(JSON.stringify({ error: '接口不存在' }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建WEB会话
|
||||
*/
|
||||
async createSession(platform: string, initialUrl?: string): Promise<WebSession> {
|
||||
if (!this.browser) {
|
||||
await this.initializeBrowser();
|
||||
}
|
||||
|
||||
const context = await this.browser!.newContext({
|
||||
viewport: this.config.viewport,
|
||||
userAgent: this.config.userAgent,
|
||||
ignoreHTTPSErrors: true
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
// 设置页面事件监听
|
||||
await this.setupPageEventListeners(page);
|
||||
|
||||
const sessionId = this.generateSessionId();
|
||||
const session: WebSession = {
|
||||
id: sessionId,
|
||||
platform,
|
||||
page,
|
||||
browser: this.browser!,
|
||||
status: 'ready',
|
||||
lastActivity: new Date(),
|
||||
currentUrl: '',
|
||||
title: '',
|
||||
cookies: [],
|
||||
localStorage: {}
|
||||
};
|
||||
|
||||
this.sessions.set(sessionId, session);
|
||||
|
||||
// 如果有初始URL,导航到该页面
|
||||
if (initialUrl) {
|
||||
await this.navigateToUrl(session, initialUrl);
|
||||
}
|
||||
|
||||
console.log(`WEB会话创建成功: ${sessionId} (${platform})`);
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置页面事件监听器
|
||||
*/
|
||||
private async setupPageEventListeners(page: Page): Promise<void> {
|
||||
// 监听页面导航
|
||||
page.on('framenavigated', async (frame) => {
|
||||
if (frame === page.mainFrame()) {
|
||||
const session = this.findSessionByPage(page);
|
||||
if (session) {
|
||||
session.currentUrl = frame.url();
|
||||
session.lastActivity = new Date();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 监听页面加载完成
|
||||
page.on('load', async () => {
|
||||
const session = this.findSessionByPage(page);
|
||||
if (session) {
|
||||
session.title = await page.title();
|
||||
session.lastActivity = new Date();
|
||||
|
||||
// 获取当前页面的cookies和localStorage
|
||||
try {
|
||||
session.cookies = await page.context().cookies();
|
||||
session.localStorage = await page.evaluate(() => {
|
||||
const storage: Record<string, any> = {};
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key) {
|
||||
storage[key] = localStorage.getItem(key);
|
||||
}
|
||||
}
|
||||
return storage;
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('获取页面存储信息失败:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 监听控制台消息
|
||||
page.on('console', (msg) => {
|
||||
const session = this.findSessionByPage(page);
|
||||
if (session) {
|
||||
console.log(`WEB会话 ${session.id} 控制台:`, msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
// 监听页面错误
|
||||
page.on('pageerror', (error) => {
|
||||
const session = this.findSessionByPage(page);
|
||||
if (session) {
|
||||
console.error(`WEB会话 ${session.id} 页面错误:`, error);
|
||||
session.status = 'error';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过页面查找会话
|
||||
*/
|
||||
private findSessionByPage(page: Page): WebSession | undefined {
|
||||
for (const session of this.sessions.values()) {
|
||||
if (session.page === page) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理WEB命令
|
||||
*/
|
||||
private async handleWebCommand(command: WebCommand): Promise<WebResponse> {
|
||||
const session = this.sessions.get(command.sessionId);
|
||||
if (!session) {
|
||||
return {
|
||||
success: false,
|
||||
type: 'error',
|
||||
error: '会话不存在',
|
||||
sessionId: command.sessionId,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
switch (command.type) {
|
||||
case 'navigate':
|
||||
return await this.handleNavigateCommand(session, command);
|
||||
case 'click':
|
||||
return await this.handleClickCommand(session, command);
|
||||
case 'type':
|
||||
return await this.handleTypeCommand(session, command);
|
||||
case 'scroll':
|
||||
return await this.handleScrollCommand(session, command);
|
||||
case 'screenshot':
|
||||
return await this.handleScreenshotCommand(session, command);
|
||||
case 'eval':
|
||||
return await this.handleEvalCommand(session, command);
|
||||
case 'cookies':
|
||||
return await this.handleCookiesCommand(session, command);
|
||||
case 'storage':
|
||||
return await this.handleStorageCommand(session, command);
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
type: 'error',
|
||||
error: '未知命令类型',
|
||||
sessionId: command.sessionId,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
type: 'error',
|
||||
error: error instanceof Error ? error.message : '命令执行失败',
|
||||
sessionId: command.sessionId,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理导航命令
|
||||
*/
|
||||
private async handleNavigateCommand(session: WebSession, command: WebCommand): Promise<WebResponse> {
|
||||
const { url, waitUntil = 'networkidle' } = command.data;
|
||||
|
||||
try {
|
||||
await session.page.goto(url, { waitUntil: waitUntil as any });
|
||||
session.status = 'active';
|
||||
session.lastActivity = new Date();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'navigate',
|
||||
data: {
|
||||
message: '页面导航成功',
|
||||
url: session.page.url(),
|
||||
title: await session.page.title()
|
||||
},
|
||||
sessionId: session.id,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
session.status = 'error';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理点击命令
|
||||
*/
|
||||
private async handleClickCommand(session: WebSession, command: WebCommand): Promise<WebResponse> {
|
||||
const { selector, x, y, button = 'left', clickCount = 1 } = command.data;
|
||||
|
||||
try {
|
||||
if (selector) {
|
||||
await session.page.click(selector, { button, clickCount });
|
||||
} else if (x !== undefined && y !== undefined) {
|
||||
await session.page.mouse.click(x, y, { button, clickCount });
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
type: 'click',
|
||||
error: '需要提供selector或x,y坐标',
|
||||
sessionId: session.id,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
session.lastActivity = new Date();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'click',
|
||||
data: { message: '点击操作成功' },
|
||||
sessionId: session.id,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理输入命令
|
||||
*/
|
||||
private async handleTypeCommand(session: WebSession, command: WebCommand): Promise<WebResponse> {
|
||||
const { selector, text, delay = 0 } = command.data;
|
||||
|
||||
try {
|
||||
if (selector) {
|
||||
await session.page.fill(selector, text);
|
||||
} else {
|
||||
await session.page.keyboard.type(text, { delay });
|
||||
}
|
||||
|
||||
session.lastActivity = new Date();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'type',
|
||||
data: { message: '输入操作成功' },
|
||||
sessionId: session.id,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理滚动命令
|
||||
*/
|
||||
private async handleScrollCommand(session: WebSession, command: WebCommand): Promise<WebResponse> {
|
||||
const { x, y, selector } = command.data;
|
||||
|
||||
try {
|
||||
if (selector) {
|
||||
await session.page.evaluate((sel) => {
|
||||
const element = document.querySelector(sel);
|
||||
if (element) {
|
||||
element.scrollIntoView();
|
||||
}
|
||||
}, selector);
|
||||
} else {
|
||||
await session.page.evaluate((scrollX, scrollY) => {
|
||||
window.scrollTo(scrollX, scrollY);
|
||||
}, x || 0, y || 0);
|
||||
}
|
||||
|
||||
session.lastActivity = new Date();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'scroll',
|
||||
data: { message: '滚动操作成功' },
|
||||
sessionId: session.id,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理截图命令
|
||||
*/
|
||||
private async handleScreenshotCommand(session: WebSession, command: WebCommand): Promise<WebResponse> {
|
||||
const { fullPage = false, quality = 80, type = 'jpeg' } = command.data;
|
||||
|
||||
try {
|
||||
const screenshot = await session.page.screenshot({
|
||||
fullPage,
|
||||
type: type as 'jpeg' | 'png',
|
||||
quality
|
||||
});
|
||||
|
||||
session.lastActivity = new Date();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'screenshot',
|
||||
data: {
|
||||
message: '截图成功',
|
||||
screenshot: screenshot.toString('base64'),
|
||||
format: 'base64',
|
||||
size: screenshot.length
|
||||
},
|
||||
sessionId: session.id,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理执行脚本命令
|
||||
*/
|
||||
private async handleEvalCommand(session: WebSession, command: WebCommand): Promise<WebResponse> {
|
||||
const { script, args = [] } = command.data;
|
||||
|
||||
try {
|
||||
const result = await session.page.evaluate((evalScript, evalArgs) => {
|
||||
// 在页面上下文中执行脚本
|
||||
const func = new Function('return ' + evalScript)();
|
||||
return func(...evalArgs);
|
||||
}, script, args);
|
||||
|
||||
session.lastActivity = new Date();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'eval',
|
||||
data: {
|
||||
message: '脚本执行成功',
|
||||
result
|
||||
},
|
||||
sessionId: session.id,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理cookies命令
|
||||
*/
|
||||
private async handleCookiesCommand(session: WebSession, command: WebCommand): Promise<WebResponse> {
|
||||
const { action, cookies } = command.data;
|
||||
|
||||
try {
|
||||
if (action === 'get') {
|
||||
const currentCookies = await session.page.context().cookies();
|
||||
session.cookies = currentCookies;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'cookies',
|
||||
data: { cookies: currentCookies },
|
||||
sessionId: session.id,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} else if (action === 'set' && cookies) {
|
||||
await session.page.context().addCookies(cookies);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'cookies',
|
||||
data: { message: 'Cookies设置成功' },
|
||||
sessionId: session.id,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
type: 'cookies',
|
||||
error: '不支持的cookies操作',
|
||||
sessionId: session.id,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理存储命令
|
||||
*/
|
||||
private async handleStorageCommand(session: WebSession, command: WebCommand): Promise<WebResponse> {
|
||||
const { action, key, value } = command.data;
|
||||
|
||||
try {
|
||||
if (action === 'get') {
|
||||
const storage = await session.page.evaluate((storageKey) => {
|
||||
return localStorage.getItem(storageKey);
|
||||
}, key);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'storage',
|
||||
data: { [key]: storage },
|
||||
sessionId: session.id,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} else if (action === 'set') {
|
||||
await session.page.evaluate((storageKey, storageValue) => {
|
||||
localStorage.setItem(storageKey, storageValue);
|
||||
}, key, value);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'storage',
|
||||
data: { message: '存储设置成功' },
|
||||
sessionId: session.id,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
type: 'storage',
|
||||
error: '不支持的存储操作',
|
||||
sessionId: session.id,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导航到URL
|
||||
*/
|
||||
private async navigateToUrl(session: WebSession, url: string): Promise<void> {
|
||||
try {
|
||||
await session.page.goto(url, { waitUntil: 'networkidle' });
|
||||
session.status = 'active';
|
||||
session.currentUrl = url;
|
||||
} catch (error) {
|
||||
session.status = 'error';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
*/
|
||||
private getSessionList(): any[] {
|
||||
return Array.from(this.sessions.values()).map(session => ({
|
||||
id: session.id,
|
||||
platform: session.platform,
|
||||
status: session.status,
|
||||
currentUrl: session.currentUrl,
|
||||
title: session.title,
|
||||
lastActivity: session.lastActivity,
|
||||
cookiesCount: session.cookies.length,
|
||||
storageCount: Object.keys(session.localStorage).length
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭会话
|
||||
*/
|
||||
async closeSession(sessionId: string): Promise<void> {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) {
|
||||
await session.page.close();
|
||||
this.sessions.delete(sessionId);
|
||||
console.log(`WEB会话已关闭: ${sessionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动WEB远程服务
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
console.log(`WEB远程控制服务启动在端口 ${this.config.port}`);
|
||||
console.log(`健康检查: http://localhost:${this.config.port}/health`);
|
||||
console.log('等待WEB远程控制客户端连接...');
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止WEB远程服务
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
// 关闭所有会话
|
||||
for (const session of this.sessions.values()) {
|
||||
await session.page.close();
|
||||
}
|
||||
this.sessions.clear();
|
||||
|
||||
// 关闭浏览器
|
||||
if (this.browser) {
|
||||
await this.browser.close();
|
||||
this.browser = null;
|
||||
}
|
||||
|
||||
// 关闭WebSocket服务器
|
||||
this.wss.close();
|
||||
|
||||
console.log('WEB远程控制服务已停止');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成会话ID
|
||||
*/
|
||||
private generateSessionId(): string {
|
||||
return `web-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出默认配置
|
||||
export const defaultWebRemoteConfig: WebRemoteConfig = {
|
||||
port: 8082,
|
||||
headless: true,
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
};
|
||||
|
||||
// 导出单例实例
|
||||
export const webRemoteService = new WebRemoteService(defaultWebRemoteConfig);
|
||||
Reference in New Issue
Block a user