tt-help-cli-ycl 1.4.30 → 1.4.32

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.4.30",
3
+ "version": "1.4.32",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/attach.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import { TikTokScraper } from "../lib/tiktok-scraper.mjs";
2
2
  import { CDNBlockedError } from "../lib/parse-ssr.mjs";
3
- import { proxy as configuredProxy } from "../lib/constants.js";
3
+ import {
4
+ proxy as configuredProxy,
5
+ userId as configuredUserId,
6
+ saveUserId,
7
+ } from "../lib/constants.js";
8
+ import { getMacOrUuid } from "../lib/mac-or-uuid.js";
4
9
  import { createApiClient } from "../lib/api-client.js";
5
10
  import v8 from "node:v8";
6
11
 
@@ -110,6 +115,15 @@ export async function handleAttach(options) {
110
115
  `[Attach] 并行数: ${attachParallel}, 空闲间隔: ${attachInterval}秒, 服务端: ${serverUrl}${countryStr}`,
111
116
  );
112
117
 
118
+ // ── 设备标识 ──
119
+ let userId = configuredUserId;
120
+ if (!userId) {
121
+ userId = await getMacOrUuid();
122
+ saveUserId(userId);
123
+ attachLog(`[Attach] 未检测到本地用户编号,已生成并使用: ${userId}`);
124
+ }
125
+ attachLog(`[Attach] 用户编号: ${userId}`);
126
+
113
127
  const { apiGet, apiPost } = createApiClient({ log: false });
114
128
 
