tt-help-cli-ycl 1.4.40 → 1.4.41

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.40",
3
+ "version": "1.4.41",
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/tag.js CHANGED
@@ -297,11 +297,64 @@ function applyFilterAndScore(videos, targetCountries, result) {
297
297
  return { filtered, matchedAuthorSet };
298
298
  }
299
299
 
300
+ /**
301
+ * 通过 webserver API 获取视频的国家信息(不走本地 HTTP view-source)
302
+ */
303
+ async function enrichViaWebserver(videos, webserverUrl, onProgress) {
304
+ const total = videos.length;
305
+ let done = 0;
306
+ const CONCURRENCY = 16;
307
+
308
+ async function worker(queue) {
309
+ while (queue.length > 0) {
310
+ const v = queue.shift();
311
+ if (!v) break;
312
+ const videoUrl = `https://www.tiktok.com/@${v.authorUniqueId}/video/${v.id}`;
313
+ try {
314
+ const res = await fetch(`${webserverUrl}/api/tiktok/video`, {
315
+ method: "POST",
316
+ headers: { "Content-Type": "application/json" },
317
+ body: JSON.stringify({ videoUrl }),
318
+ signal: AbortSignal.timeout(30000),
319
+ });
320
+ const data = await res.json();
321
+ v.locationCreated = data?.locationCreated || null;
322
+ } catch {
323
+ v.locationCreated = null;
324
+ }
325
+ done++;
326
+ if (onProgress) {
327
+ onProgress({
328
+ done,
329
+ total,
330
+ current: videoUrl,
331
+ locationCreated: v.locationCreated,
332
+ });
333
+ }
334
+ }
335
+ }
336
+
337
+ const queue = [...videos];
338
+ const workers = Array.from(
339
+ { length: Math.min(CONCURRENCY, queue.length) },
340
+ () => worker(queue),
341
+ );
342
+ await Promise.all(workers);
343
+ }
344
+
300
345
  async function processTag(
301
346
  tag,
302
347
  index,
303
348
  total,
304
- { enrich, targetLocations, noFilter, serverUrl, recordTags, port },
349
+ {
350
+ enrich,
351
+ targetLocations,
352
+ noFilter,
353
+ serverUrl,
354
+ recordTags,
355
+ port,
356
+ webserverUrl,
357
+ },
305
358
  ) {
306
359
  const prefix = total > 1 ? `[${index + 1}/${total}]` : "";
307
360
  process.stderr.write(`${prefix} 正在获取 #${tag} ... `);
@@ -323,33 +376,53 @@ async function processTag(
323
376
  let videos = result.videos;
324
377
  let filteredAuthors = result.uniqueAuthors;
325
378
 
326
- if (enrich) {
327
- const enrichMode = enrich === true ? "videos" : enrich;
328
- if (noFilter) {
379
+ if (webserverUrl || enrich) {
380
+ if (webserverUrl) {
329
381
  process.stderr.write(
330
- `\n 正在补充国家信息 (${enrichMode} 模式,不过滤)...\n`,
382
+ `\n 正在补充国家信息 (webserver API,目标: ${targetLocations.join(",")})...\n`,
331
383
  );
332
- } else {
333
- process.stderr.write(
334
- `\n 正在补充国家信息 (${enrichMode} 模式,目标: ${targetLocations.join(",")})...\n`,
384
+ await enrichViaWebserver(
385
+ videos,
386
+ webserverUrl,
387
+ ({ done, total, current, locationCreated }) => {
388
+ const loc = locationCreated || "-";
389
+ const hit =
390
+ locationCreated &&
391
+ isLocationInList(locationCreated, targetLocations);
392
+ process.stderr.write(
393
+ `\r [${done}/${total}] ${current.split("/").pop().slice(0, 20)} → ${loc}${hit ? " ✓" : ""}\x1b[K`,
394
+ );
395
+ },
335
396
  );
336
- }
337
-
338
- const enriched = await enrichVideosWithLocation(videos, {
339
- mode: enrichMode,
340
- onProgress: ({ done, total, current, locationCreated }) => {
341
- const label = enrichMode === "users" ? `@${current}` : current;
342
- const loc = locationCreated || "-";
343
- const hit =
344
- locationCreated &&
345
- isLocationInList(locationCreated, targetLocations);
397
+ process.stderr.write("\n");
398
+ } else {
399
+ const enrichMode = enrich === true ? "videos" : enrich;
400
+ if (noFilter) {
346
401
  process.stderr.write(
347
- `\r [${done}/${total}] ${label} → ${loc}${hit ? " ✓" : ""}\x1b[K`,
402
+ `\n 正在补充国家信息 (${enrichMode} 模式,不过滤)...\n`,
348
403
  );
349
- },
350
- });
351
- videos = enriched.videos;
352
- process.stderr.write("\n");
404
+ } else {
405
+ process.stderr.write(
406
+ `\n 正在补充国家信息 (${enrichMode} 模式,目标: ${targetLocations.join(",")})...\n`,
407
+ );
408
+ }
409
+
410
+ const enriched = await enrichVideosWithLocation(videos, {
411
+ mode: enrichMode,
412
+ onProgress: ({ done, total, current, locationCreated }) => {
413
+ const label = enrichMode === "users" ? `@${current}` : current;
414
+ const loc = locationCreated || "-";
415
+ const hit =
416
+ locationCreated &&
417
+ isLocationInList(locationCreated, targetLocations);
418
+ process.stderr.write(
419
+ `\r [${done}/${total}] ${label} → ${loc}${hit ? " ✓" : ""}\x1b[K`,
420
+ );
421
+ },
422
+ });
423
+ videos = enriched.videos;
424
+ process.stderr.write("\n");
425
+ }
353
426
 
354
427
  if (!noFilter) {
355
428
  const before = videos.length;
@@ -482,7 +555,7 @@ function askTagQuestion(question) {
482
555
 
483
556
  async function scoreSingleTag(
484
557
  tag,
485
- { baseUrl, cdpPort, targetCountries, effectiveProxy },
558
+ { baseUrl, cdpPort, targetCountries, effectiveProxy, webserverUrl },
486
559
  ) {
487
560
  const log = (...args) => process.stderr.write(args.join(" ") + "\n");
488
561
  const progress = (msg) => process.stderr.write(`\r ${msg}\x1b[K`);
@@ -538,22 +611,41 @@ async function scoreSingleTag(
538
611
  }
539
612
 
540
613
  // Step 2: 补充国家信息
541
- log(` ⏳ 补充国家信息 (${videos.length} 个视频)...`);
542
- const enrichOpts = {
543
- mode: "videos",
544
- onProgress: ({ done, total, current, locationCreated }) => {
545
- if (done % 10 === 0 || done === total) {
546
- const name = current.split("/").pop().slice(0, 20);
547
- progress(`[${done}/${total}] ${name} → ${locationCreated || "-"}`);
548
- }
549
- },
550
- };
551
- if (effectiveProxy) enrichOpts.proxyServer = effectiveProxy;
552
- const enriched = await enrichVideosWithLocation(videos, enrichOpts);
553
- clearLine();
554
- videos = enriched.videos;
555
- const withLoc = videos.filter((v) => v.locationCreated).length;
556
- log(` ✓ 国家信息: ${withLoc}/${videos.length} 个视频有国家`);
614
+ if (webserverUrl) {
615
+ log(
616
+ ` ⏳ 补充国家信息 (${videos.length} 个视频, via webserver ${webserverUrl})...`,
617
+ );
618
+ await enrichViaWebserver(
619
+ videos,
620
+ webserverUrl,
621
+ ({ done, total, current, locationCreated }) => {
622
+ if (done % 10 === 0 || done === total) {
623
+ const name = current.split("/").pop().slice(0, 20);
624
+ progress(`[${done}/${total}] ${name} → ${locationCreated || "-"}`);
625
+ }
626
+ },
627
+ );
628
+ clearLine();
629
+ const withLoc = videos.filter((v) => v.locationCreated).length;
630
+ log(` ✓ 国家信息: ${withLoc}/${videos.length} 个视频有国家`);
631
+ } else {
632
+ log(` ⏳ 补充国家信息 (${videos.length} 个视频)...`);
633
+ const enrichOpts = {
634
+ mode: "videos",
635
+ onProgress: ({ done, total, current, locationCreated }) => {
636
+ if (done % 10 === 0 || done === total) {
637
+ const name = current.split("/").pop().slice(0, 20);
638
+ progress(`[${done}/${total}] ${name} → ${locationCreated || "-"}`);
639
+ }
640
+ },
641
+ };
642
+ if (effectiveProxy) enrichOpts.proxyServer = effectiveProxy;
643
+ const enriched = await enrichVideosWithLocation(videos, enrichOpts);
644
+ clearLine();
645
+ videos = enriched.videos;
646
+ const withLoc = videos.filter((v) => v.locationCreated).length;
647
+ log(` ✓ 国家信息: ${withLoc}/${videos.length} 个视频有国家`);
648
+ }
557
649
 
558
650
  // Step 3: 过滤 + 算分
559
651
  const { matchedAuthorSet } = applyFilterAndScore(
@@ -610,7 +702,14 @@ async function scoreSingleTag(
610
702
 
611
703
  export async function handleScore(parsed) {
612
704
  const { tagScore } = parsed;
613
- const { tag, countries, serverUrl, port, proxy: cliProxy } = tagScore || {};
705
+ const {
706
+ tag,
707
+ countries,
708
+ serverUrl,
709
+ port,
710
+ proxy: cliProxy,
711
+ webserver,
712
+ } = tagScore || {};
614
713
 
615
714
  const baseUrl = serverUrl || DEFAULT_SERVER;
616
715
  const cdpPort = port || 9222;
@@ -680,6 +779,7 @@ export async function handleScore(parsed) {
680
779
  cdpPort,
681
780
  targetCountries,
682
781
  effectiveProxy,
782
+ webserverUrl: webserver,
683
783
  });
684
784
 
685
785
  // 如果队列还有 tag,自动继续;否则询问
@@ -743,7 +843,13 @@ async function reportToServer(baseUrl, result, clientId, meta, userId = "") {
743
843
 
744
844
  export async function handleScoreAll(parsed) {
745
845
  const { tagScoreAll } = parsed;
746
- let { countries, serverUrl, port, proxy: cliProxy } = tagScoreAll || {};
846
+ let {
847
+ countries,
848
+ serverUrl,
849
+ port,
850
+ proxy: cliProxy,
851
+ webserver,
852
+ } = tagScoreAll || {};
747
853
 
748
854
  const baseUrl = serverUrl || DEFAULT_SERVER;
749
855
  const defaultScoreAllPort =
@@ -951,19 +1057,24 @@ export async function handleScoreAll(parsed) {
951
1057
  const clientMeta = { type: "scoring", port: cdpPort };
952
1058
 
953
1059
  // 复用 TikTokScraper 实例,避免每次 enrich 都启动/关闭 headless 浏览器
954
- const enrichScraper = new TikTokScraper({
955
- poolSize: 3,
956
- proxyServer: effectiveProxy || undefined,
957
- });
958
- await enrichScraper.init();
959
- log("✅ TikTokScraper 已就绪 (enrich 复用)");
1060
+ // webserver 模式下不需要本地 scraper
1061
+ const enrichScraper = webserver
1062
+ ? null
1063
+ : new TikTokScraper({
1064
+ poolSize: 3,
1065
+ proxyServer: effectiveProxy || undefined,
1066
+ });
1067
+ if (enrichScraper) {
1068
+ await enrichScraper.init();
1069
+ log("✅ TikTokScraper 已就绪 (enrich 复用)");
1070
+ }
960
1071
  log(` 客户端 ID: ${clientId.substring(0, 8)}...`);
961
1072
  log("");
962
1073
 
963
1074
  // Ctrl+C 时关闭浏览器和 scraper
964
1075
  const cleanup = () => {
965
1076
  log("\n正在清理资源...");
966
- enrichScraper.close().catch(() => {});
1077
+ if (enrichScraper) enrichScraper.close().catch(() => {});
967
1078
  killEdgeProcesses(null, cdpPort);
968
1079
  process.exit(0);
969
1080
  };
@@ -1087,32 +1198,55 @@ export async function handleScoreAll(parsed) {
1087
1198
  throw new Error("no videos returned (network/captcha/slow-load)");
1088
1199
  }
1089
1200
 
1090
- // enrich: 先查服务端缓存,只对未知视频发 HTTP 请求
1091
- const enriched = await enrichWithServerCache(
1092
- videos,
1093
- baseUrl,
1094
- enrichScraper,
1095
- {
1096
- clientId,
1097
- clientMeta,
1098
- effectiveProxy,
1099
- log,
1100
- },
1101
- );
1102
- videos = enriched.videos;
1103
-
1104
- // CDN 限流检测:有拦截则冷却 + 重启 scraper
1105
- const cdnBlocked = enriched.cdnBlockedCount || 0;
1106
- if (cdnBlocked > 0) {
1107
- const cdnRatio = cdnBlocked / (videos.length || 1);
1108
- const coolSec = cdnRatio > 0.3 ? 120 : 60;
1201
+ // enrich: webserver API 或本地 HTTP view-source
1202
+ if (webserver) {
1109
1203
  log(
1110
- ` ⚠️ CDN 限流: ${cdnBlocked}/${videos.length} (${(cdnRatio * 100).toFixed(0)}%),冷却 ${coolSec} 秒后重启 scraper`,
1204
+ ` ⏳ 补充国家信息 (${videos.length} 个视频, via webserver ${webserver})...`,
1111
1205
  );
1112
- await new Promise((r) => setTimeout(r, coolSec * 1000));
1113
- log(` 正在重启 TikTokScraper...`);
1114
- await enrichScraper.restart();
1115
- log(` ✅ TikTokScraper 已重启`);
1206
+ await enrichViaWebserver(
1207
+ videos,
1208
+ webserver,
1209
+ ({ done, total, current, locationCreated }) => {
1210
+ if (done % 10 === 0 || done === total) {
1211
+ const name = current.split("/").pop().slice(0, 20);
1212
+ process.stderr.write(
1213
+ `\r [${done}/${total}] ${name} → ${locationCreated || "-"}\x1b[K`,
1214
+ );
1215
+ }
1216
+ },
1217
+ );
1218
+ process.stderr.write("\n");
1219
+ log(
1220
+ ` ✓ 国家信息: ${videos.filter((v) => v.locationCreated).length}/${videos.length} 个视频有国家`,
1221
+ );
1222
+ } else {
1223
+ // enrich: 先查服务端缓存,只对未知视频发 HTTP 请求
1224
+ const enriched = await enrichWithServerCache(
1225
+ videos,
1226
+ baseUrl,
1227
+ enrichScraper,
1228
+ {
1229
+ clientId,
1230
+ clientMeta,
1231
+ effectiveProxy,
1232
+ log,
1233
+ },
1234
+ );
1235
+ videos = enriched.videos;
1236
+
1237
+ // CDN 限流检测:有拦截则冷却 + 重启 scraper
1238
+ const cdnBlocked = enriched.cdnBlockedCount || 0;
1239
+ if (cdnBlocked > 0) {
1240
+ const cdnRatio = cdnBlocked / (videos.length || 1);
1241
+ const coolSec = cdnRatio > 0.3 ? 120 : 60;
1242
+ log(
1243
+ ` ⚠️ CDN 限流: ${cdnBlocked}/${videos.length} (${(cdnRatio * 100).toFixed(0)}%),冷却 ${coolSec} 秒后重启 scraper`,
1244
+ );
1245
+ await new Promise((r) => setTimeout(r, coolSec * 1000));
1246
+ log(` 正在重启 TikTokScraper...`);
1247
+ await enrichScraper.restart();
1248
+ log(` ✅ TikTokScraper 已重启`);
1249
+ }
1116
1250
  }
1117
1251
 
1118
1252
  // 过滤 + 算分 (共用函数)
@@ -1281,7 +1415,7 @@ export async function handleScoreAll(parsed) {
1281
1415
  }
1282
1416
  }
1283
1417
  } finally {
1284
- await enrichScraper.close();
1418
+ if (enrichScraper) await enrichScraper.close();
1285
1419
  log("✅ TikTokScraper 已关闭");
1286
1420
  log("正在关闭 CDP 浏览器...");
1287
1421
  try {
@@ -1336,6 +1470,7 @@ export async function handleTag(parsed) {
1336
1470
  noFilter,
1337
1471
  serverUrl,
1338
1472
  discover,
1473
+ webserver,
1339
1474
  } = tagTags;
1340
1475
 
1341
1476
  const targetLocations = locations
@@ -1373,6 +1508,7 @@ export async function handleTag(parsed) {
1373
1508
  noFilter,
1374
1509
  serverUrl,
1375
1510
  recordTags: !!discover,
1511
+ webserverUrl: webserver,
1376
1512
  });
1377
1513
 
1378
1514
  const output = { tag: result.tag };
package/src/lib/args.js CHANGED
@@ -983,6 +983,7 @@ function parseTagArgs(args) {
983
983
  let scoreTag = null;
984
984
  let scoreCountries = null;
985
985
  let scorePort = 9222;
986
+ let webserverUrl = null;
986
987
  // 延迟路由:--port 的值先暂存,等循环结束后根据已确定的 subcommand 分配
987
988
  let rawTagPort = null;
988
989
 
@@ -1033,6 +1034,8 @@ function parseTagArgs(args) {
1033
1034
  scoreProxy = args[++i];
1034
1035
  } else if (arg === "-p" || arg === "--prompt") {
1035
1036
  discoverPrompt = args[++i];
1037
+ } else if (arg === "--webserver") {
1038
+ webserverUrl = args[++i];
1036
1039
  } else if (!arg.startsWith("-")) {
1037
1040
  const cleaned = arg.replace("#", "").trim();
1038
1041
  if (cleaned.toLowerCase() === "discover") {
@@ -1107,6 +1110,7 @@ function parseTagArgs(args) {
1107
1110
  serverUrl,
1108
1111
  port: scorePort,
1109
1112
  proxy: scoreProxy,
1113
+ webserver: webserverUrl,
1110
1114
  },
1111
1115
  urls: [],
1112
1116
  outputFormat: "json",
@@ -1129,6 +1133,7 @@ function parseTagArgs(args) {
1129
1133
  serverUrl,
1130
1134
  port: scoreAllPort,
1131
1135
  proxy: scoreProxy,
1136
+ webserver: webserverUrl,
1132
1137
  },
1133
1138
  urls: [],
1134
1139
  outputFormat: "json",
@@ -1198,6 +1203,7 @@ function parseTagArgs(args) {
1198
1203
  noFilter,
1199
1204
  serverUrl,
1200
1205
  discover,
1206
+ webserver: webserverUrl,
1201
1207
  },
1202
1208
  urls: [],
1203
1209
  outputFormat: "json",