tt-help-cli-ycl 1.4.172 → 1.4.176
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/reset-login-pool.mjs +66 -0
- package/scripts/test-login-pool-e2e-real.mjs +134 -0
- package/scripts/test-login-pool-ui.mjs +211 -0
- package/scripts/test-login-pool.mjs +381 -0
- package/scripts/test-subscription-allocator.mjs +52 -0
- package/scripts/test-watchdog-subs-retry.mjs +123 -0
- package/src/clash/runtime.js +8 -4
- package/src/cli/explore.js +269 -23
- package/src/cli/search.js +241 -56
- package/src/cli/watchdog.js +34 -0
- package/src/lib/args.js +15 -2
- package/src/lib/browser/cdp.js +12 -2
- package/src/lib/browser/health-checker.js +16 -13
- package/src/lib/linux-env-init.js +258 -0
- package/src/lib/login-pool-client.js +156 -0
- package/src/scraper/modules/follow-extractor.js +23 -5
- package/src/scraper/modules/guess-extractor.js +4 -1
- package/src/watch/login-pool.js +755 -0
- package/src/watch/public/app.js +90 -44
- package/src/watch/public/index.html +17 -3
- package/src/watch/server.js +252 -0
- package/src/watch/subscription-allocator.js +18 -5
- package/src/watch/subscription-center.js +18 -2
- package/src/watch/watchdog-server.js +113 -0
- package/src/watchdog/agent.js +87 -281
- package/src/watchdog/login-apply.js +232 -0
- package/src/watchdog/login-import.js +180 -0
- package/src/watchdog/upgrader.js +59 -5
package/src/cli/explore.js
CHANGED
|
@@ -26,7 +26,18 @@ import {
|
|
|
26
26
|
switchAccount,
|
|
27
27
|
} from "../lib/browser/cdp.js";
|
|
28
28
|
import { showResourceUsage } from "../utils/index.js";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
HealthChecker,
|
|
31
|
+
MEMORY_THRESHOLD,
|
|
32
|
+
ROTATE_INTERVAL_MS,
|
|
33
|
+
} from "../lib/browser/health-checker.js";
|
|
34
|
+
import {
|
|
35
|
+
createLoginPoolClient,
|
|
36
|
+
whoAmI,
|
|
37
|
+
gotoTiktok,
|
|
38
|
+
injectPoolAccount,
|
|
39
|
+
ensurePoolLogin,
|
|
40
|
+
} from "../lib/login-pool-client.js";
|
|
30
41
|
import { createApiClient } from "../lib/api-client.js";
|
|
31
42
|
import { createCDNHandler } from "../lib/cdn-handler.js";
|
|
32
43
|
import { proxyPortOf, triggerProxySwitch } from "../clash/proxy-switch.js";
|
|
@@ -67,6 +78,7 @@ export async function handleExplore(options) {
|
|
|
67
78
|
exploreMaxUsers,
|
|
68
79
|
serverUrl,
|
|
69
80
|
explorePort,
|
|
81
|
+
exploreNoPool,
|
|
70
82
|
exploreBasePort,
|
|
71
83
|
explorePortCount,
|
|
72
84
|
exploreUserId,
|
|
@@ -83,11 +95,22 @@ export async function handleExplore(options) {
|
|
|
83
95
|
let browser = null;
|
|
84
96
|
let cdpOptions = null;
|
|
85
97
|
let shuttingDown = false;
|
|
98
|
+
// 登录池(pool 模式):账号从账号中心动态取,原地切号替代换端口
|
|
99
|
+
let poolClient = null; // 账号中心客户端
|
|
100
|
+
let poolAccount = null; // 当前在手账号(含 cookies 与租约 id)
|
|
101
|
+
let poolAccountSince = Date.now(); // 当前账号开始使用时刻(48min 换号计时)
|
|
86
102
|
|
|
87
103
|
const shutdown = async (signal) => {
|
|
88
104
|
if (shuttingDown) return;
|
|
89
105
|
shuttingDown = true;
|
|
90
|
-
console.error(`\n[Explore] 收到 ${signal}
|
|
106
|
+
console.error(`\n[Explore] 收到 ${signal},正在关闭...`);
|
|
107
|
+
// 登录池模式:先还号(本地 profile 保留当前登录态,clearOnNode 默认否),再关浏览器
|
|
108
|
+
if (poolClient && poolAccount) {
|
|
109
|
+
await poolClient
|
|
110
|
+
.release(poolAccount)
|
|
111
|
+
.then(() => console.error(`[登录池] 已释放 @${poolAccount.username}`))
|
|
112
|
+
.catch((e) => console.error(`[登录池] 释放失败(忽略): ${e.message}`));
|
|
113
|
+
}
|
|
91
114
|
// 先杀死 Edge 进程(CDP 模式下 browser.close() 只断开连接,不杀进程)
|
|
92
115
|
if (cdpOptions) {
|
|
93
116
|
await killEdgeProcesses(cdpOptions.userDataDir, cdpOptions.port);
|
|
@@ -141,14 +164,28 @@ export async function handleExplore(options) {
|
|
|
141
164
|
console.error(`服务器: ${serverUrl}(断开会自动重连)`);
|
|
142
165
|
if (exploreMaxUsers > 0) console.error(`上限: ${exploreMaxUsers} 个用户`);
|
|
143
166
|
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
167
|
+
const poolMode = !exploreNoPool;
|
|
168
|
+
const poolPort = explorePort || exploreBasePort || 9222;
|
|
169
|
+
const healthChecker = poolMode
|
|
170
|
+
? new HealthChecker({ basePort: poolPort, totalAccounts: 1 })
|
|
171
|
+
: new HealthChecker({
|
|
172
|
+
basePort: exploreBasePort,
|
|
173
|
+
totalAccounts: explorePortCount,
|
|
174
|
+
});
|
|
148
175
|
let currentAccount = healthChecker.getCurrentAccount();
|
|
149
176
|
|
|
150
177
|
cdpOptions = {};
|
|
151
|
-
if (
|
|
178
|
+
if (poolMode) {
|
|
179
|
+
// 登录池模式:端口退化为节点本地固定槽位(无轮换),账号从账号中心动态取。
|
|
180
|
+
// --base-port 兼容旧命令配置:作为固定口使用(轮换已取消);--port-count 无意义,打日志忽略
|
|
181
|
+
cdpOptions.port = explorePort || exploreBasePort || 9222;
|
|
182
|
+
cdpOptions.userDataDir = path.join(
|
|
183
|
+
os.homedir(),
|
|
184
|
+
"Library",
|
|
185
|
+
"Application Support",
|
|
186
|
+
`Microsoft Edge For Testing_p${cdpOptions.port}`,
|
|
187
|
+
);
|
|
188
|
+
} else if (explorePort) {
|
|
152
189
|
// 固定端口模式(调试用,关闭自动轮换)
|
|
153
190
|
cdpOptions.port = explorePort;
|
|
154
191
|
cdpOptions.userDataDir = path.join(
|
|
@@ -178,6 +215,21 @@ export async function handleExplore(options) {
|
|
|
178
215
|
|
|
179
216
|
console.error(`CDP 端口: ${cdpOptions.port}, 用户编号: ${userId}`);
|
|
180
217
|
console.error(`浏览器配置: ${path.basename(cdpOptions.userDataDir)}`);
|
|
218
|
+
if (poolMode) {
|
|
219
|
+
console.error(
|
|
220
|
+
`登录池模式: 账号中心 ${serverUrl},端口固定 ${cdpOptions.port}(--no-pool 回退端口轮换)`,
|
|
221
|
+
);
|
|
222
|
+
if (exploreBasePort && !explorePort) {
|
|
223
|
+
console.error(
|
|
224
|
+
` [--base-port ${exploreBasePort}] 在 pool 模式下作为固定端口使用(轮换已取消)`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (explorePortCount != null) {
|
|
228
|
+
console.error(
|
|
229
|
+
` [--port-count ${explorePortCount}] 在 pool 模式下无意义(无端口轮换),已忽略`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
181
233
|
|
|
182
234
|
const { apiGet, apiPost } = createApiClient({
|
|
183
235
|
meta: { port: cdpOptions.port },
|
|
@@ -191,7 +243,7 @@ export async function handleExplore(options) {
|
|
|
191
243
|
});
|
|
192
244
|
console.error(`种子用户: ${added} 个新增, ${skipped} 个已存在`);
|
|
193
245
|
}
|
|
194
|
-
if (!explorePort) {
|
|
246
|
+
if (!poolMode && !explorePort) {
|
|
195
247
|
const portRange = `${currentAccount.port}-${currentAccount.port + healthChecker.accounts.length - 1}`;
|
|
196
248
|
console.error(
|
|
197
249
|
`端口轮换范围: ${portRange}(共 ${healthChecker.accounts.length} 个)`,
|
|
@@ -212,8 +264,50 @@ export async function handleExplore(options) {
|
|
|
212
264
|
});
|
|
213
265
|
});
|
|
214
266
|
|
|
215
|
-
//
|
|
216
|
-
let loggedIn =
|
|
267
|
+
// 检测登录状态(legacy 模式启动时只检测一次;pool 模式在取号注入后置位)
|
|
268
|
+
let loggedIn = false;
|
|
269
|
+
if (poolMode) {
|
|
270
|
+
poolClient = createLoginPoolClient({
|
|
271
|
+
serverUrl,
|
|
272
|
+
userId,
|
|
273
|
+
port: cdpOptions.port,
|
|
274
|
+
appName: "explore",
|
|
275
|
+
});
|
|
276
|
+
// 取号 + 注入验证(60s 退避重试至 30min,仍失败退出交 watchdog 重启循环)
|
|
277
|
+
const startupDeadline = Date.now() + 30 * 60 * 1000;
|
|
278
|
+
while (!poolAccount) {
|
|
279
|
+
try {
|
|
280
|
+
poolAccount = await poolClient.acquire();
|
|
281
|
+
} catch (e) {
|
|
282
|
+
if (Date.now() > startupDeadline || shuttingDown) {
|
|
283
|
+
await poolFatalExit(`取号失败超过 30 分钟(${e.message})`);
|
|
284
|
+
}
|
|
285
|
+
console.error(`[登录池] 取号失败: ${e.message},60 秒后重试...`);
|
|
286
|
+
await new Promise((r) => setTimeout(r, 60 * 1000));
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
// 以号为主键:拿到的号直接注入验证(本地磁盘态与取号结果无关)
|
|
290
|
+
const r = await injectPoolAccount(page, poolAccount, {
|
|
291
|
+
verifyUsername: poolAccount.username,
|
|
292
|
+
}).catch((e) => ({ ok: false, reason: e.message }));
|
|
293
|
+
if (r.ok) {
|
|
294
|
+
loggedIn = true;
|
|
295
|
+
console.error(`[登录池] @${poolAccount.username} 注入验证 ✓`);
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
console.error(
|
|
299
|
+
`[登录池] @${poolAccount.username} 注入验证失败(${r.reason}),释放回池换下一个...`,
|
|
300
|
+
);
|
|
301
|
+
await poolClient.release(poolAccount).catch(() => {});
|
|
302
|
+
poolAccount = null;
|
|
303
|
+
if (Date.now() > startupDeadline || shuttingDown) {
|
|
304
|
+
await poolFatalExit("启动取号注入未成功(30 分钟窗口耗尽)");
|
|
305
|
+
}
|
|
306
|
+
await new Promise((r) => setTimeout(r, 10 * 1000));
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
loggedIn = await safeCheckLogin(page);
|
|
310
|
+
}
|
|
217
311
|
|
|
218
312
|
// 全局拦截图片资源,减少内存占用和加载时间
|
|
219
313
|
await page.route("**/*", (route) => {
|
|
@@ -239,7 +333,117 @@ export async function handleExplore(options) {
|
|
|
239
333
|
return newPage;
|
|
240
334
|
}
|
|
241
335
|
|
|
336
|
+
// ── 登录池模式辅助 ──
|
|
337
|
+
|
|
338
|
+
/** 致命退出:杀 Edge + 退出(启动期取号/注入超时等) */
|
|
339
|
+
async function poolFatalExit(msg) {
|
|
340
|
+
console.error(`[登录池] ${msg}`);
|
|
341
|
+
if (cdpOptions) {
|
|
342
|
+
await killEdgeProcesses(cdpOptions.userDataDir, cdpOptions.port).catch(() => {});
|
|
343
|
+
}
|
|
344
|
+
await browser?.close().catch(() => {});
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* 换号(以号为主键):先取新号(旧号仍在手,无空窗)→ 注入验证 → 还旧号。
|
|
350
|
+
* 候选最多试 5 个(注入失败的释放回池);池空/服务器不可达 → 保旧号返回 false,
|
|
351
|
+
* 由主循环「无在手账号」补取逻辑兑底。
|
|
352
|
+
*/
|
|
353
|
+
async function switchPoolAccount(reason) {
|
|
354
|
+
console.error(`\n[换号] ${reason},从账号中心取新账号...`);
|
|
355
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
356
|
+
let next = null;
|
|
357
|
+
try {
|
|
358
|
+
next = await poolClient.acquire();
|
|
359
|
+
} catch (e) {
|
|
360
|
+
console.error(
|
|
361
|
+
`[换号] 取新号失败(${e.message})${poolAccount ? `,保留当前账号 @${poolAccount.username}` : ",将在主循环补取"}`,
|
|
362
|
+
);
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
const r = await injectPoolAccount(page, next, {
|
|
366
|
+
verifyUsername: next.username,
|
|
367
|
+
}).catch((e) => ({ ok: false, reason: e.message }));
|
|
368
|
+
if (!r.ok) {
|
|
369
|
+
console.error(
|
|
370
|
+
`[换号] @${next.username} 注入验证失败(${r.reason}),释放候选换下一个...`,
|
|
371
|
+
);
|
|
372
|
+
await poolClient.release(next).catch(() => {});
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
// 新号验证通过:切换在手状态,再还旧号(谁持账号谁还)
|
|
376
|
+
const prev = poolAccount;
|
|
377
|
+
poolAccount = next;
|
|
378
|
+
poolAccountSince = Date.now();
|
|
379
|
+
loggedIn = true;
|
|
380
|
+
lastFollowSuccessTime = Date.now();
|
|
381
|
+
captchaCount = 0;
|
|
382
|
+
blockedCount = 0;
|
|
383
|
+
cdn.reset();
|
|
384
|
+
consecutiveNetworkErrors = 0;
|
|
385
|
+
console.error(`[换号] @${prev ? prev.username + " → " : ""}@${next.username} 注入验证通过 ✓`);
|
|
386
|
+
if (prev) {
|
|
387
|
+
await poolClient
|
|
388
|
+
.release(prev)
|
|
389
|
+
.then(() => console.error(`[换号] 旧号 @${prev.username} 已还池`))
|
|
390
|
+
.catch((e) =>
|
|
391
|
+
console.error(`[换号] 旧号释放失败(忽略,TTL 兑底): ${e.message}`),
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
console.error(`[换号] 连续 5 个候选注入验证失败,保留现场等待下轮重试`);
|
|
397
|
+
await apiPost(`${serverUrl}/api/error-report`, {
|
|
398
|
+
userId,
|
|
399
|
+
username: poolAccount ? poolAccount.username : "unknown",
|
|
400
|
+
errorType: "account_switch_failed",
|
|
401
|
+
errorMessage: "连续5个候选账号注入验证失败(详情见节点日志)",
|
|
402
|
+
stage: "account-switch",
|
|
403
|
+
}).catch(() => {});
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** 浏览器重建/重启后恢复登录态:磁盘态一致则免注入,否则用内存 cookies 补注入 */
|
|
408
|
+
async function restorePoolLogin() {
|
|
409
|
+
if (!poolAccount) return false;
|
|
410
|
+
const r = await ensurePoolLogin(page, poolAccount).catch((e) => ({
|
|
411
|
+
ok: false,
|
|
412
|
+
reason: e.message,
|
|
413
|
+
}));
|
|
414
|
+
if (r.ok) {
|
|
415
|
+
loggedIn = true;
|
|
416
|
+
console.error(
|
|
417
|
+
`[登录池] 登录态已恢复 @${poolAccount.username}${r.injected ? "(重新注入)" : "(磁盘态一致)"}`,
|
|
418
|
+
);
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
console.error(`[登录池] 登录态恢复失败(${r.reason}),转入换号流程`);
|
|
422
|
+
return await switchPoolAccount(`浏览器重启后无法恢复 @${poolAccount.username} 的登录态`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** 内存超限:同号重启浏览器(不换号——内存是浏览器进程问题,不是账号问题) */
|
|
426
|
+
async function restartPoolBrowser(reason) {
|
|
427
|
+
console.error(
|
|
428
|
+
`\n[浏览器] ${reason},同号重启浏览器(@${poolAccount ? poolAccount.username : "无在手账号"})...`,
|
|
429
|
+
);
|
|
430
|
+
try {
|
|
431
|
+
const relaunched = await relaunchBrowser(cdpOptions, cdpOptions.port || 9222);
|
|
432
|
+
browser = relaunched;
|
|
433
|
+
const rebuiltPage = await setupNewPage(browser);
|
|
434
|
+
Object.assign(page, rebuiltPage);
|
|
435
|
+
} catch (e) {
|
|
436
|
+
console.error(`[浏览器] 重启失败: ${e.message},转入换号流程`);
|
|
437
|
+
return await switchPoolAccount(`浏览器重启失败(${e.message})`);
|
|
438
|
+
}
|
|
439
|
+
return await restorePoolLogin();
|
|
440
|
+
}
|
|
441
|
+
|
|
242
442
|
async function handleAccountSwitch(reason) {
|
|
443
|
+
if (poolMode) {
|
|
444
|
+
await switchPoolAccount(reason);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
243
447
|
console.error(`\n[健康检查] ${reason},切换到下一个账户...`);
|
|
244
448
|
const oldAccount = currentAccount;
|
|
245
449
|
const nextAccount = healthChecker.getNextAccount();
|
|
@@ -407,6 +611,14 @@ export async function handleExplore(options) {
|
|
|
407
611
|
|
|
408
612
|
while (!shuttingDown) {
|
|
409
613
|
try {
|
|
614
|
+
// 登录池模式:无在手账号(上次换号未完成/候选全败)先补取,补不到退避后重来
|
|
615
|
+
if (poolMode && !poolAccount) {
|
|
616
|
+
const ok = await switchPoolAccount("补取账号(当前无在手租约)");
|
|
617
|
+
if (!ok) {
|
|
618
|
+
await new Promise((r) => setTimeout(r, 60 * 1000));
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
410
622
|
// 周期性同步服务端目标国家(内部节流,几乎零开销)
|
|
411
623
|
if (!options.locationExplicit) {
|
|
412
624
|
const synced = await syncTargetLocationsFromServer(serverUrl);
|
|
@@ -431,23 +643,44 @@ export async function handleExplore(options) {
|
|
|
431
643
|
showResourceUsage();
|
|
432
644
|
}
|
|
433
645
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (
|
|
441
|
-
|
|
442
|
-
|
|
646
|
+
if (poolMode) {
|
|
647
|
+
// 登录池模式:内存超限→同号重启浏览器;48min→换号(原地切号,不换端口)
|
|
648
|
+
const memRatio = healthChecker.memoryUsage();
|
|
649
|
+
const elapsedMs = Date.now() - poolAccountSince;
|
|
650
|
+
if (memRatio >= MEMORY_THRESHOLD) {
|
|
651
|
+
await restartPoolBrowser(`系统内存 ${(memRatio * 100).toFixed(1)}% 超阈值`);
|
|
652
|
+
} else if (elapsedMs >= ROTATE_INTERVAL_MS) {
|
|
653
|
+
await handleAccountSwitch(
|
|
654
|
+
`当前账号已运行 ${Math.round(elapsedMs / 60000)} 分钟`,
|
|
443
655
|
);
|
|
444
|
-
|
|
656
|
+
} else if (processedCount % 10 === 0) {
|
|
657
|
+
const remainMin = Math.max(
|
|
445
658
|
0,
|
|
446
|
-
Math.round(
|
|
659
|
+
Math.round((ROTATE_INTERVAL_MS - elapsedMs) / 60000),
|
|
660
|
+
);
|
|
661
|
+
console.error(
|
|
662
|
+
`\n[巡检] @${poolAccount.username} | 内存 ${(memRatio * 100).toFixed(1)}% | 距换号 ${remainMin} 分钟`,
|
|
447
663
|
);
|
|
448
|
-
followStatus = ` | 关注/粉丝上次成功 ${followElapsed} 分钟前 | 封禁检测 ${followRemaining} 分钟后`;
|
|
449
664
|
}
|
|
450
|
-
|
|
665
|
+
} else {
|
|
666
|
+
const checkResult = healthChecker.check();
|
|
667
|
+
if (checkResult.shouldSwitch) {
|
|
668
|
+
await handleAccountSwitch(checkResult.reason);
|
|
669
|
+
} else if (checkResult.info && processedCount % 10 === 0) {
|
|
670
|
+
// 关注/粉丝封禁检测状态(仅登录状态)
|
|
671
|
+
let followStatus = "";
|
|
672
|
+
if (loggedIn && exploreEnableFollow) {
|
|
673
|
+
const followElapsed = Math.round(
|
|
674
|
+
(Date.now() - lastFollowSuccessTime) / 60000,
|
|
675
|
+
);
|
|
676
|
+
const followRemaining = Math.max(
|
|
677
|
+
0,
|
|
678
|
+
Math.round(FOLLOW_BLOCK_THRESHOLD / 60000) - followElapsed,
|
|
679
|
+
);
|
|
680
|
+
followStatus = ` | 关注/粉丝上次成功 ${followElapsed} 分钟前 | 封禁检测 ${followRemaining} 分钟后`;
|
|
681
|
+
}
|
|
682
|
+
console.error(`\n[健康检查] ${checkResult.info}${followStatus}`);
|
|
683
|
+
}
|
|
451
684
|
}
|
|
452
685
|
|
|
453
686
|
// 切换任务前检测验证码
|
|
@@ -546,6 +779,7 @@ export async function handleExplore(options) {
|
|
|
546
779
|
browser = newBrowser;
|
|
547
780
|
const newPage = await setupNewPage(browser);
|
|
548
781
|
Object.assign(page, newPage);
|
|
782
|
+
if (poolMode) await restorePoolLogin();
|
|
549
783
|
// 重试当前用户
|
|
550
784
|
result = await processExplore(
|
|
551
785
|
page,
|
|
@@ -805,6 +1039,7 @@ export async function handleExplore(options) {
|
|
|
805
1039
|
browser = newBrowser;
|
|
806
1040
|
const newPage = await setupNewPage(browser);
|
|
807
1041
|
Object.assign(page, newPage);
|
|
1042
|
+
if (poolMode) await restorePoolLogin();
|
|
808
1043
|
// 重建后等待页面稳定
|
|
809
1044
|
await new Promise((r) => setTimeout(r, 3000));
|
|
810
1045
|
console.error(`[浏览器] 已重建,继续处理...`);
|
|
@@ -816,11 +1051,22 @@ export async function handleExplore(options) {
|
|
|
816
1051
|
}
|
|
817
1052
|
}
|
|
818
1053
|
|
|
1054
|
+
// 登录池模式:自然退出(上限达量/任务空转退出等)也必须还号,否则租约被占到 TTL
|
|
1055
|
+
if (poolClient && poolAccount) {
|
|
1056
|
+
await poolClient
|
|
1057
|
+
.release(poolAccount)
|
|
1058
|
+
.then(() => console.error(`[登录池] 已释放 @${poolAccount.username}`))
|
|
1059
|
+
.catch((e) => console.error(`[登录池] 释放失败(忽略): ${e.message}`));
|
|
1060
|
+
}
|
|
1061
|
+
|
|
819
1062
|
const stats = await apiGet(`${serverUrl}/api/stats`);
|
|
820
1063
|
console.error(`\n完成: ${processedCount} 个用户处理, ${errorCount} 个出错`);
|
|
821
1064
|
console.error(
|
|
822
1065
|
` 总用户: ${stats.totalUsers}, 已完成: ${stats.processedUsers}, 待处理: ${stats.pendingUsers}, 错误: ${stats.errorUsers}`,
|
|
823
1066
|
);
|
|
1067
|
+
// CDP 连接会挂住 node event loop(browser.close 只断连不保证 loop 清空,
|
|
1068
|
+
// 见知识库 POC 坑①/F-6 实录)——自然退出必须显式 exit,否则进程悬挂
|
|
1069
|
+
process.exit(0);
|
|
824
1070
|
} finally {
|
|
825
1071
|
process.removeListener("SIGINT", onSigint);
|
|
826
1072
|
process.removeListener("SIGTERM", onSigterm);
|