@xiaohhhh1/canvas-agent 0.4.23 → 0.4.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -112,11 +112,17 @@ export declare class FastMossIntegration {
|
|
|
112
112
|
private useLatestPage;
|
|
113
113
|
private openProductRanking;
|
|
114
114
|
private openVideoLearningPage;
|
|
115
|
+
private readVideoLearningItems;
|
|
116
|
+
private clickVideoLearningTab;
|
|
117
|
+
private clickVideoLearningNextPage;
|
|
118
|
+
private waitForVideoLearningNavigation;
|
|
115
119
|
}
|
|
116
120
|
export declare function fastMossSalesRankUrl(market?: string): string;
|
|
117
121
|
export declare function fastMossRankingUrl(source: FastMossRankingSource, market?: string): string;
|
|
122
|
+
export declare function fastMossVideoLearningRankUrl(market?: string): string;
|
|
118
123
|
export declare function mergeRankingRecord(target: Map<string, Record<string, unknown>>, row: Record<string, unknown>, source: FastMossRankingSource, position: number): void;
|
|
119
124
|
export declare function selectDetailCandidates(records: Array<Record<string, unknown>>, limit?: number): Record<string, unknown>[];
|
|
125
|
+
export declare function isTransientFastMossNavigation(error: unknown): boolean;
|
|
120
126
|
export declare function isAnalyzableVideoUrl(value: string): boolean;
|
|
121
127
|
export declare function videoLearningSourcesFromItems(items: VisibleVideoItem[], context: {
|
|
122
128
|
market: string;
|
|
@@ -273,7 +273,9 @@ export class FastMossIntegration {
|
|
|
273
273
|
throw new Error("FastMoss 会员已过期或当前账号没有视频榜单权限,请先更换账号");
|
|
274
274
|
if (!this.authenticated || !this.page || this.page.isClosed())
|
|
275
275
|
throw new Error("FastMoss 尚未登录;请在恢复的窗口中完成登录后重试");
|
|
276
|
-
|
|
276
|
+
// 视频学习只研究可迁移的创意机制,不应被某一个运营市场限制。
|
|
277
|
+
// FastMoss 使用空 region 表示“全部国家/地区”。
|
|
278
|
+
const market = normalizeLearningMarket(input.market);
|
|
277
279
|
const requestedTarget = Math.max(1, Math.min(VIDEO_LEARNING_TARGET, Number(input.target || VIDEO_LEARNING_TARGET)));
|
|
278
280
|
const sources = [];
|
|
279
281
|
const rejected = [];
|
|
@@ -283,7 +285,7 @@ export class FastMossIntegration {
|
|
|
283
285
|
return;
|
|
284
286
|
let previousSignature = "";
|
|
285
287
|
for (let pageNumber = 1; pageNumber <= 5 && sources.filter((item) => item.rankingDimension === dimension).length < limit; pageNumber += 1) {
|
|
286
|
-
const snapshot = await
|
|
288
|
+
const snapshot = await this.readVideoLearningItems();
|
|
287
289
|
const signature = JSON.stringify(snapshot.map((item) => [item.text, item.media, item.links]).slice(0, 20));
|
|
288
290
|
if (!snapshot.length || (pageNumber > 1 && signature === previousSignature))
|
|
289
291
|
break;
|
|
@@ -309,9 +311,9 @@ export class FastMossIntegration {
|
|
|
309
311
|
this.message = `正在读取视频学习样本:${dimension} ${sources.filter((item) => item.rankingDimension === dimension).length}/${limit},合计 ${sources.length}/${requestedTarget}`;
|
|
310
312
|
if (sources.filter((item) => item.rankingDimension === dimension).length >= limit)
|
|
311
313
|
break;
|
|
312
|
-
if (!await
|
|
314
|
+
if (!await this.clickVideoLearningNextPage())
|
|
313
315
|
break;
|
|
314
|
-
await this.
|
|
316
|
+
await this.waitForVideoLearningNavigation();
|
|
315
317
|
await this.inspect();
|
|
316
318
|
if (this.verificationRequired)
|
|
317
319
|
throw new Error("采集视频榜单时出现滑块验证;请手动完成后重新采集,未完成样本不会计数");
|
|
@@ -319,7 +321,7 @@ export class FastMossIntegration {
|
|
|
319
321
|
throw new Error("当前 FastMoss 账号没有继续查看视频榜单的权限");
|
|
320
322
|
}
|
|
321
323
|
};
|
|
322
|
-
await this.openVideoLearningPage(
|
|
324
|
+
await this.openVideoLearningPage(fastMossVideoLearningRankUrl(market), market === "GLOBAL" ? "全球视频商品榜" : `${market} 视频商品榜`);
|
|
323
325
|
await collect("commerce-video", Math.min(VIDEO_COMMERCE_TARGET, requestedTarget));
|
|
324
326
|
const aiDimensions = [
|
|
325
327
|
{ dimension: "high-sales", labels: ["高销量榜单", "High Sales"] },
|
|
@@ -329,17 +331,17 @@ export class FastMossIntegration {
|
|
|
329
331
|
];
|
|
330
332
|
if (sources.length < requestedTarget) {
|
|
331
333
|
const aiUrl = new URL(VIDEO_AI_RANKING_PATH, FASTMOSS_HOME);
|
|
332
|
-
aiUrl.searchParams.set("region", market);
|
|
333
|
-
await this.openVideoLearningPage(aiUrl.toString(), `${market} AI 带货视频榜`);
|
|
334
|
+
aiUrl.searchParams.set("region", market === "GLOBAL" ? "" : market);
|
|
335
|
+
await this.openVideoLearningPage(aiUrl.toString(), market === "GLOBAL" ? "全球 AI 带货视频榜" : `${market} AI 带货视频榜`);
|
|
334
336
|
for (const entry of aiDimensions) {
|
|
335
337
|
if (sources.length >= requestedTarget)
|
|
336
338
|
break;
|
|
337
|
-
const clicked = await
|
|
339
|
+
const clicked = await this.clickVideoLearningTab(entry.labels);
|
|
338
340
|
if (!clicked) {
|
|
339
341
|
rejected.push({ dimension: entry.dimension, reason: `没有找到${entry.labels[0]}标签` });
|
|
340
342
|
continue;
|
|
341
343
|
}
|
|
342
|
-
await this.
|
|
344
|
+
await this.waitForVideoLearningNavigation();
|
|
343
345
|
await this.inspect();
|
|
344
346
|
if (this.verificationRequired)
|
|
345
347
|
throw new Error("采集 AI 视频榜时出现滑块验证;请手动完成后重新采集");
|
|
@@ -437,10 +439,18 @@ export class FastMossIntegration {
|
|
|
437
439
|
if (!this.page || this.page.isClosed())
|
|
438
440
|
throw new Error("FastMoss 专用浏览器未打开");
|
|
439
441
|
this.message = `正在自动进入 ${label}`;
|
|
440
|
-
|
|
442
|
+
try {
|
|
443
|
+
await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 60_000 });
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
// FastMoss 会在进入 AI 榜时自行二次跳转。此时 Playwright 报 ERR_ABORTED,
|
|
447
|
+
// 但浏览器实际仍在加载目标页;继续等待可见榜单,而不是把正常跳转当成失败。
|
|
448
|
+
if (!isTransientFastMossNavigation(error))
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
441
451
|
const deadline = Date.now() + PRODUCT_TABLE_TIMEOUT_MS;
|
|
442
452
|
while (Date.now() < deadline) {
|
|
443
|
-
const items = await
|
|
453
|
+
const items = await this.readVideoLearningItems();
|
|
444
454
|
if (items.length)
|
|
445
455
|
return;
|
|
446
456
|
await this.inspect();
|
|
@@ -450,10 +460,74 @@ export class FastMossIntegration {
|
|
|
450
460
|
throw new Error("当前 FastMoss 账号没有该视频榜单权限");
|
|
451
461
|
if (!this.authenticated)
|
|
452
462
|
throw new Error("FastMoss 登录已失效,请重新登录后再次采集");
|
|
453
|
-
await this.
|
|
463
|
+
await this.waitForVideoLearningNavigation(750);
|
|
454
464
|
}
|
|
455
465
|
throw new Error(`${label}加载超时;请检查网络或在专用窗口中确认页面可见`);
|
|
456
466
|
}
|
|
467
|
+
async readVideoLearningItems() {
|
|
468
|
+
let lastError;
|
|
469
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
470
|
+
this.useLatestPage();
|
|
471
|
+
if (!this.page || this.page.isClosed())
|
|
472
|
+
throw new Error("FastMoss 专用浏览器未打开");
|
|
473
|
+
try {
|
|
474
|
+
return await readVisibleVideoLearningItems(this.page);
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
lastError = error;
|
|
478
|
+
if (!isTransientFastMossNavigation(error))
|
|
479
|
+
throw error;
|
|
480
|
+
await this.waitForVideoLearningNavigation();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
throw lastError instanceof Error ? lastError : new Error("FastMoss 视频榜页面跳转后没有恢复");
|
|
484
|
+
}
|
|
485
|
+
async clickVideoLearningTab(labels) {
|
|
486
|
+
let lastError;
|
|
487
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
488
|
+
this.useLatestPage();
|
|
489
|
+
if (!this.page || this.page.isClosed())
|
|
490
|
+
throw new Error("FastMoss 专用浏览器未打开");
|
|
491
|
+
try {
|
|
492
|
+
return await clickVisibleText(this.page, labels);
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
lastError = error;
|
|
496
|
+
if (!isTransientFastMossNavigation(error))
|
|
497
|
+
throw error;
|
|
498
|
+
await this.waitForVideoLearningNavigation();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
throw lastError instanceof Error ? lastError : new Error("FastMoss 视频榜标签切换失败");
|
|
502
|
+
}
|
|
503
|
+
async clickVideoLearningNextPage() {
|
|
504
|
+
let lastError;
|
|
505
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
506
|
+
this.useLatestPage();
|
|
507
|
+
if (!this.page || this.page.isClosed())
|
|
508
|
+
throw new Error("FastMoss 专用浏览器未打开");
|
|
509
|
+
try {
|
|
510
|
+
return await clickVisibleNextPage(this.page);
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
lastError = error;
|
|
514
|
+
if (!isTransientFastMossNavigation(error))
|
|
515
|
+
throw error;
|
|
516
|
+
await this.waitForVideoLearningNavigation();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
throw lastError instanceof Error ? lastError : new Error("FastMoss 视频榜翻页失败");
|
|
520
|
+
}
|
|
521
|
+
async waitForVideoLearningNavigation(delay = 900) {
|
|
522
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
523
|
+
this.useLatestPage();
|
|
524
|
+
if (!this.page || this.page.isClosed())
|
|
525
|
+
return;
|
|
526
|
+
await this.page.waitForLoadState("domcontentloaded", { timeout: 10_000 }).catch((error) => {
|
|
527
|
+
if (!isTransientFastMossNavigation(error))
|
|
528
|
+
throw error;
|
|
529
|
+
});
|
|
530
|
+
}
|
|
457
531
|
}
|
|
458
532
|
export function fastMossSalesRankUrl(market) {
|
|
459
533
|
return fastMossRankingUrl("sales", market);
|
|
@@ -461,11 +535,14 @@ export function fastMossSalesRankUrl(market) {
|
|
|
461
535
|
export function fastMossRankingUrl(source, market) {
|
|
462
536
|
const definition = RANKING_DEFINITIONS.find((item) => item.source === source) || RANKING_DEFINITIONS[0];
|
|
463
537
|
const url = new URL(definition.pathname, FASTMOSS_HOME);
|
|
464
|
-
url.searchParams.set("region", normalizeMarket(market));
|
|
538
|
+
url.searchParams.set("region", market === "GLOBAL" ? "" : normalizeMarket(market));
|
|
465
539
|
url.searchParams.set("page", "1");
|
|
466
540
|
url.searchParams.set("pagesize", String(PRODUCT_RANK_PAGE_SIZE));
|
|
467
541
|
return url.toString();
|
|
468
542
|
}
|
|
543
|
+
export function fastMossVideoLearningRankUrl(market) {
|
|
544
|
+
return fastMossRankingUrl("video", normalizeLearningMarket(market));
|
|
545
|
+
}
|
|
469
546
|
export function mergeRankingRecord(target, row, source, position) {
|
|
470
547
|
const key = String(row.product_id || row.title);
|
|
471
548
|
const existing = target.get(key) || {};
|
|
@@ -493,6 +570,14 @@ function normalizeMarket(value) {
|
|
|
493
570
|
const market = String(value || "MX").trim().toUpperCase();
|
|
494
571
|
return /^[A-Z]{2}$/.test(market) ? market : "MX";
|
|
495
572
|
}
|
|
573
|
+
function normalizeLearningMarket(value) {
|
|
574
|
+
const market = String(value || "GLOBAL").trim().toUpperCase();
|
|
575
|
+
return market === "GLOBAL" || market === "ALL" || market === "全球" || market === "全部" ? "GLOBAL" : normalizeMarket(market);
|
|
576
|
+
}
|
|
577
|
+
export function isTransientFastMossNavigation(error) {
|
|
578
|
+
const message = error instanceof Error ? error.message : String(error || "");
|
|
579
|
+
return /ERR_ABORTED|Execution context was destroyed|navigation interrupted|frame was detached/i.test(message);
|
|
580
|
+
}
|
|
496
581
|
function hasReadableProductRows(snapshot) {
|
|
497
582
|
return snapshot.tables.some((table) => table.rows.some((row) => row.cells.filter(Boolean).length >= 3));
|
|
498
583
|
}
|