@xiaohhhh1/canvas-agent 0.4.24 → 0.4.26

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.
@@ -30,6 +30,7 @@ type VisibleVideoItem = {
30
30
  images: string[];
31
31
  headers: string[];
32
32
  cells: string[];
33
+ previewIndex?: number;
33
34
  };
34
35
  export declare class FastMossIntegration {
35
36
  private context;
@@ -113,6 +114,12 @@ export declare class FastMossIntegration {
113
114
  private openProductRanking;
114
115
  private openVideoLearningPage;
115
116
  private readVideoLearningItems;
117
+ /**
118
+ * AI ranking cards expose their TikTok video id only through the visible
119
+ * preview action. Open only the cards needed for this batch, capture the
120
+ * public FastMoss video-detail URL, and close the temporary tab again.
121
+ */
122
+ private resolveVideoLearningCardLinks;
116
123
  private clickVideoLearningTab;
117
124
  private clickVideoLearningNextPage;
118
125
  private waitForVideoLearningNavigation;
@@ -120,10 +127,13 @@ export declare class FastMossIntegration {
120
127
  export declare function fastMossSalesRankUrl(market?: string): string;
121
128
  export declare function fastMossRankingUrl(source: FastMossRankingSource, market?: string): string;
122
129
  export declare function fastMossVideoLearningRankUrl(market?: string): string;
130
+ export declare function fastMossAiVideoLearningRankUrl(market?: string): string;
123
131
  export declare function mergeRankingRecord(target: Map<string, Record<string, unknown>>, row: Record<string, unknown>, source: FastMossRankingSource, position: number): void;
124
132
  export declare function selectDetailCandidates(records: Array<Record<string, unknown>>, limit?: number): Record<string, unknown>[];
125
133
  export declare function isTransientFastMossNavigation(error: unknown): boolean;
126
134
  export declare function isAnalyzableVideoUrl(value: string): boolean;
135
+ export declare function isFastMossVideoDetailUrl(value: string): boolean;
136
+ export declare function publicTikTokVideoUrlFromFastMoss(value: string): string;
127
137
  export declare function videoLearningSourcesFromItems(items: VisibleVideoItem[], context: {
128
138
  market: string;
129
139
  dimension: VideoLearningDimension;
@@ -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" },
@@ -285,7 +285,7 @@ export class FastMossIntegration {
285
285
  return;
286
286
  let previousSignature = "";
287
287
  for (let pageNumber = 1; pageNumber <= 5 && sources.filter((item) => item.rankingDimension === dimension).length < limit; pageNumber += 1) {
288
- const snapshot = await this.readVideoLearningItems();
288
+ const snapshot = await this.resolveVideoLearningCardLinks(await this.readVideoLearningItems(), limit);
289
289
  const signature = JSON.stringify(snapshot.map((item) => [item.text, item.media, item.links]).slice(0, 20));
290
290
  if (!snapshot.length || (pageNumber > 1 && signature === previousSignature))
291
291
  break;
@@ -330,9 +330,7 @@ export class FastMossIntegration {
330
330
  { dimension: "high-engagement", labels: ["高互动榜单", "High Engagement"] },
331
331
  ];
332
332
  if (sources.length < requestedTarget) {
333
- const aiUrl = new URL(VIDEO_AI_RANKING_PATH, FASTMOSS_HOME);
334
- aiUrl.searchParams.set("region", market === "GLOBAL" ? "" : market);
335
- await this.openVideoLearningPage(aiUrl.toString(), market === "GLOBAL" ? "全球 AI 带货视频榜" : `${market} AI 带货视频榜`);
333
+ await this.openVideoLearningPage(fastMossAiVideoLearningRankUrl(market), market === "GLOBAL" ? "全球 AI 带货视频榜" : `${market} AI 带货视频榜`);
336
334
  for (const entry of aiDimensions) {
337
335
  if (sources.length >= requestedTarget)
338
336
  break;
@@ -482,6 +480,47 @@ export class FastMossIntegration {
482
480
  }
483
481
  throw lastError instanceof Error ? lastError : new Error("FastMoss 视频榜页面跳转后没有恢复");
484
482
  }
483
+ /**
484
+ * AI ranking cards expose their TikTok video id only through the visible
485
+ * preview action. Open only the cards needed for this batch, capture the
486
+ * public FastMoss video-detail URL, and close the temporary tab again.
487
+ */
488
+ async resolveVideoLearningCardLinks(items, limit) {
489
+ if (!this.page || this.page.isClosed())
490
+ return items;
491
+ const candidates = items
492
+ .filter((item) => !item.links.some(isFastMossVideoDetailUrl) && Number.isInteger(item.previewIndex))
493
+ .slice(0, Math.max(0, limit));
494
+ if (!candidates.length)
495
+ return items;
496
+ const covers = this.page.locator('div.cursor-pointer > img[class*="aspect-"]');
497
+ const coverCount = await covers.count();
498
+ for (const item of candidates) {
499
+ const index = Number(item.previewIndex);
500
+ if (!Number.isInteger(index) || index < 0 || index >= coverCount)
501
+ continue;
502
+ let popup = null;
503
+ try {
504
+ [popup] = await Promise.all([
505
+ this.page.waitForEvent("popup", { timeout: 6_000 }),
506
+ covers.nth(index).click({ timeout: 6_000 }),
507
+ ]);
508
+ await popup.waitForURL(/\/media-source\/video\/\d+/i, { timeout: 10_000 });
509
+ const detailUrl = popup.url();
510
+ if (isFastMossVideoDetailUrl(detailUrl))
511
+ item.links.unshift(detailUrl);
512
+ }
513
+ catch {
514
+ // Keep the item unresolved. The caller records a truthful rejection
515
+ // instead of counting a cover image or ranking page as a video.
516
+ }
517
+ finally {
518
+ if (popup && !popup.isClosed())
519
+ await popup.close().catch(() => undefined);
520
+ }
521
+ }
522
+ return items;
523
+ }
485
524
  async clickVideoLearningTab(labels) {
486
525
  let lastError;
487
526
  for (let attempt = 0; attempt < 3; attempt += 1) {
@@ -543,6 +582,12 @@ export function fastMossRankingUrl(source, market) {
543
582
  export function fastMossVideoLearningRankUrl(market) {
544
583
  return fastMossRankingUrl("video", normalizeLearningMarket(market));
545
584
  }
585
+ export function fastMossAiVideoLearningRankUrl(market) {
586
+ const normalizedMarket = normalizeLearningMarket(market);
587
+ const url = new URL(VIDEO_AI_RANKING_PATH, FASTMOSS_HOME);
588
+ url.searchParams.set("region", normalizedMarket === "GLOBAL" ? "" : normalizedMarket);
589
+ return url.toString();
590
+ }
546
591
  export function mergeRankingRecord(target, row, source, position) {
547
592
  const key = String(row.product_id || row.title);
548
593
  const existing = target.get(key) || {};
@@ -688,13 +733,14 @@ async function readVisibleVideoLearningItems(page) {
688
733
  ]);
689
734
  return [...new Set([...direct, ...data].filter(Boolean).map(absoluteUrl).filter(Boolean))];
690
735
  };
691
- const serialize = (root, headers = [], cells = []) => ({
736
+ const serialize = (root, headers = [], cells = [], previewIndex) => ({
692
737
  text: root.innerText.replace(/\s+/g, " ").trim().slice(0, 8000),
693
738
  links: [...new Set([...root.querySelectorAll("a[href]")].map((link) => link.href).filter(Boolean))],
694
739
  media: mediaUrls(root),
695
740
  images: [...new Set([...root.querySelectorAll("img")].map((image) => image.currentSrc || image.src || image.dataset.src || "").filter(Boolean).map(absoluteUrl).filter(Boolean))],
696
741
  headers,
697
742
  cells,
743
+ ...(previewIndex == null ? {} : { previewIndex }),
698
744
  });
699
745
  const tableItems = [...document.querySelectorAll("table tbody tr")]
700
746
  .filter(shown)
@@ -705,8 +751,15 @@ async function readVisibleVideoLearningItems(page) {
705
751
  return serialize(row, headers, cells);
706
752
  })
707
753
  .filter((item) => item.text && (item.links.length || item.media.length || item.images.length));
754
+ const previewSelector = 'div.cursor-pointer > img[class*="aspect-"]';
755
+ const previewImages = [...document.querySelectorAll(previewSelector)];
756
+ const previewItems = previewImages.map((preview, previewIndex) => {
757
+ const root = preview.parentElement?.parentElement || preview.parentElement || preview;
758
+ return serialize(root, [], [], previewIndex);
759
+ }).filter((item) => item.text && item.images.length);
708
760
  const cardCandidates = [...document.querySelectorAll("article,li,[class*='card'],[class*='item']")]
709
761
  .filter(shown)
762
+ .filter((element) => !element.querySelector(previewSelector))
710
763
  .filter((element) => {
711
764
  const rect = element.getBoundingClientRect();
712
765
  const text = element.innerText.replace(/\s+/g, " ").trim();
@@ -727,7 +780,7 @@ async function readVisibleVideoLearningItems(page) {
727
780
  if (cards.length >= 80)
728
781
  break;
729
782
  }
730
- const combined = [...tableItems, ...cards];
783
+ const combined = [...tableItems, ...previewItems, ...cards];
731
784
  const seen = new Set();
732
785
  return combined.filter((item) => {
733
786
  const key = JSON.stringify([item.text.slice(0, 300), item.media[0], item.links[0], item.images[0]]);
@@ -804,6 +857,21 @@ export function isAnalyzableVideoUrl(value) {
804
857
  return false;
805
858
  }
806
859
  }
860
+ export function isFastMossVideoDetailUrl(value) {
861
+ try {
862
+ const url = new URL(value);
863
+ return /(?:^|\.)fastmoss\.com$/i.test(url.hostname) && /\/media-source\/video\/\d+(?:\/|$)/i.test(url.pathname);
864
+ }
865
+ catch {
866
+ return false;
867
+ }
868
+ }
869
+ export function publicTikTokVideoUrlFromFastMoss(value) {
870
+ if (!isFastMossVideoDetailUrl(value))
871
+ return "";
872
+ const id = new URL(value).pathname.match(/\/media-source\/video\/(\d+)(?:\/|$)/i)?.[1];
873
+ return id ? `https://www.tiktok.com/@_/video/${id}` : "";
874
+ }
807
875
  export function videoLearningSourcesFromItems(items, context) {
808
876
  const output = [];
809
877
  const seen = new Set();
@@ -811,7 +879,8 @@ export function videoLearningSourcesFromItems(items, context) {
811
879
  const mediaUrl = item.media.find((value) => isAnalyzableVideoUrl(value)) || "";
812
880
  const publicVideoUrl = firstHttp(item.links, /tiktok\.com\/.*(?:video|v)\//i);
813
881
  const traceUrl = firstHttp(item.links, /video|aweme|creative|material/i);
814
- const sourceUrl = mediaUrl || publicVideoUrl || traceUrl;
882
+ const publicTraceUrl = publicTikTokVideoUrlFromFastMoss(traceUrl);
883
+ const sourceUrl = mediaUrl || publicVideoUrl || publicTraceUrl;
815
884
  const platformVideoId = videoIdFromValues([sourceUrl, ...item.links, ...item.media]) || stableId(`${context.dimension}|${item.text}|${item.images[0] || ""}`);
816
885
  if (seen.has(platformVideoId))
817
886
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaohhhh1/canvas-agent",
3
- "version": "0.4.24",
3
+ "version": "0.4.26",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",