tt-help-cli-ycl 1.4.112 → 1.4.113

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.112",
3
+ "version": "1.4.113",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,11 +9,26 @@
9
9
  *
10
10
  * 用法: tt-help product-fill [-s 服务端] [-w webserver] [-i 间隔秒] [-l 每批上限]
11
11
  */
12
+ import { spawn } from "node:child_process";
13
+ import { fileURLToPath } from "node:url";
12
14
  import { getMacOrUuid } from "../lib/mac-or-uuid.js";
13
15
  import { createApiClient } from "../lib/api-client.js";
14
16
  import { userId as configuredUserId, saveUserId } from "../lib/constants.js";
15
17
 
16
18
  const CAPTCHA_BACKOFF_MS = 30_000; // 验证码后冷却时长
19
+ const WEBSERVER_READY_TIMEOUT_MS = 60_000; // 等待自动拉起的 webserver 就绪上限
20
+
21
+ /** 探测 webserver /health 是否可用 */
22
+ async function probeWebserverHealth(url, timeoutMs = 1500) {
23
+ try {
24
+ const res = await fetch(`${url}/health`, {
25
+ signal: AbortSignal.timeout(timeoutMs),
26
+ });
27
+ return res.ok;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
17
32
 
18
33
  function formatNow() {
19
34
  return new Date().toLocaleString("zh-CN", { hour12: false });
@@ -51,9 +66,16 @@ export async function handleProductFill(options) {
51
66
  productFillInterval,
52
67
  serverUrl,
53
68
  productFillWebserver,
69
+ productFillProxy,
54
70
  showHelp,
55
71
  } = options;
56
- const webserverUrl = productFillWebserver || null;
72
+ // 不指定 -w 时默认使用本地 webserver(http://127.0.0.1:3000),
73
+ // 且本地无响应时自动拉起(适合单独机器:验证码时弹浏览器人工处理)
74
+ const spawnLocalWebserver = !productFillWebserver;
75
+ const webserverUrl = productFillWebserver || "http://127.0.0.1:3000";
76
+ // 自动拉起 webserver 时透传的代理(默认本机 clash 7897)
77
+ const localWebserverProxy = productFillProxy || "http://127.0.0.1:7897";
78
+ let localWebserverChild = null;
57
79
 
58
80
  if (showHelp) {
59
81
  log(
@@ -63,7 +85,10 @@ export async function handleProductFill(options) {
63
85
  log("参数:");
64
86
  log(" -s, --server <URL> 服务端地址(默认: http://127.0.0.1:3001)");
65
87
  log(
66
- " -w, --webserver <URL> 商品详情 webserver 地址(必填,如 http://127.0.0.1:3000)",
88
+ " -w, --webserver <URL> 商品详情 webserver 地址(默认: http://127.0.0.1:3000,本地未运行会自动拉起)",
89
+ );
90
+ log(
91
+ " --proxy <URL> 自动拉起 webserver 时使用的代理(默认: http://127.0.0.1:7897)",
67
92
  );
68
93
  log(" -i, --interval <N> 无任务时轮询间隔,单位秒(默认: 10)");
69
94
  log(" -l, --limit <N> 每批领取任务数上限(默认: 5)");
@@ -73,6 +98,9 @@ export async function handleProductFill(options) {
73
98
  log(" 对每个任务调用 webserver /api/tiktok/product 抓取评分/销量/评论;");
74
99
  log(" 成功后回传 /api/product-fill/:uniqueId 写入 jobs 表。");
75
100
  log(" 商品详情验证码(503)自动冷却 30 秒后重试,其他失败计数后继续。");
101
+ log(
102
+ " 不传 -w 时本地无 webserver 会自动拉起(验证码时弹浏览器,人工解决后继续)。",
103
+ );
76
104
  log("");
77
105
  log("示例:");
78
106
  log(
@@ -82,13 +110,6 @@ export async function handleProductFill(options) {
82
110
  return;
83
111
  }
84
112
 
85
- if (!webserverUrl) {
86
- log(
87
- "错误: 缺少 webserver 地址,请用 -w/--webserver 指定(如 http://127.0.0.1:3000)",
88
- );
89
- return;
90
- }
91
-
92
113
  // ── 设备标识 ──
93
114
  let userId = configuredUserId;
94
115
  if (!userId) {
@@ -106,14 +127,101 @@ export async function handleProductFill(options) {
106
127
  const { apiGet, apiPost } = createApiClient({ log: false });
107
128
 
108
129
  let shuttingDown = false;
130
+ let forceExitTimer = null;
131
+ /** 强制退出路径清理自动拉起的 webserver(防止孤儿进程) */
132
+ const killLocalWebserver = () => {
133
+ if (localWebserverChild && !localWebserverChild.killed) {
134
+ localWebserverChild.kill("SIGKILL");
135
+ }
136
+ };
109
137
  const shutdown = () => {
110
- if (shuttingDown) return;
138
+ if (shuttingDown) {
139
+ // 第二次信号:立即强制退出(网络重试等场景可能无法优雅退出)
140
+ log(`\n[ProductFill] 再次收到信号,强制退出...`);
141
+ killLocalWebserver();
142
+ process.exit(1);
143
+ return;
144
+ }
111
145
  shuttingDown = true;
112
- log(`\n[ProductFill] 收到退出信号,完成当前批次后退出...`);
146
+ log(
147
+ `\n[ProductFill] 收到退出信号,完成当前批次后退出(再次按 Ctrl+C 强制退出)...`,
148
+ );
149
+ // 兜底:卡在网络重试/等待中时 10 秒后强制退出
150
+ forceExitTimer = setTimeout(() => {
151
+ log("[ProductFill] 等待超时,强制退出");
152
+ killLocalWebserver();
153
+ process.exit(0);
154
+ }, 10_000);
155
+ forceExitTimer.unref?.();
113
156
  };
114
157
  process.on("SIGINT", () => shutdown());
115
158
  process.on("SIGTERM", () => shutdown());
116
159
 
160
+ // ── 确保 webserver 可用:未指定 -w 且本地无响应时自动拉起 ──
161
+ if (await probeWebserverHealth(webserverUrl)) {
162
+ log(`[ProductFill] webserver 已就绪: ${webserverUrl}`);
163
+ } else {
164
+ if (!spawnLocalWebserver) {
165
+ log(
166
+ `[ProductFill] 警告: webserver ${webserverUrl} 无响应,任务将按失败重试`,
167
+ );
168
+ } else {
169
+ const portMatch = webserverUrl.match(/:(\d+)\/?$/);
170
+ const port = portMatch ? parseInt(portMatch[1], 10) : 3000;
171
+ // cli.js 在包根(源码与 npm 包结构一致),webserver 命令两者都支持
172
+ const cliJs = fileURLToPath(new URL("../../cli.js", import.meta.url));
173
+ log(
174
+ `[ProductFill] 本地 webserver 未运行,自动拉起(端口 ${port},代理 ${localWebserverProxy})...`,
175
+ );
176
+ localWebserverChild = spawn(
177
+ process.execPath,
178
+ [
179
+ cliJs,
180
+ "webserver",
181
+ "-p",
182
+ String(port),
183
+ "--proxy",
184
+ localWebserverProxy,
185
+ ],
186
+ {
187
+ stdio: ["ignore", "ignore", "inherit"], // webserver 日志(含验证码提示)透传到本终端
188
+ windowsHide: true,
189
+ },
190
+ );
191
+ localWebserverChild.on("exit", (code) => {
192
+ if (!shuttingDown && code !== null && code !== 0) {
193
+ log(`[ProductFill] ⚠ 本地 webserver 已退出 (code=${code})`);
194
+ }
195
+ });
196
+ // 等待就绪(最多 60 秒)
197
+ const deadline = Date.now() + WEBSERVER_READY_TIMEOUT_MS;
198
+ while (!shuttingDown && Date.now() < deadline) {
199
+ if (await probeWebserverHealth(webserverUrl, 1000)) break;
200
+ await new Promise((r) => setTimeout(r, 1000));
201
+ }
202
+ if (await probeWebserverHealth(webserverUrl, 1000)) {
203
+ log(`[ProductFill] webserver 已就绪: ${webserverUrl}`);
204
+ } else {
205
+ log(
206
+ `[ProductFill] 警告: webserver 启动超时,继续运行(抓取失败将自动重试)`,
207
+ );
208
+ }
209
+ }
210
+ }
211
+
212
+ /** 可被退出信号中断的等待(500ms 粒度检查 shuttingDown) */
213
+ const interruptibleSleep = (ms) =>
214
+ new Promise((resolve) => {
215
+ const start = Date.now();
216
+ const tick = () => {
217
+ if (shuttingDown) return resolve();
218
+ const left = ms - (Date.now() - start);
219
+ if (left <= 0) return resolve();
220
+ setTimeout(tick, Math.min(500, left));
221
+ };
222
+ tick();
223
+ });
224
+
117
225
  let loopCount = 0;
118
226
  let totalSuccess = 0;
119
227
  let totalFailed = 0;
@@ -132,7 +240,7 @@ export async function handleProductFill(options) {
132
240
  log(
133
241
  `[ProductFill] ⏳ 验证码冷却中(剩余 ${Math.ceil(waitMs / 1000)}s),暂不领取任务...`,
134
242
  );
135
- await new Promise((r) => setTimeout(r, waitMs));
243
+ await interruptibleSleep(waitMs);
136
244
  continue;
137
245
  }
138
246
 
@@ -146,7 +254,7 @@ export async function handleProductFill(options) {
146
254
  `[ProductFill] 当前无缺商品任务,${productFillInterval}秒后重试...`,
147
255
  );
148
256
  }
149
- await new Promise((r) => setTimeout(r, productFillInterval * 1000));
257
+ await interruptibleSleep(productFillInterval * 1000);
150
258
  continue;
151
259
  }
152
260
 
@@ -207,12 +315,17 @@ export async function handleProductFill(options) {
207
315
  log(
208
316
  ` 本批: 成功 ${batchSuccess} | 失败 ${batchFailed} | 跳过 ${batchSkipped}(累计: 成功 ${totalSuccess} | 失败 ${totalFailed} | 跳过 ${totalSkipped})\n`,
209
317
  );
210
- await new Promise((r) => setTimeout(r, 300));
318
+ await interruptibleSleep(300);
211
319
  }
212
320
  } catch (err) {
213
321
  log(`[ProductFill] 运行异常: ${err.message}`);
214
322
  throw err;
215
323
  } finally {
324
+ if (forceExitTimer) clearTimeout(forceExitTimer);
325
+ if (localWebserverChild && !localWebserverChild.killed) {
326
+ log("[ProductFill] 关闭自动拉起的本地 webserver...");
327
+ localWebserverChild.kill("SIGTERM");
328
+ }
216
329
  log(
217
330
  `[ProductFill] 已退出。总计: 成功 ${totalSuccess} | 失败 ${totalFailed} | 跳过 ${totalSkipped}`,
218
331
  );
package/src/lib/args.js CHANGED
@@ -766,6 +766,7 @@ function parseProductFillArgs(args) {
766
766
  let interval = 10;
767
767
  let serverUrl = defaultServer;
768
768
  let webserverUrl = null;
769
+ let proxyUrl = null;
769
770
 
770
771
  for (let i = 0; i < args.length; i++) {
771
772
  const arg = args[i];
@@ -773,6 +774,8 @@ function parseProductFillArgs(args) {
773
774
  serverUrl = args[++i];
774
775
  } else if (arg === "-w" || arg === "--webserver") {
775
776
  webserverUrl = args[++i];
777
+ } else if (arg === "--proxy") {
778
+ proxyUrl = args[++i] || null;
776
779
  } else if (arg === "-i" || arg === "--interval") {
777
780
  interval = parseInt(args[++i], 10) || 10;
778
781
  } else if (arg === "-l" || arg === "--limit") {
@@ -786,6 +789,7 @@ function parseProductFillArgs(args) {
786
789
  productFillInterval: interval,
787
790
  serverUrl,
788
791
  productFillWebserver: webserverUrl,
792
+ productFillProxy: proxyUrl,
789
793
  urls: [],
790
794
  outputFormat: "json",
791
795
  exploreCount: 0,
@@ -1368,6 +1372,28 @@ export function parseArgs() {
1368
1372
  const args = process.argv.slice(2);
1369
1373
 
1370
1374
  if (args.includes("-h") || args.includes("--help")) {
1375
+ // 子命令级帮助:product-fill -h 显示专属帮助,而非全局 HELP_TEXT
1376
+ if (args[0] === "product-fill") {
1377
+ return {
1378
+ subcommand: "product-fill",
1379
+ productFillLimit: 5,
1380
+ productFillInterval: 10,
1381
+ serverUrl: defaultServer,
1382
+ productFillWebserver: null,
1383
+ showHelp: true,
1384
+ showVersion: false,
1385
+ urls: [],
1386
+ outputFile: null,
1387
+ outputFormat: "json",
1388
+ exploreCount: 0,
1389
+ showConfig: false,
1390
+ customProxy: null,
1391
+ configAction: null,
1392
+ configValue: null,
1393
+ pipeMode: false,
1394
+ filterStr: null,
1395
+ };
1396
+ }
1371
1397
  return {
1372
1398
  showHelp: true,
1373
1399
  showVersion: false,