@xiaohhhh1/canvas-agent 0.4.25 → 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;
@@ -125,6 +132,8 @@ export declare function mergeRankingRecord(target: Map<string, Record<string, un
125
132
  export declare function selectDetailCandidates(records: Array<Record<string, unknown>>, limit?: number): Record<string, unknown>[];
126
133
  export declare function isTransientFastMossNavigation(error: unknown): boolean;
127
134
  export declare function isAnalyzableVideoUrl(value: string): boolean;
135
+ export declare function isFastMossVideoDetailUrl(value: string): boolean;
136
+ export declare function publicTikTokVideoUrlFromFastMoss(value: string): string;
128
137
  export declare function videoLearningSourcesFromItems(items: VisibleVideoItem[], context: {
129
138
  market: string;
130
139
  dimension: VideoLearningDimension;
@@ -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;
@@ -480,6 +480,47 @@ export class FastMossIntegration {
480
480
  }
481
481
  throw lastError instanceof Error ? lastError : new Error("FastMoss 视频榜页面跳转后没有恢复");
482
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
+ }
483
524
  async clickVideoLearningTab(labels) {
484
525
  let lastError;
485
526
  for (let attempt = 0; attempt < 3; attempt += 1) {
@@ -692,13 +733,14 @@ async function readVisibleVideoLearningItems(page) {
692
733
  ]);
693
734
  return [...new Set([...direct, ...data].filter(Boolean).map(absoluteUrl).filter(Boolean))];
694
735
  };
695
- const serialize = (root, headers = [], cells = []) => ({
736
+ const serialize = (root, headers = [], cells = [], previewIndex) => ({
696
737
  text: root.innerText.replace(/\s+/g, " ").trim().slice(0, 8000),
697
738
  links: [...new Set([...root.querySelectorAll("a[href]")].map((link) => link.href).filter(Boolean))],
698
739
  media: mediaUrls(root),
699
740
  images: [...new Set([...root.querySelectorAll("img")].map((image) => image.currentSrc || image.src || image.dataset.src || "").filter(Boolean).map(absoluteUrl).filter(Boolean))],
700
741
  headers,
701
742
  cells,
743
+ ...(previewIndex == null ? {} : { previewIndex }),
702
744
  });
703
745
  const tableItems = [...document.querySelectorAll("table tbody tr")]
704
746
  .filter(shown)
@@ -709,8 +751,15 @@ async function readVisibleVideoLearningItems(page) {
709
751
  return serialize(row, headers, cells);
710
752
  })
711
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);
712
760
  const cardCandidates = [...document.querySelectorAll("article,li,[class*='card'],[class*='item']")]
713
761
  .filter(shown)
762
+ .filter((element) => !element.querySelector(previewSelector))
714
763
  .filter((element) => {
715
764
  const rect = element.getBoundingClientRect();
716
765
  const text = element.innerText.replace(/\s+/g, " ").trim();
@@ -731,7 +780,7 @@ async function readVisibleVideoLearningItems(page) {
731
780
  if (cards.length >= 80)
732
781
  break;
733
782
  }
734
- const combined = [...tableItems, ...cards];
783
+ const combined = [...tableItems, ...previewItems, ...cards];
735
784
  const seen = new Set();
736
785
  return combined.filter((item) => {
737
786
  const key = JSON.stringify([item.text.slice(0, 300), item.media[0], item.links[0], item.images[0]]);
@@ -808,6 +857,21 @@ export function isAnalyzableVideoUrl(value) {
808
857
  return false;
809
858
  }
810
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
+ }
811
875
  export function videoLearningSourcesFromItems(items, context) {
812
876
  const output = [];
813
877
  const seen = new Set();
@@ -815,7 +879,8 @@ export function videoLearningSourcesFromItems(items, context) {
815
879
  const mediaUrl = item.media.find((value) => isAnalyzableVideoUrl(value)) || "";
816
880
  const publicVideoUrl = firstHttp(item.links, /tiktok\.com\/.*(?:video|v)\//i);
817
881
  const traceUrl = firstHttp(item.links, /video|aweme|creative|material/i);
818
- const sourceUrl = mediaUrl || publicVideoUrl || traceUrl;
882
+ const publicTraceUrl = publicTikTokVideoUrlFromFastMoss(traceUrl);
883
+ const sourceUrl = mediaUrl || publicVideoUrl || publicTraceUrl;
819
884
  const platformVideoId = videoIdFromValues([sourceUrl, ...item.links, ...item.media]) || stableId(`${context.dimension}|${item.text}|${item.images[0] || ""}`);
820
885
  if (seen.has(platformVideoId))
821
886
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaohhhh1/canvas-agent",
3
- "version": "0.4.25",
3
+ "version": "0.4.26",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",