tt-help-cli-ycl 1.3.63 → 1.3.65

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.3.63",
3
+ "version": "1.3.65",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -131,4 +131,4 @@ ECHO Max following: 5
131
131
  ECHO Max followers: 5
132
132
  ECHO Speed: stealth (slowest)
133
133
  ECHO ========================================
134
- CALL tt-help explore stealth --user-id %%USER_ID%% --base-port %%BASE_PORT%% --port-count %%PORT_COUNT%% --max-following 5 --max-followers 5 %%JOB_LOC_ARGS%%
134
+ CALL tt-help explore stealth --user-id %%USER_ID%% --base-port %%BASE_PORT%% --port-count %%PORT_COUNT%% --max-following 100 --max-followers 100 %%JOB_LOC_ARGS%%
package/src/cli/attach.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { TikTokScraper } from "../lib/tiktok-scraper.mjs";
2
+ import { proxy as configuredProxy } from "../lib/constants.js";
2
3
  import v8 from "node:v8";
3
4
 
4
5
  const MAX_RETRY_WAIT = 5 * 60 * 1000;
@@ -81,8 +82,14 @@ export async function handleAttach(options) {
81
82
  attachInterval,
82
83
  serverUrl,
83
84
  attachCountries,
85
+ customProxy,
84
86
  showHelp,
85
87
  } = options;
88
+ const effectiveProxy = customProxy || configuredProxy;
89
+
90
+ if (effectiveProxy) {
91
+ attachLog(`[Attach] 使用代理: ${effectiveProxy}`);
92
+ }
86
93
  let shuttingDown = false;
87
94
  let forceExitTimer = null;
88
95
 
