tt-help-cli-ycl 1.3.82 → 1.3.84
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 +3 -34
- package/src/cli/auto.js +3 -18
- package/src/cli/comments.js +13 -57
- package/src/cli/explore.js +281 -269
- package/src/cli/refresh.js +6 -21
- package/src/lib/api-client.js +101 -0
- package/src/lib/api-interceptor-comment.js +56 -14
- package/src/lib/api-interceptor.js +11 -1
- package/src/watch/data-store.js +36 -0
- package/src/watch/public/app.js +96 -1
- package/src/watch/public/index.html +15 -0
- package/src/watch/public/style.css +107 -0
- package/src/watch/server.js +22 -0
package/src/cli/explore.js
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "../lib/browser/cdp.js";
|
|
25
25
|
import { showResourceUsage } from "../utils/index.js";
|
|
26
26
|
import { HealthChecker } from "../lib/browser/health-checker.js";
|
|
27
|
+
import { createApiClient } from "../lib/api-client.js";
|
|
27
28
|
import path from "path";
|
|
28
29
|
import os from "os";
|
|
29
30
|
|
|
@@ -46,24 +47,6 @@ async function withRetry(label, fn) {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
async function apiPost(url, body) {
|
|
50
|
-
return withRetry(`POST ${url}`, async () => {
|
|
51
|
-
const res = await fetch(url, {
|
|
52
|
-
method: "POST",
|
|
53
|
-
headers: { "Content-Type": "application/json" },
|
|
54
|
-
body: JSON.stringify(body),
|
|
55
|
-
});
|
|
56
|
-
return res.json();
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function apiGet(url) {
|
|
61
|
-
return withRetry(`GET ${url}`, async () => {
|
|
62
|
-
const res = await fetch(url);
|
|
63
|
-
return res.json();
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
50
|
export async function handleExplore(options) {
|
|
68
51
|
const {
|
|
69
52
|
exploreUsernames,
|
|
@@ -118,15 +101,6 @@ export async function handleExplore(options) {
|
|
|
118
101
|
|
|
119
102
|
setDelayConfig(explorePreset);
|
|
120
103
|
|
|
121
|
-
await apiGet(`${serverUrl}/api/stats`);
|
|
122
|
-
|
|
123
|
-
if (exploreUsernames && exploreUsernames.length > 0) {
|
|
124
|
-
const { added, skipped } = await apiPost(`${serverUrl}/api/users`, {
|
|
125
|
-
usernames: exploreUsernames,
|
|
126
|
-
});
|
|
127
|
-
console.error(`种子用户: ${added} 个新增, ${skipped} 个已存在`);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
104
|
console.error(`\n国家筛选: ${exploreLocation}`);
|
|
131
105
|
if (exploreJobLocations) console.error(`任务国家: ${exploreJobLocations}`);
|
|
132
106
|
console.error(`视频采集: ${exploreMaxVideos || 1}`);
|
|
@@ -168,6 +142,17 @@ export async function handleExplore(options) {
|
|
|
168
142
|
|
|
169
143
|
console.error(`CDP 端口: ${cdpOptions.port}, 用户编号: ${userId}`);
|
|
170
144
|
console.error(`浏览器配置: ${path.basename(cdpOptions.userDataDir)}`);
|
|
145
|
+
|
|
146
|
+
const { apiGet, apiPost } = createApiClient({ meta: { port: cdpOptions.port } });
|
|
147
|
+
|
|
148
|
+
await apiGet(`${serverUrl}/api/stats`);
|
|
149
|
+
|
|
150
|
+
if (exploreUsernames && exploreUsernames.length > 0) {
|
|
151
|
+
const { added, skipped } = await apiPost(`${serverUrl}/api/users`, {
|
|
152
|
+
usernames: exploreUsernames,
|
|
153
|
+
});
|
|
154
|
+
console.error(`种子用户: ${added} 个新增, ${skipped} 个已存在`);
|
|
155
|
+
}
|
|
171
156
|
if (!explorePort) {
|
|
172
157
|
const portRange = `${currentAccount.port}-${currentAccount.port + healthChecker.accounts.length - 1}`;
|
|
173
158
|
console.error(
|
|
@@ -264,144 +249,121 @@ export async function handleExplore(options) {
|
|
|
264
249
|
const FOLLOW_BLOCK_THRESHOLD = 10 * 60 * 1000; // 10分钟
|
|
265
250
|
|
|
266
251
|
while (!shuttingDown) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
252
|
+
try {
|
|
253
|
+
const jobQuery = exploreJobLocations
|
|
254
|
+
? `${serverUrl}/api/job?userId=${encodeURIComponent(userId)}&locations=${encodeURIComponent(exploreJobLocations)}&loggedIn=${loggedIn}`
|
|
255
|
+
: `${serverUrl}/api/job?userId=${encodeURIComponent(userId)}&loggedIn=${loggedIn}`;
|
|
256
|
+
const job = await apiGet(jobQuery);
|
|
257
|
+
if (!job.hasJob) {
|
|
258
|
+
console.error(
|
|
259
|
+
`\n[Explore] 当前无任务,${exploreInterval} 秒后重试...`,
|
|
260
|
+
);
|
|
261
|
+
await new Promise((r) => setTimeout(r, exploreInterval * 1000));
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
276
264
|
|
|
277
|
-
|
|
278
|
-
|
|
265
|
+
const username = job.user.uniqueId;
|
|
266
|
+
processedCount++;
|
|
279
267
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
268
|
+
if (processedCount % 10 === 0) {
|
|
269
|
+
showResourceUsage();
|
|
270
|
+
}
|
|
283
271
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
272
|
+
const checkResult = healthChecker.check();
|
|
273
|
+
if (checkResult.shouldSwitch) {
|
|
274
|
+
await handleAccountSwitch(checkResult.reason);
|
|
275
|
+
} else if (checkResult.info && processedCount % 10 === 0) {
|
|
276
|
+
// 关注/粉丝封禁检测状态(仅登录状态)
|
|
277
|
+
let followStatus = "";
|
|
278
|
+
if (loggedIn && exploreEnableFollow) {
|
|
279
|
+
const followElapsed = Math.round(
|
|
280
|
+
(Date.now() - lastFollowSuccessTime) / 60000,
|
|
281
|
+
);
|
|
282
|
+
const followRemaining = Math.max(
|
|
283
|
+
0,
|
|
284
|
+
Math.round(FOLLOW_BLOCK_THRESHOLD / 60000) - followElapsed,
|
|
285
|
+
);
|
|
286
|
+
followStatus = ` | 关注/粉丝上次成功 ${followElapsed} 分钟前 | 封禁检测 ${followRemaining} 分钟后`;
|
|
287
|
+
}
|
|
288
|
+
console.error(`\n[健康检查] ${checkResult.info}${followStatus}`);
|
|
299
289
|
}
|
|
300
|
-
console.error(`\n[健康检查] ${checkResult.info}${followStatus}`);
|
|
301
|
-
}
|
|
302
290
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
291
|
+
// 切换任务前检测验证码
|
|
292
|
+
const switchCaptcha = await detectCaptcha(page);
|
|
293
|
+
if (switchCaptcha && switchCaptcha.visible) {
|
|
294
|
+
console.error(`\n[验证码] 切换任务前检测到验证码,等待3分钟...`);
|
|
295
|
+
captchaCount++;
|
|
296
|
+
console.error(` [验证码] 累计 ${captchaCount} 次`);
|
|
297
|
+
|
|
298
|
+
// 等待3分钟
|
|
299
|
+
await new Promise((r) => setTimeout(r, 3 * 60 * 1000));
|
|
300
|
+
|
|
301
|
+
// 尝试关闭
|
|
302
|
+
const closeResult = await closeCaptcha(page);
|
|
303
|
+
if (closeResult.success) {
|
|
304
|
+
console.error(` [验证码] 已关闭`);
|
|
305
|
+
} else {
|
|
306
|
+
console.error(` [验证码] 关闭失败: ${closeResult.reason}`);
|
|
307
|
+
}
|
|
309
308
|
|
|
310
|
-
|
|
311
|
-
|
|
309
|
+
// 上报
|
|
310
|
+
await withRetry("report captcha", () =>
|
|
311
|
+
apiPost(`${serverUrl}/api/error-report`, {
|
|
312
|
+
userId,
|
|
313
|
+
username: "unknown",
|
|
314
|
+
errorType: "captcha",
|
|
315
|
+
errorMessage: "切换任务前检测到验证码",
|
|
316
|
+
stage: "between-tasks",
|
|
317
|
+
errorStack: "",
|
|
318
|
+
}),
|
|
319
|
+
).catch(() => {});
|
|
312
320
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
console.error(` [验证码] 关闭失败: ${closeResult.reason}`);
|
|
321
|
+
// 累计2次切换账户
|
|
322
|
+
if (captchaCount >= 2) {
|
|
323
|
+
await handleAccountSwitch(`验证码累计 ${captchaCount} 次`);
|
|
324
|
+
captchaCount = 0;
|
|
325
|
+
}
|
|
319
326
|
}
|
|
320
327
|
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
).catch(() => {});
|
|
332
|
-
|
|
333
|
-
// 累计2次切换账户
|
|
334
|
-
if (captchaCount >= 2) {
|
|
335
|
-
await handleAccountSwitch(`验证码累计 ${captchaCount} 次`);
|
|
336
|
-
captchaCount = 0;
|
|
328
|
+
// 关注/粉丝封禁检测移到健康检查中(仅登录状态下)
|
|
329
|
+
if (loggedIn && exploreEnableFollow) {
|
|
330
|
+
const followElapsed = Date.now() - lastFollowSuccessTime;
|
|
331
|
+
if (followElapsed > FOLLOW_BLOCK_THRESHOLD) {
|
|
332
|
+
const minutes = Math.round(followElapsed / 60000);
|
|
333
|
+
console.error(
|
|
334
|
+
`\n[封禁检测] 关注/粉丝功能已 ${minutes} 分钟未成功获取,切换到下一个账户...`,
|
|
335
|
+
);
|
|
336
|
+
await handleAccountSwitch(`关注/粉丝功能被封`);
|
|
337
|
+
}
|
|
337
338
|
}
|
|
338
|
-
}
|
|
339
339
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
)
|
|
348
|
-
|
|
340
|
+
if (consecutiveNetworkErrors > 0) {
|
|
341
|
+
const waitTime =
|
|
342
|
+
consecutiveNetworkErrors <= 2
|
|
343
|
+
? 0
|
|
344
|
+
: consecutiveNetworkErrors <= 5
|
|
345
|
+
? 30000
|
|
346
|
+
: 300000;
|
|
347
|
+
if (waitTime > 0) {
|
|
348
|
+
console.error(
|
|
349
|
+
` [网络] 连续 ${consecutiveNetworkErrors} 次网络异常,等待 ${waitTime / 1000}s 后重试...`,
|
|
350
|
+
);
|
|
351
|
+
await new Promise((r) => setTimeout(r, waitTime));
|
|
352
|
+
}
|
|
349
353
|
}
|
|
350
|
-
}
|
|
351
354
|
|
|
352
|
-
|
|
353
|
-
const waitTime =
|
|
354
|
-
consecutiveNetworkErrors <= 2
|
|
355
|
-
? 0
|
|
356
|
-
: consecutiveNetworkErrors <= 5
|
|
357
|
-
? 30000
|
|
358
|
-
: 300000;
|
|
359
|
-
if (waitTime > 0) {
|
|
360
|
-
console.error(
|
|
361
|
-
` [网络] 连续 ${consecutiveNetworkErrors} 次网络异常,等待 ${waitTime / 1000}s 后重试...`,
|
|
362
|
-
);
|
|
363
|
-
await new Promise((r) => setTimeout(r, waitTime));
|
|
364
|
-
}
|
|
365
|
-
}
|
|
355
|
+
console.error(`\n[${processedCount}] 探索 @${username}...`);
|
|
366
356
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const { switchMax } = getDelayConfig();
|
|
370
|
-
await delay(switchMax, switchMax * 3);
|
|
371
|
-
|
|
372
|
-
let result = await processExplore(
|
|
373
|
-
page,
|
|
374
|
-
username,
|
|
375
|
-
{
|
|
376
|
-
maxVideos: exploreMaxVideos,
|
|
377
|
-
enableFollow: exploreEnableFollow,
|
|
378
|
-
loggedIn, // 传入登录状态,避免每次调用 isLoggedIn(page)
|
|
379
|
-
maxFollowing: exploreMaxFollowing,
|
|
380
|
-
maxFollowers: exploreMaxFollowers,
|
|
381
|
-
location: exploreLocation,
|
|
382
|
-
proxyServer: cdpOptions.proxyServer || null,
|
|
383
|
-
browser,
|
|
384
|
-
},
|
|
385
|
-
console.error,
|
|
386
|
-
);
|
|
357
|
+
const { switchMax } = getDelayConfig();
|
|
358
|
+
await delay(switchMax, switchMax * 3);
|
|
387
359
|
|
|
388
|
-
|
|
389
|
-
if (result.error && isBrowserClosedError(new Error(result.error))) {
|
|
390
|
-
const newBrowser = await relaunchBrowser(
|
|
391
|
-
cdpOptions,
|
|
392
|
-
cdpOptions.port || 9222,
|
|
393
|
-
);
|
|
394
|
-
browser = newBrowser;
|
|
395
|
-
const newPage = await setupNewPage(browser);
|
|
396
|
-
Object.assign(page, newPage);
|
|
397
|
-
// 重试当前用户
|
|
398
|
-
result = await processExplore(
|
|
360
|
+
let result = await processExplore(
|
|
399
361
|
page,
|
|
400
362
|
username,
|
|
401
363
|
{
|
|
402
364
|
maxVideos: exploreMaxVideos,
|
|
403
365
|
enableFollow: exploreEnableFollow,
|
|
404
|
-
loggedIn, //
|
|
366
|
+
loggedIn, // 传入登录状态,避免每次调用 isLoggedIn(page)
|
|
405
367
|
maxFollowing: exploreMaxFollowing,
|
|
406
368
|
maxFollowers: exploreMaxFollowers,
|
|
407
369
|
location: exploreLocation,
|
|
@@ -410,36 +372,24 @@ export async function handleExplore(options) {
|
|
|
410
372
|
},
|
|
411
373
|
console.error,
|
|
412
374
|
);
|
|
413
|
-
}
|
|
414
375
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
if (result.error) {
|
|
429
|
-
consecutiveNetworkErrors++;
|
|
430
|
-
errorCount++;
|
|
431
|
-
|
|
432
|
-
// 临时性错误:自动重试一次
|
|
433
|
-
if (result.retryable) {
|
|
434
|
-
console.error(` [临时错误] 等待 5 秒后重试 @${username}...`);
|
|
435
|
-
await new Promise((r) => setTimeout(r, 5000));
|
|
376
|
+
// 浏览器关闭检测:processExplore 内部 catch 了异常,需要从 result.error 判断
|
|
377
|
+
if (result.error && isBrowserClosedError(new Error(result.error))) {
|
|
378
|
+
const newBrowser = await relaunchBrowser(
|
|
379
|
+
cdpOptions,
|
|
380
|
+
cdpOptions.port || 9222,
|
|
381
|
+
);
|
|
382
|
+
browser = newBrowser;
|
|
383
|
+
const newPage = await setupNewPage(browser);
|
|
384
|
+
Object.assign(page, newPage);
|
|
385
|
+
// 重试当前用户
|
|
436
386
|
result = await processExplore(
|
|
437
387
|
page,
|
|
438
388
|
username,
|
|
439
389
|
{
|
|
440
390
|
maxVideos: exploreMaxVideos,
|
|
441
391
|
enableFollow: exploreEnableFollow,
|
|
442
|
-
loggedIn,
|
|
392
|
+
loggedIn, // 传入登录状态
|
|
443
393
|
maxFollowing: exploreMaxFollowing,
|
|
444
394
|
maxFollowers: exploreMaxFollowers,
|
|
445
395
|
location: exploreLocation,
|
|
@@ -448,126 +398,188 @@ export async function handleExplore(options) {
|
|
|
448
398
|
},
|
|
449
399
|
console.error,
|
|
450
400
|
);
|
|
451
|
-
// 重试成功后继续正常流程
|
|
452
|
-
if (!result.error) {
|
|
453
|
-
consecutiveNetworkErrors = 0;
|
|
454
|
-
errorCount--;
|
|
455
|
-
}
|
|
456
401
|
}
|
|
457
402
|
|
|
458
|
-
if (result.
|
|
459
|
-
|
|
403
|
+
if (result.restricted) {
|
|
404
|
+
consecutiveNetworkErrors = 0;
|
|
460
405
|
await apiPost(`${serverUrl}/api/job/${username}`, {
|
|
461
|
-
|
|
406
|
+
restricted: true,
|
|
407
|
+
userInfo: result.userInfo || {},
|
|
462
408
|
});
|
|
463
|
-
const errorType = result.error.startsWith("被封:")
|
|
464
|
-
? "被封"
|
|
465
|
-
: consecutiveNetworkErrors > 1
|
|
466
|
-
? "network"
|
|
467
|
-
: "other";
|
|
468
|
-
await withRetry("report error", () =>
|
|
469
|
-
apiPost(`${serverUrl}/api/error-report`, {
|
|
470
|
-
userId,
|
|
471
|
-
username,
|
|
472
|
-
errorType,
|
|
473
|
-
errorMessage: result.error,
|
|
474
|
-
stage: "process",
|
|
475
|
-
errorStack: result.errorStack || "",
|
|
476
|
-
}),
|
|
477
|
-
).catch(() => {});
|
|
478
|
-
if (errorType === "被封") {
|
|
479
|
-
blockedCount++;
|
|
480
|
-
console.error(` [被封] 累计 ${blockedCount} 次`);
|
|
481
|
-
if (blockedCount >= 3) {
|
|
482
|
-
await handleAccountSwitch(`账号被封累计 ${blockedCount} 次`);
|
|
483
|
-
blockedCount = 0;
|
|
484
|
-
}
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
487
409
|
if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
|
|
488
410
|
console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
|
|
489
411
|
break;
|
|
490
412
|
}
|
|
491
413
|
continue;
|
|
492
414
|
}
|
|
493
|
-
}
|
|
494
415
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
416
|
+
if (result.error) {
|
|
417
|
+
consecutiveNetworkErrors++;
|
|
418
|
+
errorCount++;
|
|
419
|
+
|
|
420
|
+
// 临时性错误:自动重试一次
|
|
421
|
+
if (result.retryable) {
|
|
422
|
+
console.error(` [临时错误] 等待 5 秒后重试 @${username}...`);
|
|
423
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
424
|
+
result = await processExplore(
|
|
425
|
+
page,
|
|
426
|
+
username,
|
|
427
|
+
{
|
|
428
|
+
maxVideos: exploreMaxVideos,
|
|
429
|
+
enableFollow: exploreEnableFollow,
|
|
430
|
+
loggedIn,
|
|
431
|
+
maxFollowing: exploreMaxFollowing,
|
|
432
|
+
maxFollowers: exploreMaxFollowers,
|
|
433
|
+
location: exploreLocation,
|
|
434
|
+
proxyServer: cdpOptions.proxyServer || null,
|
|
435
|
+
browser,
|
|
436
|
+
},
|
|
437
|
+
console.error,
|
|
438
|
+
);
|
|
439
|
+
// 重试成功后继续正常流程
|
|
440
|
+
if (!result.error) {
|
|
441
|
+
consecutiveNetworkErrors = 0;
|
|
442
|
+
errorCount--;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
498
445
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
446
|
+
if (result.error) {
|
|
447
|
+
// 上报错误(重试后仍有错误才上报)
|
|
448
|
+
await apiPost(`${serverUrl}/api/job/${username}`, {
|
|
449
|
+
error: result.error,
|
|
450
|
+
});
|
|
451
|
+
const errorType = result.error.startsWith("被封:")
|
|
452
|
+
? "被封"
|
|
453
|
+
: consecutiveNetworkErrors > 1
|
|
454
|
+
? "network"
|
|
455
|
+
: "other";
|
|
456
|
+
await withRetry("report error", () =>
|
|
457
|
+
apiPost(`${serverUrl}/api/error-report`, {
|
|
458
|
+
userId,
|
|
459
|
+
username,
|
|
460
|
+
errorType,
|
|
461
|
+
errorMessage: result.error,
|
|
462
|
+
stage: "process",
|
|
463
|
+
errorStack: result.errorStack || "",
|
|
464
|
+
}),
|
|
465
|
+
).catch(() => {});
|
|
466
|
+
if (errorType === "被封") {
|
|
467
|
+
blockedCount++;
|
|
468
|
+
console.error(` [被封] 累计 ${blockedCount} 次`);
|
|
469
|
+
if (blockedCount >= 3) {
|
|
470
|
+
await handleAccountSwitch(`账号被封累计 ${blockedCount} 次`);
|
|
471
|
+
blockedCount = 0;
|
|
472
|
+
}
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
|
|
476
|
+
console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
514
481
|
}
|
|
515
|
-
}
|
|
516
482
|
|
|
517
|
-
|
|
483
|
+
if (result.captchaDetected) {
|
|
484
|
+
captchaCount++;
|
|
485
|
+
console.error(` [验证码] 累计 ${captchaCount} 次`);
|
|
518
486
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
487
|
+
await withRetry("report captcha", () =>
|
|
488
|
+
apiPost(`${serverUrl}/api/error-report`, {
|
|
489
|
+
userId,
|
|
490
|
+
username,
|
|
491
|
+
errorType: "captcha",
|
|
492
|
+
errorMessage: result.captchaMessage || "页面出现验证码",
|
|
493
|
+
stage: result.captchaStage || "video-page",
|
|
494
|
+
errorStack: "",
|
|
495
|
+
}),
|
|
496
|
+
).catch(() => {});
|
|
497
|
+
|
|
498
|
+
// 累计2次验证码,切换账户
|
|
499
|
+
if (captchaCount >= 2) {
|
|
500
|
+
await handleAccountSwitch(`验证码累计 ${captchaCount} 次`);
|
|
501
|
+
captchaCount = 0;
|
|
502
|
+
}
|
|
526
503
|
}
|
|
527
|
-
}
|
|
528
504
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
);
|
|
563
|
-
|
|
564
|
-
|
|
505
|
+
consecutiveNetworkErrors = 0;
|
|
506
|
+
|
|
507
|
+
// 更新关注/粉丝成功时间(仅登录状态下)
|
|
508
|
+
if (result.hasFollowData && result.keepFollow) {
|
|
509
|
+
const totalFollows =
|
|
510
|
+
(result.discoveredFollowing || []).length +
|
|
511
|
+
(result.discoveredFollowers || []).length;
|
|
512
|
+
if (totalFollows > 0) {
|
|
513
|
+
lastFollowSuccessTime = Date.now();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const guessedLocation = result.locationCreated || null;
|
|
518
|
+
|
|
519
|
+
const payload = {
|
|
520
|
+
userInfo: result.userInfo || {},
|
|
521
|
+
discoveredFollowing: (result.discoveredFollowing || []).map((f) => ({
|
|
522
|
+
handle: Array.isArray(f) ? f[0] : f,
|
|
523
|
+
displayName: Array.isArray(f) ? f[1] : null,
|
|
524
|
+
guessedLocation,
|
|
525
|
+
})),
|
|
526
|
+
discoveredFollowers: (result.discoveredFollowers || []).map((f) => ({
|
|
527
|
+
handle: Array.isArray(f) ? f[0] : f,
|
|
528
|
+
displayName: Array.isArray(f) ? f[1] : null,
|
|
529
|
+
guessedLocation,
|
|
530
|
+
})),
|
|
531
|
+
processed: result.processed,
|
|
532
|
+
hasFollowData: result.hasFollowData,
|
|
533
|
+
keepFollow: result.keepFollow,
|
|
534
|
+
locationCreated: result.locationCreated,
|
|
535
|
+
noVideo: result.noVideo,
|
|
536
|
+
collectedVideos: result.collectedVideos,
|
|
537
|
+
};
|
|
538
|
+
await apiPost(`${serverUrl}/api/job/${username}`, payload);
|
|
539
|
+
|
|
540
|
+
// 视频登记
|
|
541
|
+
if (result.videoList && result.videoList.length > 0) {
|
|
542
|
+
const { registered, skipped } = await apiPost(
|
|
543
|
+
`${serverUrl}/api/videos`,
|
|
544
|
+
{
|
|
545
|
+
sourceUser: username,
|
|
546
|
+
videoList: result.videoList,
|
|
547
|
+
locationCreated: result.locationCreated,
|
|
548
|
+
ttSeller: result.userInfo?.ttSeller || false,
|
|
549
|
+
},
|
|
550
|
+
);
|
|
551
|
+
console.error(
|
|
552
|
+
` 视频登记: ${registered} 条新增, ${skipped} 条已存在`,
|
|
553
|
+
);
|
|
554
|
+
}
|
|
565
555
|
|
|
566
|
-
|
|
556
|
+
console.error(" 已提交");
|
|
567
557
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
558
|
+
if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
|
|
559
|
+
console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
} catch (e) {
|
|
563
|
+
// 浏览器关闭错误:自动重建 browser + page,然后重试当前轮次
|
|
564
|
+
if (isBrowserClosedError(e)) {
|
|
565
|
+
console.error(
|
|
566
|
+
`\n[浏览器] 检测到浏览器关闭 (${e.message}),正在重建...`,
|
|
567
|
+
);
|
|
568
|
+
const newBrowser = await relaunchBrowser(
|
|
569
|
+
cdpOptions,
|
|
570
|
+
cdpOptions.port || 9222,
|
|
571
|
+
);
|
|
572
|
+
browser = newBrowser;
|
|
573
|
+
const newPage = await setupNewPage(browser);
|
|
574
|
+
Object.assign(page, newPage);
|
|
575
|
+
// 重建后等待页面稳定
|
|
576
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
577
|
+
console.error(`[浏览器] 已重建,继续处理...`);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
// 其他未预期错误:打印堆栈并跳过本轮
|
|
581
|
+
console.error(`\n[未捕获错误] ${e.message}`);
|
|
582
|
+
console.error(e.stack || "");
|
|
571
583
|
}
|
|
572
584
|
}
|
|
573
585
|
|