tt-help-cli-ycl 1.4.112 → 1.4.114

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.
@@ -71,7 +71,12 @@ export async function handleRefresh(options) {
71
71
  exploreProxy,
72
72
  exploreWebserver,
73
73
  exploreNoProduct,
74
+ exploreMini,
74
75
  } = options;
76
+ // --mini 模式:只采集视频列表及由视频推导的信息(EC视频、最新发布时间),
77
+ // 跳过用户信息、国家采样、关注/粉丝提取(最大耗时项)和商品详情抓取(由独立商品程序异步补抓),大幅提速。
78
+ const followEnabled = !exploreMini && exploreEnableFollow !== false;
79
+ const skipProduct = exploreMini || !!exploreNoProduct;
75
80
  const effectiveProxy = exploreProxy || configuredProxy;
76
81
  let resolvedProxy = effectiveProxy;
77
82
  if (
@@ -122,8 +127,11 @@ export async function handleRefresh(options) {
122
127
  console.error(`\n=== Refresh 模式(基于 explore) ===`);
123
128
  console.error(`服务器: ${serverUrl}`);
124
129
  console.error(`视频采集: ${exploreMaxVideos || 16}`);
130
+ if (exploreMini) {
131
+ console.error(`采集范围: mini(仅视频及视频相关信息)`);
132
+ }
125
133
  console.error(
126
- `关注/粉丝: ${exploreEnableFollow ? "启用" : "禁用"} (${exploreMaxFollowing}/${exploreMaxFollowers})`,
134
+ `关注/粉丝: ${followEnabled ? "启用" : "禁用"} (${exploreMaxFollowing}/${exploreMaxFollowers})`,
127
135
  );
128
136
  console.error(`国家筛选: ${exploreLocation}`);
129
137
  if (exploreJobLocations) console.error(`任务国家: ${exploreJobLocations}`);
@@ -368,11 +376,11 @@ export async function handleRefresh(options) {
368
376
  await handleAccountSwitch(checkResult.reason);
369
377
  } else if (checkResult.info && processedCount % 10 === 0) {
370
378
  const followElapsed =
371
- loggedIn && exploreEnableFollow
379
+ loggedIn && followEnabled
372
380
  ? Math.round((Date.now() - lastFollowSuccessTime) / 60000)
373
381
  : 0;
374
382
  const followStatus =
375
- loggedIn && exploreEnableFollow
383
+ loggedIn && followEnabled
376
384
  ? ` | 关注/粉丝上次成功 ${followElapsed} 分钟前`
377
385
  : "";
378
386
  console.error(`\n[健康检查] ${checkResult.info}${followStatus}`);
@@ -446,8 +454,8 @@ export async function handleRefresh(options) {
446
454
  }
447
455
  }
448
456
 
449
- // 关注/粉丝封禁检测
450
- if (loggedIn && exploreEnableFollow) {
457
+ // 关注/粉丝封禁检测(mini 模式下不采集关注/粉丝,无需检测)
458
+ if (loggedIn && followEnabled) {
451
459
  const followElapsed = Date.now() - lastFollowSuccessTime;
452
460
  if (followElapsed > FOLLOW_BLOCK_THRESHOLD) {
453
461
  const minutes = Math.round(followElapsed / 60000);
@@ -481,7 +489,7 @@ export async function handleRefresh(options) {
481
489
  username,
482
490
  {
483
491
  maxVideos: exploreMaxVideos || 16,
484
- enableFollow: exploreEnableFollow !== false,
492
+ enableFollow: followEnabled,
485
493
  loggedIn,
486
494
  maxFollowing: exploreMaxFollowing || 100,
487
495
  maxFollowers: exploreMaxFollowers || 100,
@@ -490,7 +498,8 @@ export async function handleRefresh(options) {
490
498
  browser,
491
499
  proxyServer: cdpOptions.proxyServer || null,
492
500
  webserverUrl: exploreWebserver || null,
493
- noProduct: !!exploreNoProduct,
501
+ noProduct: skipProduct,
502
+ mini: !!exploreMini,
494
503
  },
495
504
  console.error,
496
505
  );
@@ -509,7 +518,7 @@ export async function handleRefresh(options) {
509
518
  username,
510
519
  {
511
520
  maxVideos: exploreMaxVideos || 16,
512
- enableFollow: exploreEnableFollow !== false,
521
+ enableFollow: followEnabled,
513
522
  loggedIn,
514
523
  maxFollowing: exploreMaxFollowing || 100,
515
524
  maxFollowers: exploreMaxFollowers || 100,
@@ -518,7 +527,8 @@ export async function handleRefresh(options) {
518
527
  browser,
519
528
  proxyServer: cdpOptions.proxyServer || null,
520
529
  webserverUrl: exploreWebserver || null,
521
- noProduct: !!exploreNoProduct,
530
+ noProduct: skipProduct,
531
+ mini: !!exploreMini,
522
532
  },
523
533
  console.error,
524
534
  );
@@ -559,7 +569,7 @@ export async function handleRefresh(options) {
559
569
  username,
560
570
  {
561
571
  maxVideos: exploreMaxVideos || 16,
562
- enableFollow: exploreEnableFollow !== false,
572
+ enableFollow: followEnabled,
563
573
  loggedIn,
564
574
  maxFollowing: exploreMaxFollowing || 100,
565
575
  maxFollowers: exploreMaxFollowers || 100,
@@ -568,7 +578,8 @@ export async function handleRefresh(options) {
568
578
  browser,
569
579
  proxyServer: cdpOptions.proxyServer || null,
570
580
  webserverUrl: exploreWebserver || null,
571
- noProduct: !!exploreNoProduct,
581
+ noProduct: skipProduct,
582
+ mini: !!exploreMini,
572
583
  },
573
584
  console.error,
574
585
  );
@@ -656,11 +667,16 @@ export async function handleRefresh(options) {
656
667
  result.confirmedLocation || result.locationCreated;
657
668
  const guessedLocation = refreshLocation || null;
658
669
 
659
- // 把 confirmedLocation 合并到 userInfo 中(通过 commitRedoJob 写入 DB)
670
+ // 把 confirmedLocation / latestVideoTime 合并到 userInfo 中(通过 commitRedoJob 写入 DB)
671
+ // latestVideoTime 单独合并:mini 模式下 userInfo 为 null,但 latestVideoTime
672
+ // 是视频推导的零成本信息,仍需更新服务端 latest_video_time(影响任务排序与前端显示)
660
673
  const refreshUserInfo = { ...(result.userInfo || {}) };
661
674
  if (result.confirmedLocation) {
662
675
  refreshUserInfo.confirmedLocation = result.confirmedLocation;
663
676
  }
677
+ if (result.latestVideoTime) {
678
+ refreshUserInfo.latestVideoTime = result.latestVideoTime;
679
+ }
664
680
 
665
681
  const payload = {
666
682
  userInfo: refreshUserInfo,
@@ -22,10 +22,13 @@ export async function handleWatchdog(options) {
22
22
  watchdogNoDaemon,
23
23
  } = options;
24
24
 
25
- // 服务端地址:--server 参数 > ~/.tt-help.json server > 默认值
25
+ // 服务端地址:--server 参数 > ~/.tt-help.json watchdogServer > server > 默认值
26
26
  const clientConfig = getClientConfig();
27
27
  const serverUrl =
28
- watchdogServer || clientConfig.server || "http://127.0.0.1:3001";
28
+ watchdogServer ||
29
+ clientConfig.watchdogServer ||
30
+ clientConfig.server ||
31
+ "http://127.0.0.1:3001";
29
32
 
30
33
  // 客户端 ID:优先 --client-id,其次 userId@hostname,最后 hostname+pid
31
34
  const userId = clientConfig.userId || "";
@@ -47,54 +47,69 @@ export function createApiClient(opts = {}) {
47
47
  const retryOpts = { maxRetries, backoff, log };
48
48
 
49
49
  async function apiGet(url) {
50
- return withRetry(`GET ${url}`, async () => {
51
- const res = await fetch(url, {
52
- headers: buildHeaders({}, clientId, meta),
53
- });
54
- if (checkStatus && !res.ok) {
55
- const errText = await res.text();
56
- throw new Error(`HTTP ${res.status}: ${errText.substring(0, 200)}`);
57
- }
58
- return res.json();
59
- }, retryOpts);
50
+ return withRetry(
51
+ `GET ${url}`,
52
+ async () => {
53
+ const res = await fetch(url, {
54
+ headers: buildHeaders({}, clientId, meta),
55
+ signal: AbortSignal.timeout(30000),
56
+ });
57
+ if (checkStatus && !res.ok) {
58
+ const errText = await res.text();
59
+ throw new Error(`HTTP ${res.status}: ${errText.substring(0, 200)}`);
60
+ }
61
+ return res.json();
62
+ },
63
+ retryOpts,
64
+ );
60
65
  }
61
66
 
62
67
  async function apiPost(url, body) {
63
- return withRetry(`POST ${url}`, async () => {
64
- const res = await fetch(url, {
65
- method: "POST",
66
- headers: buildHeaders(
67
- { "Content-Type": "application/json" },
68
- clientId,
69
- meta,
70
- ),
71
- body: JSON.stringify(body),
72
- });
73
- if (checkStatus && !res.ok) {
74
- const errText = await res.text();
75
- throw new Error(`HTTP ${res.status}: ${errText.substring(0, 200)}`);
76
- }
77
- return res.json();
78
- }, retryOpts);
68
+ return withRetry(
69
+ `POST ${url}`,
70
+ async () => {
71
+ const res = await fetch(url, {
72
+ method: "POST",
73
+ headers: buildHeaders(
74
+ { "Content-Type": "application/json" },
75
+ clientId,
76
+ meta,
77
+ ),
78
+ body: JSON.stringify(body),
79
+ signal: AbortSignal.timeout(30000),
80
+ });
81
+ if (checkStatus && !res.ok) {
82
+ const errText = await res.text();
83
+ throw new Error(`HTTP ${res.status}: ${errText.substring(0, 200)}`);
84
+ }
85
+ return res.json();
86
+ },
87
+ retryOpts,
88
+ );
79
89
  }
80
90
 
81
91
  async function apiPut(url, body) {
82
- return withRetry(`PUT ${url}`, async () => {
83
- const res = await fetch(url, {
84
- method: "PUT",
85
- headers: buildHeaders(
86
- { "Content-Type": "application/json" },
87
- clientId,
88
- meta,
89
- ),
90
- body: body ? JSON.stringify(body) : undefined,
91
- });
92
- if (checkStatus && !res.ok) {
93
- const errText = await res.text();
94
- throw new Error(`HTTP ${res.status}: ${errText.substring(0, 200)}`);
95
- }
96
- return res.json();
97
- }, retryOpts);
92
+ return withRetry(
93
+ `PUT ${url}`,
94
+ async () => {
95
+ const res = await fetch(url, {
96
+ method: "PUT",
97
+ headers: buildHeaders(
98
+ { "Content-Type": "application/json" },
99
+ clientId,
100
+ meta,
101
+ ),
102
+ body: body ? JSON.stringify(body) : undefined,
103
+ signal: AbortSignal.timeout(30000),
104
+ });
105
+ if (checkStatus && !res.ok) {
106
+ const errText = await res.text();
107
+ throw new Error(`HTTP ${res.status}: ${errText.substring(0, 200)}`);
108
+ }
109
+ return res.json();
110
+ },
111
+ retryOpts,
112
+ );
98
113
  }
99
114
 
100
115
  return { apiGet, apiPost, apiPut };
@@ -142,24 +142,25 @@ async function processAPIResponse(
142
142
  return videoMap;
143
143
  }
144
144
 
145
- async function fetchUserVideosAPI(page, username, maxVideos, log) {
146
- log(` [API拦截] 获取 @${username} 视频 ...`);
145
+ // 默认被封重试间隔:30/40/50 秒共 3 次,仍封才抛"被封"交给上层。
146
+ // RM_BLOCKED_WAIT_MS 仅用于测试缩短所有等待
147
+ const _rmBlockedWait = Number(process.env.RM_BLOCKED_WAIT_MS);
148
+ const DEFAULT_BLOCKED_RETRY_WAITS =
149
+ _rmBlockedWait > 0
150
+ ? [_rmBlockedWait, _rmBlockedWait, _rmBlockedWait]
151
+ : [30_000, 40_000, 50_000];
147
152
 
148
- // CDP 健康检查:确保 page 可用
149
- try {
150
- await page.evaluate(() => 1);
151
- } catch (e) {
152
- log(` [API拦截] CDP 连接异常: ${e.message}`);
153
- throw new Error(`CDP 连接异常: ${e.message}`);
154
- }
155
-
156
- let apiRequestUrl = null;
153
+ /**
154
+ * 单次尝试:注册拦截 → goto 用户主页 → 等 item_list 响应 → 解析 JSON。
155
+ * apiRequestUrlRef 跨次复用(URL 相同,保留第一次捕获的即可)
156
+ */
157
+ async function attemptItemListFetch(page, username, log, apiRequestUrlRef) {
157
158
  let sawApiRequest = false;
158
159
 
159
160
  const requestHandler = (request) => {
160
161
  if (request.url().includes("/api/post/item_list/")) {
161
162
  sawApiRequest = true;
162
- if (!apiRequestUrl) apiRequestUrl = request.url();
163
+ if (!apiRequestUrlRef.value) apiRequestUrlRef.value = request.url();
163
164
  }
164
165
  };
165
166
 
@@ -168,18 +169,28 @@ async function fetchUserVideosAPI(page, username, maxVideos, log) {
168
169
  const t0 = Date.now();
169
170
  let data = null;
170
171
  let interceptionError = null;
172
+ let responsePromise = null;
173
+ let response = null;
174
+ let rawBodyBytes = -1; // -1 未知(未读取到 body);0 = 服务端空响应(封禁判定依据)
171
175
 
172
176
  try {
177
+ // 响应监听必须在 goto 之前注册:domcontentloaded 返回时首个 item_list
178
+ // 响应可能已经到达,晚注册会错过它、被迫等下一个请求或超时
179
+ responsePromise = page.waitForResponse(
180
+ (res) => res.url().includes("/api/post/item_list/"),
181
+ { timeout: 30000 },
182
+ );
183
+ // 防 unhandledRejection:goto 卡住期间该 promise 可能先超时 reject,
184
+ // 未被 await 的 rejection 在 Node 默认配置下会直接崩进程
185
+ responsePromise.catch(() => {});
186
+
173
187
  await page.goto(`https://www.tiktok.com/@${username}`, {
174
188
  waitUntil: "domcontentloaded",
175
189
  timeout: 30000,
176
190
  });
177
191
  await assertPageUrl(page, `@${username}`);
178
192
 
179
- const response = await page.waitForResponse(
180
- (res) => res.url().includes("/api/post/item_list/"),
181
- { timeout: 30000 },
182
- );
193
+ response = await responsePromise;
183
194
 
184
195
  // 超时保护:response.json() 内部调用 CDP Network.getResponseBody,
185
196
  // 当页面刷新/验证码导致响应资源丢失时会挂起,需独立超时控制
@@ -194,50 +205,155 @@ async function fetchUserVideosAPI(page, username, maxVideos, log) {
194
205
  ]);
195
206
  } catch (e) {
196
207
  interceptionError = e.message;
208
+ // JSON 解析失败(如 "Unexpected end of JSON input")时打印原始响应,
209
+ // 便于诊断空响应/截断响应/非 JSON 响应等异常形态
210
+ if (response) {
211
+ try {
212
+ // body 读取同样可能因页面异常而挂起,加独立超时保护
213
+ const raw = await Promise.race([
214
+ response.body(),
215
+ new Promise((_, reject) =>
216
+ setTimeout(() => reject(new Error("body 读取超时(15s)")), 15000),
217
+ ),
218
+ ]);
219
+ const text = raw.toString("utf-8");
220
+ rawBodyBytes = raw.length;
221
+ // content-length 用于区分两种 0 字节形态:
222
+ // 声明长度 > 实际字节 = 传输被截断;声明缺失/为 0 = 服务端主动空响应
223
+ const declared = parseInt(
224
+ response.headers()["content-length"] || "0",
225
+ 10,
226
+ );
227
+ const clNote =
228
+ raw.length === 0
229
+ ? declared > 0
230
+ ? `content-length 声明 ${declared}(传输截断)`
231
+ : "content-length 缺失/为 0(服务端主动空响应)"
232
+ : `content-length ${declared || "无"}`;
233
+ log(
234
+ ` [API拦截] item_list 响应异常: HTTP ${response.status()}, ${raw.length} 字节, ${clNote}, 内容: ${text.slice(0, 200).replace(/\s+/g, " ")}`,
235
+ );
236
+ } catch (e2) {
237
+ log(` [API拦截] item_list 响应体读取失败: ${e2.message}`);
238
+ }
239
+ }
197
240
  } finally {
241
+ // goto 提前失败时丢弃未等待的响应 promise,避免 unhandledRejection
242
+ responsePromise?.catch(() => {});
198
243
  page.off("request", requestHandler);
199
244
  }
200
245
 
201
- const elapsed = Date.now() - t0;
202
-
203
- if (data && Array.isArray(data.itemList) && data.itemList.length > 0) {
204
- log(` [API拦截] 成功,耗时 ${elapsed}ms`);
205
- return await processAPIResponse(
206
- data,
207
- username,
208
- maxVideos,
209
- [],
210
- page,
211
- apiRequestUrl,
212
- log,
213
- );
214
- }
246
+ return {
247
+ data,
248
+ interceptionError,
249
+ rawBodyBytes,
250
+ elapsed: Date.now() - t0,
251
+ };
252
+ }
215
253
 
216
- let pageError = await detectPageError(page);
217
- if (!pageError) {
218
- pageError = await detectPageErrorWithWait(page, 5000);
219
- }
254
+ async function fetchUserVideosAPI(
255
+ page,
256
+ username,
257
+ maxVideos,
258
+ log,
259
+ options = {},
260
+ ) {
261
+ log(` [API拦截] 获取 @${username} 视频 ...`);
220
262
 
221
- if (pageError === "service_error") {
222
- throw new Error(`被封: ${username}`);
223
- }
263
+ // CDP 健康检查:带超时保护。页面主线程死锁时 evaluate 可能长时间无响应,
264
+ // 超时后自动刷新页面恢复(实测刷新页面即可解除死锁)
265
+ const evalWithTimeout = (label, ms) =>
266
+ Promise.race([
267
+ page.evaluate(() => 1),
268
+ new Promise((_, reject) =>
269
+ setTimeout(
270
+ () => reject(new Error(`${label} 超时(${ms / 1000}s)`)),
271
+ ms,
272
+ ),
273
+ ),
274
+ ]);
224
275
 
225
- if (pageError === "cdn_blocked") {
226
- throw new Error(`CDN限流: ${username}`);
276
+ try {
277
+ await evalWithTimeout("CDP 健康检查", 10000);
278
+ } catch (e) {
279
+ log(` [API拦截] CDP 连接异常: ${e.message},刷新页面恢复...`);
280
+ await page
281
+ .reload({ waitUntil: "domcontentloaded", timeout: 30000 })
282
+ .catch(() => {});
283
+ try {
284
+ await evalWithTimeout("刷新后重检", 10000);
285
+ log(` [API拦截] 页面已恢复`);
286
+ } catch (e2) {
287
+ throw new Error(`CDP 连接异常(刷新后仍失败): ${e2.message}`);
288
+ }
227
289
  }
228
290
 
229
- if (pageError) {
230
- log(` [API拦截] 数据为空,识别到页面错误: ${pageError}`);
231
- return new Map();
232
- }
291
+ let apiRequestUrlRef = { value: null };
292
+ const { blockedRetryWaitsMs = DEFAULT_BLOCKED_RETRY_WAITS } = options;
293
+ const retryWaits = Array.isArray(blockedRetryWaitsMs)
294
+ ? blockedRetryWaitsMs
295
+ : DEFAULT_BLOCKED_RETRY_WAITS;
296
+ let attempt = 0;
297
+
298
+ while (true) {
299
+ attempt++;
300
+ const { data, interceptionError, rawBodyBytes, elapsed } =
301
+ await attemptItemListFetch(page, username, log, apiRequestUrlRef);
302
+
303
+ if (data && Array.isArray(data.itemList) && data.itemList.length > 0) {
304
+ log(` [API拦截] 成功,耗时 ${elapsed}ms`);
305
+ return await processAPIResponse(
306
+ data,
307
+ username,
308
+ maxVideos,
309
+ [],
310
+ page,
311
+ apiRequestUrlRef.value,
312
+ log,
313
+ );
314
+ }
233
315
 
234
- if (interceptionError) {
235
- log(` [API拦截] 超时/拦截失败: ${interceptionError},耗时 ${elapsed}ms`);
236
- throw new Error(`@${username} 页面异常(API拦截超时,未识别到错误类型)`);
237
- }
316
+ // 0 字节空响应 = 被封(content-length 缺失/为 0 或截断,统一按封禁处理)。
317
+ // 不再依赖页面"出错了/很抱歉"文案判定,静默型封禁(页面正常渲染但 API 空响应)也能识别。
318
+ // 默认 30/40/50 秒等待重试 3 次,仍封才抛错交给上层(explore/refresh/refresh-mini 均受益)
319
+ if (rawBodyBytes === 0) {
320
+ const retryIndex = attempt - 1;
321
+ if (retryIndex < retryWaits.length) {
322
+ const waitMs = retryWaits[retryIndex];
323
+ log(
324
+ ` [API拦截] item_list 空响应(0 字节),判定为被封,${waitMs / 1000}s 后第 ${attempt}/${retryWaits.length} 次重试...`,
325
+ );
326
+ await new Promise((r) => setTimeout(r, waitMs));
327
+ continue;
328
+ }
329
+ log(
330
+ ` [API拦截] item_list 空响应(0 字节),重试 ${retryWaits.length} 次仍被封`,
331
+ );
332
+ throw new Error(`被封: ${username}`);
333
+ }
334
+
335
+ let pageError = await detectPageError(page);
336
+ if (!pageError) {
337
+ pageError = await detectPageErrorWithWait(page, 5000);
338
+ }
339
+
340
+ if (pageError === "cdn_blocked") {
341
+ throw new Error(`CDN限流: ${username}`);
342
+ }
238
343
 
239
- log(` [API拦截] 数据为空,未识别到错误类型,当作无视频处理`);
240
- return new Map();
344
+ if (pageError) {
345
+ log(` [API拦截] 数据为空,识别到页面错误: ${pageError}`);
346
+ return new Map();
347
+ }
348
+
349
+ if (interceptionError) {
350
+ log(` [API拦截] 超时/拦截失败: ${interceptionError},耗时 ${elapsed}ms`);
351
+ throw new Error(`@${username} 页面异常(API拦截超时,未识别到错误类型)`);
352
+ }
353
+
354
+ log(` [API拦截] 数据为空,未识别到错误类型,当作无视频处理`);
355
+ return new Map();
356
+ }
241
357
  }
242
358
 
243
359
  export { fetchUserVideosAPI };
package/src/lib/args.js CHANGED
@@ -515,11 +515,14 @@ function parseRefreshArgs(args) {
515
515
  let exploreJobLocations = null;
516
516
  let exploreWebserver = null;
517
517
  let exploreNoProduct = false;
518
+ let exploreMini = false;
518
519
 
519
520
  for (let i = 0; i < args.length; i++) {
520
521
  const arg = args[i];
521
522
  if (arg === "--server") {
522
523
  serverUrl = args[++i];
524
+ } else if (arg === "--mini") {
525
+ exploreMini = true;
523
526
  } else if (arg === "--preset") {
524
527
  explorePreset = args[++i];
525
528
  } else if (arg === "-i" || arg === "--interval") {
@@ -581,6 +584,85 @@ function parseRefreshArgs(args) {
581
584
  exploreProxy,
582
585
  exploreWebserver,
583
586
  exploreNoProduct,
587
+ exploreMini,
588
+ serverUrl,
589
+ urls: [],
590
+ outputFormat: "json",
591
+ exploreCount: 0,
592
+ showConfig: false,
593
+ showHelp: false,
594
+ customProxy: null,
595
+ configAction: null,
596
+ configValue: null,
597
+ pipeMode: false,
598
+ filterStr: null,
599
+ };
600
+ }
601
+
602
+ function parseRefreshMiniArgs(args) {
603
+ let serverUrl = defaultServer;
604
+ let explorePreset = "normal";
605
+ let exploreInterval = 30;
606
+ let exploreLocation = DEFAULT_TARGET_LOCATIONS_CSV;
607
+ let exploreMaxUsers = 0;
608
+ let explorePort = 4222;
609
+ let exploreUserId = null;
610
+ let exploreMaxVideos = 16;
611
+ let exploreRedoMaxAge = 0;
612
+ let exploreProxy = null;
613
+ let exploreJobLocations = null;
614
+ let rotateMin = 10;
615
+ let captchaWaitMin = 10;
616
+ let ensureDaemon = false;
617
+
618
+ for (let i = 0; i < args.length; i++) {
619
+ const arg = args[i];
620
+ if (arg === "--server") {
621
+ serverUrl = args[++i];
622
+ } else if (arg === "--preset") {
623
+ explorePreset = args[++i];
624
+ } else if (arg === "-i" || arg === "--interval") {
625
+ exploreInterval = parseInt(args[++i], 10) || 30;
626
+ } else if (arg === "--port") {
627
+ explorePort = parseInt(args[++i]) || 4222;
628
+ } else if (arg === "--user-id") {
629
+ exploreUserId = args[++i];
630
+ } else if (arg === "--max-videos") {
631
+ exploreMaxVideos = parseInt(args[++i]) || 16;
632
+ } else if (arg === "--max-age") {
633
+ exploreRedoMaxAge = parseInt(args[++i]) || 0;
634
+ } else if (arg === "--proxy") {
635
+ exploreProxy = args[++i];
636
+ } else if (arg === "--location") {
637
+ exploreLocation = args[++i];
638
+ } else if (arg === "--job-locations") {
639
+ exploreJobLocations = args[++i];
640
+ } else if (arg === "--max-users") {
641
+ exploreMaxUsers = parseInt(args[++i]) || 0;
642
+ } else if (arg === "--rotate-min") {
643
+ rotateMin = parseInt(args[++i], 10) || 0;
644
+ } else if (arg === "--captcha-wait-min") {
645
+ captchaWaitMin = parseInt(args[++i], 10) || 10;
646
+ } else if (arg === "--ensure-daemon") {
647
+ ensureDaemon = true;
648
+ }
649
+ }
650
+
651
+ return {
652
+ subcommand: "refresh-mini",
653
+ explorePreset,
654
+ exploreInterval,
655
+ exploreLocation,
656
+ exploreJobLocations,
657
+ exploreMaxUsers,
658
+ explorePort,
659
+ exploreUserId,
660
+ exploreMaxVideos,
661
+ exploreRedoMaxAge,
662
+ exploreProxy,
663
+ rotateMin,
664
+ captchaWaitMin,
665
+ ensureDaemon,
584
666
  serverUrl,
585
667
  urls: [],
586
668
  outputFormat: "json",
@@ -766,6 +848,7 @@ function parseProductFillArgs(args) {
766
848
  let interval = 10;
767
849
  let serverUrl = defaultServer;
768
850
  let webserverUrl = null;
851
+ let proxyUrl = null;
769
852
 
770
853
  for (let i = 0; i < args.length; i++) {
771
854
  const arg = args[i];
@@ -773,6 +856,8 @@ function parseProductFillArgs(args) {
773
856
  serverUrl = args[++i];
774
857
  } else if (arg === "-w" || arg === "--webserver") {
775
858
  webserverUrl = args[++i];
859
+ } else if (arg === "--proxy") {
860
+ proxyUrl = args[++i] || null;
776
861
  } else if (arg === "-i" || arg === "--interval") {
777
862
  interval = parseInt(args[++i], 10) || 10;
778
863
  } else if (arg === "-l" || arg === "--limit") {
@@ -786,6 +871,7 @@ function parseProductFillArgs(args) {
786
871
  productFillInterval: interval,
787
872
  serverUrl,
788
873
  productFillWebserver: webserverUrl,
874
+ productFillProxy: proxyUrl,
789
875
  urls: [],
790
876
  outputFormat: "json",
791
877
  exploreCount: 0,
@@ -1368,6 +1454,28 @@ export function parseArgs() {
1368
1454
  const args = process.argv.slice(2);
1369
1455
 
1370
1456
  if (args.includes("-h") || args.includes("--help")) {
1457
+ // 子命令级帮助:product-fill -h 显示专属帮助,而非全局 HELP_TEXT
1458
+ if (args[0] === "product-fill") {
1459
+ return {
1460
+ subcommand: "product-fill",
1461
+ productFillLimit: 5,
1462
+ productFillInterval: 10,
1463
+ serverUrl: defaultServer,
1464
+ productFillWebserver: null,
1465
+ showHelp: true,
1466
+ showVersion: false,
1467
+ urls: [],
1468
+ outputFile: null,
1469
+ outputFormat: "json",
1470
+ exploreCount: 0,
1471
+ showConfig: false,
1472
+ customProxy: null,
1473
+ configAction: null,
1474
+ configValue: null,
1475
+ pipeMode: false,
1476
+ filterStr: null,
1477
+ };
1478
+ }
1371
1479
  return {
1372
1480
  showHelp: true,
1373
1481
  showVersion: false,
@@ -1446,6 +1554,10 @@ export function parseArgs() {
1446
1554
  return parseRefreshArgs(args.slice(1));
1447
1555
  }
1448
1556
 
1557
+ if (args.length > 0 && args[0] === "refresh-mini") {
1558
+ return parseRefreshMiniArgs(args.slice(1));
1559
+ }
1560
+
1449
1561
  if (args.length > 0 && args[0] === "search") {
1450
1562
  return parseSearchArgs(args.slice(1));
1451
1563
  }