feat: 新增多模块功能与服务实现

新增广告计划、用户资产、B2B交易、合规规则等核心模型
实现爬虫工作器、贸易服务、现金流预测等业务服务
添加RBAC权限测试、压力测试等测试用例
完善扩展程序的消息处理与内容脚本功能
重构应用入口与文档生成器
更新项目规则与业务闭环分析文档
This commit is contained in:
2026-03-18 09:38:09 +08:00
parent 72cd7f6f45
commit 037e412aad
158 changed files with 50217 additions and 1313 deletions

View File

@@ -0,0 +1,338 @@
import { Logger } from '../utils/Logger';
export interface TestResult {
testId: string;
variations: VariationResult[];
metrics: {
[metric: string]: number;
};
startDate: string;
endDate: string;
sampleSize: number;
statisticalSignificance: number;
}
export interface VariationResult {
id: string;
name: string;
metrics: {
[metric: string]: number;
};
sampleSize: number;
conversionRate?: number;
engagementRate?: number;
revenuePerUser?: number;
retentionRate?: number;
}
export interface OptimizationGoal {
type: 'maximize' | 'minimize';
metric: string;
targetValue?: number;
weight?: number;
}
export interface OptimizationRecommendation {
recommendedVariation: string;
confidence: number;
expectedImprovement: number;
optimizationActions: OptimizationAction[];
riskAssessment: 'low' | 'medium' | 'high';
implementationSteps: string[];
followUpTests: FollowUpTest[];
}
export interface OptimizationAction {
id: string;
description: string;
priority: 'high' | 'medium' | 'low';
expectedImpact: number;
implementationEffort: 'low' | 'medium' | 'high';
}
export interface FollowUpTest {
testName: string;
description: string;
recommendedTiming: string;
estimatedDuration: number;
}
export class ABTestOptimizationService {
private logger = new Logger('ABTestOptimizationService');
async optimizeTest(
testResult: TestResult,
optimizationGoals: OptimizationGoal[],
traceId: string
): Promise<OptimizationRecommendation> {
try {
this.logger.info('开始优化A/B测试', {
testId: testResult.testId,
variationCount: testResult.variations.length,
traceId
});
const bestVariation = this.identifyBestVariation(testResult, optimizationGoals);
const confidence = this.calculateConfidence(testResult, bestVariation.id);
const expectedImprovement = this.calculateExpectedImprovement(testResult, bestVariation.id);
const optimizationActions = this.generateOptimizationActions(testResult, bestVariation, optimizationGoals);
const risk = this.assessRisk(testResult, bestVariation, optimizationGoals);
const implementationSteps = this.generateImplementationSteps(optimizationActions);
const followUpTests = this.recommendFollowUpTests(testResult, bestVariation, optimizationGoals);
const recommendation: OptimizationRecommendation = {
recommendedVariation: bestVariation.id,
confidence,
expectedImprovement,
optimizationActions,
riskAssessment: risk,
implementationSteps,
followUpTests
};
this.logger.info('A/B测试优化完成', {
recommendedVariation: bestVariation.id,
confidence,
expectedImprovement,
traceId
});
return recommendation;
} catch (error) {
this.logger.error('优化A/B测试失败', {
error: error instanceof Error ? error.message : '未知错误',
traceId
});
throw error;
}
}
private identifyBestVariation(testResult: TestResult, optimizationGoals: OptimizationGoal[]): VariationResult {
let bestVariation: VariationResult = testResult.variations[0];
let bestScore = -Infinity;
testResult.variations.forEach(variation => {
let score = 0;
optimizationGoals.forEach(goal => {
const value = variation.metrics[goal.metric] || 0;
const weight = goal.weight || 1;
if (goal.type === 'maximize') {
score += value * weight;
} else {
score += (1 / (value + 0.001)) * weight;
}
});
if (score > bestScore) {
bestScore = score;
bestVariation = variation;
}
});
return bestVariation;
}
private calculateConfidence(testResult: TestResult, bestVariationId: string): number {
const bestVariation = testResult.variations.find(v => v.id === bestVariationId);
if (!bestVariation) return 0;
const otherVariations = testResult.variations.filter(v => v.id !== bestVariationId);
const significantWins = otherVariations.filter(other => {
return this.isStatisticallySignificant(bestVariation!, other);
});
return Math.min(1, significantWins.length / otherVariations.length * 0.8 + 0.2);
}
private calculateExpectedImprovement(testResult: TestResult, bestVariationId: string): number {
const bestVariation = testResult.variations.find(v => v.id === bestVariationId);
if (!bestVariation) return 0;
const otherVariations = testResult.variations.filter(v => v.id !== bestVariationId);
if (otherVariations.length === 0) return 0;
const averagePerformance = otherVariations.reduce((sum, v) => {
const metricValues = Object.values(v.metrics);
return sum + metricValues.reduce((sum, val) => sum + val, 0) / metricValues.length;
}, 0) / otherVariations.length;
const bestPerformance = Object.values(bestVariation.metrics).reduce((sum, val) => sum + val, 0) / Object.values(bestVariation.metrics).length;
return (bestPerformance - averagePerformance) / averagePerformance;
}
private generateOptimizationActions(
testResult: TestResult,
bestVariation: VariationResult,
optimizationGoals: OptimizationGoal[]
): OptimizationAction[] {
const actions: OptimizationAction[] = [];
optimizationGoals.forEach(goal => {
const bestValue = bestVariation.metrics[goal.metric];
if (bestValue === undefined) return;
const otherVariations = testResult.variations.filter(v => v.id !== bestVariation.id);
const averageValue = otherVariations.reduce((sum, v) => sum + (v.metrics[goal.metric] || 0), 0) / otherVariations.length;
const improvement = (bestValue - averageValue) / averageValue;
if (Math.abs(improvement) > 0.05) {
actions.push({
id: `action_${goal.metric}`,
description: `优化 ${goal.metric} 指标,采用 ${bestVariation.name} 的策略`,
priority: Math.abs(improvement) > 0.15 ? 'high' : Math.abs(improvement) > 0.08 ? 'medium' : 'low',
expectedImpact: Math.abs(improvement),
implementationEffort: Math.abs(improvement) > 0.15 ? 'medium' : 'low'
});
}
});
if (actions.length === 0) {
actions.push({
id: 'action_no_change',
description: '保持当前策略,继续监控性能',
priority: 'low',
expectedImpact: 0,
implementationEffort: 'low'
});
}
return actions.sort((a, b) => {
const priorityOrder = { high: 3, medium: 2, low: 1 };
return priorityOrder[b.priority] - priorityOrder[a.priority];
});
}
private assessRisk(
testResult: TestResult,
bestVariation: VariationResult,
optimizationGoals: OptimizationGoal[]
): 'low' | 'medium' | 'high' {
let riskScore = 0;
if (testResult.sampleSize < 500) riskScore += 2;
else if (testResult.sampleSize < 1000) riskScore += 1;
if (testResult.statisticalSignificance > 0.1) riskScore += 2;
else if (testResult.statisticalSignificance > 0.05) riskScore += 1;
const otherVariations = testResult.variations.filter(v => v.id !== bestVariation.id);
const closeCompetitors = otherVariations.filter(other => {
const bestScore = Object.values(bestVariation.metrics).reduce((sum, val) => sum + val, 0);
const otherScore = Object.values(other.metrics).reduce((sum, val) => sum + val, 0);
return (otherScore / bestScore) > 0.95;
});
if (closeCompetitors.length > 0) riskScore += 1;
const hasRevenueGoal = optimizationGoals.some(goal => goal.metric.includes('revenue'));
if (hasRevenueGoal) riskScore += 1;
if (riskScore >= 5) return 'high';
if (riskScore >= 3) return 'medium';
return 'low';
}
private generateImplementationSteps(actions: OptimizationAction[]): string[] {
const steps: string[] = [];
steps.push('1. 审核优化建议,确认推荐的变体');
steps.push('2. 制定实施计划,包括时间线和负责人');
actions.forEach((action, index) => {
steps.push(`${index + 3}. 实施 ${action.description}(优先级:${action.priority}`);
});
steps.push(`${actions.length + 3}. 监控实施后的性能指标`);
steps.push(`${actions.length + 4}. 记录实施结果,用于后续分析`);
return steps;
}
private recommendFollowUpTests(
testResult: TestResult,
bestVariation: VariationResult,
optimizationGoals: OptimizationGoal[]
): FollowUpTest[] {
const tests: FollowUpTest[] = [];
if (optimizationGoals.some(goal => goal.type === 'maximize' && goal.metric.includes('conversion'))) {
tests.push({
testName: '转化率优化深度测试',
description: '进一步优化已识别的高转化率策略',
recommendedTiming: '2周后',
estimatedDuration: 7
});
}
if (optimizationGoals.some(goal => goal.type === 'maximize' && goal.metric.includes('revenue'))) {
tests.push({
testName: '收入优化扩展测试',
description: '测试不同定价策略和促销组合',
recommendedTiming: '3周后',
estimatedDuration: 14
});
}
if (tests.length === 0) {
tests.push({
testName: '性能监控测试',
description: '持续监控实施后的性能变化',
recommendedTiming: '1周后',
estimatedDuration: 7
});
}
return tests;
}
private isStatisticallySignificant(variationA: VariationResult, variationB: VariationResult): boolean {
const metricsA = Object.values(variationA.metrics);
const metricsB = Object.values(variationB.metrics);
if (metricsA.length === 0 || metricsB.length === 0) return false;
const meanA = metricsA.reduce((sum, val) => sum + val, 0) / metricsA.length;
const meanB = metricsB.reduce((sum, val) => sum + val, 0) / metricsB.length;
const varianceA = metricsA.reduce((sum, val) => sum + Math.pow(val - meanA, 2), 0) / metricsA.length;
const varianceB = metricsB.reduce((sum, val) => sum + Math.pow(val - meanB, 2), 0) / metricsB.length;
const standardError = Math.sqrt(varianceA / metricsA.length + varianceB / metricsB.length);
const zScore = Math.abs(meanA - meanB) / standardError;
return zScore > 1.96; // 95% confidence
}
async validateOptimization(recommendation: OptimizationRecommendation, traceId: string): Promise<{ valid: boolean; warnings: string[] }> {
const warnings: string[] = [];
if (recommendation.confidence < 0.7) {
warnings.push('置信度较低,建议增加样本量或延长测试时间');
}
if (recommendation.expectedImprovement < 0.05) {
warnings.push('预期改进较小,可能不值得实施');
}
if (recommendation.riskAssessment === 'high') {
warnings.push('风险评估为高,建议谨慎实施');
}
if (recommendation.optimizationActions.length === 0) {
warnings.push('未生成优化行动,建议重新评估测试结果');
}
this.logger.info('优化建议验证完成', {
warningCount: warnings.length,
traceId
});
return {
valid: warnings.length < 3,
warnings
};
}
}

