@xiaohhhh1/canvas-agent 0.4.16 → 0.4.18

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.
@@ -1,4 +1,4 @@
1
- export type FastMossPhase = "idle" | "launching" | "login_required" | "ready" | "verification_required" | "membership_expired" | "error";
1
+ export type FastMossPhase = "idle" | "saved" | "launching" | "login_required" | "ready" | "verification_required" | "membership_expired" | "error";
2
2
  export type FastMossStatus = {
3
3
  phase: FastMossPhase;
4
4
  browserOpen: boolean;
@@ -13,6 +13,7 @@ export type FastMossStatus = {
13
13
  type CaptureContext = {
14
14
  market?: string;
15
15
  category?: string;
16
+ categories?: string[];
16
17
  shopType?: string;
17
18
  periodDays?: number;
18
19
  };
@@ -28,7 +29,10 @@ export declare class FastMossIntegration {
28
29
  private lastCapture?;
29
30
  private lastMembershipCheckAt;
30
31
  private readonly profileDir;
32
+ private readonly authenticatedMarker;
31
33
  private readonly dataDir;
34
+ private savedAuthenticated;
35
+ constructor();
32
36
  status(): FastMossStatus;
33
37
  start(): Promise<FastMossStatus>;
34
38
  inspect(): Promise<FastMossStatus>;
@@ -49,5 +53,7 @@ export declare class FastMossIntegration {
49
53
  switchAccount(): Promise<FastMossStatus>;
50
54
  private loadRows;
51
55
  private useLatestPage;
56
+ private openProductRanking;
52
57
  }
58
+ export declare function fastMossSalesRankUrl(market?: string): string;
53
59
  export {};
@@ -1,11 +1,15 @@
1
1
  import { existsSync } from "node:fs";
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { chromium } from "playwright-core";
6
6
  import { CONFIG_DIR } from "../config.js";
7
7
  const FASTMOSS_HOME = "https://www.fastmoss.com/";
8
+ const FASTMOSS_SALES_RANK = "https://www.fastmoss.com/e-commerce/saleslist";
8
9
  const MEMBERSHIP_RECHECK_MS = 5 * 60_000;
10
+ const PRODUCT_TABLE_TIMEOUT_MS = 45_000;
11
+ const PRODUCT_RANK_PAGE_SIZE = 10;
12
+ const PRODUCT_RANK_MAX_PAGES = 50;
9
13
  export class FastMossIntegration {
10
14
  context = null;
11
15
  page = null;
@@ -18,7 +22,16 @@ export class FastMossIntegration {
18
22
  lastCapture;
19
23
  lastMembershipCheckAt = 0;
20
24
  profileDir = path.join(CONFIG_DIR, "fastmoss-session");
25
+ authenticatedMarker = path.join(this.profileDir, "authenticated.json");
21
26
  dataDir = path.join(CONFIG_DIR, "fastmoss-selection");
27
+ savedAuthenticated = existsSync(this.authenticatedMarker);
28
+ constructor() {
29
+ if (!this.savedAuthenticated)
30
+ return;
31
+ this.phase = "saved";
32
+ this.authenticated = true;
33
+ this.message = "FastMoss 登录已保存在本机;窗口可以关闭,抓取时会自动恢复";
34
+ }
22
35
  status() {
23
36
  return {
24
37
  phase: this.phase,
@@ -53,8 +66,10 @@ export class FastMossIntegration {
53
66
  this.context = await chromium.launchPersistentContext(this.profileDir, {
54
67
  executablePath,
55
68
  headless: false,
56
- viewport: null,
57
- args: ["--start-maximized"],
69
+ viewport: { width: 480, height: 900 },
70
+ screen: { width: 515, height: 995 },
71
+ deviceScaleFactor: 1,
72
+ args: ["--window-size=515,995", "--window-position=20,20"],
58
73
  });
59
74
  }
60
75
  catch (error) {
@@ -69,12 +84,14 @@ export class FastMossIntegration {
69
84
  this.context.on("close", () => {
70
85
  this.context = null;
71
86
  this.page = null;
72
- this.phase = "idle";
73
- this.authenticated = false;
87
+ this.phase = this.savedAuthenticated ? "saved" : "idle";
88
+ this.authenticated = this.savedAuthenticated;
74
89
  this.verificationRequired = false;
75
90
  this.membershipExpired = false;
76
91
  this.lastMembershipCheckAt = 0;
77
- this.message = "FastMoss 浏览器已关闭;本机登录资料仍保留";
92
+ this.message = this.savedAuthenticated
93
+ ? "FastMoss 窗口已关闭,登录仍保存在本机;抓取时会自动恢复"
94
+ : "FastMoss 窗口已关闭;请先登录一次";
78
95
  });
79
96
  if (!this.page.url() || this.page.url() === "about:blank")
80
97
  await this.page.goto(FASTMOSS_HOME, { waitUntil: "domcontentloaded", timeout: 60_000 });
@@ -83,31 +100,45 @@ export class FastMossIntegration {
83
100
  async inspect() {
84
101
  this.useLatestPage();
85
102
  if (!this.page || this.page.isClosed()) {
86
- this.phase = "idle";
87
- this.authenticated = false;
103
+ this.phase = this.savedAuthenticated ? "saved" : "idle";
104
+ this.authenticated = this.savedAuthenticated;
88
105
  this.verificationRequired = false;
89
106
  this.membershipExpired = false;
90
107
  this.lastMembershipCheckAt = 0;
91
- this.message = "请先启动 FastMoss 专用浏览器";
108
+ this.message = this.savedAuthenticated
109
+ ? "FastMoss 登录已保存在本机;窗口可以关闭,抓取时会自动恢复"
110
+ : "请先启动 FastMoss 专用浏览器并登录";
92
111
  return this.status();
93
112
  }
94
113
  const snapshot = await this.page.evaluate(() => ({
95
114
  title: document.title,
96
115
  url: location.href,
97
116
  text: (document.body?.innerText || "").slice(0, 12_000),
98
- hasTable: Boolean(document.querySelector("table tbody tr")),
117
+ hasTable: [...document.querySelectorAll("table tbody tr")].some((row) => {
118
+ const text = (row.textContent || "").replace(/\s+/g, " ").trim();
119
+ return row.querySelectorAll("td").length >= 3 && !/^(?:no data|loading\.?\.?.?|暂无数据|暂无商品|无数据)$/i.test(text);
120
+ }),
99
121
  })).catch(() => ({ title: "", url: this.page?.url() || "", text: "", hasTable: false }));
100
122
  const haystack = `${snapshot.title}\n${snapshot.url}\n${snapshot.text}`.toLowerCase();
101
- this.verificationRequired = /captcha|verify|verification|security check|人机验证|安全验证|滑块验证|请完成验证/.test(haystack);
102
- const loginRequired = /\/login|\/signin|sign in|log in|登录账号|密码登录|扫码登录/.test(haystack) && !snapshot.hasTable;
123
+ this.verificationRequired = /captcha|verify|verification|security check|slide to complete the puzzle|verification failed|人机验证|安全验证|滑块验证|请完成验证/.test(haystack);
124
+ const loginRequired = /\/login|\/signin|sign in|log in|登录账号|密码登录|扫码登录|登录\s*\/\s*注册/.test(haystack) && !snapshot.hasTable;
103
125
  const explicitExpiry = /(?:会员|套餐|订阅|专业版|高级版|账号).{0,16}(?:已过期|已到期|到期失效|无法使用)|(?:membership|subscription|plan|account).{0,28}(?:has\s+)?expired|(?:membership|subscription|plan).{0,20}(?:is no longer active|needs renewal)/i.test(haystack);
104
126
  const entitlementUnavailable = /(?:当前账号|当前套餐).{0,20}(?:无权|没有权限|不支持|无法)(?:查看|访问|使用)|(?:开通|升级)(?:会员|专业版|套餐).{0,16}(?:后)?(?:才可|方可)(?:查看|访问|使用)|仅限(?:会员|专业版)(?:查看|使用)/i.test(haystack);
105
127
  this.membershipExpired = explicitExpiry || (!snapshot.hasTable && entitlementUnavailable);
106
128
  this.lastMembershipCheckAt = Date.now();
107
- this.authenticated = !this.verificationRequired && !loginRequired && /fastmoss\.com/i.test(snapshot.url) && snapshot.url !== FASTMOSS_HOME;
129
+ this.authenticated = !this.verificationRequired && !loginRequired && /fastmoss\.com/i.test(snapshot.url);
130
+ if (this.authenticated) {
131
+ this.savedAuthenticated = true;
132
+ await mkdir(this.profileDir, { recursive: true });
133
+ await writeFile(this.authenticatedMarker, JSON.stringify({ authenticated: true, updatedAt: new Date().toISOString() }), "utf8");
134
+ }
135
+ else if (loginRequired) {
136
+ this.savedAuthenticated = false;
137
+ await rm(this.authenticatedMarker, { force: true });
138
+ }
108
139
  if (this.verificationRequired) {
109
140
  this.phase = "verification_required";
110
- this.message = "检测到平台验证,请在专用浏览器中手动完成后再刷新状态";
141
+ this.message = "检测到 FastMoss 滑块验证,请在移动版登录窗口中手动完成;通过后刷新状态即可继续";
111
142
  }
112
143
  else if (this.membershipExpired) {
113
144
  this.phase = "membership_expired";
@@ -115,15 +146,17 @@ export class FastMossIntegration {
115
146
  }
116
147
  else if (!this.authenticated) {
117
148
  this.phase = "login_required";
118
- this.message = "请在专用浏览器中登录 FastMoss,并打开商品榜单页";
149
+ this.message = "请在移动版窗口中登录 FastMoss;登录成功后可以关闭窗口,登录态会保存在本机";
119
150
  }
120
151
  else {
121
152
  this.phase = "ready";
122
- this.message = snapshot.hasTable ? "FastMoss 已连接,当前榜单可以抓取" : "FastMoss 已登录,请打开需要抓取的商品榜单";
153
+ this.message = snapshot.hasTable ? "FastMoss 已连接,商品榜单可以抓取" : "FastMoss 已登录;窗口可以关闭,采集时会自动进入商品榜单";
123
154
  }
124
155
  return this.status();
125
156
  }
126
157
  async capture(input = {}) {
158
+ if (!this.context || !this.page || this.page.isClosed())
159
+ await this.start();
127
160
  await this.inspect();
128
161
  if (this.verificationRequired)
129
162
  throw new Error("请先在 FastMoss 浏览器中手动完成验证");
@@ -135,40 +168,31 @@ export class FastMossIntegration {
135
168
  throw new Error("FastMoss 会员已过期或当前账号没有榜单权限,请先更换账号");
136
169
  }
137
170
  if (!this.authenticated || !this.page || this.page.isClosed())
138
- throw new Error("FastMoss 尚未登录或专用浏览器未打开");
139
- const snapshot = await this.page.evaluate(() => {
140
- const shown = (element) => Boolean(element.getClientRects().length);
141
- const tables = [...document.querySelectorAll("table")].filter(shown).map((table) => {
142
- const rows = [...table.querySelectorAll("tr")].filter(shown);
143
- const headers = [...(rows[0]?.querySelectorAll("th,td") || [])].map((cell) => cell.innerText.replace(/\s+/g, " ").trim());
144
- return {
145
- headers,
146
- rows: rows.slice(1).map((row) => ({
147
- cells: [...row.querySelectorAll("td")].map((cell) => cell.innerText.replace(/\s+/g, " ").trim()),
148
- links: [...row.querySelectorAll("a[href]")].map((link) => link.href),
149
- images: [...row.querySelectorAll("img")].map((image) => {
150
- const src = image.currentSrc || image.src || image.dataset.src || image.dataset.original || "";
151
- const href = image.closest("a[href]")?.href || "";
152
- const hint = `${image.alt} ${image.title} ${image.className} ${image.closest("td")?.textContent || ""} ${src}`;
153
- return { src, href, content: image.dataset.content || image.dataset.url || "", isQr: /qr|qrcode|二维码/i.test(hint) };
154
- }).filter((image) => image.src).concat([...row.querySelectorAll("canvas")].map((canvas) => {
155
- let src = "";
156
- try {
157
- src = canvas.toDataURL("image/png");
158
- }
159
- catch { }
160
- const href = canvas.closest("a[href]")?.href || "";
161
- const hint = `${canvas.className} ${canvas.id} ${canvas.closest("td")?.textContent || ""}`;
162
- return { src, href, content: canvas.dataset.content || canvas.dataset.url || "", isQr: /qr|qrcode|二维码/i.test(hint) };
163
- }).filter((canvas) => canvas.src)),
164
- })).filter((row) => row.cells.some(Boolean)),
165
- };
166
- }).sort((left, right) => right.rows.length - left.rows.length);
167
- return { title: document.title, url: location.href, tables: tables.slice(0, 1) };
168
- });
169
- const records = extractRows(snapshot.tables, { ...input, date: localDate() }, snapshot.url);
171
+ throw new Error("FastMoss 尚未登录;请在恢复的窗口中完成登录后重试");
172
+ await this.openProductRanking(input);
173
+ const collected = new Map();
174
+ const pages = [];
175
+ let previousSignature = "";
176
+ for (let pageNumber = 1; pageNumber <= PRODUCT_RANK_MAX_PAGES; pageNumber += 1) {
177
+ const snapshot = await readVisibleTableSnapshot(this.page);
178
+ if (!snapshot.tables[0]?.rows.length || snapshot.signature === previousSignature)
179
+ break;
180
+ previousSignature = snapshot.signature;
181
+ pages.push(snapshot.url);
182
+ const pageRecords = extractRows(snapshot.tables, { ...input, date: localDate() }, snapshot.url);
183
+ pageRecords.forEach((row) => collected.set(String(row.product_id || row.title), row));
184
+ this.captured = collected.size;
185
+ this.message = `正在读取可见榜单第 ${pageNumber} 页,已收集 ${this.captured} 条商品数据`;
186
+ if (!await clickVisibleNextPage(this.page))
187
+ break;
188
+ await this.page.waitForTimeout(800);
189
+ await this.inspect();
190
+ if (this.verificationRequired || this.membershipExpired)
191
+ break;
192
+ }
193
+ const records = [...collected.values()];
170
194
  if (!records.length)
171
- throw new Error("当前页面没有可读取的商品表格,请先打开 FastMoss 商品榜单");
195
+ throw new Error("已自动进入 FastMoss 商品榜单,但没有读取到商品数据;请检查会员权限或页面是否仍在加载");
172
196
  await mkdir(this.dataDir, { recursive: true });
173
197
  const stored = await this.loadRows();
174
198
  const indexed = new Map(stored.map((row) => [`${row.date}:${row.product_id || row.title}`, row]));
@@ -177,8 +201,8 @@ export class FastMossIntegration {
177
201
  this.lastCapture = new Date().toISOString();
178
202
  this.captured = records.length;
179
203
  await writeFile(path.join(this.dataDir, "observations.json"), JSON.stringify(allRows, null, 2), "utf8");
180
- await writeFile(path.join(this.dataDir, `capture-${localDate()}.json`), JSON.stringify({ capturedAt: this.lastCapture, page: snapshot.url, records }, null, 2), "utf8");
181
- this.message = `已读取当前页面 ${records.length} 条商品数据`;
204
+ await writeFile(path.join(this.dataDir, `capture-${localDate()}.json`), JSON.stringify({ capturedAt: this.lastCapture, pages, records }, null, 2), "utf8");
205
+ this.message = this.verificationRequired ? `已保存验证前的 ${records.length} 条数据;请手动完成滑块后再次抓取,程序会接着合并` : `已读取 ${pages.length} 个可见分页,共 ${records.length} 条商品数据`;
182
206
  return { ...this.status(), records, allRows };
183
207
  }
184
208
  async close() {
@@ -187,12 +211,14 @@ export class FastMossIntegration {
187
211
  this.page = null;
188
212
  if (context)
189
213
  await context.close();
190
- this.phase = "idle";
191
- this.authenticated = false;
214
+ this.phase = this.savedAuthenticated ? "saved" : "idle";
215
+ this.authenticated = this.savedAuthenticated;
192
216
  this.verificationRequired = false;
193
217
  this.membershipExpired = false;
194
218
  this.lastMembershipCheckAt = 0;
195
- this.message = "FastMoss 浏览器已关闭;本机登录资料仍保留";
219
+ this.message = this.savedAuthenticated
220
+ ? "FastMoss 窗口已关闭,登录仍保存在本机;抓取时会自动恢复"
221
+ : "FastMoss 窗口已关闭;请先登录一次";
196
222
  return this.status();
197
223
  }
198
224
  async switchAccount() {
@@ -205,6 +231,8 @@ export class FastMossIntegration {
205
231
  await this.page.bringToFront();
206
232
  this.phase = "login_required";
207
233
  this.authenticated = false;
234
+ this.savedAuthenticated = false;
235
+ await rm(this.authenticatedMarker, { force: true });
208
236
  this.verificationRequired = false;
209
237
  this.membershipExpired = false;
210
238
  this.lastMembershipCheckAt = 0;
@@ -224,6 +252,103 @@ export class FastMossIntegration {
224
252
  if (pages.length)
225
253
  this.page = pages[pages.length - 1];
226
254
  }
255
+ async openProductRanking(input) {
256
+ if (!this.page || this.page.isClosed())
257
+ throw new Error("FastMoss 专用浏览器未打开");
258
+ const market = normalizeMarket(input.market);
259
+ this.message = `正在自动进入 ${market} 商品榜单`;
260
+ await this.page.goto(fastMossSalesRankUrl(market), { waitUntil: "domcontentloaded", timeout: 60_000 });
261
+ const deadline = Date.now() + PRODUCT_TABLE_TIMEOUT_MS;
262
+ while (Date.now() < deadline) {
263
+ const snapshot = await readVisibleTableSnapshot(this.page);
264
+ if (hasReadableProductRows(snapshot)) {
265
+ this.phase = "ready";
266
+ this.message = `${market} 商品榜单已打开,正在开始采集`;
267
+ return;
268
+ }
269
+ await this.inspect();
270
+ if (this.verificationRequired)
271
+ throw new Error("检测到 FastMoss 滑块验证,请在专用窗口中手动完成后再次抓取");
272
+ if (this.membershipExpired)
273
+ throw new Error("FastMoss 会员已过期或当前账号没有商品榜单权限,请先更换账号");
274
+ if (!this.authenticated)
275
+ throw new Error("FastMoss 登录已失效,请在专用窗口中重新登录后再次抓取");
276
+ await this.page.waitForTimeout(750);
277
+ }
278
+ throw new Error("FastMoss 商品榜单自动加载超时;请检查网络或在窗口中完成人工验证后重试");
279
+ }
280
+ }
281
+ export function fastMossSalesRankUrl(market) {
282
+ const url = new URL(FASTMOSS_SALES_RANK);
283
+ url.searchParams.set("region", normalizeMarket(market));
284
+ url.searchParams.set("page", "1");
285
+ url.searchParams.set("pagesize", String(PRODUCT_RANK_PAGE_SIZE));
286
+ return url.toString();
287
+ }
288
+ function normalizeMarket(value) {
289
+ const market = String(value || "MX").trim().toUpperCase();
290
+ return /^[A-Z]{2}$/.test(market) ? market : "MX";
291
+ }
292
+ function hasReadableProductRows(snapshot) {
293
+ return snapshot.tables.some((table) => table.rows.some((row) => row.cells.filter(Boolean).length >= 3));
294
+ }
295
+ async function readVisibleTableSnapshot(page) {
296
+ return page.evaluate(() => {
297
+ const shown = (element) => Boolean(element.getClientRects().length);
298
+ const tables = [...document.querySelectorAll("table")].filter(shown).map((table) => {
299
+ const rows = [...table.querySelectorAll("tr")].filter(shown);
300
+ const headers = [...(rows[0]?.querySelectorAll("th,td") || [])].map((cell) => cell.innerText.replace(/\s+/g, " ").trim());
301
+ return {
302
+ headers,
303
+ rows: rows.slice(1).map((row) => ({
304
+ cells: [...row.querySelectorAll("td")].map((cell) => cell.innerText.replace(/\s+/g, " ").trim()),
305
+ links: [...row.querySelectorAll("a[href]")].map((link) => link.href),
306
+ images: [...row.querySelectorAll("img")].map((image) => {
307
+ const src = image.currentSrc || image.src || image.dataset.src || image.dataset.original || "";
308
+ const href = image.closest("a[href]")?.href || "";
309
+ const hint = `${image.alt} ${image.title} ${image.className} ${image.closest("td")?.textContent || ""} ${src}`;
310
+ return { src, href, content: image.dataset.content || image.dataset.url || "", isQr: /qr|qrcode|二维码/i.test(hint) };
311
+ }).filter((image) => image.src).concat([...row.querySelectorAll("canvas")].map((canvas) => {
312
+ let src = "";
313
+ try {
314
+ src = canvas.toDataURL("image/png");
315
+ }
316
+ catch { }
317
+ const href = canvas.closest("a[href]")?.href || "";
318
+ const hint = `${canvas.className} ${canvas.id} ${canvas.closest("td")?.textContent || ""}`;
319
+ return { src, href, content: canvas.dataset.content || canvas.dataset.url || "", isQr: /qr|qrcode|二维码/i.test(hint) };
320
+ }).filter((canvas) => canvas.src)),
321
+ })).filter((row) => {
322
+ const text = row.cells.join(" ").replace(/\s+/g, " ").trim();
323
+ return row.cells.filter(Boolean).length >= 3 && !/^(?:no data|loading\.?\.?.?|暂无数据|暂无商品|无数据)$/i.test(text);
324
+ }),
325
+ };
326
+ }).sort((left, right) => right.rows.length - left.rows.length).slice(0, 1);
327
+ const primary = tables[0];
328
+ const signature = JSON.stringify([location.href, primary?.headers, primary?.rows[0]?.cells, primary?.rows[primary.rows.length - 1]?.cells]);
329
+ return { title: document.title, url: location.href, tables, signature };
330
+ });
331
+ }
332
+ async function clickVisibleNextPage(page) {
333
+ return page.evaluate(() => {
334
+ const shown = (element) => Boolean(element.getClientRects().length);
335
+ const selectors = [
336
+ ".ant-pagination-next button", ".ant-pagination-next", ".el-pagination .btn-next",
337
+ "button[aria-label*='next' i]", "[class*='pagination'] [class*='next']", "[class*='pagination'] [aria-label*='下一页']",
338
+ ];
339
+ const candidates = [...new Set(selectors.flatMap((selector) => [...document.querySelectorAll(selector)]))];
340
+ const next = candidates.find((element) => {
341
+ if (!shown(element))
342
+ return false;
343
+ const parent = element.closest("li,button,[class*='next']");
344
+ const disabled = element.matches(":disabled") || element.getAttribute("aria-disabled") === "true" || parent?.getAttribute("aria-disabled") === "true" || /disabled/.test(`${element.className} ${parent?.className || ""}`);
345
+ return !disabled;
346
+ });
347
+ if (!next)
348
+ return false;
349
+ next.click();
350
+ return true;
351
+ });
227
352
  }
228
353
  function findChromiumExecutable() {
229
354
  const home = os.homedir();
@@ -247,11 +372,14 @@ function extractRows(tables, context, pageUrl) {
247
372
  for (const row of table.rows) {
248
373
  const raw = Object.fromEntries(row.cells.map((value, index) => [table.headers[index] || `column_${index + 1}`, value]));
249
374
  const pick = (...patterns) => Object.entries(raw).find(([key]) => patterns.some((pattern) => pattern.test(key)))?.[1] || "";
250
- const title = String(pick(/商品名|商品标题|product|title/i) || row.cells.find((cell) => cell.length > 5) || "");
375
+ const productCell = String(pick(/^商品$|商品名|商品标题|product|title/i) || row.cells.find((cell) => cell.length > 5) || "");
376
+ const title = productCell.replace(/\s*(?:售价|价格|price)\s*[::].*$/is, "").trim();
251
377
  const productUrl = row.links.find((link) => /product|goods|item|detail/i.test(link)) || row.links[0] || pageUrl;
252
- const productId = String(pick(/商品\s*id|product\s*id/i) || productUrl.match(/(?:product|goods|item)[/=_-]([\w-]+)/i)?.[1] || stableId(`${title}:${productUrl}`));
378
+ const productId = String(pick(/商品\s*id|product\s*id/i) || productUrl.match(/(?:detail|product|goods|item)[/=_-]([\w-]+)/i)?.[1] || stableId(`${title}:${productUrl}`));
253
379
  const images = row.images.filter((image) => !image.isQr).map((image) => image.src);
254
380
  const qr = row.images.find((image) => image.isQr);
381
+ const priceText = String(pick(/^价格$|^售价$|price/i) || productCell.match(/(?:售价|价格|price)\s*[::]\s*([^\s]+)/i)?.[1] || "");
382
+ const storeName = String(pick(/所属店铺|店铺|shop|store/i)).replace(/\s*店铺销量\s*[::].*$/is, "").trim();
255
383
  output.push({
256
384
  date: context.date,
257
385
  source: "fastmoss-agent",
@@ -259,17 +387,17 @@ function extractRows(tables, context, pageUrl) {
259
387
  platform_product_id: String(pick(/平台商品|tiktok.*id/i) || productId),
260
388
  title,
261
389
  product_url: productUrl,
262
- store_name: String(pick(/店铺|shop|store/i)),
263
- category: String(pick(/类目|category/i) || context.category || ""),
390
+ store_name: storeName,
391
+ category: String(pick(/类目|分类|category/i) || context.category || context.categories?.[0] || ""),
264
392
  market: String(pick(/国家|市场|market|country/i) || context.market || ""),
265
393
  shop_type: String(pick(/店铺类型|shop.*type|seller.*type/i) || context.shopType || ""),
266
394
  period_days: context.periodDays || 7,
267
- price: numberValue(pick(/价格|售价|price/i)),
395
+ price: numberValue(priceText),
268
396
  units_sold: numberValue(pick(/销量|sold|sales|orders/i)),
269
397
  gmv: numberValue(pick(/gmv|销售额|成交额/i)),
270
398
  commission: numberValue(pick(/佣金|commission/i)),
271
399
  creators: numberValue(pick(/达人|creator/i)),
272
- trend_score: numberValue(pick(/趋势|增长|growth|trend/i)),
400
+ trend_score: numberValue(pick(/趋势|增长|环比|growth|trend/i)),
273
401
  image_urls: images,
274
402
  image: images[0] || "",
275
403
  qr_image_url: qr?.src || "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaohhhh1/canvas-agent",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",