tt-help-cli-ycl 1.3.64 → 1.3.72

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.64",
3
+ "version": "1.3.72",
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/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,9 @@ 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({
138
+ proxyServer: effectiveProxy || null,
139
+ });
128
140
  const shutdown = async (signal) => {
129
141
  if (shuttingDown) return;
130
142
  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}`);
@@ -207,9 +217,11 @@ export async function handleExplore(options) {
207
217
  const oldAccount = currentAccount;
208
218
  const nextAccount = healthChecker.getNextAccount();
209
219
  currentAccount = nextAccount;
220
+ const effectiveProxy = cdpOptions.proxyServer;
210
221
  const newBrowser = await switchAccount(
211
222
  { port: oldAccount.port, userDataDir: oldAccount.userDataDir },
212
223
  { port: nextAccount.port, userDataDir: nextAccount.userDataDir },
224
+ effectiveProxy,
213
225
  );
214
226
  browser = newBrowser;
215
227
  const newPage = await setupNewPage(browser);
@@ -217,8 +229,11 @@ export async function handleExplore(options) {
217
229
  Object.assign(cdpOptions, {
218
230
  port: nextAccount.port,
219
231
  userDataDir: nextAccount.userDataDir,
232
+ ...(effectiveProxy ? { proxyServer: effectiveProxy } : {}),
220
233
  });
221
- console.error(`[健康检查] 已切换到端口 ${nextAccount.port}`);
234
+ console.error(
235
+ `[健康检查] 已切换到端口 ${nextAccount.port}${effectiveProxy ? ", 代理: " + effectiveProxy : ""}`,
236
+ );
222
237
  // 切换账户后先导航到 TikTok 页面,再重新检测登录状态
223
238
  await page.goto(STARTUP_TIKTOK_URL, {
224
239
  waitUntil: "domcontentloaded",
@@ -357,6 +372,7 @@ export async function handleExplore(options) {
357
372
  maxFollowing: exploreMaxFollowing,
358
373
  maxFollowers: exploreMaxFollowers,
359
374
  location: exploreLocation,
375
+ proxyServer: cdpOptions.proxyServer || null,
360
376
  browser,
361
377
  },
362
378
  console.error,
@@ -382,6 +398,7 @@ export async function handleExplore(options) {
382
398
  maxFollowing: exploreMaxFollowing,
383
399
  maxFollowers: exploreMaxFollowers,
384
400
  location: exploreLocation,
401
+ proxyServer: cdpOptions.proxyServer || null,
385
402
  browser,
386
403
  },
387
404
  console.error,
@@ -419,6 +436,7 @@ export async function handleExplore(options) {
419
436
  maxFollowing: exploreMaxFollowing,
420
437
  maxFollowers: exploreMaxFollowers,
421
438
  location: exploreLocation,
439
+ proxyServer: cdpOptions.proxyServer || null,
422
440
  browser,
423
441
  },
424
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;
@@ -362,7 +380,9 @@ export async function handleRefresh(options) {
362
380
  maxFollowing: exploreMaxFollowing || 100,
363
381
  maxFollowers: exploreMaxFollowers || 100,
364
382
  location: exploreLocation,
383
+ locationMode: "refresh",
365
384
  browser,
385
+ proxyServer: cdpOptions.proxyServer || null,
366
386
  },
367
387
  console.error,
368
388
  );
@@ -386,7 +406,9 @@ export async function handleRefresh(options) {
386
406
  maxFollowing: exploreMaxFollowing || 100,
387
407
  maxFollowers: exploreMaxFollowers || 100,
388
408
  location: exploreLocation,
409
+ locationMode: "refresh",
389
410
  browser,
411
+ proxyServer: cdpOptions.proxyServer || null,
390
412
  },
391
413
  console.error,
392
414
  );
@@ -423,7 +445,9 @@ export async function handleRefresh(options) {
423
445
  maxFollowing: exploreMaxFollowing || 100,
424
446
  maxFollowers: exploreMaxFollowers || 100,
425
447
  location: exploreLocation,
448
+ locationMode: "refresh",
426
449
  browser,
450
+ proxyServer: cdpOptions.proxyServer || null,
427
451
  },
428
452
  console.error,
429
453
  );
@@ -504,10 +528,20 @@ export async function handleRefresh(options) {
504
528
 
505
529
  processedCount++;
506
530
 
507
- const guessedLocation = result.locationCreated || null;
531
+ // refresh 模式:confirmedLocation 是二次确认的国家,写入 confirmed_location
532
+ // locationCreated 保持原始值不变
533
+ const refreshLocation =
534
+ result.confirmedLocation || result.locationCreated;
535
+ const guessedLocation = refreshLocation || null;
536
+
537
+ // 把 confirmedLocation 合并到 userInfo 中(通过 commitRedoJob 写入 DB)
538
+ const refreshUserInfo = { ...(result.userInfo || {}) };
539
+ if (result.confirmedLocation) {
540
+ refreshUserInfo.confirmedLocation = result.confirmedLocation;
541
+ }
508
542
 
509
543
  const payload = {
510
- userInfo: result.userInfo || {},
544
+ userInfo: refreshUserInfo,
511
545
  discoveredFollowing: (result.discoveredFollowing || []).map((f) => ({
512
546
  handle: Array.isArray(f) ? f[0] : f,
513
547
  displayName: Array.isArray(f) ? f[1] : null,
@@ -521,7 +555,6 @@ export async function handleRefresh(options) {
521
555
  processed: result.processed,
522
556
  hasFollowData: result.hasFollowData,
523
557
  keepFollow: result.keepFollow,
524
- locationCreated: result.locationCreated,
525
558
  noVideo: result.noVideo,
526
559
  collectedVideos: result.collectedVideos,
527
560
  };
@@ -534,7 +567,7 @@ export async function handleRefresh(options) {
534
567
  {
535
568
  sourceUser: username,
536
569
  videoList: result.videoList,
537
- locationCreated: result.locationCreated,
570
+ locationCreated: refreshLocation,
538
571
  ttSeller: result.userInfo?.ttSeller || false,
539
572
  },
540
573
  );
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
 
@@ -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
 
@@ -57,11 +57,13 @@ export class TikTokScraper {
57
57
  wafTtl = DEFAULT_WAF_TTL,
58
58
  warmUrl = DEFAULT_WARM_URL,
59
59
  maxRequestsPerPage = DEFAULT_MAX_REQUESTS_PER_PAGE,
60
+ proxyServer = null,
60
61
  } = {}) {
61
62
  this.poolSize = poolSize;
62
63
  this.wafTtl = wafTtl;
63
64
  this.warmUrl = warmUrl;
64
65
  this.maxRequestsPerPage = maxRequestsPerPage;
66
+ this.proxyServer = proxyServer;
65
67
  this.browser = null;
66
68
  this.context = null;
67
69
  this.slots = [];
@@ -77,17 +79,21 @@ export class TikTokScraper {
77
79
  "未找到本地浏览器(Chrome/Edge),请先安装浏览器或执行 npx playwright install",
78
80
  );
79
81
  }
82
+ const launchArgs = [
83
+ "--no-sandbox",
84
+ "--disable-setuid-sandbox",
85
+ "--disable-dev-shm-usage",
86
+ ];
87
+ if (this.proxyServer) {
88
+ launchArgs.push(`--proxy-server=${this.proxyServer}`);
89
+ }
80
90
  this.browser = await chromium.launch({
81
91
  headless: true,
82
92
  executablePath,
83
93
  handleSIGINT: false,
84
94
  handleSIGTERM: false,
85
95
  handleSIGHUP: false,
86
- args: [
87
- "--no-sandbox",
88
- "--disable-setuid-sandbox",
89
- "--disable-dev-shm-usage",
90
- ],
96
+ args: launchArgs,
91
97
  });
92
98
  this.context = await this.browser.newContext();
93
99
  for (let i = 0; i < this.poolSize; i++) {