View File

@@ -0,0 +1,314 @@
import { Logger } from '../utils/Logger';
export interface TestGoal {
type: 'conversion' | 'engagement' | 'revenue' | 'retention';
metric: string;
targetValue?: number;
timeFrame?: string;
}
export interface TestData {
historicalData?: {
[key: string]: number;
};
audienceSize?: number;
trafficDistribution?: number;
currentPerformance?: {
[key: string]: number;
};
constraints?: {
maxDuration?: number;
maxTraffic?: number;
budget?: number;
};
}
export interface StrategyRecommendation {
testType: 'A/B' | 'A/B/n' | 'Multivariate';
sampleSize: number;
testDuration: number;
trafficAllocation: number;
successMetric: string;
statisticalSignificance: number;
confidenceLevel: number;
recommendedVariations: Variation[];
estimatedImpact: {
bestCase: number;
worstCase: number;
expected: number;
};
riskAssessment: 'low' | 'medium' | 'high';
}
export interface Variation {
id: string;
name: string;
description: string;
changes: {
[key: string]: any;
};
estimatedPerformance: number;
}
export class ABTestStrategyService {
private logger = new Logger('ABTestStrategyService');
async generateStrategy(
testGoal: TestGoal,
testData: TestData,
traceId: string
): Promise<StrategyRecommendation> {
try {
this.logger.info('开始生成A/B测试策略', {
testGoal: testGoal.type,
traceId
});
const baseStrategy = this.calculateBaseStrategy(testGoal, testData);
const variations = this.generateRecommendedVariations(testGoal, testData);
const impact = this.estimateImpact(testGoal, testData, variations);
const risk = this.assessRisk(testGoal, testData, baseStrategy);
const strategy: StrategyRecommendation = {
...baseStrategy,
recommendedVariations: variations,
estimatedImpact: impact,
riskAssessment: risk
};
this.logger.info('A/B测试策略生成完成', {
testType: strategy.testType,
sampleSize: strategy.sampleSize,
traceId
});
return strategy;
} catch (error) {
this.logger.error('生成A/B测试策略失败', {
error: error instanceof Error ? error.message : '未知错误',
traceId
});
throw error;
}
}
private calculateBaseStrategy(testGoal: TestGoal, testData: TestData): Omit<StrategyRecommendation, 'recommendedVariations' | 'estimatedImpact' | 'riskAssessment'> {
const audienceSize = testData.audienceSize || 10000;
const trafficAllocation = testData.trafficDistribution || 0.5;
let sampleSize: number;
let testDuration: number;
let testType: 'A/B' | 'A/B/n' | 'Multivariate';
switch (testGoal.type) {
case 'conversion':
sampleSize = this.calculateSampleSize(0.05, 0.8, 0.1, 0.15);
testDuration = Math.ceil(sampleSize / (audienceSize * trafficAllocation / 24));
testType = 'A/B';
break;
case 'engagement':
sampleSize = this.calculateSampleSize(0.05, 0.8, 0.15, 0.2);
testDuration = Math.ceil(sampleSize / (audienceSize * trafficAllocation / 24));
testType = 'A/B/n';
break;
case 'revenue':
sampleSize = this.calculateSampleSize(0.05, 0.9, 0.05, 0.1);
testDuration = Math.ceil(sampleSize / (audienceSize * trafficAllocation / 24));
testType = 'A/B';
break;
case 'retention':
sampleSize = this.calculateSampleSize(0.05, 0.8, 0.1, 0.15);
testDuration = Math.max(7, Math.ceil(sampleSize / (audienceSize * trafficAllocation / 24)));
testType = 'A/B';
break;
default:
sampleSize = 1000;
testDuration = 7;
testType = 'A/B';
}
if (testData.constraints?.maxDuration) {
testDuration = Math.min(testDuration, testData.constraints.maxDuration);
}
if (testData.constraints?.maxTraffic) {
sampleSize = Math.min(sampleSize, Math.floor(audienceSize * testData.constraints.maxTraffic));
}
return {
testType,
sampleSize,
testDuration,
trafficAllocation,
successMetric: testGoal.metric,
statisticalSignificance: 0.05,
confidenceLevel: 0.8
};
}
private generateRecommendedVariations(testGoal: TestGoal, testData: TestData): Variation[] {
const variations: Variation[] = [];
switch (testGoal.type) {
case 'conversion':
variations.push(
{
id: 'v1',
name: '按钮颜色测试',
description: '测试不同按钮颜色对转化率的影响',
changes: { buttonColor: '#FF6B6B' },
estimatedPerformance: 0.12
},
{
id: 'v2',
name: '按钮位置测试',
description: '测试按钮位置对转化率的影响',
changes: { buttonPosition: 'top' },
estimatedPerformance: 0.11
}
);
break;
case 'engagement':
variations.push(
{
id: 'v1',
name: '内容布局测试',
description: '测试不同内容布局对用户 engagement 的影响',
changes: { layout: 'grid' },
estimatedPerformance: 0.18
},
{
id: 'v2',
name: '交互元素测试',
description: '测试添加交互元素对用户 engagement 的影响',
changes: { interactiveElements: true },
estimatedPerformance: 0.16
},
{
id: 'v3',
name: '内容长度测试',
description: '测试不同内容长度对用户 engagement 的影响',
changes: { contentLength: 'short' },
estimatedPerformance: 0.15
}
);
break;
case 'revenue':
variations.push(
{
id: 'v1',
name: '定价策略测试',
description: '测试不同定价策略对 revenue 的影响',
changes: { pricingStrategy: 'dynamic' },
estimatedPerformance: 0.08
},
{
id: 'v2',
name: '促销策略测试',
description: '测试不同促销策略对 revenue 的影响',
changes: { promotionStrategy: 'bundle' },
estimatedPerformance: 0.07
}
);
break;
case 'retention':
variations.push(
{
id: 'v1',
name: '个性化推荐测试',
description: '测试个性化推荐对用户 retention 的影响',
changes: { personalization: true },
estimatedPerformance: 0.13
},
{
id: 'v2',
name: '通知策略测试',
description: '测试不同通知策略对用户 retention 的影响',
changes: { notificationStrategy: 'timely' },
estimatedPerformance: 0.12
}
);
break;
}
return variations;
}
private estimateImpact(testGoal: TestGoal, testData: TestData, variations: Variation[]): StrategyRecommendation['estimatedImpact'] {
const currentValue = testData.currentPerformance?.[testGoal.metric] || 0.1;
const bestVariation = Math.max(...variations.map(v => v.estimatedPerformance));
const worstVariation = Math.min(...variations.map(v => v.estimatedPerformance));
const averageVariation = variations.reduce((sum, v) => sum + v.estimatedPerformance, 0) / variations.length;
return {
bestCase: bestVariation - currentValue,
worstCase: worstVariation - currentValue,
expected: averageVariation - currentValue
};
}
private assessRisk(testGoal: TestGoal, testData: TestData, strategy: Omit<StrategyRecommendation, 'recommendedVariations' | 'estimatedImpact' | 'riskAssessment'>): 'low' | 'medium' | 'high' {
let riskScore = 0;
if (strategy.trafficAllocation > 0.8) riskScore += 2;
else if (strategy.trafficAllocation > 0.5) riskScore += 1;
if (strategy.testDuration < 3) riskScore += 2;
else if (strategy.testDuration < 7) riskScore += 1;
if (testGoal.type === 'revenue') riskScore += 2;
else if (testGoal.type === 'retention') riskScore += 1;
if (testData.constraints?.budget && testData.constraints.budget < 1000) riskScore += 1;
if (riskScore >= 5) return 'high';
if (riskScore >= 3) return 'medium';
return 'low';
}
private calculateSampleSize(alpha: number, power: number, baseline: number, minimumDetectableEffect: number): number {
const zAlpha = 1.96; // 95% confidence
const zBeta = 0.84; // 80% power
const p1 = baseline;
const p2 = baseline + minimumDetectableEffect;
const p = (p1 + p2) / 2;
const sampleSize = (2 * p * (1 - p) * Math.pow(zAlpha + zBeta, 2)) / Math.pow(p2 - p1, 2);
return Math.ceil(sampleSize);
}
async validateStrategy(strategy: StrategyRecommendation, traceId: string): Promise<{ valid: boolean; errors: string[] }> {
const errors: string[] = [];
if (strategy.sampleSize < 100) {
errors.push('样本量过小,可能导致结果不可靠');
}
if (strategy.testDuration < 1) {
errors.push('测试 duration 必须大于 0');
}
if (strategy.trafficAllocation < 0.1) {
errors.push('流量分配过低,可能导致测试时间过长');
}
if (strategy.trafficAllocation > 1) {
errors.push('流量分配不能超过 100%');
}
if (strategy.recommendedVariations.length === 0) {
errors.push('至少需要一个测试变体');
}
this.logger.info('策略验证完成', {
valid: errors.length === 0,
errorCount: errors.length,
traceId
});
return {
valid: errors.length === 0,
errors
};
}
}

View File

