tt-help-cli-ycl 1.4.3 → 1.4.4

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.3",
3
+ "version": "1.4.4",
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
@@ -1,5 +1,7 @@
1
1
  import { writeFileSync } from "fs";
2
2
  import { randomUUID } from "crypto";
3
+ import { execSync } from "child_process";
4
+ import os from "os";
3
5
  import readline from "readline";
4
6
  import { fetchTagData, enrichVideosWithLocation } from "../lib/tag-fetcher.js";
5
7
  import { killEdgeProcesses, ensureBrowserReady } from "../lib/browser/cdp.js";
@@ -42,6 +44,46 @@ function formatMemoryUsage(mem = process.memoryUsage()) {
42
44
  return `rss:${(mem.rss / 1024 / 1024).toFixed(0)}MB heap:${(mem.heapUsed / 1024 / 1024).toFixed(0)}MB ext:${(mem.external / 1024 / 1024).toFixed(0)}MB ab:${(mem.arrayBuffers / 1024 / 1024).toFixed(0)}MB`;
43
45
  }
44
46
 
47
+ /**
48
+ * 获取 Edge 浏览器进程的内存占用(macOS/Linux)
49
+ */
50
+ function getBrowserMemory(port = null) {
51
+ try {
52
+ const platform = os.platform();
53
+ let cmd;
54
+ if (platform === "darwin") {
55
+ // 按端口筛选:匹配 --remote-debugging-port=PORT 的进程
56
+ if (port) {
57
+ cmd = `ps aux | grep "[M]icrosoft Edge" | grep "remote-debugging-port=${port}" | awk '{sum += $6} END {printf "%d", sum/1024}'`;
58
+ } else {
59
+ cmd =
60
+ 'ps aux | grep "[M]icrosoft Edge" | awk \'{sum += $6} END {printf "%d", sum/1024}\'';
61
+ }
62
+ } else if (platform === "linux") {
63
+ if (port) {
64
+ cmd = `ps aux | grep "[m]sedge" | grep "remote-debugging-port=${port}" | awk '{sum += $6} END {printf "%d", sum/1024}'`;
65
+ } else {
66
+ cmd =
67
+ 'ps aux | grep "[m]sedge" | awk \'{sum += $6} END {printf "%d", sum/1024}\'';
68
+ }
69
+ } else {
70
+ return null;
71
+ }
72
+ const result = execSync(cmd, { timeout: 3000 }).toString().trim();
73
+ const mb = parseInt(result, 10);
74
+ return mb > 0 ? mb : null;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function formatMemoryWithBrowser(port = null) {
81
+ const nodeMem = formatMemoryUsage();
82
+ const browserMB = getBrowserMemory(port);
83
+ const portLabel = port ? `:${port}` : "";
84
+ return browserMB ? `${nodeMem} edge${portLabel}:${browserMB}MB` : nodeMem;
85
+ }
86
+
45
87
  // 构建带客户端追踪 header 的 fetch 封装
46
88
  function buildClientHeaders(clientId, meta, extra = {}) {
47
89
  return {
@@ -576,14 +618,7 @@ async function reportToServer(baseUrl, result, clientId, meta) {
576
618
 
577
619
  export async function handleScoreAll(parsed) {
578
620
  const { tagScoreAll } = parsed;
579
- let {
580
- countries,
581
- serverUrl,
582
- autoDiscover,
583
- discoverCount = 10,
584
- port,
585
- proxy: cliProxy,
586
- } = tagScoreAll || {};
621
+ let { countries, serverUrl, port, proxy: cliProxy } = tagScoreAll || {};
587
622
 
588
623
  const baseUrl = serverUrl || DEFAULT_SERVER;
589
624
  const defaultScoreAllPort =
@@ -604,10 +639,14 @@ export async function handleScoreAll(parsed) {
604
639
  cdpPort +
605
640
  (effectiveProxy ? " 代理: " + effectiveProxy : ""),
606
641
  );
607
- if (autoDiscover) log("║ 自动发现: 开启");
642
+ log("║ 发现: 服务端定时自动(无需 --auto-discover)");
608
643
  log("╚══════════════════════════════════════════╝");
609
644
  log("");
610
645
 
646
+ // 启动时显示内存状态
647
+ log(`📊 启动内存: ${formatMemoryWithBrowser(cdpPort)}`);
648
+ log("");
649
+
611
650
  // 连接 CDP 浏览器
612
651
  const cdpOpts = { port: cdpPort };
613
652
  if (effectiveProxy) cdpOpts.proxyServer = effectiveProxy;
@@ -636,8 +675,6 @@ export async function handleScoreAll(parsed) {
636
675
  2,
637
676
  parseInt(process.env.TAG_SCOREALL_PORT_POOL_SIZE || "10", 10) || 10,
638
677
  );
639
- const switchPortOnRecycle =
640
- String(process.env.TAG_SCOREALL_SWITCH_PORT_ON_RECYCLE || "1") !== "0";
641
678
 
642
679
  function pickNextPort(currentPort) {
643
680
  const candidates = [];
@@ -649,26 +686,35 @@ export async function handleScoreAll(parsed) {
649
686
  return candidates[Math.floor(Math.random() * candidates.length)];
650
687
  }
651
688
 
652
- const memRssRecycleMb = Math.max(
653
- 256,
654
- parseInt(process.env.TAG_SCOREALL_RECYCLE_RSS_MB || "900", 10) || 900,
655
- );
656
- const memHeapRecycleMb = Math.max(
657
- 128,
658
- parseInt(process.env.TAG_SCOREALL_RECYCLE_HEAP_MB || "512", 10) || 512,
659
- );
660
- const recycleCooldownMs = Math.max(
661
- 0,
662
- parseInt(process.env.TAG_SCOREALL_RECYCLE_COOLDOWN_MS || "180000", 10) ||
663
- 180000,
664
- );
665
- // 默认关闭按固定轮次重建,仅在高内存时触发;需要可通过环境变量开启。
666
- const periodicRecycleEvery = Math.max(
667
- 0,
668
- parseInt(process.env.TAG_SCOREALL_PERIODIC_RECYCLE_EVERY || "0", 10) || 0,
669
- );
689
+ // ── 内存回收配置 ──
690
+ const RECYCLE_CONFIG = {
691
+ nodeRssMb: Math.max(
692
+ 256,
693
+ parseInt(process.env.TAG_SCOREALL_RECYCLE_RSS_MB || "900", 10) || 900,
694
+ ),
695
+ nodeHeapMb: Math.max(
696
+ 128,
697
+ parseInt(process.env.TAG_SCOREALL_RECYCLE_HEAP_MB || "512", 10) || 512,
698
+ ),
699
+ edgeMb: Math.max(
700
+ 500,
701
+ parseInt(process.env.TAG_SCOREALL_RECYCLE_EDGE_MB || "2000", 10) || 2000,
702
+ ),
703
+ cooldownMs: Math.max(
704
+ 0,
705
+ parseInt(process.env.TAG_SCOREALL_RECYCLE_COOLDOWN_MS || "180000", 10) ||
706
+ 180000,
707
+ ),
708
+ periodicEvery: Math.max(
709
+ 0,
710
+ parseInt(process.env.TAG_SCOREALL_PERIODIC_RECYCLE_EVERY || "0", 10) || 0,
711
+ ),
712
+ switchPortOnRecycle:
713
+ String(process.env.TAG_SCOREALL_SWITCH_PORT_ON_RECYCLE || "1") !== "0",
714
+ };
670
715
  let lastRecycleAt = 0;
671
716
 
717
+ // ── 轻量回收:只清空当前 tab ──
672
718
  async function recyclePage() {
673
719
  if (!page || page.isClosed()) {
674
720
  page = await getOrCreatePage(browser);
@@ -681,23 +727,24 @@ export async function handleScoreAll(parsed) {
681
727
  timeout: 5000,
682
728
  });
683
729
  } catch {
684
- // 页面状态异常时回退到重建 tab
685
730
  await page.close().catch(() => {});
686
731
  page = await getOrCreatePage(browser);
687
732
  await setupPageRequestBlocking(page);
688
733
  }
689
734
  }
690
735
 
736
+ // ── 深度回收:关闭浏览器 + 可选切换端口 ──
691
737
  async function recycleCdpSession(reason, { switchPort = true } = {}) {
692
738
  const oldPort = cdpPort;
693
- if (switchPort && switchPortOnRecycle) {
739
+ const shouldSwitch = switchPort && RECYCLE_CONFIG.switchPortOnRecycle;
740
+ if (shouldSwitch) {
694
741
  cdpPort = pickNextPort(cdpPort);
695
742
  cdpOpts.port = cdpPort;
696
743
  clientMeta.port = cdpPort;
697
744
  }
698
- const switchHint =
699
- oldPort === cdpPort ? `port=${cdpPort}` : `port ${oldPort} -> ${cdpPort}`;
700
- log(` ♻️ 重建 CDP 会话 (${reason}; ${switchHint})...`);
745
+ const portHint =
746
+ oldPort === cdpPort ? `port=${cdpPort}` : `port ${oldPort}→${cdpPort}`;
747
+ log(` ♻️ 重建 CDP 会话 (${reason}; ${portHint})...`);
701
748
  if (page) {
702
749
  await page.close().catch(() => {});
703
750
  page = null;
@@ -712,27 +759,49 @@ export async function handleScoreAll(parsed) {
712
759
  lastRecycleAt = Date.now();
713
760
  }
714
761
 
762
+ // ── 内存检查 + 自动回收 ──
763
+ // 策略:Node.js 内存超阈值 → 不换端口(只重建连接)
764
+ // Edge 内存超阈值 → 换端口(杀旧浏览器 + 启新实例,彻底释放)
765
+ // 定期轮次 → 不换端口
715
766
  async function maybeRecycleForMemory() {
716
767
  const mem = process.memoryUsage();
717
768
  const rssMB = mem.rss / 1024 / 1024;
718
769
  const heapMB = mem.heapUsed / 1024 / 1024;
719
- if (rssMB >= memRssRecycleMb || heapMB >= memHeapRecycleMb) {
720
- const now = Date.now();
721
- if (recycleCooldownMs > 0 && now - lastRecycleAt < recycleCooldownMs) {
770
+ const edgeMB = getBrowserMemory(cdpPort);
771
+
772
+ const nodeOver =
773
+ rssMB >= RECYCLE_CONFIG.nodeRssMb || heapMB >= RECYCLE_CONFIG.nodeHeapMb;
774
+ const edgeOver = edgeMB !== null && edgeMB >= RECYCLE_CONFIG.edgeMb;
775
+
776
+ if (nodeOver || edgeOver) {
777
+ // 冷却期内跳过
778
+ if (
779
+ RECYCLE_CONFIG.cooldownMs > 0 &&
780
+ Date.now() - lastRecycleAt < RECYCLE_CONFIG.cooldownMs
781
+ )
722
782
  return;
723
- }
724
- await recycleCdpSession(
725
- `mem rss=${rssMB.toFixed(0)}MB heap=${heapMB.toFixed(0)}MB (threshold rss=${memRssRecycleMb} heap=${memHeapRecycleMb})`,
726
- { switchPort: false },
727
- );
783
+
784
+ const parts = [];
785
+ if (rssMB >= RECYCLE_CONFIG.nodeRssMb)
786
+ parts.push(`rss=${rssMB.toFixed(0)}≥${RECYCLE_CONFIG.nodeRssMb}`);
787
+ if (heapMB >= RECYCLE_CONFIG.nodeHeapMb)
788
+ parts.push(`heap=${heapMB.toFixed(0)}≥${RECYCLE_CONFIG.nodeHeapMb}`);
789
+ if (edgeOver)
790
+ parts.push(`edge:${cdpPort}=${edgeMB}≥${RECYCLE_CONFIG.edgeMb}`);
791
+
792
+ await recycleCdpSession(`mem ${parts.join("+")}`, {
793
+ switchPort: edgeOver,
794
+ });
728
795
  return;
729
796
  }
797
+
798
+ // 定期轮次回收(默认关闭)
730
799
  if (
731
- periodicRecycleEvery > 0 &&
800
+ RECYCLE_CONFIG.periodicEvery > 0 &&
732
801
  totalScored > 0 &&
733
- totalScored % periodicRecycleEvery === 0
802
+ totalScored % RECYCLE_CONFIG.periodicEvery === 0
734
803
  ) {
735
- await recycleCdpSession(`periodic every ${totalScored} tasks`, {
804
+ await recycleCdpSession(`periodic #${totalScored}`, {
736
805
  switchPort: false,
737
806
  });
738
807
  }
@@ -741,12 +810,7 @@ export async function handleScoreAll(parsed) {
741
810
  let totalScored = 0;
742
811
  let emptyRounds = 0; // 连续无任务的轮数
743
812
  let consecutive403 = 0; // 连续 HTTP 403 次数,达 3 次认为被封
744
- const DISCOVER_AFTER_EMPTY = 3; // 连续 3 轮无任务时触发 discover(兜底)
745
813
  const CONSECUTIVE_403_BLOCKED = 3; // 连续 3 次 403 认为账号被封
746
- const DISCOVER_LOW_THRESHOLD = 5; // 剩余 new 标签 < 5 时提前触发发现
747
- const DISCOVER_COOLDOWN_MS = 5 * 60 * 1000; // 发现冷却时间 5 分钟,防止频繁调用 LLM
748
- let discoverPromise = null; // 正在后台执行的发现 Promise
749
- let lastDiscoverTime = 0; // 上次发现完成的时间戳
750
814
 
751
815
  // 生成客户端 ID,用于服务端追踪
752
816
  const clientId = randomUUID();
@@ -771,39 +835,6 @@ export async function handleScoreAll(parsed) {
771
835
  };
772
836
  process.on("SIGINT", cleanup);
773
837
 
774
- // 异步发现函数:后台为所有目标国家生成标签,不阻塞打分循环
775
- async function runDiscoverInBackground() {
776
- if (discoverPromise) return; // 已在执行中,不重复触发
777
- const elapsed = Date.now() - lastDiscoverTime;
778
- if (elapsed < DISCOVER_COOLDOWN_MS) {
779
- const remainMin = Math.ceil((DISCOVER_COOLDOWN_MS - elapsed) / 60000);
780
- log(`🔍 发现冷却中,还需 ${remainMin} 分钟后才能再次触发`);
781
- return;
782
- }
783
- log(`🔍 剩余标签不足,后台为 ${targetCountries.length} 个国家生成标签...`);
784
- discoverPromise = (async () => {
785
- let totalInserted = 0;
786
- for (const country of targetCountries) {
787
- try {
788
- const discRes = await fetch(
789
- `${baseUrl}/api/tags/discover?country=${country}&count=${discoverCount}`,
790
- { headers: buildClientHeaders(clientId, clientMeta) },
791
- );
792
- const discData = await discRes.json();
793
- if (discData.added) {
794
- totalInserted += discData.added;
795
- log(` ${country}: 新增 ${discData.added} 个`);
796
- }
797
- } catch (e) {
798
- log(` ${country}: 请求失败 (${e.message})`);
799
- }
800
- }
801
- log(` ✅ 发现完成,共入库 ${totalInserted} 个新标签`);
802
- lastDiscoverTime = Date.now(); // 记录完成时间,开始冷却
803
- discoverPromise = null;
804
- })();
805
- }
806
-
807
838
  try {
808
839
  while (true) {
809
840
  try {
@@ -813,41 +844,16 @@ export async function handleScoreAll(parsed) {
813
844
  });
814
845
  const tagsData = await tagsRes.json();
815
846
 
816
- // 提前触发发现:剩余 new 标签低于阈值且不在冷却期时后台生成
817
- if (
818
- autoDiscover &&
819
- !discoverPromise &&
820
- Date.now() - lastDiscoverTime >= DISCOVER_COOLDOWN_MS &&
821
- (tagsData.total || 0) < DISCOVER_LOW_THRESHOLD
822
- ) {
823
- runDiscoverInBackground();
824
- }
825
-
826
847
  if (!tagsData.tags || tagsData.tags.length === 0) {
827
848
  emptyRounds++;
828
849
 
829
- // 兜底发现:连续 N 轮无任务(提前发现可能没生效)
830
- if (autoDiscover && emptyRounds >= DISCOVER_AFTER_EMPTY) {
831
- // 等后台发现完成(如果正在跑)
832
- if (discoverPromise) {
833
- log(` ⏳ 等待后台发现完成...`);
834
- await discoverPromise;
835
- } else if (Date.now() - lastDiscoverTime >= DISCOVER_COOLDOWN_MS) {
836
- // 冷却期已过,触发一次同步发现
837
- log(`🔍 连续 ${emptyRounds} 轮无待打分标签,同步生成标签...`);
838
- await runDiscoverInBackground();
839
- await discoverPromise;
840
- } else {
841
- const remainMin = Math.ceil(
842
- (DISCOVER_COOLDOWN_MS - (Date.now() - lastDiscoverTime)) /
843
- 60000,
844
- );
845
- log(
846
- `🔍 连续 ${emptyRounds} 轮空,但发现冷却中(还需 ${remainMin} 分钟),继续等待...`,
847
- );
848
- }
850
+ // 服务端定时发现会自动补充标签,客户端只需等待
851
+ if (emptyRounds >= 5) {
852
+ log(
853
+ `⏳ 连续 ${emptyRounds} 轮无标签,等待服务端发现补充(30 秒后重试)...`,
854
+ );
855
+ await new Promise((r) => setTimeout(r, 30000));
849
856
  emptyRounds = 0;
850
- await new Promise((r) => setTimeout(r, 3000));
851
857
  continue;
852
858
  }
853
859
  log(`⏳ 暂无待打分标签(连续 ${emptyRounds} 轮),10 秒后重试...`);
@@ -926,7 +932,7 @@ export async function handleScoreAll(parsed) {
926
932
  if (!videos || videos.length === 0) {
927
933
  const deadSec = ((Date.now() - fetchStart) / 1000).toFixed(1);
928
934
  log(
929
- ` ⚠️ 无视频 (${deadSec}s) mem=${formatMemoryUsage()},标记 dead`,
935
+ ` ⚠️ 无视频 (${deadSec}s) mem=${formatMemoryWithBrowser(cdpPort)},标记 dead`,
930
936
  );
931
937
  result.status = "dead";
932
938
  result.error = "no videos found";
@@ -1004,12 +1010,18 @@ export async function handleScoreAll(parsed) {
1004
1010
  const mc = result.matchedCountries
1005
1011
  .map((c) => `${c.c}:${c.n}`)
1006
1012
  .join(" ");
1007
- const memStr = ` mem=${formatMemoryUsage()}`;
1013
+ const memStr = ` mem=${formatMemoryWithBrowser(cdpPort)}`;
1008
1014
  log(
1009
1015
  ` ${icon} ${result.status} score=${result.score} authors=${result.authorCount} matched=${result.matchedAuthors} (${elapsed}s)${mc ? " " + mc : ""}${memStr}`,
1010
1016
  );
1011
1017
  log("");
1012
1018
  consecutive403 = 0; // 成功完成,重置 403 计数器
1019
+ // 每 10 个标签输出一次内存汇总
1020
+ if (totalScored % 10 === 0) {
1021
+ log(
1022
+ `📊 [${totalScored}] 内存汇总: ${formatMemoryWithBrowser(cdpPort)}`,
1023
+ );
1024
+ }
1013
1025
  await recyclePage();
1014
1026
  await maybeRecycleForMemory();
1015
1027
  await randomDelay(3000, 7000);
package/src/lib/args.js CHANGED
@@ -735,12 +735,10 @@ function parseTagArgs(args) {
735
735
  let discoverCountries = [];
736
736
  let discoverCount = 4;
737
737
  let discoverPrompt = null;
738
- let autoDiscover = false;
739
738
  let isDiscover = false;
740
739
  let isScore = false;
741
740
  let isScoreAll = false;
742
741
  let scoreAllPort = 7222;
743
- let discoverCountScoreAll = 10;
744
742
  let scoreProxy = null;
745
743
  let scoreTag = null;
746
744
  let scoreCountries = null;
@@ -780,10 +778,6 @@ function parseTagArgs(args) {
780
778
  }
781
779
  } else if (arg === "--count") {
782
780
  discoverCount = parseInt(args[++i]) || 4;
783
- } else if (arg === "--auto-discover") {
784
- autoDiscover = true;
785
- } else if (arg === "--discover-count") {
786
- discoverCountScoreAll = parseInt(args[++i]) || 10;
787
781
  } else if (arg === "--countries") {
788
782
  scoreCountries = args[++i]
789
783
  .split(",")
@@ -881,8 +875,6 @@ function parseTagArgs(args) {
881
875
  tagScoreAll: {
882
876
  countries: scoreCountries,
883
877
  serverUrl,
884
- autoDiscover,
885
- discoverCount: discoverCountScoreAll,
886
878
  port: scoreAllPort,
887
879
  proxy: scoreProxy,
888
880
  },