437 lines
12 KiB
TypeScript
437 lines
12 KiB
TypeScript
|
|
/**
|
|||
|
|
* 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
|
|||
|
|
});
|