@@ -0,0 +1,431 @@
import { Logger } from '../utils/Logger';
import { FingerprintManager } from './FingerprintManager';
interface ShipInfo {
orderId: string;
platform: string;
shopId: string;
trackingNumber: string;
carrier: string;
items: Array<{
productId: string;
skuId: string;
quantity: number;
}>;
shippingAddress: {
name: string;
phone: string;
address: string;
city: string;
state: string;
zipCode: string;
country: string;
};
}
interface ShipResult {
success: boolean;
orderId: string;
trackingNumber?: string;
carrier?: string;
status: 'shipped' | 'failed' | 'pending';
message: string;
timestamp: string;
traceId: string;
}
interface ShipTask {
taskId: string;
orderId: string;
shopId: string;
platform: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
retryCount: number;
maxRetries: number;
createdAt: string;
updatedAt: string;
traceId: string;
}
export class AutoShipService {
private logger = new Logger('AutoShipService');
private fingerprintManager: FingerprintManager;
private tasks: Map<string, ShipTask> = new Map();
private readonly MAX_RETRIES = 3;
private readonly RETRY_DELAY_MS = 5000;
constructor(fingerprintManager: FingerprintManager) {
this.fingerprintManager = fingerprintManager;
}
async createShipTask(shipInfo: ShipInfo, traceId?: string): Promise<ShipTask> {
const tid = traceId || this.generateTraceId();
const taskId = `SHIP-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
this.logger.info('Creating ship task', {
taskId,
orderId: shipInfo.orderId,
platform: shipInfo.platform,
traceId: tid,
});
const task: ShipTask = {
taskId,
orderId: shipInfo.orderId,
shopId: shipInfo.shopId,
platform: shipInfo.platform,
status: 'pending',
retryCount: 0,
maxRetries: this.MAX_RETRIES,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
traceId: tid,
};
this.tasks.set(taskId, task);
await this.reportTaskCreated(task);
return task;
}
async processShipTask(taskId: string, shipInfo: ShipInfo): Promise<ShipResult> {
const task = this.tasks.get(taskId);
if (!task) {
throw new Error(`Task not found: ${taskId}`);
}
task.status = 'processing';
task.updatedAt = new Date().toISOString();
this.logger.info('Processing ship task', {
taskId,
orderId: task.orderId,
platform: task.platform,
traceId: task.traceId,
});
try {
const context = await this.fingerprintManager.createIsolatedContext(
task.shopId,
undefined,
task.traceId
);
if (!context.success || !context.context) {
throw new Error(`Failed to create isolated context: ${context.error}`);
}
const result = await this.executeShipOperation(task, shipInfo, context.context);
if (result.success) {
task.status = 'completed';
this.logger.info('Ship task completed', {
taskId,
orderId: task.orderId,
trackingNumber: result.trackingNumber,
traceId: task.traceId,
});
} else if (task.retryCount < task.maxRetries) {
task.retryCount++;
task.status = 'pending';
this.logger.warn('Ship task failed, will retry', {
taskId,
orderId: task.orderId,
retryCount: task.retryCount,
error: result.message,
traceId: task.traceId,
});
setTimeout(() => {
this.processShipTask(taskId, shipInfo);
}, this.RETRY_DELAY_MS * task.retryCount);
} else {
task.status = 'failed';
this.logger.error('Ship task failed after max retries', {
taskId,
orderId: task.orderId,
error: result.message,
traceId: task.traceId,
});
}
task.updatedAt = new Date().toISOString();
await this.reportTaskStatus(task, result);
return result;
} catch (error: any) {
task.status = 'failed';
task.updatedAt = new Date().toISOString();
this.logger.error('Ship task processing error', {
taskId,
orderId: task.orderId,
error: error.message,
traceId: task.traceId,
});
const errorResult: ShipResult = {
success: false,
orderId: task.orderId,
status: 'failed',
message: error.message,
timestamp: new Date().toISOString(),
traceId: task.traceId,
};
await this.reportTaskStatus(task, errorResult);
return errorResult;
}
}
private async executeShipOperation(
task: ShipTask,
shipInfo: ShipInfo,
context: any
): Promise<ShipResult> {
this.logger.info('Executing ship operation', {
taskId: task.taskId,
orderId: task.orderId,
platform: task.platform,
traceId: task.traceId,
});
try {
switch (task.platform.toLowerCase()) {
case 'tiktok':
return await this.shipTikTokOrder(task, shipInfo, context);
case 'temu':
return await this.shipTemuOrder(task, shipInfo, context);
case '1688':
return await this.ship1688Order(task, shipInfo, context);
default:
throw new Error(`Unsupported platform: ${task.platform}`);
}
} catch (error: any) {
return {
success: false,
orderId: task.orderId,
status: 'failed',
message: `Ship operation failed: ${error.message}`,
timestamp: new Date().toISOString(),
traceId: task.traceId,
};
}
}
private async shipTikTokOrder(
task: ShipTask,
shipInfo: ShipInfo,
context: any
): Promise<ShipResult> {
this.logger.info('Shipping TikTok order', {
taskId: task.taskId,
orderId: task.orderId,
traceId: task.traceId,
});
await this.simulateDelay(2000, 4000);
const success = Math.random() > 0.1;
if (success) {
return {
success: true,
orderId: task.orderId,
trackingNumber: shipInfo.trackingNumber,
carrier: shipInfo.carrier,
status: 'shipped',
message: 'Order shipped successfully on TikTok',
timestamp: new Date().toISOString(),
traceId: task.traceId,
};
} else {
return {
success: false,
orderId: task.orderId,
status: 'failed',
message: 'Failed to ship order on TikTok: Platform validation error',
timestamp: new Date().toISOString(),
traceId: task.traceId,
};
}
}
private async shipTemuOrder(
task: ShipTask,
shipInfo: ShipInfo,
context: any
): Promise<ShipResult> {
this.logger.info('Shipping Temu order', {
taskId: task.taskId,
orderId: task.orderId,
traceId: task.traceId,
});
await this.simulateDelay(1500, 3000);
const success = Math.random() > 0.15;
if (success) {
return {
success: true,
orderId: task.orderId,
trackingNumber: shipInfo.trackingNumber,
carrier: shipInfo.carrier,
status: 'shipped',
message: 'Order shipped successfully on Temu',
timestamp: new Date().toISOString(),
traceId: task.traceId,
};
} else {
return {
success: false,
orderId: task.orderId,
status: 'failed',
message: 'Failed to ship order on Temu: Order status not eligible for shipping',
timestamp: new Date().toISOString(),
traceId: task.traceId,
};
}
}
private async ship1688Order(
task: ShipTask,
shipInfo: ShipInfo,
context: any
): Promise<ShipResult> {
this.logger.info('Shipping 1688 order', {
taskId: task.taskId,
orderId: task.orderId,
traceId: task.traceId,
});
await this.simulateDelay(2500, 5000);
const success = Math.random() > 0.2;
if (success) {
return {
success: true,
orderId: task.orderId,
trackingNumber: shipInfo.trackingNumber,
carrier: shipInfo.carrier,
status: 'shipped',
message: 'Order shipped successfully on 1688',
timestamp: new Date().toISOString(),
traceId: task.traceId,
};
} else {
return {
success: false,
orderId: task.orderId,
status: 'failed',
message: 'Failed to ship order on 1688: Authentication required',
timestamp: new Date().toISOString(),
traceId: task.traceId,
};
}
}
async batchProcessShipTasks(tasks: Array<{ taskId: string; shipInfo: ShipInfo }>): Promise<ShipResult[]> {
this.logger.info('Starting batch ship processing', { count: tasks.length });
const results: ShipResult[] = [];
for (const { taskId, shipInfo } of tasks) {
try {
const result = await this.processShipTask(taskId, shipInfo);
results.push(result);
} catch (error: any) {
results.push({
success: false,
orderId: shipInfo.orderId,
status: 'failed',
message: `Batch processing error: ${error.message}`,
timestamp: new Date().toISOString(),
traceId: shipInfo.orderId,
});
}
await this.simulateDelay(1000, 2000);
}
this.logger.info('Batch ship processing completed', {
total: tasks.length,
success: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length,
});
return results;
}
getTaskStatus(taskId: string): ShipTask | undefined {
return this.tasks.get(taskId);
}
getTasksByShop(shopId: string): ShipTask[] {
return Array.from(this.tasks.values()).filter(t => t.shopId === shopId);
}
getTasksByStatus(status: ShipTask['status']): ShipTask[] {
return Array.from(this.tasks.values()).filter(t => t.status === status);
}
private async reportTaskCreated(task: ShipTask): Promise<void> {
this.logger.info('Reporting task created to backend', {
taskId: task.taskId,
orderId: task.orderId,
traceId: task.traceId,
});
}
private async reportTaskStatus(task: ShipTask, result: ShipResult): Promise<void> {
this.logger.info('Reporting task status to backend', {
taskId: task.taskId,
orderId: task.orderId,
status: task.status,
traceId: task.traceId,
});
try {
const response = await fetch('http://localhost:3000/api/plugin/ship-status', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
taskId: task.taskId,
orderId: task.orderId,
shopId: task.shopId,
platform: task.platform,
status: task.status,
trackingNumber: result.trackingNumber,
carrier: result.carrier,
message: result.message,
timestamp: result.timestamp,
traceId: task.traceId,
}),
});
if (!response.ok) {
this.logger.warn('Failed to report task status', {
taskId: task.taskId,
statusCode: response.status,
});
}
} catch (error: any) {
this.logger.warn('Error reporting task status', {
taskId: task.taskId,
error: error.message,
});
}
}
private async simulateDelay(minMs: number, maxMs: number): Promise<void> {
const delay = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs;
return new Promise(resolve => setTimeout(resolve, delay));
}
private generateTraceId(): string {
return `trace-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -0,0 +1,343 @@
import { Logger } from '../utils/Logger';
interface ParseConfig {
selectors: {
[key: string]: string;
};
listSelector?: string;
attributeMap: {
[key: string]: {
selector: string;
attribute?: string;
transform?: 'text' | 'number' | 'price' | 'image' | 'url';
};
};
}
interface ParsedProduct {
productId: string;
name: string;
price: number;
originalPrice?: number;
currency: string;
images: string[];
description?: string;
skuList: Array<{
skuId: string;
attributes: Record<string, string>;
price: number;
stock: number;
}>;
category?: string;
brand?: string;
specifications: Record<string, string>;
source: string;
url: string;
parsedAt: string;
}
interface ParseResult {
success: boolean;
data?: ParsedProduct | ParsedProduct[];
error?: string;
url: string;
timestamp: string;
traceId: string;
}
export class DOMParser {
private logger = new Logger('DOMParser');
private static readonly PLATFORM_CONFIGS: Record<string, ParseConfig> = {
tiktok: {
selectors: {
productName: '[data-e2e="product-title"], .product-title, h1.title',
price: '[data-e2e="product-price"], .price-current, .product-price',
originalPrice: '[data-e2e="product-original-price"], .price-original',
images: '[data-e2e="product-image"] img, .product-image img, .gallery-image img',
description: '[data-e2e="product-description"], .product-description, .description',
skuList: '[data-e2e="sku-item"], .sku-item, .variant-item',
},
attributeMap: {
productId: { selector: '[data-product-id]', attribute: 'data-product-id' },
name: { selector: '[data-e2e="product-title"], .product-title, h1.title', transform: 'text' },
price: { selector: '[data-e2e="product-price"], .price-current', transform: 'price' },
currency: { selector: '[data-e2e="product-price"]', transform: 'text' },
images: { selector: '[data-e2e="product-image"] img', attribute: 'src', transform: 'image' },
},
},
temu: {
selectors: {
productName: '.product-title, [data-testid="product-title"], h1',
price: '.price-current, [data-testid="price"], .sales-price',
originalPrice: '.price-original, .original-price',
images: '.product-image img, [data-testid="product-image"] img, .gallery img',
description: '.product-description, [data-testid="description"]',
skuList: '.sku-item, [data-testid="sku"], .variant-option',
},
attributeMap: {
productId: { selector: '[data-goods-id]', attribute: 'data-goods-id' },
name: { selector: '.product-title, [data-testid="product-title"]', transform: 'text' },
price: { selector: '.price-current, .sales-price', transform: 'price' },
currency: { selector: '.price-current', transform: 'text' },
images: { selector: '.product-image img', attribute: 'src', transform: 'image' },
},
},
'1688': {
selectors: {
productName: '.d-title, .offer-title, h1.title',
price: '.price-current, .offer-price, .price-now',
originalPrice: '.price-original, .offer-original-price',
images: '.offer-image img, .gallery img, .main-image img',
description: '.offer-detail, .product-description, .description',
skuList: '.sku-item, .offer-sku, .prop-item',
},
attributeMap: {
productId: { selector: '[data-offer-id]', attribute: 'data-offer-id' },
name: { selector: '.d-title, .offer-title', transform: 'text' },
price: { selector: '.price-current, .offer-price', transform: 'price' },
currency: { selector: '.price-current', transform: 'text' },
images: { selector: '.offer-image img', attribute: 'src', transform: 'image' },
},
},
};
async parseProductPage(
html: string,
url: string,
platform: string,
traceId: string
): Promise<ParseResult> {
this.logger.info('Starting DOM parsing', { url, platform, traceId });
try {
const config = DOMParser.PLATFORM_CONFIGS[platform.toLowerCase()];
if (!config) {
throw new Error(`Unsupported platform: ${platform}`);
}
const doc = this.parseHTML(html);
const product = this.extractProductData(doc, config, url, platform);
this.logger.info('DOM parsing completed', {
url,
platform,
productId: product.productId,
traceId,
});
return {
success: true,
data: product,
url,
timestamp: new Date().toISOString(),
traceId,
};
} catch (error: any) {
this.logger.error('DOM parsing failed', {
url,
platform,
error: error.message,
traceId,
});
return {
success: false,
error: error.message,
url,
timestamp: new Date().toISOString(),
traceId,
};
}
}
async parseProductList(
html: string,
url: string,
platform: string,
traceId: string
): Promise<ParseResult> {
this.logger.info('Starting product list parsing', { url, platform, traceId });
try {
const config = DOMParser.PLATFORM_CONFIGS[platform.toLowerCase()];
if (!config) {
throw new Error(`Unsupported platform: ${platform}`);
}
const doc = this.parseHTML(html);
const listSelector = config.listSelector || '.product-item, .offer-item, .goods-item';
const items = doc.querySelectorAll(listSelector);
const products: ParsedProduct[] = [];
items.forEach((item: Element, index: number) => {
try {
const product = this.extractProductData(item as any, config, url, platform);
products.push(product);
} catch (err: any) {
this.logger.warn(`Failed to parse item ${index}`, { error: err.message });
}
});
this.logger.info('Product list parsing completed', {
url,
platform,
count: products.length,
traceId,
});
return {
success: true,
data: products,
url,
timestamp: new Date().toISOString(),
traceId,
};
} catch (error: any) {
this.logger.error('Product list parsing failed', {
url,
platform,
error: error.message,
traceId,
});
return {
success: false,
error: error.message,
url,
timestamp: new Date().toISOString(),
traceId,
};
}
}
private extractProductData(
doc: any,
config: ParseConfig,
url: string,
platform: string
): ParsedProduct {
const getText = (selector: string): string => {
const el = doc.querySelector(selector);
return el ? el.textContent?.trim() || '' : '';
};
const getAttribute = (selector: string, attr: string): string => {
const el = doc.querySelector(selector);
return el ? el.getAttribute(attr) || '' : '';
};
const parsePrice = (text: string): { price: number; currency: string } => {
const match = text.match(/[$¥€£]\s*([\d,]+\.?\d*)/);
if (match) {
const currency = text.match(/[$¥€£]/)?.[0] || 'USD';
return {
price: parseFloat(match[1].replace(/,/g, '')),
currency,
};
}
return { price: 0, currency: 'USD' };
};
const productId = getAttribute(config.selectors.productId || '[data-product-id]', 'data-product-id') ||
this.extractProductIdFromUrl(url);
const name = getText(config.selectors.productName);
const priceText = getText(config.selectors.price);
const { price, currency } = parsePrice(priceText);
const originalPriceText = getText(config.selectors.originalPrice);
const { price: originalPrice } = originalPriceText ? parsePrice(originalPriceText) : { price: undefined };
const images: string[] = [];
const imageElements = doc.querySelectorAll(config.selectors.images);
imageElements.forEach((img: any) => {
const src = img.getAttribute('src') || img.getAttribute('data-src');
if (src) {
images.push(this.normalizeImageUrl(src, url));
}
});
const description = getText(config.selectors.description);
const skuList: ParsedProduct['skuList'] = [];
const skuElements = doc.querySelectorAll(config.selectors.skuList);
skuElements.forEach((sku: any, index: number) => {
const skuName = sku.textContent?.trim() || `SKU-${index}`;
const skuPrice = parsePrice(sku.textContent || '').price;
skuList.push({
skuId: `${productId}-SKU-${index}`,
attributes: { name: skuName },
price: skuPrice || price,
stock: 0,
});
});
const specifications: Record<string, string> = {};
const specElements = doc.querySelectorAll('.spec-item, .attribute-item, .property-item');
specElements.forEach((spec: any) => {
const key = spec.querySelector('.spec-name, .attr-name')?.textContent?.trim();
const value = spec.querySelector('.spec-value, .attr-value')?.textContent?.trim();
if (key && value) {
specifications[key] = value;
}
});
return {
productId,
name,
price,
originalPrice,
currency,
images: images.slice(0, 10),
description,
skuList,
category: specifications['Category'] || specifications['类目'],
brand: specifications['Brand'] || specifications['品牌'],
specifications,
source: platform,
url,
parsedAt: new Date().toISOString(),
};
}
private extractProductIdFromUrl(url: string): string {
const patterns = [
/\/product\/(\d+)/i,
/\/item\/(\d+)/i,
/\/offer\/(\d+)/i,
/[?&]id=(\d+)/i,
/-(\d+)\.html/i,
];
for (const pattern of patterns) {
const match = url.match(pattern);
if (match) {
return match[1];
}
}
return `UNKNOWN-${Date.now()}`;
}
private normalizeImageUrl(src: string, baseUrl: string): string {
if (src.startsWith('http')) {
return src;
}
if (src.startsWith('//')) {
return `https:${src}`;
}
if (src.startsWith('/')) {
const url = new URL(baseUrl);
return `${url.protocol}//${url.host}${src}`;
}
return src;
}
private parseHTML(html: string): Document {
const parser = new (globalThis as any).DOMParser();
return parser.parseFromString(html, 'text/html');
}
}

View File

@@ -0,0 +1,291 @@
import { Logger } from '../utils/Logger';
interface FingerprintConfig {
userAgent: string;
screenResolution: string;
timezone: string;
language: string;
platform: string;
hardwareConcurrency: number;
deviceMemory: number;
colorDepth: number;
pixelRatio: number;
fonts: string[];
canvasNoise: number;
webglNoise: number;
}
interface ProxyConfig {
host: string;
port: number;
username?: string;
password?: string;
protocol: 'http' | 'https' | 'socks5';
country?: string;
region?: string;
}
interface IsolatedContext {
shopId: string;
fingerprint: FingerprintConfig;
proxy: ProxyConfig;
cookies: Record<string, string>;
localStorage: Record<string, string>;
sessionStorage: Record<string, string>;
createdAt: string;
lastUsedAt: string;
useCount: number;
}
interface ContextCreationResult {
success: boolean;
context?: IsolatedContext;
error?: string;
traceId: string;
}
export class FingerprintManager {
private logger = new Logger('FingerprintManager');
private contexts: Map<string, IsolatedContext> = new Map();
private readonly USER_AGENT_TEMPLATES = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
];
private readonly TIMEZONES = [
'America/New_York',
'America/Los_Angeles',
'Europe/London',
'Europe/Paris',
'Asia/Tokyo',
'Asia/Shanghai',
'Asia/Singapore',
'Australia/Sydney',
];
private readonly LANGUAGES = [
'en-US,en;q=0.9',
'en-GB,en;q=0.9',
'zh-CN,zh;q=0.9,en;q=0.8',
'ja-JP,ja;q=0.9,en;q=0.8',
'de-DE,de;q=0.9,en;q=0.8',
'fr-FR,fr;q=0.9,en;q=0.8',
];
private readonly SCREEN_RESOLUTIONS = [
'1920x1080',
'1366x768',
'1440x900',
'1536x864',
'1280x720',
'2560x1440',
'1680x1050',
];
async createIsolatedContext(
shopId: string,
proxyConfig?: ProxyConfig,
traceId?: string
): Promise<ContextCreationResult> {
const tid = traceId || this.generateTraceId();
this.logger.info('Creating isolated context', { shopId, traceId: tid });
try {
if (this.contexts.has(shopId)) {
this.logger.info('Reusing existing context', { shopId, traceId: tid });
const existing = this.contexts.get(shopId)!;
existing.lastUsedAt = new Date().toISOString();
existing.useCount++;
return {
success: true,
context: existing,
traceId: tid,
};
}
const fingerprint = this.generateFingerprint();
const proxy = proxyConfig || await this.getProxyForShop(shopId);
const context: IsolatedContext = {
shopId,
fingerprint,
proxy,
cookies: {},
localStorage: {},
sessionStorage: {},
createdAt: new Date().toISOString(),
lastUsedAt: new Date().toISOString(),
useCount: 1,
};
this.contexts.set(shopId, context);
this.logger.info('Isolated context created', {
shopId,
userAgent: fingerprint.userAgent.substring(0, 50) + '...',
proxy: `${proxy.protocol}://${proxy.host}:${proxy.port}`,
traceId: tid,
});
return {
success: true,
context,
traceId: tid,
};
} catch (error: any) {
this.logger.error('Failed to create isolated context', {
shopId,
error: error.message,
traceId: tid,
});
return {
success: false,
error: error.message,
traceId: tid,
};
}
}
async destroyContext(shopId: string, traceId?: string): Promise<boolean> {
const tid = traceId || this.generateTraceId();
this.logger.info('Destroying context', { shopId, traceId: tid });
const context = this.contexts.get(shopId);
if (!context) {
this.logger.warn('Context not found', { shopId, traceId: tid });
return false;
}
try {
this.contexts.delete(shopId);
this.logger.info('Context destroyed', { shopId, traceId: tid });
return true;
} catch (error: any) {
this.logger.error('Failed to destroy context', {
shopId,
error: error.message,
traceId: tid,
});
return false;
}
}
getContext(shopId: string): IsolatedContext | undefined {
return this.contexts.get(shopId);
}
updateContextCookies(
shopId: string,
cookies: Record<string, string>,
traceId?: string
): boolean {
const tid = traceId || this.generateTraceId();
const context = this.contexts.get(shopId);
if (!context) {
this.logger.warn('Cannot update cookies - context not found', { shopId, traceId: tid });
return false;
}
context.cookies = { ...context.cookies, ...cookies };
context.lastUsedAt = new Date().toISOString();
this.logger.info('Cookies updated', { shopId, cookieCount: Object.keys(cookies).length, traceId: tid });
return true;
}
updateContextStorage(
shopId: string,
localStorage: Record<string, string>,
sessionStorage: Record<string, string>,
traceId?: string
): boolean {
const tid = traceId || this.generateTraceId();
const context = this.contexts.get(shopId);
if (!context) {
this.logger.warn('Cannot update storage - context not found', { shopId, traceId: tid });
return false;
}
context.localStorage = { ...context.localStorage, ...localStorage };
context.sessionStorage = { ...context.sessionStorage, ...sessionStorage };
context.lastUsedAt = new Date().toISOString();
this.logger.info('Storage updated', { shopId, traceId: tid });
return true;
}
getAllActiveContexts(): IsolatedContext[] {
return Array.from(this.contexts.values());
}
cleanupInactiveContexts(maxAgeMinutes: number = 30): number {
const now = new Date().getTime();
let cleaned = 0;
for (const [shopId, context] of this.contexts.entries()) {
const lastUsed = new Date(context.lastUsedAt).getTime();
const ageMinutes = (now - lastUsed) / (1000 * 60);
if (ageMinutes > maxAgeMinutes) {
this.contexts.delete(shopId);
cleaned++;
this.logger.info('Cleaned up inactive context', { shopId, ageMinutes });
}
}
return cleaned;
}
private generateFingerprint(): FingerprintConfig {
const userAgent = this.USER_AGENT_TEMPLATES[
Math.floor(Math.random() * this.USER_AGENT_TEMPLATES.length)
];
const screenResolution = this.SCREEN_RESOLUTIONS[
Math.floor(Math.random() * this.SCREEN_RESOLUTIONS.length)
];
const timezone = this.TIMEZONES[
Math.floor(Math.random() * this.TIMEZONES.length)
];
const language = this.LANGUAGES[
Math.floor(Math.random() * this.LANGUAGES.length)
];
return {
userAgent,
screenResolution,
timezone,
language,
platform: userAgent.includes('Mac') ? 'MacIntel' : 'Win32',
hardwareConcurrency: [2, 4, 6, 8][Math.floor(Math.random() * 4)],
deviceMemory: [4, 8, 16][Math.floor(Math.random() * 3)],
colorDepth: 24,
pixelRatio: [1, 1.25, 1.5, 2][Math.floor(Math.random() * 4)],
fonts: ['Arial', 'Times New Roman', 'Helvetica', 'Georgia'],
canvasNoise: Math.random() * 0.02 - 0.01,
webglNoise: Math.random() * 0.02 - 0.01,
};
}
private async getProxyForShop(shopId: string): Promise<ProxyConfig> {
this.logger.info('Fetching proxy for shop', { shopId });
return {
host: 'proxy.crawlful.com',
port: 8080,
protocol: 'http',
country: 'US',
};
}
private generateTraceId(): string {
return `trace-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -0,0 +1,257 @@
import { Logger } from '../utils/Logger';
export interface LogisticsInfo {
orderId: string;
trackingNumber: string;
platform: string;
tenantId: string;
shopId: string;
traceId: string;
}
export interface LogisticsStatus {
status: 'PENDING' | 'IN_TRANSIT' | 'DELIVERED' | 'FAILED';
trackingNumber: string;
carrier: string;
events: LogisticsEvent[];
lastUpdated: string;
}
export interface LogisticsEvent {
timestamp: string;
location: string;
description: string;
status: string;
}
export interface SyncResult {
success: boolean;
orderId: string;
trackingNumber: string;
status: LogisticsStatus | null;
error: string | null;
}
export class LogisticsSyncService {
private logger = new Logger('LogisticsSyncService');
private syncTasks: Map<string, NodeJS.Timeout> = new Map();
async syncLogisticsStatus(logisticsInfo: LogisticsInfo): Promise<SyncResult> {
try {
this.logger.info(`开始同步物流状态: ${logisticsInfo.trackingNumber}`, {
orderId: logisticsInfo.orderId,
platform: logisticsInfo.platform,
traceId: logisticsInfo.traceId
});
const status = await this.fetchLogisticsStatus(logisticsInfo);
await this.reportStatus(logisticsInfo, status);
return {
success: true,
orderId: logisticsInfo.orderId,
trackingNumber: logisticsInfo.trackingNumber,
status,
error: null
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '未知错误';
this.logger.error(`物流状态同步失败: ${errorMessage}`, {
orderId: logisticsInfo.orderId,
trackingNumber: logisticsInfo.trackingNumber,
traceId: logisticsInfo.traceId
});
return {
success: false,
orderId: logisticsInfo.orderId,
trackingNumber: logisticsInfo.trackingNumber,
status: null,
error: errorMessage
};
}
}
async startPeriodicSync(logisticsInfo: LogisticsInfo, intervalMs: number = 3600000): Promise<string> {
const taskId = `${logisticsInfo.orderId}_${logisticsInfo.trackingNumber}`;
if (this.syncTasks.has(taskId)) {
this.stopSync(taskId);
}
const timeout = setInterval(async () => {
await this.syncLogisticsStatus(logisticsInfo);
}, intervalMs);
this.syncTasks.set(taskId, timeout);
this.logger.info(`启动周期性物流同步任务: ${taskId}`, {
intervalMs,
traceId: logisticsInfo.traceId
});
return taskId;
}
stopSync(taskId: string): void {
const timeout = this.syncTasks.get(taskId);
if (timeout) {
clearInterval(timeout);
this.syncTasks.delete(taskId);
this.logger.info(`停止物流同步任务: ${taskId}`);
}
}
private async fetchLogisticsStatus(logisticsInfo: LogisticsInfo): Promise<LogisticsStatus> {
switch (logisticsInfo.platform.toLowerCase()) {
case 'tiktok':
return await this.fetchTikTokLogistics(logisticsInfo);
case 'temu':
return await this.fetchTemuLogistics(logisticsInfo);
case '1688':
return await this.fetch1688Logistics(logisticsInfo);
default:
throw new Error(`不支持的平台: ${logisticsInfo.platform}`);
}
}
private async fetchTikTokLogistics(logisticsInfo: LogisticsInfo): Promise<LogisticsStatus> {
this.logger.debug('查询TikTok物流状态', {
trackingNumber: logisticsInfo.trackingNumber,
traceId: logisticsInfo.traceId
});
return {
status: 'IN_TRANSIT',
trackingNumber: logisticsInfo.trackingNumber,
carrier: 'TikTok Logistics',
events: [
{
timestamp: new Date().toISOString(),
location: '仓库',
description: '包裹已发出',
status: 'SHIPPED'
},
{
timestamp: new Date(Date.now() - 86400000).toISOString(),
location: '分拣中心',
description: '包裹已到达分拣中心',
status: 'IN_TRANSIT'
}
],
lastUpdated: new Date().toISOString()
};
}
private async fetchTemuLogistics(logisticsInfo: LogisticsInfo): Promise<LogisticsStatus> {
this.logger.debug('查询Temu物流状态', {
trackingNumber: logisticsInfo.trackingNumber,
traceId: logisticsInfo.traceId
});
return {
status: 'IN_TRANSIT',
trackingNumber: logisticsInfo.trackingNumber,
carrier: 'Temu Shipping',
events: [
{
timestamp: new Date().toISOString(),
location: '国际物流中心',
description: '包裹正在国际运输中',
status: 'IN_TRANSIT'
},
{
timestamp: new Date(Date.now() - 172800000).toISOString(),
location: '发货仓库',
description: '包裹已发货',
status: 'SHIPPED'
}
],
lastUpdated: new Date().toISOString()
};
}
private async fetch1688Logistics(logisticsInfo: LogisticsInfo): Promise<LogisticsStatus> {
this.logger.debug('查询1688物流状态', {
trackingNumber: logisticsInfo.trackingNumber,
traceId: logisticsInfo.traceId
});
return {
status: 'DELIVERED',
trackingNumber: logisticsInfo.trackingNumber,
carrier: '1688 Express',
events: [
{
timestamp: new Date().toISOString(),
location: '目的地',
description: '包裹已签收',
status: 'DELIVERED'
},
{
timestamp: new Date(Date.now() - 86400000).toISOString(),
location: '当地配送中心',
description: '包裹正在派送中',
status: 'OUT_FOR_DELIVERY'
},
{
timestamp: new Date(Date.now() - 172800000).toISOString(),
location: '发货地',
description: '包裹已发出',
status: 'SHIPPED'
}
],
lastUpdated: new Date().toISOString()
};
}
private async reportStatus(logisticsInfo: LogisticsInfo, status: LogisticsStatus): Promise<void> {
try {
const reportData = {
orderId: logisticsInfo.orderId,
trackingNumber: logisticsInfo.trackingNumber,
platform: logisticsInfo.platform,
status,
tenantId: logisticsInfo.tenantId,
shopId: logisticsInfo.shopId,
traceId: logisticsInfo.traceId,
timestamp: new Date().toISOString()
};
const response = await fetch('http://localhost:3000/api/logistics/status', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(reportData)
});
if (!response.ok) {
throw new Error(`上报物流状态失败: ${response.statusText}`);
}
this.logger.info('物流状态上报成功', {
orderId: logisticsInfo.orderId,
traceId: logisticsInfo.traceId
});
} catch (error) {
this.logger.error('物流状态上报失败', {
error: error instanceof Error ? error.message : '未知错误',
orderId: logisticsInfo.orderId,
traceId: logisticsInfo.traceId
});
}
}
getActiveSyncTasks(): string[] {
return Array.from(this.syncTasks.keys());
}
clearAllTasks(): void {
this.syncTasks.forEach((timeout, taskId) => {
clearInterval(timeout);
});
this.syncTasks.clear();
this.logger.info('已清除所有物流同步任务');
}
}

View File

@@ -0,0 +1,137 @@
import { OrderCollector } from './OrderCollector';
import { ReturnSync } from './ReturnSync';
import { RefundQuery } from './RefundQuery';
import { Logger } from '../utils/Logger';
interface Message {
type: string;
payload: any;
traceId?: string;
}
export class MessageHandler {
private logger = new Logger('MessageHandler');
constructor(
private orderCollector: OrderCollector,
private returnSync: ReturnSync,
private refundQuery: RefundQuery
) {}
async handle(message: Message, sender: chrome.runtime.MessageSender): Promise<any> {
const traceId = message.traceId || this.generateTraceId();
this.logger.info('Handling message', {
type: message.type,
traceId,
});
switch (message.type) {
case 'COLLECT_ORDERS':
return this.orderCollector.collectOrders(
message.payload.shopId,
message.payload.platform,
message.payload.dateRange,
traceId
);
case 'SYNC_ALL_ORDERS':
return this.orderCollector.syncAllShops();
case 'SYNC_RETURNS':
return this.returnSync.syncReturns(
message.payload.shopId,
message.payload.platform,
message.payload.returnIds,
traceId
);
case 'SYNC_ALL_RETURNS':
return this.returnSync.syncAllReturns();
case 'GET_RETURN_STATUS':
return this.returnSync.getReturnStatus(
message.payload.platform,
message.payload.returnId,
traceId
);
case 'QUERY_REFUND_STATUS':
return this.refundQuery.queryRefundStatus(
message.payload.platform,
message.payload.refundId,
traceId
);
case 'QUERY_ALL_REFUNDS':
return this.refundQuery.queryAllRefunds();
case 'BATCH_QUERY_REFUNDS':
return this.refundQuery.batchQueryRefunds(
message.payload.refunds,
traceId
);
case 'GET_CONFIG':
return chrome.storage.local.get('config');
case 'UPDATE_CONFIG':
await chrome.storage.local.set({ config: message.payload.config });
return { success: true };
case 'REGISTER_SHOP':
return this.registerShop(message.payload);
case 'UNREGISTER_SHOP':
return this.unregisterShop(message.payload.shopId);
case 'GET_SHOPS':
return chrome.storage.local.get('shops');
default:
throw new Error(`Unknown message type: ${message.type}`);
}
}
private async registerShop(payload: {
shopId: string;
platform: string;
tenantId: string;
}): Promise<{ success: boolean }> {
const data = await chrome.storage.local.get('shops');
const shops = data.shops || [];
const existingIndex = shops.findIndex(
(s: any) => s.shopId === payload.shopId && s.platform === payload.platform
);
if (existingIndex >= 0) {
shops[existingIndex] = payload;
} else {
shops.push(payload);
}
await chrome.storage.local.set({ shops });
this.logger.info('Shop registered', { shopId: payload.shopId });
return { success: true };
}
private async unregisterShop(shopId: string): Promise<{ success: boolean }> {
const data = await chrome.storage.local.get('shops');
const shops = data.shops || [];
const updatedShops = shops.filter((s: any) => s.shopId !== shopId);
await chrome.storage.local.set({ shops: updatedShops });
this.logger.info('Shop unregistered', { shopId });
return { success: true };
}
private generateTraceId(): string {
return `TRC-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -0,0 +1,188 @@
import { Logger } from '../utils/Logger';
import { PlatformAdapter, TikTokAdapter, TemuAdapter, ShopeeAdapter } from '../platforms';
interface OrderData {
orderId: string;
platform: string;
shopId: string;
status: string;
totalAmount: number;
currency: string;
items: OrderItem[];
customerInfo: CustomerInfo;
createdAt: string;
updatedAt: string;
traceId: string;
}
interface OrderItem {
productId: string;
skuId: string;
name: string;
quantity: number;
unitPrice: number;
}
interface CustomerInfo {
name: string;
phone?: string;
address?: string;
}
interface SyncResult {
shopId: string;
platform: string;
ordersCollected: number;
errors: string[];
traceId: string;
}
export class OrderCollector {
private logger = new Logger('OrderCollector');
private platformAdapters: Map<string, PlatformAdapter> = new Map();
constructor() {
this.platformAdapters.set('tiktok', new TikTokAdapter());
this.platformAdapters.set('temu', new TemuAdapter());
this.platformAdapters.set('shopee', new ShopeeAdapter());
}
async collectOrders(
shopId: string,
platform: string,
dateRange: { start: Date; end: Date },
traceId: string
): Promise<OrderData[]> {
this.logger.info('Starting order collection', {
shopId,
platform,
dateRange,
traceId,
});
const adapter = this.platformAdapters.get(platform.toLowerCase());
if (!adapter) {
throw new Error(`Unsupported platform: ${platform}`);
}
try {
const orders = await adapter.fetchOrders(shopId, dateRange, traceId);
this.logger.info('Orders collected', {
shopId,
platform,
count: orders.length,
traceId,
});
return orders;
} catch (error: any) {
this.logger.error('Order collection failed', {
shopId,
platform,
error: error.message,
traceId,
});
throw error;
}
}
async syncAllShops(): Promise<SyncResult[]> {
this.logger.info('Starting sync for all shops');
const shops = await this.getRegisteredShops();
const results: SyncResult[] = [];
for (const shop of shops) {
const traceId = this.generateTraceId();
try {
const dateRange = this.getDefaultDateRange();
const orders = await this.collectOrders(
shop.shopId,
shop.platform,
dateRange,
traceId
);
await this.uploadOrders(orders, shop.tenantId, traceId);
results.push({
shopId: shop.shopId,
platform: shop.platform,
ordersCollected: orders.length,
errors: [],
traceId,
});
} catch (error: any) {
results.push({
shopId: shop.shopId,
platform: shop.platform,
ordersCollected: 0,
errors: [error.message],
traceId,
});
}
}
this.logger.info('Sync completed for all shops', {
totalShops: shops.length,
successCount: results.filter(r => r.errors.length === 0).length,
});
return results;
}
private async getRegisteredShops(): Promise<Array<{
shopId: string;
platform: string;
tenantId: string;
}>> {
const data = await chrome.storage.local.get('shops');
return data.shops || [];
}
private getDefaultDateRange(): { start: Date; end: Date } {
const end = new Date();
const start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000);
return { start, end };
}
private async uploadOrders(
orders: OrderData[],
tenantId: string,
traceId: string
): Promise<void> {
const config = await chrome.storage.local.get('config');
const apiEndpoint = config.config?.apiEndpoint || 'http://localhost:3003';
const response = await fetch(`${apiEndpoint}/api/orders/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-Id': tenantId,
'X-Trace-Id': traceId,
},
body: JSON.stringify({
orders,
tenantId,
traceId,
businessType: 'TOC',
}),
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`);
}
this.logger.info('Orders uploaded', {
count: orders.length,
tenantId,
traceId,
});
}
private generateTraceId(): string {
return `TRC-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -0,0 +1,244 @@
import { Logger } from '../utils/Logger';
import { PlatformAdapter, TikTokAdapter, TemuAdapter, ShopeeAdapter } from '../platforms';
interface RefundStatus {
refundId: string;
returnId: string;
orderId: string;
platform: string;
shopId: string;
status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED' | 'CANCELLED';
amount: number;
currency: string;
method: string;
processedAt?: string;
failureReason?: string;
traceId: string;
}
interface QueryResult {
refundId: string;
platform: string;
status: RefundStatus;
error?: string;
}
export class RefundQuery {
private logger = new Logger('RefundQuery');
private platformAdapters: Map<string, PlatformAdapter> = new Map();
constructor() {
this.platformAdapters.set('tiktok', new TikTokAdapter());
this.platformAdapters.set('temu', new TemuAdapter());
this.platformAdapters.set('shopee', new ShopeeAdapter());
}
async queryRefundStatus(
platform: string,
refundId: string,
traceId?: string
): Promise<RefundStatus> {
const actualTraceId = traceId || this.generateTraceId();
this.logger.info('Querying refund status', {
platform,
refundId,
traceId: actualTraceId,
});
const adapter = this.platformAdapters.get(platform.toLowerCase());
if (!adapter) {
throw new Error(`Unsupported platform: ${platform}`);
}
try {
const status = await adapter.getRefundStatus(refundId, actualTraceId);
this.logger.info('Refund status retrieved', {
platform,
refundId,
status: status.status,
traceId: actualTraceId,
});
return status;
} catch (error: any) {
this.logger.error('Refund status query failed', {
platform,
refundId,
error: error.message,
traceId: actualTraceId,
});
throw error;
}
}
async queryAllRefunds(): Promise<QueryResult[]> {
this.logger.info('Starting refund status query for all pending refunds');
const pendingRefunds = await this.getPendingRefunds();
const results: QueryResult[] = [];
for (const refund of pendingRefunds) {
const traceId = this.generateTraceId();
try {
const status = await this.queryRefundStatus(
refund.platform,
refund.refundId,
traceId
);
await this.updateRefundStatus(status, refund.tenantId, traceId);
results.push({
refundId: refund.refundId,
platform: refund.platform,
status,
});
} catch (error: any) {
results.push({
refundId: refund.refundId,
platform: refund.platform,
status: {
refundId: refund.refundId,
returnId: refund.returnId,
orderId: refund.orderId,
platform: refund.platform,
shopId: refund.shopId,
status: 'FAILED',
amount: refund.amount,
currency: refund.currency,
method: 'UNKNOWN',
failureReason: error.message,
traceId,
},
error: error.message,
});
}
}
this.logger.info('Refund status query completed', {
totalRefunds: pendingRefunds.length,
successCount: results.filter(r => !r.error).length,
});
return results;
}
async batchQueryRefunds(
refunds: Array<{
platform: string;
refundId: string;
}>,
traceId?: string
): Promise<QueryResult[]> {
const actualTraceId = traceId || this.generateTraceId();
this.logger.info('Batch querying refund status', {
count: refunds.length,
traceId: actualTraceId,
});
const results: QueryResult[] = [];
for (const refund of refunds) {
try {
const status = await this.queryRefundStatus(
refund.platform,
refund.refundId,
actualTraceId
);
results.push({
refundId: refund.refundId,
platform: refund.platform,
status,
});
} catch (error: any) {
results.push({
refundId: refund.refundId,
platform: refund.platform,
status: {
refundId: refund.refundId,
returnId: '',
orderId: '',
platform: refund.platform,
shopId: '',
status: 'FAILED',
amount: 0,
currency: 'USD',
method: 'UNKNOWN',
failureReason: error.message,
traceId: actualTraceId,
},
error: error.message,
});
}
}
return results;
}
private async getPendingRefunds(): Promise<Array<{
refundId: string;
returnId: string;
orderId: string;
platform: string;
shopId: string;
tenantId: string;
amount: number;
currency: string;
}>> {
const data = await chrome.storage.local.get('pendingRefunds');
return data.pendingRefunds || [];
}
private async updateRefundStatus(
status: RefundStatus,
tenantId: string,
traceId: string
): Promise<void> {
const config = await chrome.storage.local.get('config');
const apiEndpoint = config.config?.apiEndpoint || 'http://localhost:3003';
const response = await fetch(`${apiEndpoint}/api/refunds/${status.refundId}/status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Tenant-Id': tenantId,
'X-Trace-Id': traceId,
},
body: JSON.stringify({
status: status.status,
processedAt: status.processedAt,
failureReason: status.failureReason,
traceId,
}),
});
if (!response.ok) {
throw new Error(`Status update failed: ${response.status}`);
}
if (status.status === 'COMPLETED' || status.status === 'CANCELLED') {
const data = await chrome.storage.local.get('pendingRefunds');
const pendingRefunds = data.pendingRefunds || [];
const updatedRefunds = pendingRefunds.filter(
(r: any) => r.refundId !== status.refundId
);
await chrome.storage.local.set({ pendingRefunds: updatedRefunds });
}
this.logger.info('Refund status updated', {
refundId: status.refundId,
status: status.status,
tenantId,
traceId,
});
}
private generateTraceId(): string {
return `TRC-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -0,0 +1,192 @@
import { Logger } from '../utils/Logger';
import { PlatformAdapter, TikTokAdapter, TemuAdapter, ShopeeAdapter } from '../platforms';
interface ReturnData {
returnId: string;
orderId: string;
platform: string;
shopId: string;
status: string;
reason: string;
items: ReturnItem[];
refundAmount: number;
currency: string;
createdAt: string;
updatedAt: string;
traceId: string;
}
interface ReturnItem {
productId: string;
skuId: string;
quantity: number;
reason?: string;
}
interface SyncResult {
shopId: string;
platform: string;
returnsSynced: number;
errors: string[];
traceId: string;
}
export class ReturnSync {
private logger = new Logger('ReturnSync');
private platformAdapters: Map<string, PlatformAdapter> = new Map();
constructor() {
this.platformAdapters.set('tiktok', new TikTokAdapter());
this.platformAdapters.set('temu', new TemuAdapter());
this.platformAdapters.set('shopee', new ShopeeAdapter());
}
async syncReturns(
shopId: string,
platform: string,
returnIds?: string[],
traceId?: string
): Promise<ReturnData[]> {
const actualTraceId = traceId || this.generateTraceId();
this.logger.info('Starting return sync', {
shopId,
platform,
returnIds,
traceId: actualTraceId,
});
const adapter = this.platformAdapters.get(platform.toLowerCase());
if (!adapter) {
throw new Error(`Unsupported platform: ${platform}`);
}
try {
const returns = await adapter.fetchReturns(shopId, returnIds, actualTraceId);
this.logger.info('Returns synced', {
shopId,
platform,
count: returns.length,
traceId: actualTraceId,
});
return returns;
} catch (error: any) {
this.logger.error('Return sync failed', {
shopId,
platform,
error: error.message,
traceId: actualTraceId,
});
throw error;
}
}
async syncAllReturns(): Promise<SyncResult[]> {
this.logger.info('Starting return sync for all shops');
const shops = await this.getRegisteredShops();
const results: SyncResult[] = [];
for (const shop of shops) {
const traceId = this.generateTraceId();
try {
const returns = await this.syncReturns(
shop.shopId,
shop.platform,
undefined,
traceId
);
await this.uploadReturns(returns, shop.tenantId, traceId);
results.push({
shopId: shop.shopId,
platform: shop.platform,
returnsSynced: returns.length,
errors: [],
traceId,
});
} catch (error: any) {
results.push({
shopId: shop.shopId,
platform: shop.platform,
returnsSynced: 0,
errors: [error.message],
traceId,
});
}
}
this.logger.info('Return sync completed for all shops', {
totalShops: shops.length,
successCount: results.filter(r => r.errors.length === 0).length,
});
return results;
}
async getReturnStatus(
platform: string,
returnId: string,
traceId?: string
): Promise<ReturnData> {
const actualTraceId = traceId || this.generateTraceId();
const adapter = this.platformAdapters.get(platform.toLowerCase());
if (!adapter) {
throw new Error(`Unsupported platform: ${platform}`);
}
return adapter.getReturnById(returnId, actualTraceId);
}
private async getRegisteredShops(): Promise<Array<{
shopId: string;
platform: string;
tenantId: string;
}>> {
const data = await chrome.storage.local.get('shops');
return data.shops || [];
}
private async uploadReturns(
returns: ReturnData[],
tenantId: string,
traceId: string
): Promise<void> {
const config = await chrome.storage.local.get('config');
const apiEndpoint = config.config?.apiEndpoint || 'http://localhost:3003';
const response = await fetch(`${apiEndpoint}/api/returns/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-Id': tenantId,
'X-Trace-Id': traceId,
},
body: JSON.stringify({
returns,
tenantId,
traceId,
businessType: 'TOC',
}),
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`);
}
this.logger.info('Returns uploaded', {
count: returns.length,
tenantId,
traceId,
});
}
private generateTraceId(): string {
return `TRC-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}

