tt-help-cli-ycl 1.3.45 → 1.3.47

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.
Files changed (67) hide show
  1. package/README.md +33 -33
  2. package/cli.js +9 -9
  3. package/package.json +52 -52
  4. package/scripts/run-explore copy.bat +101 -101
  5. package/scripts/run-explore.bat +134 -134
  6. package/scripts/run-explore.ps1 +159 -159
  7. package/scripts/run-explore.sh +121 -121
  8. package/src/cli/attach.js +331 -313
  9. package/src/cli/auto.js +265 -265
  10. package/src/cli/comments.js +620 -620
  11. package/src/cli/config.js +170 -170
  12. package/src/cli/db-import.js +51 -51
  13. package/src/cli/explore.js +555 -555
  14. package/src/cli/info.js +10 -16
  15. package/src/cli/open.js +111 -111
  16. package/src/cli/progress.js +111 -111
  17. package/src/cli/refresh.js +288 -288
  18. package/src/cli/scrape.js +47 -47
  19. package/src/cli/utils.js +18 -18
  20. package/src/cli/videos.js +41 -41
  21. package/src/cli/videostats.js +196 -196
  22. package/src/cli/watch.js +30 -30
  23. package/src/cli/webserver.js +19 -0
  24. package/src/lib/api-interceptor.js +161 -161
  25. package/src/lib/args.js +809 -778
  26. package/src/lib/browser/anti-detect.js +23 -23
  27. package/src/lib/browser/cdp.js +261 -261
  28. package/src/lib/browser/health-checker.js +114 -114
  29. package/src/lib/browser/launch.js +43 -43
  30. package/src/lib/browser/page.js +184 -184
  31. package/src/lib/constants.js +297 -287
  32. package/src/lib/delay.js +54 -54
  33. package/src/lib/explore-fetch.js +118 -118
  34. package/src/lib/fetcher.js +45 -45
  35. package/src/lib/filter.js +66 -66
  36. package/src/lib/io.js +54 -54
  37. package/src/lib/output.js +80 -80
  38. package/src/lib/page-error-detector.js +109 -109
  39. package/src/lib/parse-ssr.mjs +69 -69
  40. package/src/lib/parser.js +47 -47
  41. package/src/lib/retry.js +45 -45
  42. package/src/lib/scrape.js +90 -89
  43. package/src/lib/target-locations.js +61 -61
  44. package/src/lib/tiktok-scraper.mjs +173 -105
  45. package/src/lib/url.js +52 -52
  46. package/src/main.js +73 -70
  47. package/src/npm-main.js +70 -69
  48. package/src/scraper/auto-core.js +203 -203
  49. package/src/scraper/core.js +255 -255
  50. package/src/scraper/explore-core.js +208 -208
  51. package/src/scraper/modules/captcha-handler.js +114 -114
  52. package/src/scraper/modules/follow-extractor.js +250 -250
  53. package/src/scraper/modules/guess-extractor.js +51 -51
  54. package/src/scraper/modules/page-helpers.js +48 -48
  55. package/src/scraper/refresh-core.js +213 -213
  56. package/src/videos/core.js +143 -143
  57. package/src/watch/data-store.js +2980 -2978
  58. package/src/watch/public/index.html +2355 -2345
  59. package/src/watch/server.js +727 -727
  60. package/src/webserver/server.mjs +174 -0
  61. package/scripts/test-captcha-lib.mjs +0 -68
  62. package/scripts/test-captcha.mjs +0 -81
  63. package/scripts/test-incognito-lib.mjs +0 -36
  64. package/scripts/test-login-state.mjs +0 -128
  65. package/scripts/test-safe-click.mjs +0 -45
  66. package/scripts/test-watch-db-smoke.mjs +0 -246
  67. package/src/results/user-videos-bar.lar.lar.moeta.json +0 -37
@@ -1,3 +1,5 @@
1
+ import os from "os";
2
+ import path from "path";
1
3
  import { chromium } from "playwright";
2
4
  import { detectBrowser } from "./browser/launch.js";
3
5
  import { parseUserInfo, parseVideoInfo } from "./parse-ssr.mjs";
@@ -7,11 +9,28 @@ const DEFAULT_WAF_TTL = 120000;
7
9
  const DEFAULT_WARM_URL = "https://www.tiktok.com/@nike";
8
10
  const BROWSER_CLOSE_TIMEOUT = 5000;
