tt-help-cli-ycl 1.4.51 → 1.4.53

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.51",
3
+ "version": "1.4.53",
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
@@ -57,11 +57,13 @@ export async function handleAttach(options) {
57
57
  attachCountries,
58
58
  customProxy,
59
59
  attachPoolSize,
60
+ attachWebserver,
60
61
  showHelp,
61
62
  } = options;
63
+ const webserverUrl = attachWebserver || null;
62
64
  const effectiveProxy = customProxy || configuredProxy;
63
65
 
64
- if (effectiveProxy) {
66
+ if (effectiveProxy && !webserverUrl) {
65
67
  attachLog(`[Attach] 使用代理: ${effectiveProxy}`);
66
68
  }
67
69
  let shuttingDown = false;
@@ -87,12 +89,18 @@ export async function handleAttach(options) {
87
89
  attachLog(
88
90
  " --size <N> 浏览器页面池大小(默认: 3,不建议超过 3)",
89
91
  );
92
+ attachLog(
93
+ " --webserver <URL> webserver 模式:通过 HTTP 调用集中式 webserver,不启动本地浏览器",
94
+ );
90
95
  attachLog("");
91
96
  attachLog("说明:");
92
97
  attachLog(
93
98
  " 后台轮询服务端 /api/user-update-tasks 接口,自动抓取 TikTok 用户信息",
94
99
  );
95
100
  attachLog(" 抓取完成后通过 POST /api/user-info-batch 批量回传结果");
101
+ attachLog(
102
+ " 默认启动本地浏览器(CDP 模式),传 --webserver 则走 HTTP 接口,无需浏览器",
103
+ );
96
104
  attachLog(" 浏览器崩溃时自动重启,支持长时间无人值守运行");
97
105
  attachLog(
98
106
  " --size 控制单个浏览器内并发的 page 数,增大可提升吞吐但可能触发 TikTok 限流",
@@ -126,10 +134,32 @@ export async function handleAttach(options) {
126
134
 
127
135
  const { apiGet, apiPost } = createApiClient({ log: false });
128
136
 
129
- const scraper = new TikTokScraper({
130
- poolSize: attachPoolSize || 3,
131
- proxyServer: effectiveProxy || null,
132
- });
137
+ // ── webserver 模式:不启动本地浏览器,通过 HTTP 调用集中式 webserver ──
138
+ let scraper = null;
139
+ let getUserInfo;
140
+
141
+ if (webserverUrl) {
142
+ attachLog(`[Attach] webserver 模式: ${webserverUrl}`);
143
+ getUserInfo = async (uniqueId) => {
144
+ const res = await fetch(`${webserverUrl}/api/tiktok/user`, {
145
+ method: "POST",
146
+ headers: { "Content-Type": "application/json" },
147
+ body: JSON.stringify({ uniqueId }),
148
+ signal: AbortSignal.timeout(30000),
149
+ });
150
+ const data = await res.json();
151
+ if (!res.ok || data.error) return null;
152
+ return data;
153
+ };
154
+ } else {
155
+ scraper = new TikTokScraper({
156
+ poolSize: attachPoolSize || 3,
157
+ proxyServer: effectiveProxy || null,
158
+ });
159
+ await scraper.init();
160
+ attachLog("[Attach] 浏览器初始化完成,开始循环接收任务...");
161
+ getUserInfo = (uniqueId) => scraper.getUserInfo(uniqueId);
162
+ }
133
163
  const shutdown = async (signal) => {
134
164
  if (shuttingDown) return;
135
165
  shuttingDown = true;
@@ -152,9 +182,6 @@ export async function handleAttach(options) {
152
182
  process.on("SIGTERM", onSigterm);
153
183
 
154
184
  try {
155
- await scraper.init();
156
- attachLog("[Attach] 浏览器初始化完成,开始循环接收任务...");
157
-
158
185
  let loopCount = 0;
159
186
  let browserRestartCount = 0;
160
187
  let taskBatchCount = 0;
@@ -162,8 +189,8 @@ export async function handleAttach(options) {
162
189
  while (!shuttingDown) {
163
190
  loopCount++;
164
191
 
165
- // 检查浏览器是否存活,不存活则重启
166
- if (!scraper.isAlive()) {
192
+ // 检查浏览器是否存活,不存活则重启(仅本地浏览器模式)
193
+ if (scraper && !scraper.isAlive()) {
167
194
  attachLog(
168
195
  `[Attach] 浏览器已关闭,正在重启 (${++browserRestartCount})...`,
169
196
  );
@@ -174,7 +201,7 @@ export async function handleAttach(options) {
174
201
  // 收到退出信号且当前无任务正在处理,则退出
175
202
  if (shuttingDown) {
176
203
  attachLog("[Attach] 当前任务已完成,正在关闭浏览器...");
177
- await scraper.close().catch(() => {});
204
+ if (scraper) await scraper.close().catch(() => {});
178
205
  if (forceExitTimer) {
179
206
  clearTimeout(forceExitTimer);
180
207
  forceExitTimer = null;
@@ -192,12 +219,14 @@ export async function handleAttach(options) {
192
219
  );
193
220
 
194
221
  if (!tasks || tasks.length === 0) {
195
- const heap = getHeapUsage();
196
- if (heap.ratio >= HEAP_RESTART_RATIO) {
197
- await recycleScraper(
198
- scraper,
199
- `空闲时堆占用 ${(heap.ratio * 100).toFixed(1)}%`,
200
- );
222
+ if (scraper) {
223
+ const heap = getHeapUsage();
224
+ if (heap.ratio >= HEAP_RESTART_RATIO) {
225
+ await recycleScraper(
226
+ scraper,
227
+ `空闲时堆占用 ${(heap.ratio * 100).toFixed(1)}%`,
228
+ );
229
+ }
201
230
  }
202
231
  if (loopCount === 1) {
203
232
  attachLog(`[Attach] 当前无待更新任务,${attachInterval}秒后重试...`);
@@ -223,7 +252,7 @@ export async function handleAttach(options) {
223
252
  : "";
224
253
  attachLog(` → 获取 @${uniqueId} 的用户信息...${countryTag}`);
225
254
  try {
226
- const info = await scraper.getUserInfo(uniqueId);
255
+ const info = await getUserInfo(uniqueId);
227
256
  return { uniqueId, info, error: null };
228
257
  } catch (err) {
229
258
  return { uniqueId, info: null, error: err };
@@ -319,38 +348,40 @@ export async function handleAttach(options) {
319
348
 
320
349
  attachLog(` 本批结果: ${successCount} 成功, ${failCount} 失败\n`);
321
350
 
322
- // CDN 限流比例超过 30% 时,冷却 + 重启浏览器
323
- const cdnRatio = cdnBlockedCount / tasks.length;
324
- if (cdnRatio > 0.3) {
325
- const coolDownSeconds = cdnRatio > 0.8 ? 120 : 60;
326
- attachLog(
327
- ` [Attach] CDN限流比例 ${(cdnRatio * 100).toFixed(0)}% (${cdnBlockedCount}/${tasks.length}),冷却 ${coolDownSeconds} 秒后重启浏览器...`,
328
- );
329
- await new Promise((r) => setTimeout(r, coolDownSeconds * 1000));
330
- await recycleScraper(
331
- scraper,
332
- `CDN限流比例过高 (${cdnBlockedCount}/${tasks.length})`,
333
- );
334
- browserRestartCount++;
335
- taskBatchCount = 0;
336
- }
351
+ // CDN 限流比例超过 30% 时,冷却 + 重启浏览器(仅本地浏览器模式)
352
+ if (scraper) {
353
+ const cdnRatio = cdnBlockedCount / tasks.length;
354
+ if (cdnRatio > 0.3) {
355
+ const coolDownSeconds = cdnRatio > 0.8 ? 120 : 60;
356
+ attachLog(
357
+ ` [Attach] CDN限流比例 ${(cdnRatio * 100).toFixed(0)}% (${cdnBlockedCount}/${tasks.length}),冷却 ${coolDownSeconds} 秒后重启浏览器...`,
358
+ );
359
+ await new Promise((r) => setTimeout(r, coolDownSeconds * 1000));
360
+ await recycleScraper(
361
+ scraper,
362
+ `CDN限流比例过高 (${cdnBlockedCount}/${tasks.length})`,
363
+ );
364
+ browserRestartCount++;
365
+ taskBatchCount = 0;
366
+ }
337
367
 
338
- const heap = getHeapUsage();
339
- if (heap.ratio >= HEAP_RESTART_RATIO) {
340
- await recycleScraper(
341
- scraper,
342
- `本批后堆占用 ${(heap.ratio * 100).toFixed(1)}%`,
343
- );
344
- browserRestartCount++;
345
- taskBatchCount = 0;
346
- } else if (taskBatchCount >= MAX_TASK_BATCHES_BEFORE_RESTART) {
347
- await recycleScraper(scraper, `已连续处理 ${taskBatchCount} 批任务`);
348
- browserRestartCount++;
349
- taskBatchCount = 0;
350
- }
368
+ const heap = getHeapUsage();
369
+ if (heap.ratio >= HEAP_RESTART_RATIO) {
370
+ await recycleScraper(
371
+ scraper,
372
+ `本批后堆占用 ${(heap.ratio * 100).toFixed(1)}%`,
373
+ );
374
+ browserRestartCount++;
375
+ taskBatchCount = 0;
376
+ } else if (taskBatchCount >= MAX_TASK_BATCHES_BEFORE_RESTART) {
377
+ await recycleScraper(scraper, `已连续处理 ${taskBatchCount} 批任务`);
378
+ browserRestartCount++;
379
+ taskBatchCount = 0;
380
+ }
351
381
 
352
- if (needRestart) {
353
- attachLog("[Attach] 检测到浏览器异常,将在下一轮重启...");
382
+ if (needRestart) {
383
+ attachLog("[Attach] 检测到浏览器异常,将在下一轮重启...");
384
+ }
354
385
  }
355
386
 
356
387
  await new Promise((r) => setTimeout(r, 500));
@@ -367,7 +398,7 @@ export async function handleAttach(options) {
367
398
  process.removeListener("SIGINT", onSigint);
368
399
  process.removeListener("SIGTERM", onSigterm);
369
400
  // 只有在非信号退出时才关闭浏览器(信号退出已在循环内关闭)
370
- if (!shuttingDown) {
401
+ if (!shuttingDown && scraper) {
371
402
  await scraper.close().catch(() => {});
372
403
  }
373
404
  }
package/src/lib/args.js CHANGED
@@ -418,7 +418,13 @@ function parseWebserverArgs(args) {
418
418
  } else if (arg === "--proxy") {
419
419
  webserverProxy = args[++i] || null;
420
420
  } else if (arg === "--notify") {
421
- webserverNotify = args[++i] || null;
421
+ const next = args[i + 1];
422
+ // 没写地址或下一个是别的参数 → 使用默认 watch 服务器
423
+ if (!next || next.startsWith("--")) {
424
+ webserverNotify = "http://117.71.53.99:17301";
425
+ } else {
426
+ webserverNotify = args[++i];
427
+ }
422
428
  }
423
429
  }
424
430
 
@@ -621,6 +627,7 @@ function parseAttachArgs(args) {
621
627
  let countries = [];
622
628
  let customProxy = null;
623
629
  let poolSize = 3;
630
+ let attachWebserver = null;
624
631
 
625
632
  for (let i = 0; i < args.length; i++) {
626
633
  const arg = args[i];
@@ -639,6 +646,8 @@ function parseAttachArgs(args) {
639
646
  customProxy = args[++i];
640
647
  } else if (arg === "--size") {
641
648
  poolSize = parseInt(args[++i], 10) || 3;
649
+ } else if (arg === "--webserver") {
650
+ attachWebserver = args[++i];
642
651
  }
643
652
  }
644
653
 
@@ -650,6 +659,7 @@ function parseAttachArgs(args) {
650
659
  attachCountries: countries,
651
660
  customProxy,
652
661
  attachPoolSize: poolSize,
662
+ attachWebserver,
653
663
  urls: [],
654
664
  outputFormat: "json",
655
665
  exploreCount: 0,
@@ -188,12 +188,13 @@ const HELP_TEXT = [
188
188
  " 示例: tt-help info https://www.tiktok.com/@nike",
189
189
  " tt-help info https://www.tiktok.com/@nike/video/7234567890 --onlyvideo",
190
190
  "",
191
- " tt-help attach [-p 并行数] [-i 间隔秒数] [-s 服务端地址] [-c 国家列表]",
191
+ " tt-help attach [-p 并行数] [-i 间隔秒数] [-s 服务端地址] [-c 国家列表] [--webserver URL]",
192
192
  " 后台轮询服务端任务接口,自动抓取 TikTok 用户信息",
193
193
  " -p, --parallel <N> 并行抓取数(默认: 1)",
194
194
  " -i, --interval <N> 无任务时轮询间隔,单位秒(默认: 10)",
195
195
  " -s, --server <URL> 服务端地址(默认: http://127.0.0.1:3001)",
196
196
  " -c, --countries <A,B> 猜测国家列表(逗号分隔,如 PL,DE,FR)",
197
+ " --webserver <URL> webserver 模式(不启动本地浏览器,走 HTTP 接口)",
197
198
  " 示例: tt-help attach -p 5 -i 10",
198
199
  " tt-help attach -c PL,DE,FR -p 5",
199
200
  "",
@@ -246,13 +247,14 @@ const HELP_TEXT = [
246
247
  " -p, --port <端口号> 监听端口,默认 3000",
247
248
  " --pool-size <数量> 浏览器页面池大小,默认 8",
248
249
  " --proxy <地址> HTTP 代理(如 127.0.0.1:7897)",
250
+ " --notify [地址] 验证码通知,默认 http://117.71.53.99:17301",
249
251
  " API:",
250
252
  " GET /health 健康检查",
251
253
  " GET / 服务信息",
252
254
  " POST /api/tiktok/user 获取用户信息 { uniqueId: string }",
253
255
  " POST /api/tiktok/video 获取视频信息 { videoUrl: string }",
254
256
  " POST /api/tiktok/lookup 同时获取视频和作者信息 { videoUrl: string }",
255
- " 示例: tt-help webserver -p 3000 --pool-size 8 --proxy 127.0.0.1:7897",
257
+ " 示例: tt-help webserver -p 3000 --pool-size 8 --proxy 127.0.0.1:7897 --notify",
256
258
  "",
257
259
  " comments [选项]",
258
260
  " 评论采集:从视频评论中发现新用户,来源标记为 comment",
@@ -200,6 +200,10 @@ async function handleTiktokProduct(req, res) {
200
200
  if (!body.url) {
201
201
  return sendJSON(res, 400, { error: "missing url" });
202
202
  }
203
+ const clientIP =
204
+ req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ||
205
+ req.socket.remoteAddress ||
206
+ "";
203
207
  // URL 标准化:www.tiktok.com/shop/pdp → shop.tiktok.com/us/pdp
204
208
  const normalizedUrl = body.url.replace(
205
209
  "www.tiktok.com/shop/pdp",
@@ -213,9 +217,13 @@ async function handleTiktokProduct(req, res) {
213
217
  captchaFallback: false,
214
218
  },
215
219
  );
220
+ console.error(
221
+ `[webserver] product OK | ${clientIP} | rating=${result.rating ?? "-"} sold=${result.soldCount ?? "-"} name=${(result.productName || "").slice(0, 40)}`,
222
+ );
216
223
  return sendJSON(res, 200, result);
217
224
  } catch (e) {
218
225
  // 先返回错误,不阻塞请求
226
+ console.error(`[webserver] product FAIL | ${clientIP} | ${e.message}`);
219
227
  sendJSON(res, 502, { error: e.message });
220
228
 
221
229
  // 如果是 captcha,异步弹浏览器(不阻塞其他请求)
@@ -244,7 +252,7 @@ async function scheduleCaptchaRecovery(url, proxy) {
244
252
  headers: { "Content-Type": "application/json" },
245
253
  body: JSON.stringify({
246
254
  userId: `webserver-${process.env.HOSTNAME || os.hostname() || "unknown"}`,
247
- errorType: "captcha_blocked",
255
+ errorType: "captcha",
248
256
  errorMessage:
249
257
  "TikTok Shop captcha detected, browser opened - manual intervention required",
250
258
  stage: "product_info",