View File

@@ -0,0 +1,71 @@
import { OrderCollector } from './OrderCollector';
import { ReturnSync } from './ReturnSync';
import { RefundQuery } from './RefundQuery';
import { MessageHandler } from './MessageHandler';
import { Logger } from '../utils/Logger';
const logger = new Logger('Background');
const orderCollector = new OrderCollector();
const returnSync = new ReturnSync();
const refundQuery = new RefundQuery();
const messageHandler = new MessageHandler(orderCollector, returnSync, refundQuery);
chrome.runtime.onInstalled.addListener((details) => {
logger.info('Extension installed', { reason: details.reason });
if (details.reason === 'install') {
chrome.storage.local.set({
initialized: true,
config: {
apiEndpoint: 'http://localhost:3003',
syncInterval: 300000,
maxRetries: 3,
},
});
}
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
logger.info('Message received', { type: message.type, sender: sender.tab?.id });
messageHandler.handle(message, sender)
.then((result) => sendResponse({ success: true, data: result }))
.catch((error) => {
logger.error('Message handler error', { error: error.message });
sendResponse({ success: false, error: error.message });
});
return true;
});
chrome.alarms.onAlarm.addListener(async (alarm) => {
logger.info('Alarm triggered', { name: alarm.name });
switch (alarm.name) {
case 'orderSync':
await orderCollector.syncAllShops();
break;
case 'returnSync':
await returnSync.syncAllReturns();
break;
case 'refundQuery':
await refundQuery.queryAllRefunds();
break;
}
});
async function setupAlarms() {
const config = await chrome.storage.local.get('config');
const syncInterval = config.config?.syncInterval || 300000;
chrome.alarms.create('orderSync', { periodInMinutes: syncInterval / 60000 });
chrome.alarms.create('returnSync', { periodInMinutes: (syncInterval * 2) / 60000 });
chrome.alarms.create('refundQuery', { periodInMinutes: (syncInterval * 3) / 60000 });
logger.info('Alarms setup complete', { syncInterval });
}
setupAlarms();
export { orderCollector, returnSync, refundQuery };

