@xiaohhhh1/canvas-agent 0.4.40 → 0.4.42
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.
|
@@ -138,6 +138,7 @@ export declare class FastMossIntegration {
|
|
|
138
138
|
private loadRows;
|
|
139
139
|
private useLatestPage;
|
|
140
140
|
private openProductRanking;
|
|
141
|
+
private waitForManualVerification;
|
|
141
142
|
private openVideoLearningPage;
|
|
142
143
|
private readVideoLearningItems;
|
|
143
144
|
/**
|
|
@@ -154,6 +155,11 @@ export declare class FastMossIntegration {
|
|
|
154
155
|
private clickVideoLearningNextPage;
|
|
155
156
|
private waitForVideoLearningNavigation;
|
|
156
157
|
}
|
|
158
|
+
export declare function waitForFastMossVerificationClear(inspect: () => Promise<Pick<FastMossStatus, "verificationRequired" | "membershipExpired" | "authenticated">>, options?: {
|
|
159
|
+
timeoutMs?: number;
|
|
160
|
+
pollMs?: number;
|
|
161
|
+
sleep?: (milliseconds: number) => Promise<unknown>;
|
|
162
|
+
}): Promise<void>;
|
|
157
163
|
export declare function videoEvidenceTimestamps(durationMs: number): number[];
|
|
158
164
|
export declare function fastMossSalesRankUrl(market?: string): string;
|
|
159
165
|
export declare function fastMossRankingUrl(source: FastMossRankingSource, market?: string): string;
|
|
@@ -6,6 +6,7 @@ import { chromium } from "playwright-core";
|
|
|
6
6
|
import { CONFIG_DIR } from "../config.js";
|
|
7
7
|
const FASTMOSS_HOME = "https://www.fastmoss.com/";
|
|
8
8
|
const MEMBERSHIP_RECHECK_MS = 5 * 60_000;
|
|
9
|
+
const MANUAL_VERIFICATION_TIMEOUT_MS = 10 * 60_000;
|
|
9
10
|
const PRODUCT_TABLE_TIMEOUT_MS = 45_000;
|
|
10
11
|
const PRODUCT_RANK_PAGE_SIZE = 10;
|
|
11
12
|
const PRODUCT_RANK_MAX_PAGES = 50;
|
|
@@ -175,7 +176,7 @@ export class FastMossIntegration {
|
|
|
175
176
|
await this.start();
|
|
176
177
|
await this.inspect();
|
|
177
178
|
if (this.verificationRequired)
|
|
178
|
-
|
|
179
|
+
await this.waitForManualVerification();
|
|
179
180
|
if (this.membershipExpired)
|
|
180
181
|
throw new Error("FastMoss 会员已过期或当前账号没有榜单权限,请先更换账号");
|
|
181
182
|
if (Date.now() - this.lastMembershipCheckAt > MEMBERSHIP_RECHECK_MS) {
|
|
@@ -192,7 +193,16 @@ export class FastMossIntegration {
|
|
|
192
193
|
let previousSignature = "";
|
|
193
194
|
for (let pageNumber = 1; pageNumber <= PRODUCT_RANK_MAX_PAGES; pageNumber += 1) {
|
|
194
195
|
const snapshot = await readVisibleTableSnapshot(this.page);
|
|
195
|
-
if (!snapshot.tables[0]?.rows.length
|
|
196
|
+
if (!snapshot.tables[0]?.rows.length) {
|
|
197
|
+
await this.inspect();
|
|
198
|
+
if (this.verificationRequired) {
|
|
199
|
+
await this.waitForManualVerification();
|
|
200
|
+
pageNumber -= 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
if (snapshot.signature === previousSignature)
|
|
196
206
|
break;
|
|
197
207
|
previousSignature = snapshot.signature;
|
|
198
208
|
pages.push(snapshot.url);
|
|
@@ -204,10 +214,12 @@ export class FastMossIntegration {
|
|
|
204
214
|
break;
|
|
205
215
|
await this.page.waitForTimeout(800);
|
|
206
216
|
await this.inspect();
|
|
207
|
-
if (this.verificationRequired
|
|
217
|
+
if (this.verificationRequired)
|
|
218
|
+
await this.waitForManualVerification();
|
|
219
|
+
if (this.membershipExpired)
|
|
208
220
|
break;
|
|
209
221
|
}
|
|
210
|
-
if (this.
|
|
222
|
+
if (this.membershipExpired)
|
|
211
223
|
break;
|
|
212
224
|
}
|
|
213
225
|
const records = [...collected.values()];
|
|
@@ -227,7 +239,7 @@ export class FastMossIntegration {
|
|
|
227
239
|
: `采集在${stage === "ranking" ? "榜单" : "商品详情互证"}阶段发现会员权限异常;已保留上次 ${previousProducts} 个商品结果`;
|
|
228
240
|
return { ...this.status(), records: marketRecords, allRows: previousRows, trendProducts: previousProducts, detailFailures, interrupted: true };
|
|
229
241
|
};
|
|
230
|
-
if (this.
|
|
242
|
+
if (this.membershipExpired)
|
|
231
243
|
return retainPreviousOnInterruption("ranking");
|
|
232
244
|
const detailCandidates = selectDetailCandidates(marketRecords, DETAIL_CANDIDATE_LIMIT);
|
|
233
245
|
const capturePage = this.page;
|
|
@@ -242,11 +254,13 @@ export class FastMossIntegration {
|
|
|
242
254
|
if ((offset / DETAIL_FETCH_CONCURRENCY) % 10 === 0) {
|
|
243
255
|
this.useLatestPage();
|
|
244
256
|
await this.inspect();
|
|
245
|
-
if (this.verificationRequired
|
|
257
|
+
if (this.verificationRequired)
|
|
258
|
+
await this.waitForManualVerification();
|
|
259
|
+
if (this.membershipExpired)
|
|
246
260
|
break;
|
|
247
261
|
}
|
|
248
262
|
}
|
|
249
|
-
if (this.
|
|
263
|
+
if (this.membershipExpired)
|
|
250
264
|
return retainPreviousOnInterruption("detail", trendRows, detailFailures);
|
|
251
265
|
if (!trendRows.length && !this.verificationRequired)
|
|
252
266
|
throw new Error("榜单读取成功,但未能读取商品详情的最近 7 天销量;请检查会员权限后重试");
|
|
@@ -257,9 +271,7 @@ export class FastMossIntegration {
|
|
|
257
271
|
await writeFile(path.join(this.dataDir, "observations.json"), JSON.stringify(allRows, null, 2), "utf8");
|
|
258
272
|
await writeFile(path.join(this.dataDir, `capture-${localDate()}.json`), JSON.stringify({ capturedAt: this.lastCapture, pages, records: marketRecords, detailCandidates: detailCandidates.map((record) => record.product_id), trendRows, detailFailures }, null, 2), "utf8");
|
|
259
273
|
const trendProducts = new Set(trendRows.map((row) => String(row.product_id))).size;
|
|
260
|
-
this.message =
|
|
261
|
-
? `已保存验证前的 ${trendProducts} 个商品趋势;请手动完成滑块后再次抓取`
|
|
262
|
-
: `四榜单去重后 ${marketRecords.length} 个商品,优先互证 ${detailCandidates.length} 个,其中 ${trendProducts} 个取得最近 ${DETAIL_TREND_DAYS} 天趋势`;
|
|
274
|
+
this.message = `四榜单去重后 ${marketRecords.length} 个商品,优先互证 ${detailCandidates.length} 个,其中 ${trendProducts} 个取得最近 ${DETAIL_TREND_DAYS} 天趋势`;
|
|
263
275
|
return { ...this.status(), records: marketRecords, allRows, trendProducts, detailFailures };
|
|
264
276
|
}
|
|
265
277
|
/**
|
|
@@ -527,7 +539,7 @@ export class FastMossIntegration {
|
|
|
527
539
|
const market = normalizeMarket(input.market);
|
|
528
540
|
this.message = `正在自动进入 ${market} ${ranking.label}`;
|
|
529
541
|
await this.page.goto(fastMossRankingUrl(ranking.source, market), { waitUntil: "domcontentloaded", timeout: 60_000 });
|
|
530
|
-
|
|
542
|
+
let deadline = Date.now() + PRODUCT_TABLE_TIMEOUT_MS;
|
|
531
543
|
while (Date.now() < deadline) {
|
|
532
544
|
const snapshot = await readVisibleTableSnapshot(this.page);
|
|
533
545
|
if (hasReadableProductRows(snapshot)) {
|
|
@@ -536,8 +548,11 @@ export class FastMossIntegration {
|
|
|
536
548
|
return;
|
|
537
549
|
}
|
|
538
550
|
await this.inspect();
|
|
539
|
-
if (this.verificationRequired)
|
|
540
|
-
|
|
551
|
+
if (this.verificationRequired) {
|
|
552
|
+
await this.waitForManualVerification();
|
|
553
|
+
deadline = Date.now() + PRODUCT_TABLE_TIMEOUT_MS;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
541
556
|
if (this.membershipExpired)
|
|
542
557
|
throw new Error("FastMoss 会员已过期或当前账号没有商品榜单权限,请先更换账号");
|
|
543
558
|
if (!this.authenticated)
|
|
@@ -546,6 +561,28 @@ export class FastMossIntegration {
|
|
|
546
561
|
}
|
|
547
562
|
throw new Error("FastMoss 商品榜单自动加载超时;请检查网络或在窗口中完成人工验证后重试");
|
|
548
563
|
}
|
|
564
|
+
async waitForManualVerification() {
|
|
565
|
+
if (!this.verificationRequired)
|
|
566
|
+
return;
|
|
567
|
+
this.phase = "verification_required";
|
|
568
|
+
this.message = "采集已暂停等待人工滑块;完成后会自动继续,无需刷新或重新点击";
|
|
569
|
+
await this.page?.bringToFront().catch(() => undefined);
|
|
570
|
+
await waitForFastMossVerificationClear(async () => {
|
|
571
|
+
const status = await this.inspect();
|
|
572
|
+
if (status.verificationRequired)
|
|
573
|
+
this.message = "采集已暂停等待人工滑块;完成后会自动继续,无需刷新或重新点击";
|
|
574
|
+
return status;
|
|
575
|
+
}, {
|
|
576
|
+
timeoutMs: MANUAL_VERIFICATION_TIMEOUT_MS,
|
|
577
|
+
sleep: (milliseconds) => this.page && !this.page.isClosed()
|
|
578
|
+
? this.page.waitForTimeout(milliseconds)
|
|
579
|
+
: new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
580
|
+
});
|
|
581
|
+
this.phase = "ready";
|
|
582
|
+
this.message = "人工验证已完成,正在自动继续采集";
|
|
583
|
+
await this.page?.waitForLoadState("domcontentloaded", { timeout: 10_000 }).catch(() => undefined);
|
|
584
|
+
await this.page?.waitForTimeout(1_000).catch(() => undefined);
|
|
585
|
+
}
|
|
549
586
|
async openVideoLearningPage(url, label) {
|
|
550
587
|
if (!this.page || this.page.isClosed())
|
|
551
588
|
throw new Error("FastMoss 专用浏览器未打开");
|
|
@@ -722,6 +759,24 @@ export class FastMossIntegration {
|
|
|
722
759
|
});
|
|
723
760
|
}
|
|
724
761
|
}
|
|
762
|
+
export async function waitForFastMossVerificationClear(inspect, options = {}) {
|
|
763
|
+
const timeoutMs = Math.max(1, Number(options.timeoutMs || MANUAL_VERIFICATION_TIMEOUT_MS));
|
|
764
|
+
const pollMs = Math.max(1, Number(options.pollMs || 1_000));
|
|
765
|
+
const sleep = options.sleep || ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
|
|
766
|
+
const deadline = Date.now() + timeoutMs;
|
|
767
|
+
while (Date.now() < deadline) {
|
|
768
|
+
const status = await inspect();
|
|
769
|
+
if (status.membershipExpired)
|
|
770
|
+
throw new Error("FastMoss 会员已过期或当前账号没有商品榜单权限,请先更换账号");
|
|
771
|
+
if (!status.verificationRequired) {
|
|
772
|
+
if (!status.authenticated)
|
|
773
|
+
throw new Error("FastMoss 登录已失效,请在专用窗口中重新登录后再次抓取");
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
await sleep(pollMs);
|
|
777
|
+
}
|
|
778
|
+
throw new Error("等待人工滑块超过 10 分钟,本次采集已停止;完成验证后可重新开始,已保存结果不会丢失");
|
|
779
|
+
}
|
|
725
780
|
export function videoEvidenceTimestamps(durationMs) {
|
|
726
781
|
const maximumFrames = 24;
|
|
727
782
|
const safeDuration = Math.max(1_000, Math.min(30 * 60_000, Math.round(Number(durationMs) || 0)));
|
package/dist/workflow/manager.js
CHANGED
|
@@ -418,7 +418,7 @@ function scriptChunkPrompt(id, task, ordinals) {
|
|
|
418
418
|
必须使用 MCP 工具 flow_c_get_script_task 读取完整任务,再只创作 ordinal ${ordinals[0]} 到 ${ordinals.at(-1)}(精确列表:${ordinals.join(", ")})。
|
|
419
419
|
本段 productIndex 必须严格按此映射填写:${scriptProductAssignments(task.product_quantities, ordinals)}。不得凭产品名称猜测或把相邻产品编号混用。
|
|
420
420
|
脚本质量绝不能因批量而降低:每条都必须独立构思、完整、真实合规,严格遵守任务 instructions、产品图片顺序和 productIndex;使用目标市场 ${task.market} 的自然本地语言与偏快但清晰的短视频节奏。${longVideoRules}
|
|
421
|
-
每一个 ordinal
|
|
421
|
+
每一个 ordinal 都必须从头到尾创作一份完整、独立的原生带货脚本,画面和口播必须在同一份脚本中一起独立构思。先让它成为一个当地真人在具体生活情境里自然发现、吐槽、试用或验证商品的短内容,再完成带货;禁止写成品牌广告片、棚拍宣传片、电视购物、逐条念卖点或全程完美对镜讲解。独立脚本天然包含独立口播:不得把任何一份口播当作整批公共模板,不得复用完整台词,也不得只换人物、场景或少数词后保留近似口播。音色身份可以为同一人物保持一致,但每条的 Hook、人物行动、证明表达和 CTA 都必须重新写。
|
|
422
422
|
用户没有指定带货方向时,不得随意只用一种泛化形式;必须按任务 instructions 中“已批准的创意方向”逐条做产品适配轮换。轮换必须按每个产品自己的序号连续计算,不能因 30/15/10 条分段、换会话或跨产品边界而从第一个方向重新开始;让同一产品在重复某一方向前优先覆盖其他适配方向。
|
|
423
423
|
工厂风格 A/B 是默认轮换中的演绎带货布景,不是商品来源声明;不得因用户未提供真实工厂资料而跳过,也绝不能写成我们的真实工厂、真实生产流程、真实产地、工厂直销、厂家出货或仓库现货。
|
|
424
424
|
写完后必须调用 flow_c_submit_script_chunk 一次回传这 ${ordinals.length} 条,handoffId=${id}。不要创建付费批次,不要调用供应商模型,不要在聊天输出大段 JSON。工具返回成功后仅简短结束。`;
|