@xiaohhhh1/canvas-agent 0.4.23 → 0.4.25

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,18 @@ 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;
123
+ export declare function fastMossAiVideoLearningRankUrl(market?: string): string;
118
124
  export declare function mergeRankingRecord(target: Map<string, Record<string, unknown>>, row: Record<string, unknown>, source: FastMossRankingSource, position: number): void;
119
125
  export declare function selectDetailCandidates(records: Array<Record<string, unknown>>, limit?: number): Record<string, unknown>[];
126
+ export declare function isTransientFastMossNavigation(error: unknown): boolean;
120
127
  export declare function isAnalyzableVideoUrl(value: string): boolean;
121
128
  export declare function videoLearningSourcesFromItems(items: VisibleVideoItem[], context: {
122
129
  market: string;
@@ -15,7 +15,7 @@ const DETAIL_FETCH_CONCURRENCY = 3;
15
15
  const VIDEO_LEARNING_TARGET = 36;
16
16
  const VIDEO_COMMERCE_TARGET = 12;
17
17
  const VIDEO_AI_TARGET_PER_DIMENSION = 6;
18
- const VIDEO_AI_RANKING_PATH = "/zh/creativecenter/top-ai-videos";
18
+ const VIDEO_AI_RANKING_PATH = "/zh/media-source/top-ai-videos";
19
19
  const RANKING_DEFINITIONS = [
20
20
  { source: "new", label: "新品榜", pathname: "/e-commerce/newProducts" },
21
21
  { source: "hot", label: "热推榜", pathname: "/e-commerce/hotlist" },
@@ -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
- const market = normalizeMarket(input.market || "US");
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 readVisibleVideoLearningItems(this.page);
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 clickVisibleNextPage(this.page))
314
+ if (!await this.clickVideoLearningNextPage())
313
315
  break;
314
- await this.page.waitForTimeout(900);
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(fastMossRankingUrl("video", market), `${market} 视频商品榜`);
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"] },
@@ -328,18 +330,16 @@ export class FastMossIntegration {
328
330
  { dimension: "high-engagement", labels: ["高互动榜单", "High Engagement"] },
329
331
  ];
330
332
  if (sources.length < requestedTarget) {
331
- const aiUrl = new URL(VIDEO_AI_RANKING_PATH, FASTMOSS_HOME);
332
- aiUrl.searchParams.set("region", market);
333
- await this.openVideoLearningPage(aiUrl.toString(), `${market} AI 带货视频榜`);
333
+ await this.openVideoLearningPage(fastMossAiVideoLearningRankUrl(market), market === "GLOBAL" ? "全球 AI 带货视频榜" : `${market} AI 带货视频榜`);
334
334
  for (const entry of aiDimensions) {
335
335
  if (sources.length >= requestedTarget)
336
336
  break;
337
- const clicked = await clickVisibleText(this.page, entry.labels);
337
+ const clicked = await this.clickVideoLearningTab(entry.labels);
338
338
  if (!clicked) {
339
339
  rejected.push({ dimension: entry.dimension, reason: `没有找到${entry.labels[0]}标签` });
340
340
  continue;
341
341
  }
342
- await this.page.waitForTimeout(900);
342
+ await this.waitForVideoLearningNavigation();
343
343
  await this.inspect();
344
344
  if (this.verificationRequired)
345
345
  throw new Error("采集 AI 视频榜时出现滑块验证;请手动完成后重新采集");
@@ -437,10 +437,18 @@ export class FastMossIntegration {
437
437
  if (!this.page || this.page.isClosed())
438
438
  throw new Error("FastMoss 专用浏览器未打开");
439
439
  this.message = `正在自动进入 ${label}`;
440
- await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 60_000 });
440
+ try {
441
+ await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 60_000 });
442
+ }
443
+ catch (error) {
444
+ // FastMoss 会在进入 AI 榜时自行二次跳转。此时 Playwright 报 ERR_ABORTED,
445
+ // 但浏览器实际仍在加载目标页;继续等待可见榜单,而不是把正常跳转当成失败。
446
+ if (!isTransientFastMossNavigation(error))
447
+ throw error;
448
+ }
441
449
  const deadline = Date.now() + PRODUCT_TABLE_TIMEOUT_MS;
442
450
  while (Date.now() < deadline) {
443
- const items = await readVisibleVideoLearningItems(this.page);
451
+ const items = await this.readVideoLearningItems();
444
452
  if (items.length)
445
453
  return;
446
454
  await this.inspect();
@@ -450,10 +458,74 @@ export class FastMossIntegration {
450
458
  throw new Error("当前 FastMoss 账号没有该视频榜单权限");
451
459
  if (!this.authenticated)
452
460
  throw new Error("FastMoss 登录已失效,请重新登录后再次采集");
453
- await this.page.waitForTimeout(750);
461
+ await this.waitForVideoLearningNavigation(750);
454
462
  }
455
463
  throw new Error(`${label}加载超时;请检查网络或在专用窗口中确认页面可见`);
456
464
  }
465
+ async readVideoLearningItems() {
466
+ let lastError;
467
+ for (let attempt = 0; attempt < 4; attempt += 1) {
468
+ this.useLatestPage();
469
+ if (!this.page || this.page.isClosed())
470
+ throw new Error("FastMoss 专用浏览器未打开");
471
+ try {
472
+ return await readVisibleVideoLearningItems(this.page);
473
+ }
474
+ catch (error) {
475
+ lastError = error;
476
+ if (!isTransientFastMossNavigation(error))
477
+ throw error;
478
+ await this.waitForVideoLearningNavigation();
479
+ }
480
+ }
481
+ throw lastError instanceof Error ? lastError : new Error("FastMoss 视频榜页面跳转后没有恢复");
482
+ }
483
+ async clickVideoLearningTab(labels) {
484
+ let lastError;
485
+ for (let attempt = 0; attempt < 3; attempt += 1) {
486
+ this.useLatestPage();
487
+ if (!this.page || this.page.isClosed())
488
+ throw new Error("FastMoss 专用浏览器未打开");
489
+ try {
490
+ return await clickVisibleText(this.page, labels);
491
+ }
492
+ catch (error) {
493
+ lastError = error;
494
+ if (!isTransientFastMossNavigation(error))
495
+ throw error;
496
+ await this.waitForVideoLearningNavigation();
497
+ }
498
+ }
499
+ throw lastError instanceof Error ? lastError : new Error("FastMoss 视频榜标签切换失败");
500
+ }
501
+ async clickVideoLearningNextPage() {
502
+ let lastError;
503
+ for (let attempt = 0; attempt < 3; attempt += 1) {
504
+ this.useLatestPage();
505
+ if (!this.page || this.page.isClosed())
506
+ throw new Error("FastMoss 专用浏览器未打开");
507
+ try {
508
+ return await clickVisibleNextPage(this.page);
509
+ }
510
+ catch (error) {
511
+ lastError = error;
512
+ if (!isTransientFastMossNavigation(error))
513
+ throw error;
514
+ await this.waitForVideoLearningNavigation();
515
+ }
516
+ }
517
+ throw lastError instanceof Error ? lastError : new Error("FastMoss 视频榜翻页失败");
518
+ }
519
+ async waitForVideoLearningNavigation(delay = 900) {
520
+ await new Promise((resolve) => setTimeout(resolve, delay));
521
+ this.useLatestPage();
522
+ if (!this.page || this.page.isClosed())
523
+ return;
524
+ await this.page.waitForLoadState("domcontentloaded", { timeout: 10_000 }).catch((error) => {
525
+ if (!isTransientFastMossNavigation(error))
526
+ throw error;
527
+ });
528
+ }
457
529
  }