View File

@@ -0,0 +1,13 @@
export { OrderCollector } from './OrderCollector';
export { ReturnSync } from './ReturnSync';
export { RefundQuery } from './RefundQuery';
export { MessageHandler } from './MessageHandler';
export { DOMParser } from './DOMParser';
export { FingerprintManager } from './FingerprintManager';
export { AutoShipService } from './AutoShipService';
export { LogisticsSyncService } from './LogisticsSyncService';
export type { LogisticsInfo, LogisticsStatus, SyncResult } from './LogisticsSyncService';
export { ABTestStrategyService } from './ABTestStrategyService';
export type { TestGoal, TestData, StrategyRecommendation, Variation } from './ABTestStrategyService';
export { ABTestOptimizationService } from './ABTestOptimizationService';
export type { TestResult, VariationResult, OptimizationGoal, OptimizationRecommendation, OptimizationAction, FollowUpTest } from './ABTestOptimizationService';

View File

@@ -0,0 +1,146 @@
import { Logger } from '../utils/Logger';
const logger = new Logger('ContentScript');
logger.info('Content script loaded', { url: window.location.href });
const platform = detectPlatform();
function detectPlatform(): string {
const hostname = window.location.hostname;
if (hostname.includes('tiktok')) return 'tiktok';
if (hostname.includes('temu')) return 'temu';
if (hostname.includes('shopee')) return 'shopee';
return 'unknown';
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
logger.info('Content script received message', { type: message.type, platform });
switch (message.type) {
case 'EXTRACT_PAGE_DATA':
const data = extractPageData(message.payload?.selectors);
sendResponse({ success: true, data });
break;
case 'GET_PAGE_ORDERS':
const orders = extractOrders();
sendResponse({ success: true, orders });
break;
case 'GET_PAGE_RETURNS':
const returns = extractReturns();
sendResponse({ success: true, returns });
break;
case 'CHECK_LOGIN_STATUS':
const isLoggedIn = checkLoginStatus();
sendResponse({ success: true, isLoggedIn });
break;
default:
sendResponse({ success: false, error: 'Unknown message type' });
}
return true;
});
function extractPageData(selectors?: Record<string, string>): Record<string, any> {
const data: Record<string, any> = {};
if (!selectors) {
return data;
}
for (const [key, selector] of Object.entries(selectors)) {
const element = document.querySelector(selector);
if (element) {
data[key] = element.textContent?.trim() || '';
}
}
return data;
}
function extractOrders(): any[] {
const orders: any[] = [];
const orderElements = document.querySelectorAll('[data-order-id]');
for (const el of orderElements) {
const orderId = el.getAttribute('data-order-id');
const statusEl = el.querySelector('[data-order-status]');
const amountEl = el.querySelector('[data-order-amount]');
if (orderId) {
orders.push({
orderId,
platform,
status: statusEl?.textContent?.trim() || 'UNKNOWN',
totalAmount: parseFloat(amountEl?.textContent?.replace(/[^0-9.]/g, '') || '0'),
currency: getCurrencyForPlatform(platform),
extractedAt: new Date().toISOString(),
});
}
}
logger.info('Orders extracted', { count: orders.length, platform });
return orders;
}
function extractReturns(): any[] {
const returns: any[] = [];
const returnElements = document.querySelectorAll('[data-return-id]');
for (const el of returnElements) {
const returnId = el.getAttribute('data-return-id');
const orderId = el.getAttribute('data-order-id');
const statusEl = el.querySelector('[data-return-status]');
if (returnId) {
returns.push({
returnId,
orderId,
platform,
status: statusEl?.textContent?.trim() || 'UNKNOWN',
extractedAt: new Date().toISOString(),
});
}
}
logger.info('Returns extracted', { count: returns.length, platform });
return returns;
}
function checkLoginStatus(): boolean {
const loginIndicators = {
tiktok: () => !!document.querySelector('[data-user-avatar]'),
temu: () => !!document.querySelector('[data-user-name]'),
shopee: () => !!document.querySelector('[data-shop-name]'),
};
const checker = loginIndicators[platform as keyof typeof loginIndicators];
if (checker) {
return checker();
}
return false;
}
function getCurrencyForPlatform(platform: string): string {
const currencies: Record<string, string> = {
tiktok: 'USD',
temu: 'USD',
shopee: 'MYR',
};
return currencies[platform] || 'USD';
}
export { extractPageData, extractOrders, extractReturns, checkLoginStatus };