115
129
  const scraper = new TikTokScraper({
@@ -174,7 +188,7 @@ export async function handleAttach(options) {
174
188
  ? `&countries=${encodeURIComponent(attachCountries.join(","))}`
175
189
  : "";
176
190
  const { total, tasks } = await apiGet(
177
- `${serverUrl}/api/user-update-tasks?limit=${attachParallel}${countryParam}`,
191
+ `${serverUrl}/api/user-update-tasks?limit=${attachParallel}&userId=${encodeURIComponent(userId)}${countryParam}`,
178
192
  );
179
193
 
180
194
  if (!tasks || tasks.length === 0) {
@@ -683,8 +683,8 @@ async function runAutoMode(options) {
683
683
  let tasks;
684
684
  try {
685
685
  const apiUrl = jobLocations
686
- ? `${serverUrl}/api/comment-tasks?limit=${actualParallel}&locations=${encodeURIComponent(jobLocations)}`
687
- : `${serverUrl}/api/comment-tasks?limit=${actualParallel}`;
686
+ ? `${serverUrl}/api/comment-tasks?limit=${actualParallel}&locations=${encodeURIComponent(jobLocations)}&userId=${encodeURIComponent(userId)}`
687
+ : `${serverUrl}/api/comment-tasks?limit=${actualParallel}&userId=${encodeURIComponent(userId)}`;
688
688
  const resp = await apiGet(apiUrl);
689
689
  tasks = resp.tasks || [];
690
690
  } catch (err) {
@@ -736,15 +736,13 @@ async function runAutoMode(options) {
736
736
  try {
737
737
  const modals = await detectModals(session.page);
738
738
  const loginModal = modals.find(
739
- (m) =>
740
- m.type === "login-modal" || m.type === "modal-container",
739
+ (m) => m.type === "login-modal" || m.type === "modal-container",
741
740
  );
742
741
  if (loginModal) {
743
742
  console.error(` 🔔 检测到登录弹窗,正在关闭...`);
744
- const { closed, failed } = await closeAllModals(
745
- session.page,
746
- { waitMs: 300 },
747
- );
743
+ const { closed, failed } = await closeAllModals(session.page, {
744
+ waitMs: 300,
745
+ });
748
746
  console.error(
749
747
  ` 🔔 弹窗关闭: closed=${JSON.stringify(closed)} failed=${JSON.stringify(failed)}`,
750
748
  );
@@ -168,6 +168,9 @@ export async function handleExplore(options) {
168
168
 
169
169
  const page = await getOrCreatePage(browser);
170
170
 
171
+ // 创建专用商品页(后台复用,避免重复打开/关闭闪屏)
172
+ const shopPage = await browser.contexts()[0].newPage();
173
+
171
174
  // 先导航到 TikTok 页面,再检测登录状态
172
175
  await withRetry("启动页面导航", async () => {
173
176
  await page.goto(STARTUP_TIKTOK_URL, {
@@ -397,6 +400,7 @@ export async function handleExplore(options) {
397
400
  location: exploreLocation,
398
401
  proxyServer: cdpOptions.proxyServer || null,
399
402
  browser,
403
+ shopPage,
400
404
  },
401
405
  console.error,
402
406
  );
@@ -435,6 +439,7 @@ export async function handleExplore(options) {
435
439
  location: exploreLocation,
436
440
  proxyServer: cdpOptions.proxyServer || null,
437
441
  browser,
442
+ shopPage,
438
443
  },
439
444
  console.error,
440
445
  );
@@ -521,6 +526,9 @@ export async function handleExplore(options) {
521
526
 
522
527
  const payload = {
523
528
  userInfo: result.userInfo || {},
529
+ recentEcVideos: result.recentEcVideos || null,
530
+ topRecentVideo: result.topRecentVideo || null,
531
+ productDetail: result.productDetail || null,
524
532
  discoveredFollowing: (result.discoveredFollowing || []).map((f) => ({
525
533
  handle: Array.isArray(f) ? f[0] : f,
526
534
  displayName: Array.isArray(f) ? f[1] : null,
@@ -8,7 +8,12 @@
8
8
 
9
9
  import { TikTokScraper } from "../lib/tiktok-scraper.mjs";
10
10
  import { CDNBlockedError } from "../lib/parse-ssr.mjs";
11
- import { proxy as configuredProxy } from "../lib/constants.js";
11
+ import {
12
+ proxy as configuredProxy,
13
+ userId as configuredUserId,
14
+ saveUserId,
15
+ } from "../lib/constants.js";
16
+ import { getMacOrUuid } from "../lib/mac-or-uuid.js";
12
17
  import { createApiClient } from "../lib/api-client.js";
13
18
  import v8 from "node:v8";
14
19
 
@@ -120,6 +125,15 @@ export async function handleRecheck(options) {
120
125
  `[Recheck] 并行数: ${attachParallel}, 空闲间隔: ${attachInterval}秒, 服务端: ${serverUrl}${countryStr}`,
121
126
  );
122
127
 
128
+ // ── 设备标识 ──
129
+ let userId = configuredUserId;
130
+ if (!userId) {
131
+ userId = await getMacOrUuid();
132
+ saveUserId(userId);
133
+ recheckLog(`[Recheck] 未检测到本地用户编号,已生成并使用: ${userId}`);
134
+ }
135
+ recheckLog(`[Recheck] 用户编号: ${userId}`);
136
+
123
137
  const { apiGet, apiPost } = createApiClient({ log: false });
124
138
 
125
139
  const scraper = new TikTokScraper({
@@ -189,7 +203,7 @@ export async function handleRecheck(options) {
189
203
  ? `&countries=${encodeURIComponent(attachCountries.join(","))}`
190
204
  : "";
191
205
  const { total, tasks } = await apiGet(
192
- `${serverUrl}/api/recheck-jobs?limit=${attachParallel}${countryParam}`,
206
+ `${serverUrl}/api/recheck-jobs?limit=${attachParallel}&userId=${encodeURIComponent(userId)}${countryParam}`,
193
207
  );
194
208
 
195
209
  if (!tasks || tasks.length === 0) {
@@ -170,6 +170,9 @@ export async function handleRefresh(options) {
170
170
 
171
171
  const page = await getOrCreatePage(browser);
172
172
 
173
+ // 创建专用商品页(后台复用,避免重复打开/关闭闪屏)
174
+ const shopPage = await browser.contexts()[0].newPage();
175
+
173
176
  // 导航到 TikTok 页面
174
177
  await withRetry("启动页面导航", async () => {
175
178
  await page.goto(STARTUP_TIKTOK_URL, {
@@ -381,6 +384,7 @@ export async function handleRefresh(options) {
381
384
  location: exploreLocation,
382
385
  locationMode: "refresh",
383
386
  browser,
387
+ shopPage,
384
388
  proxyServer: cdpOptions.proxyServer || null,
385
389
  },
386
390
  console.error,
@@ -407,6 +411,7 @@ export async function handleRefresh(options) {
407
411
  location: exploreLocation,
408
412
  locationMode: "refresh",
409
413
  browser,
414
+ shopPage,
410
415
  proxyServer: cdpOptions.proxyServer || null,
411
416
  },
412
417
  console.error,
@@ -446,6 +451,7 @@ export async function handleRefresh(options) {
446
451
  location: exploreLocation,
447
452
  locationMode: "refresh",
448
453
  browser,
454
+ shopPage,
449
455
  proxyServer: cdpOptions.proxyServer || null,
450
456
  },
451
457
  console.error,
@@ -542,6 +548,8 @@ export async function handleRefresh(options) {
542
548
  const payload = {
543
549
  userInfo: refreshUserInfo,
544
550
  recentEcVideos: result.recentEcVideos || null,
551
+ topRecentVideo: result.topRecentVideo || null,
552
+ productDetail: result.productDetail || null,
545
553
  discoveredFollowing: (result.discoveredFollowing || []).map((f) => ({
546
554
  handle: Array.isArray(f) ? f[0] : f,
547
555
  displayName: Array.isArray(f) ? f[1] : null,
package/src/cli/search.js CHANGED
@@ -43,7 +43,10 @@ import { HealthChecker } from "../lib/browser/health-checker.js";
43
43
  import {
44
44
  server as cfgServer,
45
45
  proxy as configuredProxy,
46
+ userId as configuredUserId,
47
+ saveUserId,
46
48
  } from "../lib/constants.js";
49
+ import { getMacOrUuid } from "../lib/mac-or-uuid.js";
47
50
 
48
51
  const DEFAULT_SERVER = cfgServer || "http://127.0.0.1:3000";
49
52
  const STARTUP_TIKTOK_URL = "https://www.tiktok.com/@ycl5007";
@@ -71,6 +74,7 @@ export async function handleSearch(parsed) {
71
74
  serverUrl,
72
75
  basePort = 3333,
73
76
  portCount = 10,
77
+ parallel = 1,
74
78
  proxy: cliProxy,
75
79
  } = searchOpts || {};
76
80
 
@@ -128,6 +132,15 @@ export async function handleSearch(parsed) {
128
132
  meta: { port: cdpOptions.port, type: "tag-search" },
129
133
  });
130
134
 
135
+ // ── 设备标识 ──
136
+ let userId = configuredUserId;
137
+ if (!userId) {
138
+ userId = await getMacOrUuid();
139
+ saveUserId(userId);
140
+ log(`[Search] 未检测到本地用户编号,已生成并使用: ${userId}`);
141
+ }
142
+ log(`[Search] 用户编号: ${userId}`);
143
+
131
144
  setDelayConfig("normal");
132
145
 
133
146
  try {
@@ -196,7 +209,9 @@ export async function handleSearch(parsed) {
196
209
  try {
197
210
  return await p.evaluate(async () => {
198
211
  try {
199
- const res = await fetch("https://www.tiktok.com/passport/web/account/info/");
212
+ const res = await fetch(
213
+ "https://www.tiktok.com/passport/web/account/info/",
214
+ );
200
215
  const data = await res.json();
201
216
  return data?.data?.unique_id || null;
202
217
  } catch {}
@@ -366,9 +381,12 @@ export async function handleSearch(parsed) {
366
381
  await delay(switchMax, switchMax * 3);
367
382
 
368
383
  // ── 1. 获取搜索任务 ──
369
- const taskRes = await fetch(`${baseUrl}/api/tags/search-task`, {
370
- headers: headers(),
371
- });
384
+ const taskRes = await fetch(
385
+ `${baseUrl}/api/tags/search-task?userId=${encodeURIComponent(userId)}`,
386
+ {
387
+ headers: headers(),
388
+ },
389
+ );
372
390
  const task = await taskRes.json();
373
391
 
374
392
  if (!task.tag) {
@@ -393,11 +411,14 @@ export async function handleSearch(parsed) {
393
411
  );
394
412
 
395
413
  // ── 2. 锁定标签 ──
396
- const claimRes = await fetch(`${baseUrl}/api/tags/search-claim`, {
397
- method: "POST",
398
- headers: headers({ "Content-Type": "application/json" }),
399
- body: JSON.stringify({ tag }),
400
- });
414
+ const claimRes = await fetch(
415
+ `${baseUrl}/api/tags/search-claim?userId=${encodeURIComponent(userId)}`,
416
+ {
417
+ method: "POST",
418
+ headers: headers({ "Content-Type": "application/json" }),
419
+ body: JSON.stringify({ tag }),
420
+ },
421
+ );
401
422
  const claimData = await claimRes.json();
402
423
  if (!claimData.ok) {
403
424
  log(` ⏭️ ${claimData.error},跳过`);
@@ -410,6 +431,7 @@ export async function handleSearch(parsed) {
410
431
  let searchError = null;
411
432
  try {
412
433
  ids = await collectSearchAuthors(browser, tag, {
434
+ parallel,
413
435
  log,
414
436
  exclude: myUsername ? [myUsername] : [],
415
437
  });
@@ -432,6 +454,7 @@ export async function handleSearch(parsed) {
432
454
  // 重试搜索
433
455
  try {
434
456
  ids = await collectSearchAuthors(browser, tag, {
457
+ parallel,
435
458
  log,
436
459
  exclude: myUsername ? [myUsername] : [],
437
460
  });
@@ -457,6 +480,7 @@ export async function handleSearch(parsed) {
457
480
  await new Promise((r) => setTimeout(r, 5000));
458
481
  try {
459
482
  ids = await collectSearchAuthors(browser, tag, {
483
+ parallel,
460
484
  log,
461
485
  exclude: myUsername ? [myUsername] : [],
462
486
  });
@@ -485,11 +509,14 @@ export async function handleSearch(parsed) {
485
509
  ).catch(() => {});
486
510
 
487
511
  // 仍提交空结果,释放标签锁定
488
- await fetch(`${baseUrl}/api/tags/search-result`, {
489
- method: "POST",
490
- headers: headers({ "Content-Type": "application/json" }),
491
- body: JSON.stringify({ tag, authors: [], country }),
492
- }).catch(() => {});
512
+ await fetch(
513
+ `${baseUrl}/api/tags/search-result?userId=${encodeURIComponent(userId)}`,
514
+ {
515
+ method: "POST",
516
+ headers: headers({ "Content-Type": "application/json" }),
517
+ body: JSON.stringify({ tag, authors: [], country }),
518
+ },
519
+ ).catch(() => {});
493
520
  log(` 提交: 0 个作者(搜索异常)`);
494
521
  totalSearched++;
495
522
  continue;
@@ -525,20 +552,26 @@ export async function handleSearch(parsed) {
525
552
  // ── 4. 提交结果 ──
526
553
  let pushed = 0;
527
554
  if (ids.length > 0) {
528
- const resultRes = await fetch(`${baseUrl}/api/tags/search-result`, {
529
- method: "POST",
530
- headers: headers({ "Content-Type": "application/json" }),
531
- body: JSON.stringify({ tag, authors: ids, country }),
532
- });
555
+ const resultRes = await fetch(
556
+ `${baseUrl}/api/tags/search-result?userId=${encodeURIComponent(userId)}`,
557
+ {
558
+ method: "POST",
559
+ headers: headers({ "Content-Type": "application/json" }),
560
+ body: JSON.stringify({ tag, authors: ids, country }),
561
+ },
562
+ );
533
563
  const result = await resultRes.json();
534
564
  pushed = result.pushed || 0;
535
565
  log(` 提交: 新增 ${pushed} 个, 跳过 ${result.skipped} 个`);
536
566
  } else {
537
- await fetch(`${baseUrl}/api/tags/search-result`, {
538
- method: "POST",
539
- headers: headers({ "Content-Type": "application/json" }),
540
- body: JSON.stringify({ tag, authors: [], country }),
541
- });
567
+ await fetch(
568
+ `${baseUrl}/api/tags/search-result?userId=${encodeURIComponent(userId)}`,
569
+ {
570
+ method: "POST",
571
+ headers: headers({ "Content-Type": "application/json" }),
572
+ body: JSON.stringify({ tag, authors: [], country }),
573
+ },
574
+ );
542
575
  log(` 提交: 0 个作者`);
543
576
  }
544
577
 
package/src/cli/tag.js CHANGED
@@ -724,9 +724,10 @@ export async function handleScore(parsed) {
724
724
  log("");
725
725
  }
726
726
 
727
- async function reportToServer(baseUrl, result, clientId, meta) {
727
+ async function reportToServer(baseUrl, result, clientId, meta, userId = "") {
728
728
  try {
729
- const res = await fetch(`${baseUrl}/api/tags/score-result`, {
729
+ const userIdParam = userId ? `?userId=${encodeURIComponent(userId)}` : "";
730
+ const res = await fetch(`${baseUrl}/api/tags/score-result${userIdParam}`, {
730
731
  method: "POST",
731
732
  headers: buildClientHeaders(clientId, meta, {
732
733
  "Content-Type": "application/json",
@@ -972,9 +973,12 @@ export async function handleScoreAll(parsed) {
972
973
  while (true) {
973
974
  try {
974
975
  // 从服务端取下一个 new 标签(同时查剩余数量)
975
- const tagsRes = await fetch(`${baseUrl}/api/tags?status=new&limit=1`, {
976
- headers: buildClientHeaders(clientId, clientMeta),
977
- });
976
+ const tagsRes = await fetch(
977
+ `${baseUrl}/api/tags?status=new&limit=1&userId=${encodeURIComponent(userId)}`,
978
+ {
979
+ headers: buildClientHeaders(clientId, clientMeta),
980
+ },
981
+ );
978
982
  const tagsData = await tagsRes.json();
979
983
 
980
984
  if (!tagsData.tags || tagsData.tags.length === 0) {
@@ -1018,13 +1022,16 @@ export async function handleScoreAll(parsed) {
1018
1022
  };
1019
1023
 
1020
1024
  // 锁定 tag(meta 中不放入 tag,避免非 ASCII 字符导致 header ByteString 报错)
1021
- const claimRes = await fetch(`${baseUrl}/api/tags/claim`, {
1022
- method: "POST",
1023
- headers: buildClientHeaders(clientId, clientMeta, {
1024
- "Content-Type": "application/json",
1025
- }),
1026
- body: JSON.stringify({ tag }),
1027
- });
1025
+ const claimRes = await fetch(
1026
+ `${baseUrl}/api/tags/claim?userId=${encodeURIComponent(userId)}`,
1027
+ {
1028
+ method: "POST",
1029
+ headers: buildClientHeaders(clientId, clientMeta, {
1030
+ "Content-Type": "application/json",
1031
+ }),
1032
+ body: JSON.stringify({ tag }),
1033
+ },
1034
+ );
1028
1035
  const claimData = await claimRes.json();
1029
1036
  if (!claimData.ok) {
1030
1037
  // already claimed: 其他机器抢先了,跳过不标 dead
@@ -1036,7 +1043,7 @@ export async function handleScoreAll(parsed) {
1036
1043
  log(` ⚠️ 无法锁定 (${claimData.error}),标记为 dead 并跳过`);
1037
1044
  result.error = claimData.error;
1038
1045
  result.status = "dead";
1039
- await reportToServer(baseUrl, result, clientId, clientMeta);
1046
+ await reportToServer(baseUrl, result, clientId, clientMeta, userId);
1040
1047
  totalScored++;
1041
1048
  await recyclePage();
1042
1049
  continue;
@@ -1068,7 +1075,7 @@ export async function handleScoreAll(parsed) {
1068
1075
  log(` ⚠️ 标签不存在 (${deadSec}s),标记 dead`);
1069
1076
  result.status = "dead";
1070
1077
  result.error = "tag_not_found";
1071
- await reportToServer(baseUrl, result, clientId, clientMeta);
1078
+ await reportToServer(baseUrl, result, clientId, clientMeta, userId);
1072
1079
  totalScored++;
1073
1080
  await randomDelay(0, 5000);
1074
1081
  await recyclePage();
@@ -1128,7 +1135,7 @@ export async function handleScoreAll(parsed) {
1128
1135
  }
1129
1136
 
1130
1137
  // 上报结果
1131
- await reportToServer(baseUrl, result, clientId, clientMeta);
1138
+ await reportToServer(baseUrl, result, clientId, clientMeta, userId);
1132
1139
 
1133
1140
  totalScored++;
1134
1141
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
@@ -1622,9 +1629,12 @@ export async function handleTagRefresh(parsed) {
1622
1629
  while (true) {
1623
1630
  try {
1624
1631
  // 从服务端获取下一个 productive 标签
1625
- const taskRes = await fetch(`${baseUrl}/api/tags/refresh-task`, {
1626
- headers: buildClientHeaders(clientId, clientMeta),
1627
- });
1632
+ const taskRes = await fetch(
1633
+ `${baseUrl}/api/tags/refresh-task?userId=${encodeURIComponent(userId)}`,
1634
+ {
1635
+ headers: buildClientHeaders(clientId, clientMeta),
1636
+ },
1637
+ );
1628
1638
  const taskData = await taskRes.json();
1629
1639
 
1630
1640
  if (!taskData.tag) {
@@ -1661,13 +1671,16 @@ export async function handleTagRefresh(parsed) {
1661
1671
  };
1662
1672
 
1663
1673
  // 锁定标签
1664
- const claimRes = await fetch(`${baseUrl}/api/tags/refresh-claim`, {
1665
- method: "POST",
1666
- headers: buildClientHeaders(clientId, clientMeta, {
1667
- "Content-Type": "application/json",
1668
- }),
1669
- body: JSON.stringify({ tag }),
1670
- });
1674
+ const claimRes = await fetch(
1675
+ `${baseUrl}/api/tags/refresh-claim?userId=${encodeURIComponent(userId)}`,
1676
+ {
1677
+ method: "POST",
1678
+ headers: buildClientHeaders(clientId, clientMeta, {
1679
+ "Content-Type": "application/json",
1680
+ }),
1681
+ body: JSON.stringify({ tag }),
1682
+ },
1683
+ );
1671
1684
  const claimData = await claimRes.json();
1672
1685
  if (!claimData.ok) {
1673
1686
  if (
@@ -1705,13 +1718,20 @@ export async function handleTagRefresh(parsed) {
1705
1718
  const deadSec = ((Date.now() - fetchStart) / 1000).toFixed(1);
1706
1719
  log(` ⚠️ 无视频 (${deadSec}s),保留 productive 状态,等待下次刷新`);
1707
1720
  result.error = "no videos found";
1708
- await fetch(`${baseUrl}/api/tags/refresh-result`, {
1709
- method: "POST",
1710
- headers: buildClientHeaders(clientId, clientMeta, {
1711
- "Content-Type": "application/json",
1712
- }),
1713
- body: JSON.stringify({ tag, pushedUsers: 0, error: result.error }),
1714
- }).catch(() => {});
1721
+ await fetch(
1722
+ `${baseUrl}/api/tags/refresh-result?userId=${encodeURIComponent(userId)}`,
1723
+ {
1724
+ method: "POST",
1725
+ headers: buildClientHeaders(clientId, clientMeta, {
1726
+ "Content-Type": "application/json",
1727
+ }),
1728
+ body: JSON.stringify({
1729
+ tag,
1730
+ pushedUsers: 0,
1731
+ error: result.error,
1732
+ }),
1733
+ },
1734
+ ).catch(() => {});
1715
1735
  totalRefreshed++;
1716
1736
  await randomDelay(0, 5000);
1717
1737
  await recyclePage();
@@ -1775,17 +1795,20 @@ export async function handleTagRefresh(parsed) {
1775
1795
  }
1776
1796
 
1777
1797
  // 上报刷新结果(只更新 pushed_users,不改变 score/status)
1778
- await fetch(`${baseUrl}/api/tags/refresh-result`, {
1779
- method: "POST",
1780
- headers: buildClientHeaders(clientId, clientMeta, {
1781
- "Content-Type": "application/json",
1782
- }),
1783
- body: JSON.stringify({
1784
- tag,
1785
- pushedUsers: result.pushedUsers,
1786
- error: result.error,
1787
- }),
1788
- }).catch(() => {});
1798
+ await fetch(
1799
+ `${baseUrl}/api/tags/refresh-result?userId=${encodeURIComponent(userId)}`,
1800
+ {
1801
+ method: "POST",
1802
+ headers: buildClientHeaders(clientId, clientMeta, {
1803
+ "Content-Type": "application/json",
1804
+ }),
1805
+ body: JSON.stringify({
1806
+ tag,
1807
+ pushedUsers: result.pushedUsers,
1808
+ error: result.error,
1809
+ }),
1810
+ },
1811
+ ).catch(() => {});
1789
1812
 
1790
1813
  totalRefreshed++;
1791
1814
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
@@ -5,6 +5,65 @@ import {
5
5
  detectPageErrorWithWait,
6
6
  } from "./page-error-detector.js";
7
7
 
8
+ /**
9
+ * 从 item.anchors 中提取 TikTok Shop 商品信息
10
+ * anchors[].extra 是两层 JSON 字符串:
11
+ * 第1层 parse → [{id, keyword, component_key, extra: "...", ...}]
12
+ * 第2层 parse → {product_id, title, seo_url, skus, seller_id, ...}
13
+ *
14
+ * 注意:product_id / seller_id 为 19 位数字,超出 JS Number.MAX_SAFE_INTEGER,
15
+ * 必须用正则从原始字符串中提取,避免 JSON.parse 精度丢失。
16
+ */
17
+ function extractProductInfo(item) {
18
+ if (!item.anchors || !Array.isArray(item.anchors)) return null;
19
+ for (const anchor of item.anchors) {
20
+ try {
21
+ const extraArr = JSON.parse(anchor.extra || "[]");
22
+ if (!Array.isArray(extraArr)) continue;
23
+ for (const e of extraArr) {
24
+ if (!e.extra || !e.extra.includes("product_id")) continue;
25
+
26
+ // 从原始 JSON 字符串中用正则提取,绕过 JS 大整数精度问题
27
+ const raw = e.extra;
28
+ const pidMatch = raw.match(/"product_id":(\d+)/);
29
+ const sidMatch = raw.match(/"seller_id":"?(\d+)"?/);
30
+ const seoMatch = raw.match(/"seo_url":"((?:[^"\\]|\\.)*)"/);
31
+ const titleMatch = raw.match(/"title":"((?:[^"\\]|\\.)*)"/);
32
+
33
+ return {
34
+ productId: pidMatch ? pidMatch[1] : "",
35
+ productTitle: titleMatch ? titleMatch[1] : e.keyword || "",
36
+ seoUrl: seoMatch ? seoMatch[1] : "",
37
+ sellerId: sidMatch ? sidMatch[1] : "",
38
+ };
39
+ }
40
+ } catch {
41
+ // 解析失败则跳过该 anchor
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+
47
+ /**
48
+ * 构建统一的视频对象
49
+ */
50
+ function buildVideoObject(item, username) {
51
+ const href = `https://www.tiktok.com/@${username}/video/${item.id}`;
52
+ const productInfo = extractProductInfo(item);
53
+ return {
54
+ id: item.id,
55
+ href,
56
+ createTime: item.createTime || null,
57
+ playCount: item.stats?.playCount || 0,
58
+ isECVideo: item.isECVideo ? 1 : 0,
59
+ isAd: item.isAd ? 1 : 0,
60
+ productId: productInfo?.productId || null,
61
+ productTitle: productInfo?.productTitle || null,
62
+ seoUrl: productInfo?.seoUrl || null,
63
+ sellerId: productInfo?.sellerId || null,
64
+ };
65
+ }
66
+
8
67
  async function processAPIResponse(
9
68
  data,
10
69
  username,
@@ -17,15 +76,7 @@ async function processAPIResponse(
17
76
  const firstPageItems = data.itemList || [];
18
77
  for (const item of firstPageItems) {
19
78
  if (items.length >= maxVideos) break;
20
- const href = `https://www.tiktok.com/@${username}/video/${item.id}`;
21
- items.push({
22
- id: item.id,
23
- href,
24
- createTime: item.createTime || null,
25
- playCount: item.stats?.playCount || 0,
26
- isECVideo: item.isECVideo ? 1 : 0,
27
- isAd: item.isAd ? 1 : 0,
28
- });
79
+ items.push(buildVideoObject(item, username));
29
80
  }
30
81
 
31
82
  let cursor = data.cursor;
@@ -68,15 +119,7 @@ async function processAPIResponse(
68
119
  if (pageData && pageData.itemList) {
69
120
  for (const item of pageData.itemList) {
70
121
  if (items.length >= maxVideos) break;
71
- const href = `https://www.tiktok.com/@${username}/video/${item.id}`;
72
- items.push({
73
- id: item.id,
74
- href,
75
- createTime: item.createTime || null,
76
- playCount: item.stats?.playCount || 0,
77
- isECVideo: item.isECVideo ? 1 : 0,
78
- isAd: item.isAd ? 1 : 0,
79
- });
122
+ items.push(buildVideoObject(item, username));
80
123
  }
81
124
  }
82
125
 
package/src/lib/args.js CHANGED
@@ -560,6 +560,7 @@ function parseSearchArgs(args) {
560
560
  let serverUrl = null;
561
561
  let basePort = 3333;
562
562
  let portCount = 10;
563
+ let parallel = 1;
563
564
  let proxy = null;
564
565
 
565
566
  for (let i = 0; i < args.length; i++) {
@@ -570,6 +571,8 @@ function parseSearchArgs(args) {
570
571
  basePort = parseInt(args[++i]) || 3333;
571
572
  } else if (arg === "--port-count") {
572
573
  portCount = parseInt(args[++i]) || 10;
574
+ } else if (arg === "-p" || arg === "--parallel") {
575
+ parallel = parseInt(args[++i]) || 1;
573
576
  } else if (arg === "--proxy") {
574
577
  proxy = args[++i];
575
578
  }
@@ -577,7 +580,7 @@ function parseSearchArgs(args) {
577
580
 
578
581
  return {
579
582
  subcommand: "search",
580
- search: { serverUrl, basePort, portCount, proxy },
583
+ search: { serverUrl, basePort, portCount, parallel, proxy },
581
584
  urls: [],
582
585
  outputFormat: "json",
583
586
  exploreCount: 0,