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 +1 -1
- package/src/cli/attach.js +13 -1
- package/src/cli/config.js +46 -3
- package/src/cli/explore.js +22 -4
- package/src/cli/refresh.js +46 -13
- package/src/lib/args.js +11 -2
- package/src/lib/browser/cdp.js +12 -4
- package/src/lib/constants.js +37 -36
- package/src/lib/scrape.js +20 -4
- package/src/lib/tiktok-scraper.mjs +11 -5
- package/src/scraper/explore-core.js +43 -20
- package/src/watch/data-store.js +44 -0
- package/src/watch/public/app.js +202 -5
- package/src/watch/public/index.html +4 -6
- package/src/watch/public/style.css +88 -1
- package/src/watch/server.js +23 -0
package/package.json
CHANGED
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 (
|
|
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
|
|
package/src/cli/explore.js
CHANGED
|
@@ -12,7 +12,11 @@ import {
|
|
|
12
12
|
detectCaptcha,
|
|
13
13
|
closeCaptcha,
|
|
14
14
|
} from "../scraper/modules/captcha-handler.js";
|
|
15
|
-
import {
|
|
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
|
-
|
|
155
|
-
|
|
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(
|
|
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,
|
package/src/cli/refresh.js
CHANGED
|
@@ -12,7 +12,11 @@ import {
|
|
|
12
12
|
detectCaptcha,
|
|
13
13
|
closeCaptcha,
|
|
14
14
|
} from "../scraper/modules/captcha-handler.js";
|
|
15
|
-
import {
|
|
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(
|
|
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 (
|
|
153
|
-
cdpOptions.proxyServer =
|
|
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 =
|
|
253
|
-
|
|
254
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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;
|
package/src/lib/browser/cdp.js
CHANGED
|
@@ -164,7 +164,7 @@ function launchEdgeWithCDP(port, userDataDir, proxyServer) {
|
|
|
164
164
|
"--disable-sync",
|
|
165
165
|
];
|
|
166
166
|
if (proxyServer) {
|
|
167
|
-
extraArgs.push(`--proxy-server
|
|
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(
|
|
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
|
|
package/src/lib/constants.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
2
|
-
import {
|
|
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++) {
|