@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
- 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"] },
@@ -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 clickVisibleText(this.page, entry.labels);
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.page.waitForTimeout(900);
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
- await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 60_000 });
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 readVisibleVideoLearningItems(this.page);
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.page.waitForTimeout(750);
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
  }
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.24",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",