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 +1 -1
- package/scripts/run-explore.bat +1 -1
- package/src/cli/attach.js +11 -1
- package/src/cli/config.js +46 -3
- package/src/cli/explore.js +25 -8
- package/src/cli/refresh.js +30 -9
- package/src/lib/args.js +11 -2
- package/src/lib/browser/cdp.js +12 -4
- package/src/lib/browser/page.js +41 -0
- 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 +7 -1
- package/src/watch/data-store.js +128 -2
- package/src/watch/public/app.js +293 -42
- package/src/watch/public/index.html +63 -0
- package/src/watch/public/style.css +80 -3
- package/src/watch/server.js +88 -1
package/package.json
CHANGED
package/scripts/run-explore.bat
CHANGED
|
@@ -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
|
|
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 (
|
|
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}`);
|
|
@@ -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 {
|
|
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
|
|
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(
|
|
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
|
|
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,
|
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;
|
|
@@ -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
|
-
|
|
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/browser/page.js
CHANGED
|
@@ -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 的可见性问题
|
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
|
|