@@ -100,6 +107,9 @@ export async function handleAttach(options) {
100
107
  attachLog(
101
108
  " -c, --countries <A,B,C> 猜测国家列表(逗号分隔,如 PL,DE,FR),服务端优先返回这些国家的任务",
102
109
  );
110
+ attachLog(
111
+ " --proxy <代理地址> HTTP 代理地址(如 http://127.0.0.1:7890),不配置则从 ~/.tt-help.json 读取",
112
+ );
103
113
  attachLog("");
104
114
  attachLog("说明:");
105
115
  attachLog(
@@ -124,7 +134,7 @@ export async function handleAttach(options) {
124
134
  `[Attach] 并行数: ${attachParallel}, 空闲间隔: ${attachInterval}秒, 服务端: ${serverUrl}${countryStr}`,
125
135
  );
126
136
 
127
- const scraper = new TikTokScraper();
137
+ const scraper = new TikTokScraper(effectiveProxy || null);
128
138
  const shutdown = async (signal) => {
129
139
  if (shuttingDown) return;
130
140
  shuttingDown = true;
package/src/cli/config.js CHANGED
@@ -33,7 +33,29 @@ function showUsage(helpText = HELP_TEXT) {
33
33
  process.exit(0);
34
34
  }
35
35
 
36
+ const CONFIG_KEYS = [
37
+ "proxy",
38
+ "server",
39
+ "browser",
40
+ "userId",
41
+ "maxFollowing",
42
+ "maxFollowers",
43
+ "maxVideos",
44
+ "maxComments",
45
+ ];
46
+
36
47
  function handleConfig(action, key, value) {
48
+ // 简写语法兼容:tt-help config proxy "值" → tt-help config set proxy "值"
49
+ if (CONFIG_KEYS.includes(action) && key !== undefined && key !== null) {
50
+ // action 是 key 名(如 "proxy"),key 是值(如 "http://...")
51
+ // 转换为 set 模式
52
+ const realKey = action;
53
+ const realValue = key;
54
+ action = "set";
55
+ key = realKey;
56
+ value = realValue;
57
+ }
58
+
37
59
  switch (action) {
38
60
  case "show": {
39
61
  const configLines = getConfigText();
@@ -52,13 +74,13 @@ function handleConfig(action, key, value) {
52
74
 
53
75
  switch (key) {
54
76
  case "proxy":
55
- if (!value) {
77
+ if (value === undefined || value === null) {
56
78
  console.error("请提供 proxy 的值");
57
79
  console.error("用法: tt-help config set proxy <代理地址>");
58
80
  return;
59
81
  }
60
82
  saveProxy(value);
61
- console.error(`代理已更新: ${value}`);
83
+ console.error(`代理已更新: ${value || "(空,不使用代理)"}`);
62
84
  break;
63
85
 
64
86
  case "server":
@@ -135,6 +157,27 @@ function handleConfig(action, key, value) {
135
157
  break;
136
158
  }
137
159
 
160
+ case "unset": {
161
+ if (!key) {
162
+ console.error("用法: tt-help config unset <key>");
163
+ console.error(
164
+ " 可用 key: proxy, server, browser, userId, maxFollowing, maxFollowers, maxVideos, maxComments",
165
+ );
166
+ break;
167
+ }
168
+ const cfg = existsSync(configPath)
169
+ ? JSON.parse(readFileSync(configPath, "utf-8"))
170
+ : {};
171
+ if (cfg[key] !== undefined) {
172
+ delete cfg[key];
173
+ writeFileSync(configPath, JSON.stringify(cfg, null, 2), "utf-8");
174
+ console.error(`已删除配置: ${key}`);
175
+ } else {
176
+ console.error(`配置项 ${key} 不存在`);
177
+ }
178
+ break;
179
+ }
180
+
138
181
  case "reset": {
139
182
  if (existsSync(configPath)) {
140
183
  writeFileSync(configPath, "{}", "utf-8");
@@ -147,7 +190,7 @@ function handleConfig(action, key, value) {
147
190
 
148
191
  default:
149
192
  console.error(`未知配置命令: ${action}`);
150
- console.error("用法: tt-help config [show|set|reset]");
193
+ console.error("用法: tt-help config [show|set|unset|reset]");
151
194
  }
152
195
  }
153
196
 
@@ -12,7 +12,11 @@ import {
12
12
  detectCaptcha,
13
13
  closeCaptcha,
14
14
  } from "../scraper/modules/captcha-handler.js";
15
- import { userId as configuredUserId, saveUserId } from "../lib/constants.js";
15
+ import {
16
+ userId as configuredUserId,
17
+ saveUserId,
18
+ proxy as configuredProxy,
19
+ } from "../lib/constants.js";
16
20
  import { getMacOrUuid } from "../lib/mac-or-uuid.js";
17
21
  import {
18
22
  ensureBrowserReady as ensureBrowserReadyCDP,
@@ -151,8 +155,14 @@ export async function handleExplore(options) {
151
155
  cdpOptions.userDataDir = currentAccount.userDataDir;
152
156
  }
153
157
 
154
- if (exploreProxy) {
155
- cdpOptions.proxyServer = exploreProxy;
158
+ const effectiveProxy = exploreProxy || configuredProxy;
159
+ if (effectiveProxy) {
160
+ cdpOptions.proxyServer = effectiveProxy;
161
+ console.error(
162
+ `代理: ${effectiveProxy}${exploreProxy ? " (命令行指定)" : " (配置文件)"}`,
163
+ );
164
+ } else {
165
+ console.error(`代理: 未配置`);
156
166
  }
157
167
 
158
168
  console.error(`CDP 端口: ${cdpOptions.port}, 用户编号: ${userId}`);
@@ -166,7 +176,7 @@ export async function handleExplore(options) {
166
176
 
167
177
  browser = await ensureBrowserReadyCDP(cdpOptions);
168
178
  const { processExplore } = await import("../scraper/explore-core.js");
169
- const { isLoggedIn } = await import("../lib/browser/page.js");
179
+ const { safeCheckLogin } = await import("../lib/browser/page.js");
170
180
 
171
181
  const page = await getOrCreatePage(browser);
172
182
 
@@ -176,8 +186,7 @@ export async function handleExplore(options) {
176
186
  });
177
187
 
178
188
  // 检测登录状态(启动时只检测一次)
179
- let loggedIn = await isLoggedIn(page);
180
- console.error(`登录状态: ${loggedIn ? "已登录" : "未登录"}`);
189
+ let loggedIn = await safeCheckLogin(page);
181
190
 
182
191
  // 全局拦截图片资源,减少内存占用和加载时间
183
192
  await page.route("**/*", (route) => {
@@ -208,9 +217,11 @@ export async function handleExplore(options) {
208
217
  const oldAccount = currentAccount;
209
218
  const nextAccount = healthChecker.getNextAccount();
210
219
  currentAccount = nextAccount;
220
+ const effectiveProxy = cdpOptions.proxyServer;
211
221
  const newBrowser = await switchAccount(
212
222
  { port: oldAccount.port, userDataDir: oldAccount.userDataDir },
213
223
  { port: nextAccount.port, userDataDir: nextAccount.userDataDir },
224
+ effectiveProxy,
214
225
  );
215
226
  browser = newBrowser;
216
227
  const newPage = await setupNewPage(browser);
@@ -218,13 +229,16 @@ export async function handleExplore(options) {
218
229
  Object.assign(cdpOptions, {
219
230
  port: nextAccount.port,
220
231
  userDataDir: nextAccount.userDataDir,
232
+ ...(effectiveProxy ? { proxyServer: effectiveProxy } : {}),
221
233
  });
222
- console.error(`[健康检查] 已切换到端口 ${nextAccount.port}`);
234
+ console.error(
235
+ `[健康检查] 已切换到端口 ${nextAccount.port}${effectiveProxy ? ", 代理: " + effectiveProxy : ""}`,
236
+ );
223
237
  // 切换账户后先导航到 TikTok 页面,再重新检测登录状态
224
238
  await page.goto(STARTUP_TIKTOK_URL, {
225
239
  waitUntil: "domcontentloaded",
226
240
  });
227
- loggedIn = await isLoggedIn(page);
241
+ loggedIn = await safeCheckLogin(page);
228
242
  console.error(
229
243
  `[健康检查] 新账户登录状态: ${loggedIn ? "已登录" : "未登录"}`,
230
244
  );
@@ -358,6 +372,7 @@ export async function handleExplore(options) {
358
372
  maxFollowing: exploreMaxFollowing,
359
373
  maxFollowers: exploreMaxFollowers,
360
374
  location: exploreLocation,
375
+ proxyServer: cdpOptions.proxyServer || null,
361
376
  browser,
362
377
  },
363
378
  console.error,
@@ -383,6 +398,7 @@ export async function handleExplore(options) {
383
398
  maxFollowing: exploreMaxFollowing,
384
399
  maxFollowers: exploreMaxFollowers,
385
400
  location: exploreLocation,
401
+ proxyServer: cdpOptions.proxyServer || null,
386
402
  browser,
387
403
  },
388
404
  console.error,
@@ -420,6 +436,7 @@ export async function handleExplore(options) {
420
436
  maxFollowing: exploreMaxFollowing,
421
437
  maxFollowers: exploreMaxFollowers,
422
438
  location: exploreLocation,
439
+ proxyServer: cdpOptions.proxyServer || null,
423
440
  browser,
424
441
  },
425
442
  console.error,
@@ -12,7 +12,11 @@ import {
12
12
  detectCaptcha,
13
13
  closeCaptcha,
14
14
  } from "../scraper/modules/captcha-handler.js";
15
- import { userId as configuredUserId, saveUserId } from "../lib/constants.js";
15
+ import {
16
+ userId as configuredUserId,
17
+ proxy as configuredProxy,
18
+ saveUserId,
19
+ } from "../lib/constants.js";
16
20
  import { getMacOrUuid } from "../lib/mac-or-uuid.js";
17
21
  import {
18
22
  ensureBrowserReady as ensureBrowserReadyCDP,
@@ -77,6 +81,7 @@ export async function handleRefresh(options) {
77
81
  exploreRedoMaxAge,
78
82
  exploreProxy,
79
83
  } = options;
84
+ const effectiveProxy = exploreProxy || configuredProxy;
80
85
 
81
86
  let userId = exploreUserId || configuredUserId;
82
87
  let browser = null;
@@ -116,9 +121,16 @@ export async function handleRefresh(options) {
116
121
  console.error(`\n=== Refresh 模式(基于 explore) ===`);
117
122
  console.error(`服务器: ${serverUrl}`);
118
123
  console.error(`视频采集: ${exploreMaxVideos || 16}`);
119
- console.error(`关注/粉丝: ${exploreEnableFollow ? "启用" : "禁用"} (${exploreMaxFollowing}/${exploreMaxFollowers})`);
124
+ console.error(
125
+ `关注/粉丝: ${exploreEnableFollow ? "启用" : "禁用"} (${exploreMaxFollowing}/${exploreMaxFollowers})`,
126
+ );
120
127
  console.error(`国家筛选: ${exploreLocation}`);
121
128
  console.error(`空闲间隔: ${exploreInterval || 30} 秒`);
129
+ if (effectiveProxy) {
130
+ console.error(
131
+ `代理: ${effectiveProxy}${exploreProxy ? " (命令行指定)" : " (配置文件)"}`,
132
+ );
133
+ }
122
134
  if (exploreMaxUsers > 0) console.error(`上限: ${exploreMaxUsers} 个用户`);
123
135
 
124
136
  const healthChecker = new HealthChecker({
@@ -149,8 +161,8 @@ export async function handleRefresh(options) {
149
161
  cdpOptions.userDataDir = currentAccount.userDataDir;
150
162
  }
151
163
 
152
- if (exploreProxy) {
153
- cdpOptions.proxyServer = exploreProxy;
164
+ if (effectiveProxy) {
165
+ cdpOptions.proxyServer = effectiveProxy;
154
166
  }
155
167
 
156
168
  console.error(`CDP 端口: ${cdpOptions.port}, 用户编号: ${userId}`);
@@ -217,6 +229,9 @@ export async function handleRefresh(options) {
217
229
  port: nextAccount.port,
218
230
  userDataDir: nextAccount.userDataDir,
219
231
  });
232
+ if (effectiveProxy) {
233
+ cdpOptions.proxyServer = effectiveProxy;
234
+ }
220
235
  console.error(`[健康检查] 已切换到端口 ${nextAccount.port}`);
221
236
  await page.goto(STARTUP_TIKTOK_URL, {
222
237
  waitUntil: "domcontentloaded",
@@ -249,9 +264,10 @@ export async function handleRefresh(options) {
249
264
  if (checkResult.shouldSwitch) {
250
265
  await handleAccountSwitch(checkResult.reason);
251
266
  } else if (checkResult.info && processedCount % 10 === 0) {
252
- const followElapsed = loggedIn && exploreEnableFollow
253
- ? Math.round((Date.now() - lastFollowSuccessTime) / 60000)
254
- : 0;
267
+ const followElapsed =
268
+ loggedIn && exploreEnableFollow
269
+ ? Math.round((Date.now() - lastFollowSuccessTime) / 60000)
270
+ : 0;
255
271
  const followStatus =
256
272
  loggedIn && exploreEnableFollow
257
273
  ? ` | 关注/粉丝上次成功 ${followElapsed} 分钟前`
@@ -270,14 +286,16 @@ export async function handleRefresh(options) {
270
286
  } catch (e) {
271
287
  consecutiveNetworkErrors++;
272
288
  console.error(
273
- ` [网络] 获取任务失败 (${consecutiveNetworkErrors}): ${e.message}`
289
+ ` [网络] 获取任务失败 (${consecutiveNetworkErrors}): ${e.message}`,
274
290
  );
275
291
  await new Promise((r) => setTimeout(r, 10000));
276
292
  continue;
277
293
  }
278
294
 
279
295
  if (!job.hasJob) {
280
- console.error(`\n[空闲] 暂无 redo 任务,${exploreInterval || 30}s 后重试...`);
296
+ console.error(
297
+ `\n[空闲] 暂无 redo 任务,${exploreInterval || 30}s 后重试...`,
298
+ );
281
299
  await new Promise((r) => setTimeout(r, (exploreInterval || 30) * 1000));
282
300
  consecutiveNetworkErrors = 0;
283
301
  continue;
@@ -363,6 +381,7 @@ export async function handleRefresh(options) {
363
381
  maxFollowers: exploreMaxFollowers || 100,
364
382
  location: exploreLocation,
365
383
  browser,
384
+ proxyServer: cdpOptions.proxyServer || null,
366
385
  },
367
386
  console.error,
368
387
  );
@@ -387,6 +406,7 @@ export async function handleRefresh(options) {
387
406
  maxFollowers: exploreMaxFollowers || 100,
388
407
  location: exploreLocation,
389
408
  browser,
409
+ proxyServer: cdpOptions.proxyServer || null,
390
410
  },
391
411
  console.error,
392
412
  );
@@ -424,6 +444,7 @@ export async function handleRefresh(options) {
424
444
  maxFollowers: exploreMaxFollowers || 100,
425
445
  location: exploreLocation,
426
446
  browser,
447
+ proxyServer: cdpOptions.proxyServer || null,
427
448
  },
428
449
  console.error,
429
450
  );
package/src/lib/args.js CHANGED
@@ -533,6 +533,7 @@ function parseAttachArgs(args) {
533
533
  let interval = 10;
534
534
  let serverUrl = defaultServer;
535
535
  let countries = [];
536
+ let customProxy = null;
536
537
 
537
538
  for (let i = 0; i < args.length; i++) {
538
539
  const arg = args[i];
@@ -547,6 +548,8 @@ function parseAttachArgs(args) {
547
548
  .split(",")
548
549
  .map((c) => c.trim().toUpperCase())
549
550
  .filter(Boolean);
551
+ } else if (arg === "--proxy") {
552
+ customProxy = args[++i];
550
553
  }
551
554
  }
552
555
 
@@ -556,12 +559,12 @@ function parseAttachArgs(args) {
556
559
  attachInterval: interval,
557
560
  serverUrl,
558
561
  attachCountries: countries,
562
+ customProxy,
559
563
  urls: [],
560
564
  outputFormat: "json",
561
565
  exploreCount: 0,
562
566
  showConfig: false,
563
567
  showHelp: false,
564
- customProxy: null,
565
568
  configAction: null,
566
569
  configValue: null,
567
570
  pipeMode: false,
@@ -806,8 +809,14 @@ export function parseArgs() {
806
809
  configKey = args[i + 2];
807
810
  configValue = args[i + 3];
808
811
  i += 3;
812
+ } else if (configAction === "unset" || configAction === "reset") {
813
+ configKey = args[i + 2];
814
+ i += 2;
809
815
  } else {
810
- i++;
816
+ // 简写语法:tt-help config proxy "值" → 当作 set 处理
817
+ // args.js 层只负责把参数传过去,在 handleConfig 中转换
818
+ configKey = args[i + 2];
819
+ i += 2;
811
820
  }
812
821
  } else if (arg === "--pipe") {
813
822
  pipeMode = true;
@@ -164,7 +164,7 @@ function launchEdgeWithCDP(port, userDataDir, proxyServer) {
164
164
  "--disable-sync",
165
165
  ];
166
166
  if (proxyServer) {
167
- extraArgs.push(`--proxy-server="${proxyServer}"`);
167
+ extraArgs.push(`--proxy-server=${proxyServer}`);
168
168
  }
169
169
 
170
170
  const argsStr = extraArgs.join(" ");
@@ -177,6 +177,8 @@ function launchEdgeWithCDP(port, userDataDir, proxyServer) {
177
177
  command = `msedge ${argsStr} &`;
178
178
  }
179
179
 
180
+ console.error(`[浏览器] 启动命令: ${command}`);
181
+
180
182
  exec(command, (err) => {
181
183
  if (err) reject(new Error(`启动 Edge 浏览器失败: ${err.message}`));
182
184
  else resolve();
@@ -219,7 +221,9 @@ export async function ensureBrowserReady(options = {}) {
219
221
 
220
222
  if (needLaunch) {
221
223
  if (isCustom) {
222
- console.error(`CDP 端口 ${port} 未就绪,正在启动 Edge 浏览器...`);
224
+ console.error(
225
+ `CDP 端口 ${port} 未就绪,正在启动 Edge 浏览器...${proxyServer ? " (代理: " + proxyServer + ")" : ""}`,
226
+ );
223
227
  } else {
224
228
  const edgeRunning = await isEdgeRunning();
225
229
  if (edgeRunning) {
@@ -230,7 +234,7 @@ export async function ensureBrowserReady(options = {}) {
230
234
  console.error(`CDP 端口 ${port} 未就绪,正在启动 Edge 浏览器...`);
231
235
  }
232
236
  }
233
- await launchEdgeWithCDP(port, userDataDir);
237
+ await launchEdgeWithCDP(port, userDataDir, proxyServer);
234
238
 
235
239
  console.error("等待浏览器启动...");
236
240
  const launched = await waitForCDP(port);
@@ -247,7 +251,7 @@ export async function ensureBrowserReady(options = {}) {
247
251
  return browser;
248
252
  }
249
253
 
250
- export async function switchAccount(oldAccount, newAccount) {
254
+ export async function switchAccount(oldAccount, newAccount, proxyServer) {
251
255
  console.error(`\n[账户切换] 从端口 ${oldAccount.port} -> ${newAccount.port}`);
252
256
  console.error(` [账户切换] 等待 30 秒,请完成当前操作后自动切换...`);
253
257
  await new Promise((r) => setTimeout(r, 30000));
@@ -258,6 +262,10 @@ export async function switchAccount(oldAccount, newAccount) {
258
262
  const newCdpOptions = {};
259
263
  newCdpOptions.port = newAccount.port;
260
264
  newCdpOptions.userDataDir = newAccount.userDataDir;
265
+ if (proxyServer) {
266
+ newCdpOptions.proxyServer = proxyServer;
267
+ console.error(` [账户切换] 代理: ${proxyServer}`);
268
+ }
261
269
 
262
270
  const browser = await ensureBrowserReady(newCdpOptions);
263
271
 
@@ -68,6 +68,8 @@ export async function withBrowserRecovery(fn, browser, page, cdpOptions, port) {
68
68
 
69
69
  const DOM_CHECK_TIMEOUT = 20000; // 单次 DOM 检测超时 20 秒
70
70
  const DOM_CHECK_RETRIES = 3; // DOM 检测最大重试次数
71
+ const SAFE_CHECK_ROUNDS = 2; // 安全登录检测轮数(首次检测 + 额外1轮确认)
72
+ const SAFE_CHECK_INTERVAL = 5000; // 安全登录检测每轮间隔 5 秒
71
73
 
72
74
  /**
73
75
  * 判断登录状态:Cookie 为主,DOM 验真为辅。
@@ -102,6 +104,45 @@ export async function isLoggedIn(page) {
102
104
  return true;
103
105
  }
104
106
 
107
+ /**
108
+ * 安全登录检测:发现未登录时多检测几轮,避免因 TikTok 页面渲染延迟导致误判。
109
+ * - 首次检测为已登录 → 直接返回 true
110
+ * - 首次检测为未登录 → 等待后重新导航并检测,连续 SAFE_CHECK_ROUNDS 轮都为未登录才确认
111
+ * - 任何一轮检测为已登录 → 立即返回 true
112
+ */
113
+ export async function safeCheckLogin(page) {
114
+ // 第一轮检测
115
+ let loggedIn = await isLoggedIn(page);
116
+ if (loggedIn) {
117
+ console.error(`[安全登录检测] 第 1 轮: 已登录 ✓`);
118
+ return true;
119
+ }
120
+ console.error(
121
+ `[安全登录检测] 第 1 轮: 未登录 ✗,等待 ${SAFE_CHECK_INTERVAL / 1000}s 后重检...`,
122
+ );
123
+
124
+ // 后续轮次:等待后重新导航到 TikTok 页面再检测
125
+ for (let round = 2; round <= SAFE_CHECK_ROUNDS; round++) {
126
+ await new Promise((r) => setTimeout(r, SAFE_CHECK_INTERVAL));
127
+ // 重新导航到 TikTok 页面,确保页面状态刷新
128
+ await page.goto("https://www.tiktok.com", {
129
+ waitUntil: "domcontentloaded",
130
+ });
131
+ loggedIn = await isLoggedIn(page);
132
+ if (loggedIn) {
133
+ console.error(`[安全登录检测] 第 ${round} 轮: 已登录 ✓`);
134
+ return true;
135
+ }
136
+ console.error(`[安全登录检测] 第 ${round} 轮: 未登录 ✗`);
137
+ }
138
+
139
+ // 连续 SAFE_CHECK_ROUNDS 轮都为未登录
140
+ console.error(
141
+ `[安全登录检测] 连续 ${SAFE_CHECK_ROUNDS} 轮均为未登录,确认为未登录`,
142
+ );
143
+ return false;
144
+ }
145
+
105
146
  /**
106
147
  * 通过 DOM 元素判断登录状态(验真方案)
107
148
  * 使用 locator API + state: 'attached' 来避免 CDP 连接下 waitForSelector 的可见性问题
@@ -8,7 +8,7 @@ const __dirname = dirname(__filename);
8
8
  const homeDir = process.env.HOME || process.env.USERPROFILE || "";
9
9
  const configPath = join(homeDir, ".tt-help.json");
10
10
 
11
- const DEFAULT_PROXY = "http://127.0.0.1:7897";
11
+ const DEFAULT_PROXY = null;
12
12
  const DEFAULT_OUTPUT = "tiktok_data.json";
13
13
 
14
14
  let proxy = DEFAULT_PROXY;
@@ -24,8 +24,8 @@ let maxComments = 10;
24
24
  try {
25
25
  if (existsSync(configPath)) {
26
26
  const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
27
- if (cfg.proxy) {
28
- proxy = cfg.proxy;
27
+ if (cfg.proxy !== undefined) {
28
+ proxy = cfg.proxy || null;
29
29
  }
30
30
  if (cfg.server) {
31
31
  server = cfg.server;
@@ -131,33 +131,33 @@ const HELP_TEXT = [
131
131
  " --disable-follow 禁用关注/粉丝提取",
132
132
  " --max-following <数量> 最大获取关注数,默认 50",
133
133
  " --max-followers <数量> 最大获取粉丝数,默认 50",
134
- " --max-users <数量> 最大处理用户数,默认无限制",
135
- " -i, --interval <秒数> 无任务时轮询间隔,默认 10 秒",
136
- " --port <端口号> 固定 CDP 端口(调试用,关闭自动轮换)",
137
- " --base-port <端口号> 起始端口,默认 9222",
138
- " --port-count <数量> 端口数量(账户数),默认 10",
139
- " --user-id <编号> 客户端编号(设备ID),默认自动生成",
140
- " --proxy <地址> 浏览器代理(如 socks5://127.0.0.1:1080",
141
- "",
142
- " tt-help refresh [选项]",
143
- " 对目标商家用户进行轮回刷新,重新采集视频 + 关注 + 粉丝",
144
- " 筛选条件: tt_seller=1, verified=0, 目标国家",
145
- " 选项:",
146
- " --server <URL> 服务端地址,默认 http://127.0.0.1:3001",
147
- ` --location <国家代码> 国家筛选,逗号分隔,默认 ${DEFAULT_TARGET_LOCATIONS_CSV}`,
148
- " --max-videos <数量> 每用户最大视频数,默认 16",
149
- " --enable-follow 启用关注/粉丝提取(默认启用)",
150
- " --disable-follow 禁用关注/粉丝提取",
151
- " --max-following <数量> 最大获取关注数,默认 100",
152
- " --max-followers <数量> 最大获取粉丝数,默认 100",
153
- " --max-users <数量> 最大处理用户数,默认无限制",
154
- " -i, --interval <秒数> 无任务时轮询间隔,默认 30 秒",
155
- " --max-age <秒数> 最小刷新间隔,默认 43200(12小时)",
156
- " --port <端口号> 固定 CDP 端口(调试用,关闭自动轮换)",
157
- " --base-port <端口号> 起始端口,默认 9222",
158
- " --port-count <数量> 端口数量(账户数),默认 10",
159
- " --user-id <编号> 客户端编号(设备ID),默认自动生成",
160
- " --proxy <地址> 浏览器代理(如 socks5://127.0.0.1:1080",
134
+ " --max-users <数量> 最大处理用户数,默认无限制",
135
+ " -i, --interval <秒数> 无任务时轮询间隔,默认 10 秒",
136
+ " --port <端口号> 固定 CDP 端口(调试用,关闭自动轮换)",
137
+ " --base-port <端口号> 起始端口,默认 9222",
138
+ " --port-count <数量> 端口数量(账户数),默认 10",
139
+ " --user-id <编号> 客户端编号(设备ID),默认自动生成",
140
+ "--proxy <地址> 浏览器代理(如 socks5://127.0.0.1:1080),未指定时使用配置文件 proxy",
141
+ "",
142
+ " tt-help refresh [选项]",
143
+ " 对目标商家用户进行轮回刷新,重新采集视频 + 关注 + 粉丝",
144
+ " 筛选条件: tt_seller=1, verified=0, 目标国家",
145
+ " 选项:",
146
+ " --server <URL> 服务端地址,默认 http://127.0.0.1:3001",
147
+ ` --location <国家代码> 国家筛选,逗号分隔,默认 ${DEFAULT_TARGET_LOCATIONS_CSV}`,
148
+ " --max-videos <数量> 每用户最大视频数,默认 16",
149
+ " --enable-follow 启用关注/粉丝提取(默认启用)",
150
+ " --disable-follow 禁用关注/粉丝提取",
151
+ " --max-following <数量> 最大获取关注数,默认 100",
152
+ " --max-followers <数量> 最大获取粉丝数,默认 100",
153
+ " --max-users <数量> 最大处理用户数,默认无限制",
154
+ " -i, --interval <秒数> 无任务时轮询间隔,默认 30 秒",
155
+ " --max-age <秒数> 最小刷新间隔,默认 43200(12小时)",
156
+ " --port <端口号> 固定 CDP 端口(调试用,关闭自动轮换)",
157
+ " --base-port <端口号> 起始端口,默认 9222",
158
+ " --port-count <数量> 端口数量(账户数),默认 10",
159
+ " --user-id <编号> 客户端编号(设备ID),默认自动生成",
160
+ "--proxy <地址> 浏览器代理(如 socks5://127.0.0.1:1080),未指定时使用配置文件 proxy",
161
161
  "",
162
162
  " tt-help info <URL> [URL2 ...] [--onlyvideo]",
163
163
  " 获取用户/视频信息,支持多个 URL",
@@ -208,19 +208,20 @@ const HELP_TEXT = [
208
208
  " POST /api/tiktok/lookup 同时获取视频和作者信息 { videoUrl: string }",
209
209
  " 示例: tt-help webserver -p 3000",
210
210
  "",
211
- " config [show|set|reset]",
211
+ " config [show|set|unset|reset]",
212
212
  " config 查看当前配置",
213
213
  " config set <key> <value> 设置配置(key: proxy, server, browser, userId, maxFollowing, maxFollowers, maxVideos, maxComments)",
214
+ " config unset <key> 删除某项配置",
214
215
  " config reset 重置所有配置为默认",
215
216
  "",
216
217
  " 全局选项:",
217
218
  " -h, --help 显示帮助",
218
219
  " --version 显示版本号",
219
220
  "",
220
- " 示例: tt-help info https://www.tiktok.com/@nike https://www.tiktok.com/@adidas",
221
- " tt-help explore qiqi23280 fast --location ES --max-comments 50",
222
- " tt-help refresh --server http://127.0.0.1:3001 --port 9222",
223
- " tt-help config set server http://127.0.0.1:3001",
221
+ " 示例: tt-help info https://www.tiktok.com/@nike https://www.tiktok.com/@adidas",
222
+ " tt-help explore qiqi23280 fast --location ES --max-comments 50",
223
+ " tt-help refresh --server http://127.0.0.1:3001 --port 9222",
224
+ " tt-help config set server http://127.0.0.1:3001",
224
225
  " tt-help attach -p 5 -i 10",
225
226
  " tt-help watch -o data/result.db",
226
227
  " tt-help videostats data/result.db -p 3",
@@ -282,7 +283,7 @@ function getConfigText() {
282
283
  "tt-help v1.0.1",
283
284
  "",
284
285
  "配置:",
285
- ` 代理: ${proxy}`,
286
+ ` 代理: ${proxy || "未配置"}`,
286
287
  ` 服务端: ${server}`,
287
288
  ` 浏览器: ${browser || "未配置(将自动探测或回退)"}`,
288
289
  ` 用户号: ${currentUserId || "未设置(首次运行 auto 自动创建)"}`,
package/src/lib/scrape.js CHANGED
@@ -1,15 +1,31 @@
1
- import { TikTokScraper } from './tiktok-scraper.mjs';
2
- import { isProfileUrl, isVideoUrl, extractUniqueId, normalizeUsername } from './url.js';
1
+ import { TikTokScraper } from "./tiktok-scraper.mjs";
2
+ import {
3
+ isProfileUrl,
4
+ isVideoUrl,
5
+ extractUniqueId,
6
+ normalizeUsername,
7
+ } from "./url.js";
3
8
 
4
9
  // Lazy singleton for TikTokScraper
5
10
  let scraperInstance = null;
6
11
  let scraperInitPromise = null;
12
+ let scraperProxyServer = null;
13
+
14
+ export function setScraperProxy(proxyServer) {
15
+ // 只在代理变化时才关闭重建
16
+ if (scraperProxyServer === proxyServer) return;
17
+ scraperProxyServer = proxyServer;
18
+ if (scraperInstance) {
19
+ scraperInstance.close().catch(() => {});
20
+ scraperInstance = null;
21
+ }
22
+ }
7
23
 
8
24
  async function getScraper() {
9
25
  if (scraperInstance) return scraperInstance;
10
26
  if (scraperInitPromise) return scraperInitPromise;
11
27
  scraperInitPromise = (async () => {
12
- const scraper = new TikTokScraper();
28
+ const scraper = new TikTokScraper({ proxyServer: scraperProxyServer });
13
29
  await scraper.init();
14
30
  scraperInstance = scraper;
15
31
  scraperInitPromise = null;
@@ -57,7 +73,7 @@ export async function extractUserData(url) {
57
73
  const uniqueId = extractUniqueId(url);
58
74
  if (!uniqueId) throw new Error(`无法从URL提取用户名: ${url}`);
59
75
  const user = await scraper.getUserInfo(normalizeUsername(uniqueId));
60
- if (!user) throw new Error('无法解析用户信息');
76
+ if (!user) throw new Error("无法解析用户信息");
61
77
  return mapUserInfo(user);
62
78
  }
63
79