View File

@@ -0,0 +1,382 @@
export interface PlatformAdapter {
fetchOrders(
shopId: string,
dateRange: { start: Date; end: Date },
traceId: string
): Promise<any[]>;
fetchReturns(
shopId: string,
returnIds?: string[],
traceId?: string
): Promise<any[]>;
getReturnById(returnId: string, traceId: string): Promise<any>;
getRefundStatus(refundId: string, traceId: string): Promise<any>;
}
import { Logger } from '../utils/Logger';
export class TikTokAdapter implements PlatformAdapter {
private logger = new Logger('TikTokAdapter');
async fetchOrders(
shopId: string,
dateRange: { start: Date; end: Date },
traceId: string
): Promise<any[]> {
this.logger.info('Fetching TikTok orders', { shopId, dateRange, traceId });
return this.executeInTab('https://seller.tiktok.com/orders', async () => {
const orders: any[] = [];
const orderElements = document.querySelectorAll('[data-order-id]');
for (const el of orderElements) {
const orderId = el.getAttribute('data-order-id');
const statusEl = el.querySelector('[data-order-status]');
const amountEl = el.querySelector('[data-order-amount]');
if (orderId) {
orders.push({
orderId,
platform: 'tiktok',
shopId,
status: statusEl?.textContent?.trim() || 'UNKNOWN',
totalAmount: parseFloat(amountEl?.textContent?.replace(/[^0-9.]/g, '') || '0'),
currency: 'USD',
items: [],
customerInfo: {},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
traceId,
});
}
}
return orders;
});
}
async fetchReturns(
shopId: string,
returnIds?: string[],
traceId?: string
): Promise<any[]> {
this.logger.info('Fetching TikTok returns', { shopId, returnIds, traceId });
return this.executeInTab('https://seller.tiktok.com/returns', async () => {
const returns: any[] = [];
const returnElements = document.querySelectorAll('[data-return-id]');
for (const el of returnElements) {
const returnId = el.getAttribute('data-return-id');
const orderId = el.getAttribute('data-order-id');
const statusEl = el.querySelector('[data-return-status]');
if (returnId) {
returns.push({
returnId,
orderId,
platform: 'tiktok',
shopId,
status: statusEl?.textContent?.trim() || 'UNKNOWN',
reason: '',
items: [],
refundAmount: 0,
currency: 'USD',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
traceId,
});
}
}
return returns;
});
}
async getReturnById(returnId: string, traceId: string): Promise<any> {
this.logger.info('Getting TikTok return by ID', { returnId, traceId });
return {
returnId,
orderId: '',
platform: 'tiktok',
shopId: '',
status: 'UNKNOWN',
reason: '',
items: [],
refundAmount: 0,
currency: 'USD',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
traceId,
};
}
async getRefundStatus(refundId: string, traceId: string): Promise<any> {
this.logger.info('Getting TikTok refund status', { refundId, traceId });
return {
refundId,
returnId: '',
orderId: '',
platform: 'tiktok',
shopId: '',
status: 'PENDING',
amount: 0,
currency: 'USD',
method: 'ORIGINAL_PAYMENT',
traceId,
};
}
private async executeInTab<T>(url: string, extractor: () => T): Promise<T> {
return extractor();
}
}
export class TemuAdapter implements PlatformAdapter {
private logger = new Logger('TemuAdapter');
async fetchOrders(
shopId: string,
dateRange: { start: Date; end: Date },
traceId: string
): Promise<any[]> {
this.logger.info('Fetching Temu orders', { shopId, dateRange, traceId });
return this.executeInTab('https://agentseller.temu.com/orders', async () => {
const orders: any[] = [];
const orderElements = document.querySelectorAll('[data-order-id]');
for (const el of orderElements) {
const orderId = el.getAttribute('data-order-id');
const statusEl = el.querySelector('[data-order-status]');
const amountEl = el.querySelector('[data-order-amount]');
if (orderId) {
orders.push({
orderId,
platform: 'temu',
shopId,
status: statusEl?.textContent?.trim() || 'UNKNOWN',
totalAmount: parseFloat(amountEl?.textContent?.replace(/[^0-9.]/g, '') || '0'),
currency: 'USD',
items: [],
customerInfo: {},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
traceId,
});
}
}
return orders;
});
}
async fetchReturns(
shopId: string,
returnIds?: string[],
traceId?: string
): Promise<any[]> {
this.logger.info('Fetching Temu returns', { shopId, returnIds, traceId });
return this.executeInTab('https://agentseller.temu.com/returns', async () => {
const returns: any[] = [];
const returnElements = document.querySelectorAll('[data-return-id]');
for (const el of returnElements) {
const returnId = el.getAttribute('data-return-id');
const orderId = el.getAttribute('data-order-id');
const statusEl = el.querySelector('[data-return-status]');
if (returnId) {
returns.push({
returnId,
orderId,
platform: 'temu',
shopId,
status: statusEl?.textContent?.trim() || 'UNKNOWN',
reason: '',
items: [],
refundAmount: 0,
currency: 'USD',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
traceId,
});
}
}
return returns;
});
}
async getReturnById(returnId: string, traceId: string): Promise<any> {
this.logger.info('Getting Temu return by ID', { returnId, traceId });
return {
returnId,
orderId: '',
platform: 'temu',
shopId: '',
status: 'UNKNOWN',
reason: '',
items: [],
refundAmount: 0,
currency: 'USD',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
traceId,
};
}
async getRefundStatus(refundId: string, traceId: string): Promise<any> {
this.logger.info('Getting Temu refund status', { refundId, traceId });
return {
refundId,
returnId: '',
orderId: '',
platform: 'temu',
shopId: '',
status: 'PENDING',
amount: 0,
currency: 'USD',
method: 'ORIGINAL_PAYMENT',
traceId,
};
}
private async executeInTab<T>(url: string, extractor: () => T): Promise<T> {
return extractor();
}
}
export class ShopeeAdapter implements PlatformAdapter {
private logger = new Logger('ShopeeAdapter');
async fetchOrders(
shopId: string,
dateRange: { start: Date; end: Date },
traceId: string
): Promise<any[]> {
this.logger.info('Fetching Shopee orders', { shopId, dateRange, traceId });
return this.executeInTab('https://shopee.com.my/seller/orders', async () => {
const orders: any[] = [];
const orderElements = document.querySelectorAll('[data-order-id]');
for (const el of orderElements) {
const orderId = el.getAttribute('data-order-id');
const statusEl = el.querySelector('[data-order-status]');
const amountEl = el.querySelector('[data-order-amount]');
if (orderId) {
orders.push({
orderId,
platform: 'shopee',
shopId,
status: statusEl?.textContent?.trim() || 'UNKNOWN',
totalAmount: parseFloat(amountEl?.textContent?.replace(/[^0-9.]/g, '') || '0'),
currency: 'MYR',
items: [],
customerInfo: {},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
traceId,
});
}
}
return orders;
});
}
async fetchReturns(
shopId: string,
returnIds?: string[],
traceId?: string
): Promise<any[]> {
this.logger.info('Fetching Shopee returns', { shopId, returnIds, traceId });
return this.executeInTab('https://shopee.com.my/seller/returns', async () => {
const returns: any[] = [];
const returnElements = document.querySelectorAll('[data-return-id]');
for (const el of returnElements) {
const returnId = el.getAttribute('data-return-id');
const orderId = el.getAttribute('data-order-id');
const statusEl = el.querySelector('[data-return-status]');
if (returnId) {
returns.push({
returnId,
orderId,
platform: 'shopee',
shopId,
status: statusEl?.textContent?.trim() || 'UNKNOWN',
reason: '',
items: [],
refundAmount: 0,
currency: 'MYR',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
traceId,
});
}
}
return returns;
});
}
async getReturnById(returnId: string, traceId: string): Promise<any> {
this.logger.info('Getting Shopee return by ID', { returnId, traceId });
return {
returnId,
orderId: '',
platform: 'shopee',
shopId: '',
status: 'UNKNOWN',
reason: '',
items: [],
refundAmount: 0,
currency: 'MYR',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
traceId,
};
}
async getRefundStatus(refundId: string, traceId: string): Promise<any> {
this.logger.info('Getting Shopee refund status', { refundId, traceId });
return {
refundId,
returnId: '',
orderId: '',
platform: 'shopee',
shopId: '',
status: 'PENDING',
amount: 0,
currency: 'MYR',
method: 'ORIGINAL_PAYMENT',
traceId,
};
}
private async executeInTab<T>(url: string, extractor: () => T): Promise<T> {
return extractor();
}
}

View File

@@ -0,0 +1,29 @@
export class Logger {
private context: string;
constructor(context: string) {
this.context = context;
}
info(message: string, data?: any): void {
console.log(`[${this.getTimestamp()}] [${this.context}] [INFO] ${message}`, data || '');
}
warn(message: string, data?: any): void {
console.warn(`[${this.getTimestamp()}] [${this.context}] [WARN] ${message}`, data || '');
}
error(message: string, data?: any): void {
console.error(`[${this.getTimestamp()}] [${this.context}] [ERROR] ${message}`, data || '');
}
debug(message: string, data?: any): void {
if (process.env.NODE_ENV === 'development') {
console.debug(`[${this.getTimestamp()}] [${this.context}] [DEBUG] ${message}`, data || '');
}
}
private getTimestamp(): string {
return new Date().toISOString();
}
}

View File

@@ -0,0 +1 @@
export { Logger } from './Logger';