458
530
  export function fastMossSalesRankUrl(market) {
459
531
  return fastMossRankingUrl("sales", market);
@@ -461,11 +533,20 @@ export function fastMossSalesRankUrl(market) {
461
533
  export function fastMossRankingUrl(source, market) {
462
534
  const definition = RANKING_DEFINITIONS.find((item) => item.source === source) || RANKING_DEFINITIONS[0];
463
535
  const url = new URL(definition.pathname, FASTMOSS_HOME);
464
- url.searchParams.set("region", normalizeMarket(market));
536
+ url.searchParams.set("region", market === "GLOBAL" ? "" : normalizeMarket(market));
465
537
  url.searchParams.set("page", "1");
466
538
  url.searchParams.set("pagesize", String(PRODUCT_RANK_PAGE_SIZE));
467
539
  return url.toString();
468
540
  }
541
+ export function fastMossVideoLearningRankUrl(market) {
542
+ return fastMossRankingUrl("video", normalizeLearningMarket(market));
543
+ }
544
+ export function fastMossAiVideoLearningRankUrl(market) {
545
+ const normalizedMarket = normalizeLearningMarket(market);
546
+ const url = new URL(VIDEO_AI_RANKING_PATH, FASTMOSS_HOME);
547
+ url.searchParams.set("region", normalizedMarket === "GLOBAL" ? "" : normalizedMarket);
548
+ return url.toString();
549
+ }
469
550
  export function mergeRankingRecord(target, row, source, position) {
470
551
  const key = String(row.product_id || row.title);
471
552
  const existing = target.get(key) || {};
@@ -493,6 +574,14 @@ function normalizeMarket(value) {
493
574
  const market = String(value || "MX").trim().toUpperCase();
494
575
  return /^[A-Z]{2}$/.test(market) ? market : "MX";
495
576
  }
577
+ function normalizeLearningMarket(value) {
578
+ const market = String(value || "GLOBAL").trim().toUpperCase();
579
+ return market === "GLOBAL" || market === "ALL" || market === "全球" || market === "全部" ? "GLOBAL" : normalizeMarket(market);
580
+ }
581
+ export function isTransientFastMossNavigation(error) {
582
+ const message = error instanceof Error ? error.message : String(error || "");
583
+ return /ERR_ABORTED|Execution context was destroyed|navigation interrupted|frame was detached/i.test(message);
584
+ }
496
585
  function hasReadableProductRows(snapshot) {
497
586
  return snapshot.tables.some((table) => table.rows.some((row) => row.cells.filter(Boolean).length >= 3));
498
587
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaohhhh1/canvas-agent",
3
- "version": "0.4.23",
3
+ "version": "0.4.25",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",