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.
- package/package.json +1 -1
- package/src/cli/explore.js +5 -0
- package/src/cli/refresh.js +437 -163
- package/src/lib/args.js +54 -8
- package/src/lib/browser/cdp.js +11 -5
- package/src/lib/browser/page.js +44 -20
- package/src/lib/constants.js +31 -9
- package/src/main.js +3 -0
- package/src/npm-main.js +3 -0
- package/src/watch/data-store.js +17 -7
- package/src/watch/public/app.js +5 -0
- package/src/watch/public/index.html +1 -0
- package/src/watch/server.js +3 -1
- package/src/scraper/refresh-core.js +0 -213
package/src/cli/refresh.js
CHANGED
|
@@ -3,61 +3,82 @@ import {
|
|
|
3
3
|
isBrowserClosedError,
|
|
4
4
|
relaunchBrowser,
|
|
5
5
|
} from "../lib/browser/page.js";
|
|
6
|
-
import {
|
|
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 {
|
|
10
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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 (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
72
|
+
exploreBasePort,
|
|
73
|
+
explorePortCount,
|
|
58
74
|
exploreUserId,
|
|
59
|
-
|
|
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
|
-
|
|
113
|
+
// 连接服务器验证
|
|
114
|
+
await apiGet(`${serverUrl}/api/stats`);
|
|
115
|
+
|
|
116
|
+
console.error(`\n=== Refresh 模式(基于 explore) ===`);
|
|
94
117
|
console.error(`服务器: ${serverUrl}`);
|
|
95
|
-
console.error(
|
|
96
|
-
|
|
97
|
-
console.error(
|
|
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)
|
|
102
|
-
|
|
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 (
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
serverUrl,
|
|
381
|
+
username,
|
|
144
382
|
{
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
448
|
+
username,
|
|
207
449
|
errorType,
|
|
208
450
|
errorMessage: result.error,
|
|
209
451
|
stage: "process",
|
|
210
452
|
errorStack: result.errorStack || "",
|
|
211
|
-
})
|
|
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
|
-
|
|
217
|
-
|
|
476
|
+
await withRetry("report captcha", () =>
|
|
477
|
+
apiPost(`${serverUrl}/api/error-report`, {
|
|
218
478
|
userId,
|
|
219
|
-
username
|
|
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
|
-
|
|
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
|
-
|
|
505
|
+
processedCount++;
|
|
230
506
|
|
|
231
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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
|
-
|
|
262
|
-
} catch (e) {
|
|
263
|
-
consecutiveNetworkErrors++;
|
|
264
|
-
errorCount++;
|
|
265
|
-
console.error(`\n[错误] ${e.message}`);
|
|
544
|
+
console.error(" 已提交");
|
|
266
545
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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);
|