tt-help-cli-ycl 1.3.61 → 1.3.63

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.
@@ -3,61 +3,82 @@ import {
3
3
  isBrowserClosedError,
4
4
  relaunchBrowser,
5
5
  } from "../lib/browser/page.js";
6
- import { delay, setDelayConfig } from "../scraper/modules/page-helpers.js";
6
+ import {
7
+ delay,
8
+ getDelayConfig,
9
+ setDelayConfig,
10
+ } from "../scraper/modules/page-helpers.js";
11
+ import {
12
+ detectCaptcha,
13
+ closeCaptcha,
14
+ } from "../scraper/modules/captcha-handler.js";
7
15
  import { userId as configuredUserId, saveUserId } from "../lib/constants.js";
8
16
  import { getMacOrUuid } from "../lib/mac-or-uuid.js";
9
- import { ensureBrowserReady as ensureBrowserReadyCDP } from "../lib/browser/cdp.js";
10
- import { processRefresh } from "../scraper/refresh-core.js";
17
+ import {
18
+ ensureBrowserReady as ensureBrowserReadyCDP,
19
+ switchAccount,
20
+ } from "../lib/browser/cdp.js";
21
+ import { HealthChecker } from "../lib/browser/health-checker.js";
11
22
  import path from "path";
12
23
  import os from "os";
13
24
 
