@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/
|
|
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
|
-
|
|
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
|
|
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;
|