9
11
  const DEFAULT_MAX_REQUESTS_PER_PAGE = 50;
12
+ const FALLBACK_PROFILE_PORT = 9999;
10
13
 
11
14
  function delay(ms) {
12
15
  return new Promise((r) => setTimeout(r, ms));
13
16
  }
14
17
 
18
+ function getFallbackProfileDir() {
19
+ const profile = `p${FALLBACK_PROFILE_PORT}`;
20
+ return path.join(
21
+ os.homedir(),
22
+ "Library",
23
+ "Application Support",
24
+ `Microsoft Edge For Testing_${profile}`,
25
+ );
26
+ }
27
+
28
+ function fallbackProfileExists() {
29
+ const fs = require("fs");
30
+ const dir = getFallbackProfileDir();
31
+ return fs.existsSync(dir);
32
+ }
33
+
15
34
  class PageSlot {
16
35
  constructor(page) {
17
36
  this.page = page;
@@ -47,6 +66,46 @@ class PromiseQueue {
47
66
  }
48
67
  }
49
68
 
69
+ function createLaunchOptions(executablePath) {
70
+ return {
71
+ headless: true,
72
+ executablePath,
73
+ handleSIGINT: false,
74
+ handleSIGTERM: false,
75
+ handleSIGHUP: false,
76
+ args: [
77
+ "--no-sandbox",
78
+ "--disable-setuid-sandbox",
79
+ "--disable-dev-shm-usage",
80
+ ],
81
+ };
82
+ }
83
+
84
+ async function initContext(executablePath, poolSize, userDataDir) {
85
+ let context;
86
+ let browser = null;
87
+ const slots = [];
88
+
89
+ if (userDataDir) {
90
+ context = await chromium.launchPersistentContext(userDataDir, createLaunchOptions(executablePath));
91
+ const existing = context.pages();
92
+ if (existing.length > 0) {
93
+ slots.push(new PageSlot(existing[0]));
94
+ }
95
+ for (let i = slots.length; i < poolSize; i++) {
96
+ slots.push(new PageSlot(await context.newPage()));
97
+ }
98
+ } else {
99
+ browser = await chromium.launch(createLaunchOptions(executablePath));
100
+ context = await browser.newContext();
101
+ for (let i = 0; i < poolSize; i++) {
102
+ slots.push(new PageSlot(await context.newPage()));
103
+ }
104
+ }
105
+
106
+ return { browser, context, slots };
107
+ }
108
+
50
109
  export class TikTokScraper {
51
110
  constructor({
52
111
  poolSize = DEFAULT_POOL_SIZE,
@@ -64,6 +123,10 @@ export class TikTokScraper {
64
123
  this.slotIdx = 0;
65
124
  this.lastWarmTime = 0;
66
125
  this.warmPromise = null;
126
+ this._fallbackBrowser = null;
127
+ this._fallbackContext = null;
128
+ this._fallbackSlots = [];
129
+ this._fallbackSlotIdx = 0;
67
130
  }
68
131
 
69
132
  async init() {
@@ -73,49 +136,60 @@ export class TikTokScraper {
73
136
  "未找到本地浏览器(Chrome/Edge),请先安装浏览器或执行 npx playwright install",
74
137
  );
75
138
  }
76
- this.browser = await chromium.launch({
77
- headless: true,
78
- executablePath,
79
- handleSIGINT: false,
80
- handleSIGTERM: false,
81
- handleSIGHUP: false,
82
- args: [
83
- "--no-sandbox",
84
- "--disable-setuid-sandbox",
85
- "--disable-dev-shm-usage",
86
- ],
87
- });
88
- this.context = await this.browser.newContext();
89
- for (let i = 0; i < this.poolSize; i++) {
90
- this.slots.push(new PageSlot(await this.context.newPage()));
91
- }
139
+ const { browser, context, slots } = await initContext(executablePath, this.poolSize, null);
140
+ this.browser = browser;
141
+ this.context = context;
142
+ this.slots = slots;
92
143
  await this.warmWaf();
93
144
  }
94
145
 
146
+ async _ensureFallback(executablePath) {
147
+ if (this._fallbackContext) return;
148
+ if (!fallbackProfileExists()) {
149
+ return false;
150
+ }
151
+ const fallbackDir = getFallbackProfileDir();
152
+ console.error(`[TikTokScraper] 无登录态获取失败,fallback 到登录态 profile: ${fallbackDir}`);
153
+ const { browser, context, slots } = await initContext(executablePath, 1, fallbackDir);
154
+ this._fallbackBrowser = browser;
155
+ this._fallbackContext = context;
156
+ this._fallbackSlots = slots;
157
+ return true;
158
+ }
159
+
95
160
  async close() {
96
- if (this.browser) {
97
- const browser = this.browser;
98
- let closeTimedOut = false;
99
- const closePromise = browser.close().catch((error) => {
100
- console.error(
101
- `[TikTokScraper] browser.close() failed: ${error.message}`,
102
- );
103
- });
104
- await Promise.race([
105
- closePromise,
106
- delay(BROWSER_CLOSE_TIMEOUT).then(() => {
107
- closeTimedOut = true;
108
- }),
109
- ]);
110
- if (closeTimedOut) {
111
- console.error(
112
- `[TikTokScraper] browser.close() 超时 ${BROWSER_CLOSE_TIMEOUT}ms,跳过等待并继续退出`,
113
- );
161
+ const closeAll = async (browser) => {
162
+ if (browser) {
163
+ let closeTimedOut = false;
164
+ const closePromise = browser.close().catch((error) => {
165
+ console.error(
166
+ `[TikTokScraper] browser.close() failed: ${error.message}`,
167
+ );
168
+ });
169
+ await Promise.race([
170
+ closePromise,
171
+ delay(BROWSER_CLOSE_TIMEOUT).then(() => { closeTimedOut = true; }),
172
+ ]);
173
+ if (closeTimedOut) {
174
+ console.error(
175
+ `[TikTokScraper] browser.close() 超时 ${BROWSER_CLOSE_TIMEOUT}ms,跳过等待并继续退出`,
176
+ );
177
+ }
114
178
  }
115
- this.browser = null;
116
- this.context = null;
117
- this.slots = [];
179
+ };
180
+
181
+ // 无登录态的 browser 是 chromium.launch() 返回的,直接 close
182
+ await closeAll(this.browser);
183
+ // fallback 的 context 是 launchPersistentContext 返回的,直接 close
184
+ if (this._fallbackContext) {
185
+ try { await this._fallbackContext.close(); } catch {}
118
186
  }
187
+ this.browser = null;
188
+ this.context = null;
189
+ this.slots = [];
190
+ this._fallbackBrowser = null;
191
+ this._fallbackContext = null;
192
+ this._fallbackSlots = [];
119
193
  }
120
194
 
121
195
  async restart() {
@@ -125,7 +199,7 @@ export class TikTokScraper {
125
199
 
126
200
  isAlive() {
127
201
  try {
128
- return !!(this.browser && this.browser.isConnected());
202
+ return !!(this.context && (!this.browser || this.browser.isConnected()));
129
203
  } catch {
130
204
  return false;
131
205
  }
@@ -152,98 +226,92 @@ export class TikTokScraper {
152
226
  return this.warmPromise;
153
227
  }
154
228
 
155
- _needWarm() {
229
+ isWarmExpired() {
156
230
  return Date.now() - this.lastWarmTime > this.wafTtl;
157
231
  }
158
232
 
159
- _pickSlot() {
160
- const slot = this.slots[this.slotIdx % this.poolSize];
161
- this.slotIdx++;
233
+ _pickSlot(slots, idx, setIdx) {
234
+ const slot = slots[idx % slots.length];
162
235
  return slot;
163
236
  }
164
237
 
165
- async _ensurePage(slot) {
238
+ async _ensurePage(slot, context) {
166
239
  try {
167
- if (
168
- !slot.page.isClosed() &&
169
- slot.requestCount < this.maxRequestsPerPage
170
- ) {
240
+ if (!slot.page.isClosed() && slot.requestCount < this.maxRequestsPerPage) {
171
241
  return slot.page;
172
242
  }
173
243
  } catch {}
174
244
  await slot.page?.close().catch(() => {});
175
- slot.page = await this.context.newPage();
245
+ slot.page = await context.newPage();
176
246
  slot.requestCount = 0;
177
247
  return slot.page;
178
248
  }
179
249
 
180
- async _fetchViewSource(url, slot) {
181
- const page = await this._ensurePage(slot);
182
-
183
- await page.goto("view-source:" + url, {
184
- waitUntil: "domcontentloaded",
185
- timeout: 15000,
186
- });
250
+ async _fetchViewSource(url, ctx) {
251
+ const slots = ctx === this._fallbackContext ? this._fallbackSlots : this.slots;
252
+ const slot = slots[this.slotIdx % slots.length];
253
+ this.slotIdx++;
187
254
 
188
- const content = await page.evaluate(() => {
189
- const rows = document.querySelectorAll("tr");
190
- let content = "";
191
- rows.forEach((r) => {
192
- const lc = r.querySelector(".line-content");
193
- if (lc) content += lc.textContent + "\n";
255
+ try {
256
+ return await slot.lock.run(async () => {
257
+ const page = await this._ensurePage(slot, ctx);
258
+ slot.requestCount++;
259
+ if (ctx === this.context && this.isWarmExpired()) {
260
+ await this.warmWaf();
261
+ }
262
+ await page.goto(url, {
263
+ waitUntil: "domcontentloaded",
264
+ timeout: 15000,
265
+ });
266
+ await delay(1500);
267
+ return await page.content();
194
268
  });
195
- return content;
196
- });
269
+ } catch (e) {
270
+ console.error(`[TikTokScraper] _fetchViewSource failed: ${e.message}`);
271
+ return null;
272
+ }
273
+ }
197
274
 
198
- // 导航到 about:blank 释放当前页面的 DOM 和 JS 堆
199
- await page
200
- .goto("about:blank", {
201
- waitUntil: "domcontentloaded",
202
- timeout: 5000,
203
- })
204
- .catch(() => {});
275
+ async getUserInfo(username) {
276
+ const normalizedUsername = username.startsWith("@")
277
+ ? username.slice(1)
278
+ : username;
279
+ const url = `https://www.tiktok.com/@${normalizedUsername}`;
280
+ const executablePath = detectBrowser();
205
281
 
206
- slot.requestCount += 1;
282
+ // 第一趟:无登录态
283
+ let rawHtml = await this._fetchViewSource(url, this.context);
284
+ let result = rawHtml ? parseUserInfo(rawHtml) : null;
207
285
 
208
- return content;
209
- }
210
-
211
- async getUserInfo(uniqueId) {
212
- const slot = this._pickSlot();
213
- return slot.lock.run(async () => {
214
- let rawHtml = await this._fetchViewSource(
215
- `https://www.tiktok.com/@${uniqueId}`,
216
- slot,
217
- );
218
- let result = parseUserInfo(rawHtml);
219
- if (!result) {
220
- try {
221
- await this.warmWaf();
222
- } catch {}
223
- rawHtml = await this._fetchViewSource(
224
- `https://www.tiktok.com/@${uniqueId}`,
225
- slot,
226
- );
227
- result = parseUserInfo(rawHtml);
286
+ // 解析失败:fallback 到登录态
287
+ if (!result) {
288
+ const ok = await this._ensureFallback(executablePath);
289
+ if (ok) {
290
+ rawHtml = await this._fetchViewSource(url, this._fallbackContext);
291
+ result = rawHtml ? parseUserInfo(rawHtml) : null;
228
292
  }
229
- return result || null;
230
- });
293
+ }
294
+
295
+ return result || null;
231
296
  }
232
297
 
233
298
  async getVideoInfo(videoUrl) {
234
- const slot = this._pickSlot();
235
- return slot.lock.run(async () => {
236
- let rawHtml = await this._fetchViewSource(videoUrl, slot);
237
- let result = parseVideoInfo(rawHtml);
238
- if (!result) {
239
- try {
240
- await this.warmWaf();
241
- } catch {}
242
- rawHtml = await this._fetchViewSource(videoUrl, slot);
243
- result = parseVideoInfo(rawHtml);
299
+ const executablePath = detectBrowser();
300
+
301
+ // 第一趟:无登录态
302
+ let rawHtml = await this._fetchViewSource(videoUrl, this.context);
303
+ let result = rawHtml ? parseVideoInfo(rawHtml) : null;
304
+
305
+ // 解析失败:fallback 到登录态
306
+ if (!result) {
307
+ const ok = await this._ensureFallback(executablePath);
308
+ if (ok) {
309
+ rawHtml = await this._fetchViewSource(videoUrl, this._fallbackContext);
310
+ result = rawHtml ? parseVideoInfo(rawHtml) : null;
244
311
  }
245
- return result || null;
246
- });
312
+ }
313
+
314
+ return result || null;
247
315
  }
248
316
 
249
317
  async getUserAndVideo(videoUrl) {
package/src/lib/url.js CHANGED
@@ -1,52 +1,52 @@
1
- const BASE_URL = 'https://www.tiktok.com';
2
-
3
- export function extractUniqueId(url) {
4
- const m = url.match(/\/@([\w.-]+)/);
5
- return m ? m[1] : null;
6
- }
7
-
8
- export function extractVideoId(url) {
9
- const m = url.match(/\/video\/(\d+)/);
10
- return m ? m[1] : null;
11
- }
12
-
13
- export function normalizeUsername(input) {
14
- return (input || '').replace(/^@/, '');
15
- }
16
-
17
- export function toProfileUrl(handle) {
18
- const clean = normalizeUsername(handle);
19
- return `${BASE_URL}/@${clean}`;
20
- }
21
-
22
- export function toVideoUrl(handle, videoId) {
23
- const clean = normalizeUsername(handle);
24
- return `${BASE_URL}/@${clean}/video/${videoId}`;
25
- }
26
-
27
- export function ensureAbsoluteUrl(href) {
28
- if (href.startsWith('http')) return href;
29
- return `${BASE_URL}${href}`;
30
- }
31
-
32
- export function isProfileUrl(url) {
33
- return /\/@[\w.-]+(?:$|[?#])/.test(url);
34
- }
35
-
36
- export function isVideoUrl(url) {
37
- return /\/video\/\d+/.test(url);
38
- }
39
-
40
- export function extractDisplayPath(url) {
41
- try {
42
- const parts = new URL(url).pathname.split('/').filter(Boolean);
43
- return parts.slice(-2).join('/');
44
- } catch {
45
- return url;
46
- }
47
- }
48
-
49
- export function extractAuthorFromVideoUrl(url) {
50
- const m = url.match(/@([^/]+)\/video/);
51
- return m ? '@' + m[1] : null;
52
- }
1
+ const BASE_URL = 'https://www.tiktok.com';
2
+
3
+ export function extractUniqueId(url) {
4
+ const m = url.match(/\/@([\w.-]+)/);
5
+ return m ? m[1] : null;
6
+ }
7
+
8
+ export function extractVideoId(url) {
9
+ const m = url.match(/\/video\/(\d+)/);
10
+ return m ? m[1] : null;
11
+ }
12
+
13
+ export function normalizeUsername(input) {
14
+ return (input || '').replace(/^@/, '');
15
+ }
16
+
17
+ export function toProfileUrl(handle) {
18
+ const clean = normalizeUsername(handle);
19
+ return `${BASE_URL}/@${clean}`;
20
+ }
21
+
22
+ export function toVideoUrl(handle, videoId) {
23
+ const clean = normalizeUsername(handle);
24
+ return `${BASE_URL}/@${clean}/video/${videoId}`;
25
+ }
26
+
27
+ export function ensureAbsoluteUrl(href) {
28
+ if (href.startsWith('http')) return href;
29
+ return `${BASE_URL}${href}`;
30
+ }
31
+
32
+ export function isProfileUrl(url) {
33
+ return /\/@[\w.-]+(?:$|[?#])/.test(url);
34
+ }
35
+
36
+ export function isVideoUrl(url) {
37
+ return /\/video\/\d+/.test(url);
38
+ }
39
+
40
+ export function extractDisplayPath(url) {
41
+ try {
42
+ const parts = new URL(url).pathname.split('/').filter(Boolean);
43
+ return parts.slice(-2).join('/');
44
+ } catch {
45
+ return url;
46
+ }
47
+ }
48
+
49
+ export function extractAuthorFromVideoUrl(url) {
50
+ const m = url.match(/@([^/]+)\/video/);
51
+ return m ? '@' + m[1] : null;
52
+ }
package/src/main.js CHANGED
@@ -1,70 +1,73 @@
1
- import { parseArgs } from "./lib/args.js";
2
- import { proxy, HELP_TEXT, getConfigText } from "./lib/constants.js";
3
- import { handleInfo } from "./cli/info.js";
4
- import { handleExplore } from "./cli/explore.js";
5
- import { handleAttach } from "./cli/attach.js";
6
- import { handleWatch } from "./cli/watch.js";
7
- import { handleConfig, showConfig, showUsage, version } from "./cli/config.js";
8
- import { handleOpen } from "./cli/open.js";
9
- import { handleComments } from "./cli/comments.js";
10
- import { handleVideoStats } from "./cli/videostats.js";
11
- import { handleDbImport } from "./cli/db-import.js";
12
-
13
- async function main() {
14
- const parsed = parseArgs();
15
-
16
- switch (parsed.subcommand) {
17
- case "explore":
18
- return handleExplore(parsed);
19
- case "info":
20
- return handleInfo(parsed);
21
- case "attach":
22
- return handleAttach(parsed);
23
- case "watch":
24
- return handleWatch(parsed);
25
- case "open":
26
- return handleOpen(parsed);
27
- case "comments":
28
- return handleComments(parsed);
29
- case "videostats":
30
- return handleVideoStats(parsed);
31
- case "db-import":
32
- return handleDbImport(parsed);
33
- }
34
-
35
- const {
36
- urls,
37
- outputFile,
38
- outputFormat,
39
- exploreCount,
40
- showConfig: showCfg,
41
- showHelp,
42
- showVersion,
43
- customProxy,
44
- configAction,
45
- configKey,
46
- configValue,
47
- } = parsed;
48
-
49
- if (showVersion) {
50
- console.log(version);
51
- process.exit(0);
52
- }
53
- if (showHelp) return showUsage();
54
- if (configAction) return handleConfig(configAction, configKey, configValue);
55
- if (showCfg) return showConfig(urls, outputFile);
56
- if (urls.length === 0 && exploreCount === 0) return showUsage();
57
-
58
- // 默认行为:URL info,--explore 走 explore
59
- if (exploreCount > 0) {
60
- return handleExplore({ ...parsed, subcommand: "explore" });
61
- }
62
-
63
- // URL 默认走 info
64
- return handleInfo(parsed);
65
- }
66
-
67
- main().catch((err) => {
68
- console.error(`错误: ${err.message}`);
69
- process.exit(1);
70
- });
1
+ import { parseArgs } from "./lib/args.js";
2
+ import { proxy, HELP_TEXT, getConfigText } from "./lib/constants.js";
3
+ import { handleInfo } from "./cli/info.js";
4
+ import { handleExplore } from "./cli/explore.js";
5
+ import { handleAttach } from "./cli/attach.js";
6
+ import { handleWatch } from "./cli/watch.js";
7
+ import { handleConfig, showConfig, showUsage, version } from "./cli/config.js";
8
+ import { handleOpen } from "./cli/open.js";
9
+ import { handleComments } from "./cli/comments.js";
10
+ import { handleVideoStats } from "./cli/videostats.js";
11
+ import { handleDbImport } from "./cli/db-import.js";
12
+ import { handleWebserver } from "./cli/webserver.js";
13
+
14
+ async function main() {
15
+ const parsed = parseArgs();
16
+
17
+ switch (parsed.subcommand) {
18
+ case "explore":
19
+ return handleExplore(parsed);
20
+ case "info":
21
+ return handleInfo(parsed);
22
+ case "attach":
23
+ return handleAttach(parsed);
24
+ case "watch":
25
+ return handleWatch(parsed);
26
+ case "webserver":
27
+ return handleWebserver(parsed);
28
+ case "open":
29
+ return handleOpen(parsed);
30
+ case "comments":
31
+ return handleComments(parsed);
32
+ case "videostats":
33
+ return handleVideoStats(parsed);
34
+ case "db-import":
35
+ return handleDbImport(parsed);
36
+ }
37
+
38
+ const {
39
+ urls,
40
+ outputFile,
41
+ outputFormat,
42
+ exploreCount,
43
+ showConfig: showCfg,
44
+ showHelp,
45
+ showVersion,
46
+ customProxy,
47
+ configAction,
48
+ configKey,
49
+ configValue,
50
+ } = parsed;
51
+
52
+ if (showVersion) {
53
+ console.log(version);
54
+ process.exit(0);
55
+ }
56
+ if (showHelp) return showUsage();
57
+ if (configAction) return handleConfig(configAction, configKey, configValue);
58
+ if (showCfg) return showConfig(urls, outputFile);
59
+ if (urls.length === 0 && exploreCount === 0) return showUsage();
60
+
61
+ // 默认行为:URL 走 info,--explore 走 explore
62
+ if (exploreCount > 0) {
63
+ return handleExplore({ ...parsed, subcommand: "explore" });
64
+ }
65
+
66
+ // 有 URL 默认走 info
67
+ return handleInfo(parsed);
68
+ }
69
+
70
+ main().catch((err) => {
71
+ console.error(`错误: ${err.message}`);
72
+ process.exit(1);
73
+ });