14
- async function withRetry(fn, maxRetries = 5) {
15
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
25
+ const MAX_RETRY_WAIT = 5 * 60 * 1000;
26
+ const STARTUP_TIKTOK_URL = "https://www.tiktok.com/@ycl5007";
27
+
28
+ async function withRetry(label, fn) {
29
+ let backoff = 1000;
30
+ while (true) {
16
31
  try {
17
32
  return await fn();
18
- } catch (e) {
19
- if (attempt < maxRetries) {
20
- const waitTime =
21
- attempt <= 2
22
- ? 5000 + Math.random() * 5000
23
- : attempt <= 4
24
- ? 10000 + Math.random() * 10000
25
- : 20000 + Math.random() * 10000;
26
- console.error(
27
- ` [网络] 请求失败 (${attempt}/${maxRetries}),${Math.round(waitTime / 1000)}s 后重试...`,
28
- );
29
- await delay(waitTime / 1000, waitTime / 1000);
30
- } else {
31
- throw e;
32
- }
33
+ } catch (err) {
34
+ console.error(
35
+ `[连接] ${label} 失败: ${err.message},${backoff / 1000}秒后重试...`,
36
+ );
37
+ await new Promise((r) => setTimeout(r, backoff));
38
+ if (backoff < MAX_RETRY_WAIT) backoff *= 2;
33
39
  }
34
40
  }
35
41
  }
36
42
 
37
- async function apiGet(url) {
38
- const resp = await fetch(url);
39
- if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
40
- return resp.json();
43
+ async function apiPost(url, body) {
44
+ return withRetry(`POST ${url}`, async () => {
45
+ const res = await fetch(url, {
46
+ method: "POST",
47
+ headers: { "Content-Type": "application/json" },
48
+ body: JSON.stringify(body),
49
+ });
50
+ return res.json();
51
+ });
41
52
  }
42
53
 
43
- async function apiPost(url, body) {
44
- const resp = await fetch(url, {
45
- method: "POST",
46
- headers: { "Content-Type": "application/json" },
47
- body: JSON.stringify(body),
54
+ async function apiGet(url) {
55
+ return withRetry(`GET ${url}`, async () => {
56
+ const res = await fetch(url);
57
+ return res.json();
48
58
  });
49
- if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
50
- return resp.json();
51
59
  }
52
60
 
53
61
  export async function handleRefresh(options) {
54
62
  const {
55
63
  explorePreset,
64
+ exploreInterval,
65
+ exploreEnableFollow,
66
+ exploreMaxFollowing,
67
+ exploreMaxFollowers,
68
+ exploreLocation,
69
+ exploreMaxUsers,
70
+ serverUrl,
56
71
  explorePort,
57
- exploreProfile,
72
+ exploreBasePort,
73
+ explorePortCount,
58
74
  exploreUserId,
59
- serverUrl,
75
+ exploreMaxVideos,
76
+ exploreProfile,
77
+ exploreRedoMaxAge,
78
+ exploreProxy,
60
79
  } = options;
80
+
81
+ let userId = exploreUserId || configuredUserId;
61
82
  let browser = null;
62
83
  let shuttingDown = false;
63
84
 
@@ -81,7 +102,6 @@ export async function handleRefresh(options) {
81
102
  process.once("SIGTERM", onSigterm);
82
103
 
83
104
  try {
84
- let userId = exploreUserId || configuredUserId;
85
105
  if (!userId) {
86
106
  userId = await getMacOrUuid();
87
107
  saveUserId(userId);
@@ -90,196 +110,450 @@ export async function handleRefresh(options) {
90
110
 
91
111
  setDelayConfig(explorePreset);
92
112
 
93
- console.error(`\n=== Refresh 模式 ===`);
113
+ // 连接服务器验证
114
+ await apiGet(`${serverUrl}/api/stats`);
115
+
116
+ console.error(`\n=== Refresh 模式(基于 explore) ===`);
94
117
  console.error(`服务器: ${serverUrl}`);
95
- console.error(`CDP 端口: ${explorePort || 9222}, 用户编号: ${userId}`);
96
- if (exploreProfile) console.error(`浏览器配置: ${exploreProfile}`);
97
- console.error(`刷新: 视频 100 + 关注 100 + 粉丝 100`);
98
- console.error(`新用户探索: 评论 + 猜你喜欢 + 关注/粉丝`);
118
+ console.error(`视频采集: ${exploreMaxVideos || 16}`);
119
+ console.error(`关注/粉丝: ${exploreEnableFollow ? "启用" : "禁用"} (${exploreMaxFollowing}/${exploreMaxFollowers})`);
120
+ console.error(`国家筛选: ${exploreLocation}`);
121
+ console.error(`空闲间隔: ${exploreInterval || 30} 秒`);
122
+ if (exploreMaxUsers > 0) console.error(`上限: ${exploreMaxUsers} 个用户`);
123
+
124
+ const healthChecker = new HealthChecker({
125
+ basePort: exploreBasePort,
126
+ totalAccounts: explorePortCount,
127
+ });
128
+ let currentAccount = healthChecker.getCurrentAccount();
99
129
 
100
130
  const cdpOptions = {};
101
- if (explorePort) cdpOptions.port = explorePort;
102
- if (exploreProfile) {
131
+ if (explorePort) {
132
+ cdpOptions.port = explorePort;
133
+ cdpOptions.userDataDir = path.join(
134
+ os.homedir(),
135
+ "Library",
136
+ "Application Support",
137
+ `Microsoft Edge For Testing_p${explorePort}`,
138
+ );
139
+ } else if (exploreProfile) {
103
140
  cdpOptions.userDataDir = path.join(
104
141
  os.homedir(),
105
142
  "Library",
106
143
  "Application Support",
107
144
  `Microsoft Edge For Testing_${exploreProfile}`,
108
145
  );
146
+ cdpOptions.port = currentAccount.port;
147
+ } else {
148
+ cdpOptions.port = currentAccount.port;
149
+ cdpOptions.userDataDir = currentAccount.userDataDir;
150
+ }
151
+
152
+ if (exploreProxy) {
153
+ cdpOptions.proxyServer = exploreProxy;
154
+ }
155
+
156
+ console.error(`CDP 端口: ${cdpOptions.port}, 用户编号: ${userId}`);
157
+ console.error(`浏览器配置: ${path.basename(cdpOptions.userDataDir)}`);
158
+ if (!explorePort && !exploreProfile) {
159
+ const portRange = `${currentAccount.port}-${currentAccount.port + healthChecker.accounts.length - 1}`;
160
+ console.error(
161
+ `端口轮换范围: ${portRange}(共 ${healthChecker.accounts.length} 个)`,
162
+ );
109
163
  }
110
164
 
111
165
  browser = await ensureBrowserReadyCDP(cdpOptions);
166
+ const { processExplore } = await import("../scraper/explore-core.js");
167
+ const { isLoggedIn } = await import("../lib/browser/page.js");
168
+
112
169
  const page = await getOrCreatePage(browser);
113
170
 
171
+ // 导航到 TikTok 页面
172
+ await page.goto(STARTUP_TIKTOK_URL, {
173
+ waitUntil: "domcontentloaded",
174
+ });
175
+
176
+ // 检测登录状态
177
+ let loggedIn = await isLoggedIn(page);
178
+ console.error(`登录状态: ${loggedIn ? "已登录" : "未登录"}`);
179
+
180
+ // 全局拦截图片资源
181
+ await page.route("**/*", (route) => {
182
+ const resourceType = route.request().resourceType();
183
+ if (resourceType === "image" || resourceType === "stylesheet") {
184
+ route.abort();
185
+ } else {
186
+ route.continue();
187
+ }
188
+ });
189
+
190
+ // 账户切换后,重新初始化 page
191
+ async function setupNewPage(newBrowser) {
192
+ const newPage = await getOrCreatePage(newBrowser);
193
+ await newPage.route("**/*", (route) => {
194
+ const resourceType = route.request().resourceType();
195
+ if (resourceType === "image" || resourceType === "stylesheet") {
196
+ route.abort();
197
+ } else {
198
+ route.continue();
199
+ }
200
+ });
201
+ return newPage;
202
+ }
203
+
204
+ async function handleAccountSwitch(reason) {
205
+ console.error(`\n[健康检查] ${reason},切换到下一个账户...`);
206
+ const oldAccount = currentAccount;
207
+ const nextAccount = healthChecker.getNextAccount();
208
+ currentAccount = nextAccount;
209
+ const newBrowser = await switchAccount(
210
+ { port: oldAccount.port, userDataDir: oldAccount.userDataDir },
211
+ { port: nextAccount.port, userDataDir: nextAccount.userDataDir },
212
+ );
213
+ browser = newBrowser;
214
+ const newPage = await setupNewPage(browser);
215
+ Object.assign(page, newPage);
216
+ Object.assign(cdpOptions, {
217
+ port: nextAccount.port,
218
+ userDataDir: nextAccount.userDataDir,
219
+ });
220
+ console.error(`[健康检查] 已切换到端口 ${nextAccount.port}`);
221
+ await page.goto(STARTUP_TIKTOK_URL, {
222
+ waitUntil: "domcontentloaded",
223
+ });
224
+ loggedIn = await isLoggedIn(page);
225
+ console.error(
226
+ `[健康检查] 新账户登录状态: ${loggedIn ? "已登录" : "未登录"}`,
227
+ );
228
+ lastFollowSuccessTime = Date.now();
229
+ captchaCount = 0;
230
+ blockedCount = 0;
231
+ consecutiveNetworkErrors = 0;
232
+ }
233
+
114
234
  let processedCount = 0;
115
235
  let errorCount = 0;
116
236
  let consecutiveNetworkErrors = 0;
237
+ let captchaCount = 0;
238
+ let blockedCount = 0;
239
+ let lastFollowSuccessTime = Date.now();
240
+ const FOLLOW_BLOCK_THRESHOLD = 10 * 60 * 1000;
117
241
 
118
242
  console.error(`\n开始循环刷新任务...\n`);
119
243
 
120
- while (!shuttingDown) {
244
+ while (true) {
245
+ if (shuttingDown) break;
246
+
247
+ // 健康检查
248
+ const checkResult = healthChecker.check();
249
+ if (checkResult.shouldSwitch) {
250
+ await handleAccountSwitch(checkResult.reason);
251
+ } else if (checkResult.info && processedCount % 10 === 0) {
252
+ const followElapsed = loggedIn && exploreEnableFollow
253
+ ? Math.round((Date.now() - lastFollowSuccessTime) / 60000)
254
+ : 0;
255
+ const followStatus =
256
+ loggedIn && exploreEnableFollow
257
+ ? ` | 关注/粉丝上次成功 ${followElapsed} 分钟前`
258
+ : "";
259
+ console.error(`\n[健康检查] ${checkResult.info}${followStatus}`);
260
+ }
261
+
262
+ // 领取 redo 任务
263
+ let redoUrl = `${serverUrl}/api/redo-job?userId=${userId}`;
264
+ if (exploreRedoMaxAge > 0) {
265
+ redoUrl += `&maxAge=${exploreRedoMaxAge}`;
266
+ }
267
+ let job = null;
121
268
  try {
122
- const jobData = await withRetry(() =>
123
- apiGet(`${serverUrl}/api/redo-job?userId=${userId}`),
269
+ job = await apiGet(redoUrl);
270
+ } catch (e) {
271
+ consecutiveNetworkErrors++;
272
+ console.error(
273
+ ` [网络] 获取任务失败 (${consecutiveNetworkErrors}): ${e.message}`
124
274
  );
275
+ await new Promise((r) => setTimeout(r, 10000));
276
+ continue;
277
+ }
125
278
 
126
- if (!jobData.hasJob) {
127
- console.error(`\n[空闲] 暂无 redo 任务,30s 后重试...`);
128
- await delay(30000, 30000);
129
- continue;
279
+ if (!job.hasJob) {
280
+ console.error(`\n[空闲] 暂无 redo 任务,${exploreInterval || 30}s 后重试...`);
281
+ await new Promise((r) => setTimeout(r, (exploreInterval || 30) * 1000));
282
+ consecutiveNetworkErrors = 0;
283
+ continue;
284
+ }
285
+
286
+ consecutiveNetworkErrors = 0;
287
+ const username = job.user.uniqueId;
288
+ const nickname = job.user.nickname;
289
+ console.error(`\n[${processedCount}] 刷新 @${username}...`);
290
+
291
+ // 切换任务前检测验证码
292
+ const captchaResult = await detectCaptcha(page);
293
+ if (captchaResult && captchaResult.visible) {
294
+ console.error(`\n[验证码] 切换任务前检测到验证码,等待3分钟...`);
295
+ captchaCount++;
296
+ console.error(` [验证码] 累计 ${captchaCount} 次`);
297
+
298
+ await new Promise((r) => setTimeout(r, 180000));
299
+
300
+ const closeResult = await closeCaptcha(page);
301
+ if (closeResult.success) {
302
+ console.error(" [验证码] 已关闭验证码");
303
+ await new Promise((r) => setTimeout(r, 10000));
304
+ } else {
305
+ console.error(" [验证码] 无法关闭验证码,继续处理");
130
306
  }
131
307
 
132
- const { uniqueId, nickname } = jobData.user;
133
- consecutiveNetworkErrors = 0;
134
- processedCount++;
308
+ await withRetry("report captcha", () =>
309
+ apiPost(`${serverUrl}/api/error-report`, {
310
+ userId,
311
+ username: "unknown",
312
+ errorType: "captcha",
313
+ errorMessage: "切换任务前检测到验证码",
314
+ stage: "between-tasks",
315
+ errorStack: "",
316
+ }),
317
+ ).catch(() => {});
135
318
 
136
- console.error(
137
- `\n[${processedCount}] 刷新 @${uniqueId} (${nickname || "未知"})...`,
138
- );
319
+ if (captchaCount >= 2) {
320
+ await handleAccountSwitch(`验证码累计 ${captchaCount} 次`);
321
+ captchaCount = 0;
322
+ }
323
+ }
324
+
325
+ // 关注/粉丝封禁检测
326
+ if (loggedIn && exploreEnableFollow) {
327
+ const followElapsed = Date.now() - lastFollowSuccessTime;
328
+ if (followElapsed > FOLLOW_BLOCK_THRESHOLD) {
329
+ const minutes = Math.round(followElapsed / 60000);
330
+ console.error(
331
+ `\n[封禁检测] 关注/粉丝功能已 ${minutes} 分钟未成功获取,切换到下一个账户...`,
332
+ );
333
+ await handleAccountSwitch(`关注/粉丝功能被封`);
334
+ }
335
+ }
336
+
337
+ if (consecutiveNetworkErrors > 0) {
338
+ const waitTime =
339
+ consecutiveNetworkErrors <= 2
340
+ ? 0
341
+ : consecutiveNetworkErrors <= 5
342
+ ? 30000
343
+ : 300000;
344
+ if (waitTime > 0) {
345
+ console.error(
346
+ ` [网络] 连续 ${consecutiveNetworkErrors} 次网络异常,等待 ${waitTime / 1000}s 后重试...`,
347
+ );
348
+ await new Promise((r) => setTimeout(r, waitTime));
349
+ }
350
+ }
139
351
 
140
- const result = await processRefresh(
352
+ const { switchMax } = getDelayConfig();
353
+ await delay(switchMax, switchMax * 3);
354
+
355
+ let result = await processExplore(
356
+ page,
357
+ username,
358
+ {
359
+ maxVideos: exploreMaxVideos || 16,
360
+ enableFollow: exploreEnableFollow !== false,
361
+ loggedIn,
362
+ maxFollowing: exploreMaxFollowing || 100,
363
+ maxFollowers: exploreMaxFollowers || 100,
364
+ location: exploreLocation,
365
+ browser,
366
+ },
367
+ console.error,
368
+ );
369
+
370
+ // 浏览器关闭检测
371
+ if (result.error && isBrowserClosedError(new Error(result.error))) {
372
+ const newBrowser = await relaunchBrowser(
373
+ cdpOptions,
374
+ cdpOptions.port || 9222,
375
+ );
376
+ browser = newBrowser;
377
+ const newPage = await setupNewPage(browser);
378
+ Object.assign(page, newPage);
379
+ result = await processExplore(
141
380
  page,
142
- uniqueId,
143
- serverUrl,
381
+ username,
144
382
  {
145
- maxFollowing: 100,
146
- maxFollowers: 100,
147
- maxVideos: 100,
383
+ maxVideos: exploreMaxVideos || 16,
384
+ enableFollow: exploreEnableFollow !== false,
385
+ loggedIn,
386
+ maxFollowing: exploreMaxFollowing || 100,
387
+ maxFollowers: exploreMaxFollowers || 100,
388
+ location: exploreLocation,
389
+ browser,
148
390
  },
149
391
  console.error,
150
392
  );
393
+ }
151
394
 
152
- if (result.restricted) {
153
- console.error(` @${uniqueId} 页面受限,跳过`);
154
- await apiPost(`${serverUrl}/api/redo-job/${uniqueId}`, {
155
- restricted: true,
156
- userInfo: result.userInfo || {},
157
- });
158
- continue;
395
+ if (result.restricted) {
396
+ consecutiveNetworkErrors = 0;
397
+ await apiPost(`${serverUrl}/api/redo-job/${username}`, {
398
+ restricted: true,
399
+ userInfo: result.userInfo || {},
400
+ });
401
+ if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
402
+ console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
403
+ break;
159
404
  }
405
+ continue;
406
+ }
160
407
 
161
- if (result.error) {
162
- // 浏览器关闭检测
163
- if (isBrowserClosedError(new Error(result.error))) {
164
- const newBrowser = await relaunchBrowser(
165
- cdpOptions,
166
- explorePort || 9222,
167
- );
168
- browser = newBrowser;
169
- const newPage = await getOrCreatePage(browser);
170
- Object.assign(page, newPage);
171
- // 重试当前用户
172
- const retryResult = await processRefresh(
173
- page,
174
- uniqueId,
175
- serverUrl,
176
- {
177
- maxFollowing: 100,
178
- maxFollowers: 100,
179
- maxVideos: 100,
180
- },
181
- console.error,
182
- );
183
- Object.assign(result, retryResult);
184
- // 继续下方逻辑,检查重试后的 result
185
- } else {
186
- consecutiveNetworkErrors++;
187
- errorCount++;
188
- console.error(` [错误] ${result.error}`);
189
-
190
- if (consecutiveNetworkErrors >= 3) {
191
- console.error(
192
- ` [警告] 连续 ${consecutiveNetworkErrors} 次错误,等待 60s 后重试...`,
193
- );
194
- await delay(60000, 60000);
195
- consecutiveNetworkErrors = 0;
196
- }
408
+ if (result.error) {
409
+ consecutiveNetworkErrors++;
410
+ errorCount++;
411
+
412
+ // 临时性错误:自动重试一次
413
+ if (result.retryable) {
414
+ console.error(` [临时错误] 等待 5 秒后重试 @${username}...`);
415
+ await new Promise((r) => setTimeout(r, 5000));
416
+ result = await processExplore(
417
+ page,
418
+ username,
419
+ {
420
+ maxVideos: exploreMaxVideos || 16,
421
+ enableFollow: exploreEnableFollow !== false,
422
+ loggedIn,
423
+ maxFollowing: exploreMaxFollowing || 100,
424
+ maxFollowers: exploreMaxFollowers || 100,
425
+ location: exploreLocation,
426
+ browser,
427
+ },
428
+ console.error,
429
+ );
430
+ if (!result.error) {
431
+ consecutiveNetworkErrors = 0;
432
+ errorCount--;
433
+ }
434
+ }
197
435
 
198
- await apiPost(`${serverUrl}/api/redo-job/${uniqueId}`, {
199
- error: result.error,
200
- userInfo: result.userInfo || {},
201
- });
202
- const errorType =
203
- consecutiveNetworkErrors > 1 ? "network" : "other";
436
+ if (result.error) {
437
+ await apiPost(`${serverUrl}/api/redo-job/${username}`, {
438
+ error: result.error,
439
+ });
440
+ const errorType = result.error.startsWith("被封:")
441
+ ? "被封"
442
+ : consecutiveNetworkErrors > 1
443
+ ? "network"
444
+ : "other";
445
+ await withRetry("report error", () =>
204
446
  apiPost(`${serverUrl}/api/error-report`, {
205
447
  userId,
206
- username: uniqueId,
448
+ username,
207
449
  errorType,
208
450
  errorMessage: result.error,
209
451
  stage: "process",
210
452
  errorStack: result.errorStack || "",
211
- }).catch(() => {});
453
+ }),
454
+ ).catch(() => {});
455
+ if (errorType === "被封") {
456
+ blockedCount++;
457
+ console.error(` [被封] 累计 ${blockedCount} 次`);
458
+ if (blockedCount >= 3) {
459
+ await handleAccountSwitch(`账号被封累计 ${blockedCount} 次`);
460
+ blockedCount = 0;
461
+ }
212
462
  continue;
213
463
  }
464
+ if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
465
+ console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
466
+ break;
467
+ }
468
+ continue;
214
469
  }
470
+ }
471
+
472
+ if (result.captchaDetected) {
473
+ captchaCount++;
474
+ console.error(` [验证码] 累计 ${captchaCount} 次`);
215
475
 
216
- if (result.captchaDetected) {
217
- await apiPost(`${serverUrl}/api/error-report`, {
476
+ await withRetry("report captcha", () =>
477
+ apiPost(`${serverUrl}/api/error-report`, {
218
478
  userId,
219
- username: uniqueId,
479
+ username,
220
480
  errorType: "captcha",
221
481
  errorMessage: result.captchaMessage || "页面出现验证码",
222
482
  stage: result.captchaStage || "video-page",
223
483
  errorStack: "",
224
- });
484
+ }),
485
+ ).catch(() => {});
486
+
487
+ if (captchaCount >= 2) {
488
+ await handleAccountSwitch(`验证码累计 ${captchaCount} 次`);
489
+ captchaCount = 0;
225
490
  }
491
+ }
226
492
 
227
- consecutiveNetworkErrors = 0;
493
+ consecutiveNetworkErrors = 0;
494
+
495
+ // 更新关注/粉丝成功时间
496
+ if (result.hasFollowData && result.keepFollow) {
497
+ const totalFollows =
498
+ (result.discoveredFollowing || []).length +
499
+ (result.discoveredFollowers || []).length;
500
+ if (totalFollows > 0) {
501
+ lastFollowSuccessTime = Date.now();
502
+ }
503
+ }
228
504
 
229
- const guessedLocation = result.locationCreated || null;
505
+ processedCount++;
230
506
 
231
- await apiPost(`${serverUrl}/api/redo-job/${uniqueId}`, {
232
- userInfo: result.userInfo || {},
233
- discoveredVideoAuthors: (result.discoveredVideoAuthors || []).map(
234
- (item) =>
235
- typeof item === "object" ? { ...item, guessedLocation } : item,
236
- ),
237
- discoveredCommentAuthors: (result.discoveredCommentAuthors || []).map(
238
- (author) => ({ author, guessedLocation }),
239
- ),
240
- discoveredGuessAuthors: (result.discoveredGuessAuthors || []).map(
241
- (author) => ({ author, guessedLocation }),
242
- ),
243
- discoveredFollowing: (result.discoveredFollowing || []).map((f) => ({
244
- handle: Array.isArray(f) ? f[0] : f,
245
- displayName: Array.isArray(f) ? f[1] : null,
246
- guessedLocation,
247
- })),
248
- discoveredFollowers: (result.discoveredFollowers || []).map((f) => ({
249
- handle: Array.isArray(f) ? f[0] : f,
250
- displayName: Array.isArray(f) ? f[1] : null,
251
- guessedLocation,
252
- })),
253
- newUsersAdded: result.newUsersAdded || 0,
254
- collectedVideos: result.collectedVideos || 0,
255
- });
507
+ const guessedLocation = result.locationCreated || null;
256
508
 
257
- console.error(
258
- ` [完成] 视频: ${result.collectedVideos}, 评论作者: ${result.discoveredCommentAuthors?.length || 0}, 关注: ${result.discoveredFollowing?.length || 0}, 粉丝: ${result.discoveredFollowers?.length || 0}, 新增用户: ${result.newUsersAdded}`,
509
+ const payload = {
510
+ userInfo: result.userInfo || {},
511
+ discoveredFollowing: (result.discoveredFollowing || []).map((f) => ({
512
+ handle: Array.isArray(f) ? f[0] : f,
513
+ displayName: Array.isArray(f) ? f[1] : null,
514
+ guessedLocation,
515
+ })),
516
+ discoveredFollowers: (result.discoveredFollowers || []).map((f) => ({
517
+ handle: Array.isArray(f) ? f[0] : f,
518
+ displayName: Array.isArray(f) ? f[1] : null,
519
+ guessedLocation,
520
+ })),
521
+ processed: result.processed,
522
+ hasFollowData: result.hasFollowData,
523
+ keepFollow: result.keepFollow,
524
+ locationCreated: result.locationCreated,
525
+ noVideo: result.noVideo,
526
+ collectedVideos: result.collectedVideos,
527
+ };
528
+ await apiPost(`${serverUrl}/api/redo-job/${username}`, payload);
529
+
530
+ // 视频登记
531
+ if (result.videoList && result.videoList.length > 0) {
532
+ const { registered, skipped } = await apiPost(
533
+ `${serverUrl}/api/videos`,
534
+ {
535
+ sourceUser: username,
536
+ videoList: result.videoList,
537
+ locationCreated: result.locationCreated,
538
+ ttSeller: result.userInfo?.ttSeller || false,
539
+ },
259
540
  );
541
+ console.error(` 视频登记: ${registered} 条新增, ${skipped} 条已存在`);
542
+ }
260
543
 
261
- await delay(3000, 5000);
262
- } catch (e) {
263
- consecutiveNetworkErrors++;
264
- errorCount++;
265
- console.error(`\n[错误] ${e.message}`);
544
+ console.error(" 已提交");
266
545
 
267
- if (consecutiveNetworkErrors >= 3) {
268
- console.error(
269
- `[警告] 连续 ${consecutiveNetworkErrors} 次网络异常,等待 60s 后重试...`,
270
- );
271
- await delay(60000, 60000);
272
- consecutiveNetworkErrors = 0;
273
- } else {
274
- const waitTime =
275
- consecutiveNetworkErrors <= 2
276
- ? 5000 + Math.random() * 5000
277
- : 10000 + Math.random() * 10000;
278
- console.error(` 等待 ${Math.round(waitTime / 1000)}s 后重试...`);
279
- await delay(waitTime / 1000, waitTime / 1000);
280
- }
546
+ if (exploreMaxUsers > 0 && processedCount >= exploreMaxUsers) {
547
+ console.error(`\n已达上限 ${exploreMaxUsers} 个用户,停止处理`);
548
+ break;
281
549
  }
282
550
  }
551
+
552
+ const stats = await apiGet(`${serverUrl}/api/stats`);
553
+ console.error(`\n完成: ${processedCount} 个用户处理, ${errorCount} 个出错`);
554
+ console.error(
555
+ ` 总用户: ${stats.totalUsers}, 已完成: ${stats.processedUsers}, 待处理: ${stats.pendingUsers}, 错误: ${stats.errorUsers}`,
556
+ );
283
557
  } finally {
284
558
  process.removeListener("SIGINT", onSigint);
285
559
  process.removeListener("SIGTERM", onSigterm);