tt-help-cli-ycl 1.4.183 → 1.4.185
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/test-candidate-retry.mjs +496 -0
- package/scripts/test-crash-circuit-breaker.mjs +250 -0
- package/scripts/test-error-auto-reset.mjs +103 -0
- package/scripts/test-error-report.mjs +215 -0
- package/scripts/test-login-pool-acquire-order.mjs +92 -26
- package/scripts/test-login-pool-faceban-e2e.mjs +131 -0
- package/scripts/test-login-pool-faceban.mjs +235 -0
- package/scripts/test-login-pool-ui.mjs +2 -1
- package/scripts/test-login-pool.mjs +97 -54
- package/scripts/test-process-manager-label.mjs +145 -0
- package/src/cli/explore.js +171 -25
- package/src/cli/search.js +171 -18
- package/src/lib/browser/health-checker.js +19 -3
- package/src/lib/browser/page.js +23 -0
- package/src/lib/error-report.js +119 -0
- package/src/lib/login-pool-client.js +123 -0
- package/src/watch/data-store.js +25 -0
- package/src/watch/login-pool.js +118 -81
- package/src/watch/public/app.js +7 -2
- package/src/watch/public/index.html +2 -1
- package/src/watch/server.js +53 -8
- package/src/watchdog/process-manager.js +71 -6
- package/src/watchdog/reporter.js +2 -0
package/src/watch/data-store.js
CHANGED
|
@@ -2391,6 +2391,23 @@ export function createStore(filePath, options = {}) {
|
|
|
2391
2391
|
return { saved: true };
|
|
2392
2392
|
}
|
|
2393
2393
|
|
|
2394
|
+
/**
|
|
2395
|
+
* 自动恢复候选:updated_at 早于 maxAgeMs 的 error 行(B 方案:无次数上限全自动回流)。
|
|
2396
|
+
* 仅扫 jobs——commitJob 会把 error 行从 task_queue 搬到 jobs(与页面「批量重新处理」同口径),
|
|
2397
|
+
* 回流本身交给 resetJob(三表迁移 + 空池缓存失效 + 统计置脏全复用)。
|
|
2398
|
+
* @returns {string[]} unique_id 列表(无 DB 的 JSON 模式返回空)
|
|
2399
|
+
*/
|
|
2400
|
+
function listStaleErrorJobs(maxAgeMs, limit = 500) {
|
|
2401
|
+
if (!getDb()) return [];
|
|
2402
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
2403
|
+
return getDb()
|
|
2404
|
+
.prepare(
|
|
2405
|
+
`SELECT unique_id FROM jobs WHERE status = 'error' AND updated_at < ? ORDER BY updated_at LIMIT ?`,
|
|
2406
|
+
)
|
|
2407
|
+
.all(cutoff, limit)
|
|
2408
|
+
.map((r) => r.unique_id);
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2394
2411
|
function togglePin(uniqueId) {
|
|
2395
2412
|
if (getDb()) {
|
|
2396
2413
|
const user = getJob(uniqueId);
|
|
@@ -2686,6 +2703,8 @@ export function createStore(filePath, options = {}) {
|
|
|
2686
2703
|
errorStack,
|
|
2687
2704
|
hostname,
|
|
2688
2705
|
clientId,
|
|
2706
|
+
poolUsername,
|
|
2707
|
+
proxyNode,
|
|
2689
2708
|
) {
|
|
2690
2709
|
const existing = clientErrors.get(userId);
|
|
2691
2710
|
if (existing) {
|
|
@@ -2708,6 +2727,9 @@ export function createStore(filePath, options = {}) {
|
|
|
2708
2727
|
existing.reportCount = (existing.reportCount || 1) + 1;
|
|
2709
2728
|
}
|
|
2710
2729
|
if (username) existing.username = username;
|
|
2730
|
+
// 在手池号/代理节点(方案甲新增):非空才覆盖,同 username 语义
|
|
2731
|
+
if (poolUsername) existing.poolUsername = poolUsername;
|
|
2732
|
+
if (proxyNode) existing.proxyNode = proxyNode;
|
|
2711
2733
|
} else {
|
|
2712
2734
|
clientErrors.set(userId, {
|
|
2713
2735
|
userId,
|
|
@@ -2718,6 +2740,8 @@ export function createStore(filePath, options = {}) {
|
|
|
2718
2740
|
errorStack: errorStack || "",
|
|
2719
2741
|
username,
|
|
2720
2742
|
stage: stage || "",
|
|
2743
|
+
poolUsername: poolUsername || "",
|
|
2744
|
+
proxyNode: proxyNode || "",
|
|
2721
2745
|
timestamp: Date.now(),
|
|
2722
2746
|
reportCount: 1,
|
|
2723
2747
|
captchaCount: errorType === "captcha" ? 1 : 0,
|
|
@@ -3816,6 +3840,7 @@ export function createStore(filePath, options = {}) {
|
|
|
3816
3840
|
commitJob,
|
|
3817
3841
|
commitNewExplore,
|
|
3818
3842
|
resetJob,
|
|
3843
|
+
listStaleErrorJobs,
|
|
3819
3844
|
togglePin,
|
|
3820
3845
|
getNextRedoJob,
|
|
3821
3846
|
commitRedoJob,
|
package/src/watch/login-pool.js
CHANGED
|
@@ -34,11 +34,17 @@ const POOL_FILE = join(homeDir, ".tt-help-watchdog-logins.json");
|
|
|
34
34
|
|
|
35
35
|
// ── 常量 ──
|
|
36
36
|
const SAVE_DEBOUNCE_MS = 3 * 1000;
|
|
37
|
-
const
|
|
37
|
+
const ZOMBIE_LEASE_MS = 2 * 60 * 60 * 1000; // 租约持有硬顶:从取号起算超 2h 视为僵尸回收(2026-11-03 砍掉心跳续期,无续期语义)
|
|
38
|
+
const ERROR_REASONS = new Set(["banned", "captcha", "network"]); // 换号原因中的错误类;scheduled=健康证明清零;inject_fail/recover_fail/无原因=中性忽略
|
|
38
39
|
const CMD_TTL_MS = 10 * 60 * 1000; // 下发命令 10 分钟未被取走作废(节点离线),租约自动回收
|
|
39
40
|
const HISTORY_MAX = 20; // 每账号保留最近 20 条分配历史
|
|
40
41
|
const POOL_MAX = 256; // 池容量上限
|
|
41
42
|
const VALID_STATUS = ["pool", "leased", "suspect", "retired"];
|
|
43
|
+
// faceBans(2026-11-02):按数据面标注可用性——登录态有效但 explore/search 某一面拿不到数据
|
|
44
|
+
// 的「面残缺号」。与 status 正交(leased 号也可标注,元数据不动租约,下次派发生效);
|
|
45
|
+
// 缺失/null = 未验证 = 默认可用,不做数据迁移,读取懒判空。
|
|
46
|
+
const VALID_FACES = ["explore", "search"];
|
|
47
|
+
const VALID_FACE_SOURCES = ["manual", "probe"]; // 标注来源:人工(页面/API)/探针(verify-env 账号闸)
|
|
42
48
|
|
|
43
49
|
// ── 内存状态 ──
|
|
44
50
|
let _accounts = []; // 账号注册表(含明文 cookies,仅服务端内存/文件持有)
|
|
@@ -68,6 +74,17 @@ function pushHistory(acc, event, detail = "") {
|
|
|
68
74
|
if (acc.history.length > HISTORY_MAX) acc.history.length = HISTORY_MAX;
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
/** 请求面判定:appName === 'search' → search 面;其余(含未知/缺省)一律 explore 面 */
|
|
78
|
+
export function faceOfAppName(appName) {
|
|
79
|
+
return appName === "search" ? "search" : "explore";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 该账号该面是否被禁用:仅显式 ban=true 才算;缺失/null/字段残缺 = 未验证 = 可用 */
|
|
83
|
+
export function isFaceBanned(acc, face) {
|
|
84
|
+
const e = acc && acc.faceBans && acc.faceBans[face];
|
|
85
|
+
return !!(e && e.ban === true);
|
|
86
|
+
}
|
|
87
|
+
|
|
71
88
|
/** 从 cookies 数组提取账号名(无显式 username 时的兜底) */
|
|
72
89
|
export function deriveUsername(cookies) {
|
|
73
90
|
for (const c of Array.isArray(cookies) ? cookies : []) {
|
|
@@ -204,10 +221,6 @@ function publicView(acc) {
|
|
|
204
221
|
userId: acc.lease.userId,
|
|
205
222
|
port: acc.lease.port,
|
|
206
223
|
assignedAt: acc.lease.assignedAt,
|
|
207
|
-
expireAt: acc.lease.expireAt,
|
|
208
|
-
lastHealthAt: acc.lease.lastHealthAt || null,
|
|
209
|
-
lastBusinessOkAt: acc.lease.lastBusinessOkAt || null,
|
|
210
|
-
emptyStreak: acc.lease.emptyStreak || 0,
|
|
211
224
|
lastAppliedAt: acc.lease.lastAppliedAt || null,
|
|
212
225
|
lastAppliedOk: acc.lease.lastAppliedOk != null ? acc.lease.lastAppliedOk : null,
|
|
213
226
|
persisted: acc.lease.persisted != null ? acc.lease.persisted : null,
|
|
@@ -230,11 +243,17 @@ function publicView(acc) {
|
|
|
230
243
|
// 最近使用镜像(脱离 lease 独立存在,释放/回收后仍可见)
|
|
231
244
|
lastHealthAt: acc.lastHealthAt || null,
|
|
232
245
|
lastHealth: acc.lastHealth || null,
|
|
233
|
-
lastBusinessOkAt: acc.lastBusinessOkAt || null,
|
|
234
246
|
lastUsedAt: acc.lastUsedAt || null,
|
|
235
247
|
lastAppName: acc.lastAppName || null,
|
|
236
248
|
prevAppName: acc.prevAppName || null,
|
|
237
|
-
|
|
249
|
+
lastSwitchReason: acc.lastSwitchReason || null,
|
|
250
|
+
switchStreak: acc.switchStreak || 0,
|
|
251
|
+
errStreak: acc.errStreak || 0,
|
|
252
|
+
// 面标注(2026-11-02 faceBans):{ban,source,at,by?} | null,null=未验证=默认可用
|
|
253
|
+
faceBans: {
|
|
254
|
+
explore: (acc.faceBans && acc.faceBans.explore) || null,
|
|
255
|
+
search: (acc.faceBans && acc.faceBans.search) || null,
|
|
256
|
+
},
|
|
238
257
|
lease,
|
|
239
258
|
history: acc.history || [],
|
|
240
259
|
createdAt: acc.createdAt,
|
|
@@ -300,14 +319,20 @@ function dropStaleCommands() {
|
|
|
300
319
|
// ── 租约维护 ──
|
|
301
320
|
|
|
302
321
|
/** 回收过期租约(节点掉线 > TTL)。返回回收数量。 */
|
|
303
|
-
|
|
322
|
+
/**
|
|
323
|
+
* 僵尸租约回收(2026-11-03 替代原 reapExpiredLeases/12h TTL):无续期语义,
|
|
324
|
+
* 从 assignedAt(取号时刻)硬顶 ZOMBIE_LEASE_MS=2h,超时即回收。由各 API 入口
|
|
325
|
+
* 与心跳尾部调用(每次调用顺带执行一遍,成本低)。
|
|
326
|
+
* 返回回收数量。
|
|
327
|
+
*/
|
|
328
|
+
export function reapZombieLeases() {
|
|
304
329
|
ensureLoaded();
|
|
305
330
|
dropStaleCommands();
|
|
306
331
|
const t = now();
|
|
307
332
|
let reaped = 0;
|
|
308
333
|
for (const acc of _accounts) {
|
|
309
|
-
if (acc.status === "leased" && acc.lease && t -
|
|
310
|
-
pushHistory(acc, "
|
|
334
|
+
if (acc.status === "leased" && acc.lease && t - acc.lease.assignedAt > ZOMBIE_LEASE_MS) {
|
|
335
|
+
pushHistory(acc, "zombieReaped", `${acc.lease.userId}:${acc.lease.port} 持有超2h,僵尸回收`);
|
|
311
336
|
acc.status = "pool";
|
|
312
337
|
acc.lastUsedAt = t;
|
|
313
338
|
acc.lease = null;
|
|
@@ -323,16 +348,17 @@ export function reapExpiredLeases() {
|
|
|
323
348
|
// ── 心跳集成(watch 主进程心跳热路径调用) ──
|
|
324
349
|
|
|
325
350
|
/**
|
|
326
|
-
*
|
|
351
|
+
* 心跳处理:写回命令执行回报 + 返回待下发命令。
|
|
352
|
+
* 2026-11-03 砍掉续租与全部盖章/镜像(lastHealthAt/lastUsedAt/lastBusinessOkAt/emptyStreak
|
|
353
|
+
* 不再被心跳触碰),僵尸租约由 reapZombieLeases 按 assignedAt 硬顶回收。
|
|
327
354
|
* @param userId 节点 userId(body.config.userId)
|
|
328
|
-
* @param body 心跳 body
|
|
355
|
+
* @param body 心跳 body
|
|
329
356
|
* @returns loginPoolCommands(无则 undefined)
|
|
330
357
|
*/
|
|
331
358
|
export function heartbeatLoginPool(userId, body) {
|
|
332
359
|
ensureLoaded();
|
|
333
360
|
const t = now();
|
|
334
361
|
const poolBody = body && body.loginPool;
|
|
335
|
-
const report = poolBody && poolBody.ports;
|
|
336
362
|
|
|
337
363
|
// 0) 节点执行回报(applyLogin/clearLogin 结果):写回租约与历史
|
|
338
364
|
if (Array.isArray(poolBody && poolBody.applied)) {
|
|
@@ -358,37 +384,8 @@ export function heartbeatLoginPool(userId, body) {
|
|
|
358
384
|
scheduleSave();
|
|
359
385
|
}
|
|
360
386
|
|
|
361
|
-
// 1) 续租该节点持有的所有租约 +
|
|
362
|
-
//
|
|
363
|
-
// 带实例后缀(F-11-8222-2 = {节点}-{端口}-{实例})→ 精确相等 + "-" 边界前缀匹配都续。
|
|
364
|
-
// 边界防节点名互为前缀:"F-1" 不得续 "F-11-*"(startsWith("F-1-") 为 false)。
|
|
365
|
-
// ⚠️ 铁律:acquireAccount 里「自动释放同使用者旧租约」必须保持精确相等
|
|
366
|
-
// (一实例一号不变量),与本处续租匹配是两套语义,不许跟着改成前缀。
|
|
367
|
-
for (const acc of _accounts) {
|
|
368
|
-
if (
|
|
369
|
-
acc.status === "leased" &&
|
|
370
|
-
acc.lease &&
|
|
371
|
-
(acc.lease.userId === userId || acc.lease.userId.startsWith(userId + "-"))
|
|
372
|
-
) {
|
|
373
|
-
acc.lease.lastHealthAt = t;
|
|
374
|
-
acc.lastHealthAt = t; // 镜像到账号本体,释放后仍可查
|
|
375
|
-
acc.lastUsedAt = t;
|
|
376
|
-
_dirty = true; // 续租也是变更:配合下方低频持久化落盘
|
|
377
|
-
if (report && report[acc.lease.port]) {
|
|
378
|
-
const h = report[acc.lease.port];
|
|
379
|
-
if (typeof h.lastBusinessOkAt === "number") {
|
|
380
|
-
acc.lease.lastBusinessOkAt = h.lastBusinessOkAt;
|
|
381
|
-
acc.lastBusinessOkAt = h.lastBusinessOkAt; // 镜像:最近一次业务出活
|
|
382
|
-
}
|
|
383
|
-
if (typeof h.emptyStreak === "number") acc.lease.emptyStreak = h.emptyStreak;
|
|
384
|
-
if (h.username && !acc.username) acc.username = h.username;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// 1.5) 心跳续租的低频持久化:只在我们真的持有账号状态时才写,且每 5 分钟最多一次
|
|
390
|
-
// (冷启动空状态下绝不落盘,防盖掉对方刚写的数据;TTL 12h,5min 漂移可忽略)
|
|
391
|
-
if (_loaded && _accounts.length > 0 && t - _lastFileWriteAt > 300000) scheduleSave();
|
|
387
|
+
// (2026-11-03 砍掉原「1) 续租该节点持有的所有租约 + 采集健康数据」与
|
|
388
|
+
// 「1.5) 低频持久化」:心跳不再刷新/弄脏任何池状态,落盘只在操作性变更时发生。)
|
|
392
389
|
|
|
393
390
|
// 2) 取走待下发命令(一次性;跨进程:命令由 watch 进程经池文件投递)
|
|
394
391
|
const list = _queue.filter((c) => c.userId === userId);
|
|
@@ -415,8 +412,8 @@ export function heartbeatLoginPool(userId, body) {
|
|
|
415
412
|
scheduleSave();
|
|
416
413
|
}
|
|
417
414
|
|
|
418
|
-
// 3)
|
|
419
|
-
|
|
415
|
+
// 3) 僵尸租约回收(每次心跳顺带检查,成本低)
|
|
416
|
+
reapZombieLeases();
|
|
420
417
|
|
|
421
418
|
return commands && commands.length > 0 ? { commands } : undefined;
|
|
422
419
|
}
|
|
@@ -431,7 +428,7 @@ function fullView(acc) {
|
|
|
431
428
|
/** 池视图(脱敏) */
|
|
432
429
|
export function getLoginPoolView() {
|
|
433
430
|
ensureLoaded();
|
|
434
|
-
|
|
431
|
+
reapZombieLeases();
|
|
435
432
|
return {
|
|
436
433
|
accounts: _accounts.map(publicView),
|
|
437
434
|
total: _accounts.length,
|
|
@@ -506,6 +503,9 @@ export function clearAccountHistory({ id, by } = {}) {
|
|
|
506
503
|
acc.history = [];
|
|
507
504
|
acc.useCount = 0;
|
|
508
505
|
acc.switchStats = {};
|
|
506
|
+
acc.lastSwitchReason = null;
|
|
507
|
+
acc.switchStreak = 0;
|
|
508
|
+
acc.errStreak = 0;
|
|
509
509
|
acc.updatedAt = now();
|
|
510
510
|
pushHistory(acc, "clearLogs", `清除日志与计数(来源 ${String(by || "page").slice(0, 80)})`);
|
|
511
511
|
scheduleSave();
|
|
@@ -521,6 +521,9 @@ export function clearAllAccountHistory({ by } = {}) {
|
|
|
521
521
|
acc.history = [];
|
|
522
522
|
acc.useCount = 0;
|
|
523
523
|
acc.switchStats = {};
|
|
524
|
+
acc.lastSwitchReason = null;
|
|
525
|
+
acc.switchStreak = 0;
|
|
526
|
+
acc.errStreak = 0;
|
|
524
527
|
acc.updatedAt = t;
|
|
525
528
|
pushHistory(acc, "clearLogs", `清除日志与计数(来源 ${String(by || "page").slice(0, 80)} · 批量)`);
|
|
526
529
|
}
|
|
@@ -529,6 +532,41 @@ export function clearAllAccountHistory({ by } = {}) {
|
|
|
529
532
|
return { cleared };
|
|
530
533
|
}
|
|
531
534
|
|
|
535
|
+
/**
|
|
536
|
+
* 面标注(2026-11-02 faceBans):按面记录可用性,派号时按面过滤(见 acquireAccount)。
|
|
537
|
+
* { id|username, face, ban, source='manual', by } — 定位二选一;ban=true 标挂 / false 恢复;
|
|
538
|
+
* 与 status 正交:leased 号也可标注(不动租约,下次派发生效)。
|
|
539
|
+
* 写 faceBans[face] → pushHistory 落痕 → saveNow() 立即落盘(操作性变更不走防抖)。
|
|
540
|
+
*/
|
|
541
|
+
export function setAccountFace({ id, username, face, ban, source = "manual", by } = {}) {
|
|
542
|
+
ensureLoaded();
|
|
543
|
+
if (!VALID_FACES.includes(face)) {
|
|
544
|
+
throw new Error(`非法 face: ${face}(仅限 ${VALID_FACES.join("/")})`);
|
|
545
|
+
}
|
|
546
|
+
if (!VALID_FACE_SOURCES.includes(source)) {
|
|
547
|
+
throw new Error(`非法 source: ${source}(仅限 ${VALID_FACE_SOURCES.join("/")})`);
|
|
548
|
+
}
|
|
549
|
+
if (typeof ban !== "boolean") {
|
|
550
|
+
throw new Error("ban 必填且为 boolean(true=标挂禁用 / false=恢复)");
|
|
551
|
+
}
|
|
552
|
+
const acc = _accounts.find((a) =>
|
|
553
|
+
id ? a.id === id : username ? a.username === username : false,
|
|
554
|
+
);
|
|
555
|
+
if (!acc) throw new Error(`账号不存在: ${id || username}`);
|
|
556
|
+
const t = now();
|
|
557
|
+
acc.faceBans = acc.faceBans || {};
|
|
558
|
+
acc.faceBans[face] = { ban, source, at: t, ...(by ? { by: String(by).slice(0, 80) } : {}) };
|
|
559
|
+
acc.updatedAt = t;
|
|
560
|
+
pushHistory(
|
|
561
|
+
acc,
|
|
562
|
+
ban ? "faceBan" : "faceRestore",
|
|
563
|
+
`${face}=${ban ? "禁用" : "恢复"} (${source}${by ? " · " + String(by).slice(0, 60) : ""})`,
|
|
564
|
+
);
|
|
565
|
+
_dirty = true; // 先置脏再落盘(与 enqueueCommand 同式):saveNow 的 !_dirty 早退会吞掉连续标注
|
|
566
|
+
saveNow();
|
|
567
|
+
return publicView(acc);
|
|
568
|
+
}
|
|
569
|
+
|
|
532
570
|
/** 删除(retired 才可删;其余先 retired) */
|
|
533
571
|
export function deleteAccount(id) {
|
|
534
572
|
ensureLoaded();
|
|
@@ -584,10 +622,6 @@ export function assignAccount({ id, userId, port, force } = {}) {
|
|
|
584
622
|
userId,
|
|
585
623
|
port,
|
|
586
624
|
assignedAt: t,
|
|
587
|
-
expireAt: t + LEASE_TTL_MS,
|
|
588
|
-
lastHealthAt: t,
|
|
589
|
-
lastBusinessOkAt: null,
|
|
590
|
-
emptyStreak: null,
|
|
591
625
|
};
|
|
592
626
|
acc.updatedAt = t;
|
|
593
627
|
pushHistory(acc, "assigned", `${userId}:${port}${force ? " (force)" : ""}`);
|
|
@@ -639,7 +673,7 @@ export function releaseAccount({ id, clearOnNode } = {}) {
|
|
|
639
673
|
*/
|
|
640
674
|
export function takeAccount({ userId, port, username, force } = {}) {
|
|
641
675
|
ensureLoaded();
|
|
642
|
-
|
|
676
|
+
reapZombieLeases();
|
|
643
677
|
if (!userId || !port) throw new Error("userId 和 port 必填");
|
|
644
678
|
let acc;
|
|
645
679
|
if (username) {
|
|
@@ -667,11 +701,12 @@ export function takeAccount({ userId, port, username, force } = {}) {
|
|
|
667
701
|
* - port 可选:仅作展示元数据(浏览器口),不参与任何键/约束
|
|
668
702
|
* - appName: 记录在租约与账号本体(lastAppName,释放后保留),参与取号排序 K2 非同类优先
|
|
669
703
|
* - reason: 可选换号原因受控 code(banned/recover_fail/scheduled/inject_fail/captcha/network),
|
|
670
|
-
*
|
|
704
|
+
* 仅显式上报才计入「最近换号原因 + 连续次数」(lastSwitchReason/switchStreak,2026-11-03
|
|
705
|
+
* 替代原 switchStats 终身累加);未知 code 原样存不拒绝(旧客户端兼容)
|
|
671
706
|
*/
|
|
672
707
|
export function acquireAccount({ userId, port, appName, reason } = {}) {
|
|
673
708
|
ensureLoaded();
|
|
674
|
-
|
|
709
|
+
reapZombieLeases();
|
|
675
710
|
if (!userId) throw new Error("userId 必填");
|
|
676
711
|
let portNum = null;
|
|
677
712
|
if (port !== undefined && port !== null && port !== "") {
|
|
@@ -694,6 +729,8 @@ export function acquireAccount({ userId, port, appName, reason } = {}) {
|
|
|
694
729
|
// 请求无 appName 时该键中性化全等,不参与)
|
|
695
730
|
// K3 lastUsedAt 升序(从未用过组内退化为 createdAt 升序,兼容旧排序)
|
|
696
731
|
const MIN_IDLE_MS = 30 * 60 * 1000;
|
|
732
|
+
// (2026-11-03 砍掉 faceBans 按面过滤:explore/search 双面派号未验证出价值,机制退役;
|
|
733
|
+
// faceBans 数据与 /api/login-pool/face 端点保留休眠,探针标注不受影响)
|
|
697
734
|
const pool = _accounts.filter((a) => a.status === "pool");
|
|
698
735
|
const sortFn = (a, b) => {
|
|
699
736
|
// K1: 从未用过最前
|
|
@@ -715,7 +752,9 @@ export function acquireAccount({ userId, port, appName, reason } = {}) {
|
|
|
715
752
|
const acc = eligible[0] || pool.slice().sort(sortFn)[0];
|
|
716
753
|
if (!acc) {
|
|
717
754
|
const leased = _accounts.filter((a) => a.status === "leased").length;
|
|
718
|
-
throw new Error(
|
|
755
|
+
throw new Error(
|
|
756
|
+
`池中暂无可用账号(共 ${_accounts.length} 个,租约中 ${leased} 个)`,
|
|
757
|
+
);
|
|
719
758
|
}
|
|
720
759
|
|
|
721
760
|
acc.status = "leased";
|
|
@@ -728,19 +767,19 @@ export function acquireAccount({ userId, port, appName, reason } = {}) {
|
|
|
728
767
|
acc.lastAppName = appNameKey;
|
|
729
768
|
}
|
|
730
769
|
if (reasonKey) {
|
|
731
|
-
//
|
|
732
|
-
|
|
733
|
-
acc.
|
|
770
|
+
// 换号原因:只留「最近原因 + 连续次数」(2026-11-03 替代 switchStats 终身累加:
|
|
771
|
+
// 同原因连击 +1,换原因重置为 1。例:例行换号×2 → 被封×2,页面只显示 被封×2)
|
|
772
|
+
if (acc.lastSwitchReason === reasonKey) acc.switchStreak = (acc.switchStreak || 0) + 1;
|
|
773
|
+
else {
|
|
774
|
+
acc.lastSwitchReason = reasonKey;
|
|
775
|
+
acc.switchStreak = 1;
|
|
776
|
+
}
|
|
734
777
|
}
|
|
735
778
|
acc.lease = {
|
|
736
779
|
userId,
|
|
737
780
|
port: portNum,
|
|
738
781
|
appName: appNameKey,
|
|
739
782
|
assignedAt: t,
|
|
740
|
-
expireAt: t + LEASE_TTL_MS,
|
|
741
|
-
lastHealthAt: t,
|
|
742
|
-
lastBusinessOkAt: null,
|
|
743
|
-
emptyStreak: null,
|
|
744
783
|
};
|
|
745
784
|
acc.updatedAt = t;
|
|
746
785
|
pushHistory(
|
|
@@ -760,7 +799,13 @@ export function acquireAccount({ userId, port, appName, reason } = {}) {
|
|
|
760
799
|
other.lease &&
|
|
761
800
|
other.lease.userId === userId
|
|
762
801
|
) {
|
|
763
|
-
|
|
802
|
+
// 连败计数 errStreak(2026-11-03 替代心跳通道 cmdFailStreak):记在被换掉的旧号头上——
|
|
803
|
+
// 原因码描述的是旧号的遭遇。错误(banned/captcha/network)+1;scheduled=干满周期
|
|
804
|
+
// 健康证明清零;inject_fail/recover_fail/无原因中性不动(inject_fail 候选号在取新号前
|
|
805
|
+
// 已单独 release,此处本就无旧租约,物理上天然忽略)
|
|
806
|
+
if (ERROR_REASONS.has(reasonKey)) other.errStreak = (other.errStreak || 0) + 1;
|
|
807
|
+
else if (reasonKey === "scheduled") other.errStreak = 0;
|
|
808
|
+
pushHistory(other, "released", `自动释放(${userId} 取新号${reasonKey ? " · " + reasonKey : ""})`);
|
|
764
809
|
other.status = "pool";
|
|
765
810
|
other.lastUsedAt = t;
|
|
766
811
|
other.lease = null;
|
|
@@ -790,7 +835,7 @@ export function acquireAccount({ userId, port, appName, reason } = {}) {
|
|
|
790
835
|
*/
|
|
791
836
|
export function importAccount({ userId, port, username, cookies, note, proxyHint } = {}) {
|
|
792
837
|
ensureLoaded();
|
|
793
|
-
|
|
838
|
+
reapZombieLeases();
|
|
794
839
|
if (!userId) throw new Error("userId 必填");
|
|
795
840
|
if (!username) throw new Error("username 必填(须为 passport 验证过的登录号)");
|
|
796
841
|
const norm = normalizeCookies(cookies);
|
|
@@ -846,28 +891,26 @@ const importMeta = (userId, portNum, at) => ({ userId, port: portNum, at });
|
|
|
846
891
|
*/
|
|
847
892
|
export function reloginAccount({ username, cookies, userId, note } = {}) {
|
|
848
893
|
ensureLoaded();
|
|
849
|
-
|
|
894
|
+
reapZombieLeases();
|
|
850
895
|
if (!username) throw new Error("username 必填(重登录目标)");
|
|
851
896
|
if (!userId) throw new Error("userId 必填(操作来源)");
|
|
852
897
|
const norm = normalizeCookies(cookies);
|
|
853
898
|
if (!norm) throw new Error("cookies 必须是非空数组,且每项含 name/value");
|
|
854
899
|
const acc = _accounts.find((a) => a.username === username);
|
|
855
900
|
if (!acc) throw new Error(`账号 ${username} 不在池中(重登录仅针对已有账号,新号走 import/add)`);
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
`账号 ${username} 使用中(${w?.userId || "?"}${w?.port ? ":" + w.port : ""}),先释放再重登录`,
|
|
860
|
-
);
|
|
861
|
-
}
|
|
901
|
+
// 2026-11-03 改(X 方案):使用中也可重登录——只替换池内 cookies,状态与租约原样保留,
|
|
902
|
+
// 不再置 pool(避免租约挂着状态却变 pool 被别的节点取走);风险自担:本机重登录窗口
|
|
903
|
+
// 与在租节点同 sessionid 并用,可能触发风控踢会话,节点会自愈重取
|
|
862
904
|
const revived = acc.status === "retired";
|
|
863
905
|
const wasSuspect = acc.status === "suspect";
|
|
906
|
+
const inUse = !!acc.lease;
|
|
864
907
|
acc.cookies = norm;
|
|
865
|
-
acc.status = "pool";
|
|
908
|
+
if (!inUse) acc.status = "pool";
|
|
866
909
|
acc.updatedAt = now();
|
|
867
910
|
pushHistory(
|
|
868
911
|
acc,
|
|
869
912
|
"relogin",
|
|
870
|
-
`${revived ? "重登录复活(已停用→池)" : wasSuspect ? "重登录(疑似→池)" : "重登录刷新"} · ${userId}${note ? ` · ${String(note).slice(0, 80)}` : ""}`,
|
|
913
|
+
`${inUse ? "重登录刷新(使用中,仅换 cookies)" : revived ? "重登录复活(已停用→池)" : wasSuspect ? "重登录(疑似→池)" : "重登录刷新"} · ${userId}${note ? ` · ${String(note).slice(0, 80)}` : ""}`,
|
|
871
914
|
);
|
|
872
915
|
scheduleSave();
|
|
873
916
|
return { account: publicView(acc), revived, wasSuspect };
|
|
@@ -879,7 +922,7 @@ export function reloginAccount({ username, cookies, userId, note } = {}) {
|
|
|
879
922
|
*/
|
|
880
923
|
export function healthTargets({ ids } = {}) {
|
|
881
924
|
ensureLoaded();
|
|
882
|
-
|
|
925
|
+
reapZombieLeases();
|
|
883
926
|
const idSet = Array.isArray(ids) && ids.length ? new Set(ids) : null;
|
|
884
927
|
return _accounts
|
|
885
928
|
.filter((a) => a.status !== "retired")
|
|
@@ -926,16 +969,10 @@ export function recordHealth({ id, ok, detail } = {}) {
|
|
|
926
969
|
*/
|
|
927
970
|
export function prepareRelogin({ username } = {}) {
|
|
928
971
|
ensureLoaded();
|
|
929
|
-
|
|
972
|
+
reapZombieLeases();
|
|
930
973
|
if (!username) throw new Error("username 必填");
|
|
931
974
|
const acc = _accounts.find((a) => a.username === username);
|
|
932
975
|
if (!acc) throw new Error(`账号 ${username} 不在池中`);
|
|
933
|
-
if (acc.status === "leased") {
|
|
934
|
-
const w = acc.lease;
|
|
935
|
-
throw new Error(
|
|
936
|
-
`账号 ${username} 使用中(${w?.userId || "?"}${w?.port ? ":" + w.port : ""}),先释放再重登录`,
|
|
937
|
-
);
|
|
938
|
-
}
|
|
939
976
|
return { account: fullView(acc) };
|
|
940
977
|
}
|
|
941
978
|
|
package/src/watch/public/app.js
CHANGED
|
@@ -122,6 +122,10 @@ async function fetchClientErrors() {
|
|
|
122
122
|
const msgDetail = c.errorMessage
|
|
123
123
|
? `<br><span style="color:#666;font-size:11px">${escapeHtml(c.errorMessage)}</span>`
|
|
124
124
|
: "";
|
|
125
|
+
// 旧字段 username 语义混杂(池号/占位/目标用户),改造后挪进详情区小字保留可见性
|
|
126
|
+
const legacyUsernameDetail = c.username
|
|
127
|
+
? `<br><span style="color:#666;font-size:10px">上报账号: @${escapeHtml(c.username)}</span>`
|
|
128
|
+
: "";
|
|
125
129
|
const stackDetail = c.errorStack
|
|
126
130
|
? `<br><span style="color:#555;font-size:10px;max-width:250px;display:block;word-break:break-all">${escapeHtml(c.errorStack)}</span>`
|
|
127
131
|
: "";
|
|
@@ -134,8 +138,9 @@ async function fetchClientErrors() {
|
|
|
134
138
|
<td style="color:#f87171;font-weight:600">${c.reportCount || 1}</td>
|
|
135
139
|
<td style="color:#f59e0b;font-weight:600">${captchaText}${captchaDetail}</td>
|
|
136
140
|
<td style="color:#a78bfa;font-size:12px">${stageText}</td>
|
|
137
|
-
<td style="color:#ccc;font-size:12px;max-width:300px;word-break:break-all">${msgDetail}${stackDetail}</td>
|
|
138
|
-
<td style="color:#
|
|
141
|
+
<td style="color:#ccc;font-size:12px;max-width:300px;word-break:break-all">${msgDetail}${legacyUsernameDetail}${stackDetail}</td>
|
|
142
|
+
<td style="color:#34d399;font-size:12px">@${escapeHtml(c.poolUsername || "-")}</td>
|
|
143
|
+
<td style="color:#94a3b8;font-size:11px;font-family:monospace;max-width:120px;word-break:break-all">${escapeHtml(c.proxyNode || "-")}</td>
|
|
139
144
|
<td style="color:#888;font-size:12px">${new Date(c.timestamp).toLocaleTimeString()}</td>
|
|
140
145
|
<td><button class="btn-delete" onclick="deleteClientError('${escapeHtml(c.userId)}')" style="background:#991b1b;color:#fff;border:none;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:12px">删除</button></td>
|
|
141
146
|
</tr>`;
|
package/src/watch/server.js
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
prepareRelogin,
|
|
25
25
|
clearAccountHistory,
|
|
26
26
|
clearAllAccountHistory,
|
|
27
|
+
setAccountFace,
|
|
27
28
|
} from "./login-pool.js";
|
|
28
29
|
import { checkPoolAccounts } from "./login-health.js";
|
|
29
30
|
|
|
@@ -738,7 +739,6 @@ async function load() {
|
|
|
738
739
|
((b.lastUsedAt || b.createdAt || 0) - (a.lastUsedAt || a.createdAt || 0)));
|
|
739
740
|
const rows = list.map((a, idx) => {
|
|
740
741
|
const l = a.lease;
|
|
741
|
-
const biz = a.lastBusinessOkAt ? fmt(a.lastBusinessOkAt) : '无记录';
|
|
742
742
|
const expired = a.sessionExpiresAt && a.sessionExpiresAt < Date.now();
|
|
743
743
|
const st = ST[a.status] || [a.status, ''];
|
|
744
744
|
const who = l
|
|
@@ -748,16 +748,17 @@ async function load() {
|
|
|
748
748
|
const lastApp = l ? (a.prevAppName || null) : (a.lastAppName || null);
|
|
749
749
|
const recent = [];
|
|
750
750
|
if (lastApp) recent.push(esc(lastApp));
|
|
751
|
-
|
|
752
|
-
if (sw.length) recent.push('<span class="muted">' + sw.map(kv => (RS[kv[0]] || esc(kv[0])) + '×' + kv[1]).join(' · ') + '</span>');
|
|
751
|
+
if (a.lastSwitchReason) recent.push('<span class="muted">' + (RS[a.lastSwitchReason] || esc(a.lastSwitchReason)) + '×' + (a.switchStreak || 1) + '</span>');
|
|
753
752
|
const health = a.lastHealth === 'alive' ? '<span style="color:#16a34a">✓</span>'
|
|
754
753
|
: a.lastHealth === 'dead' ? '<span class="warn">✗</span>'
|
|
755
754
|
: a.lastHealth === 'error' ? '<span class="muted">?</span>'
|
|
756
755
|
: '<span class="muted">—</span>';
|
|
756
|
+
// (2026-11-03 faceBadge 面标注徽章随 explore/search 列一并移除,数据与 API 保留休眠)
|
|
757
757
|
const meta = [];
|
|
758
758
|
if (a.note) meta.push(esc(a.note));
|
|
759
759
|
if (a.proxyHint) meta.push('出口 ' + esc(a.proxyHint));
|
|
760
760
|
if (a.cookieCount) meta.push('cookies ' + a.cookieCount + ' 条');
|
|
761
|
+
if (a.errStreak > 0) meta.push('<span class="warn">连败×' + a.errStreak + '</span>');
|
|
761
762
|
return '<tr>' +
|
|
762
763
|
'<td class="muted">' + (idx + 1) + '</td>' +
|
|
763
764
|
'<td><b>' + esc(a.username) + '</b>' +
|
|
@@ -765,7 +766,7 @@ async function load() {
|
|
|
765
766
|
'<td><span class="badge ' + st[1] + '">' + st[0] + '</span></td>' +
|
|
766
767
|
'<td>' + who + '</td>' +
|
|
767
768
|
'<td>' + (recent.length ? recent.join('<br>') : '<span class="muted">—</span>') + '</td>' +
|
|
768
|
-
'<td title="最后使用: ' + fmt(a.lastUsedAt) + '
|
|
769
|
+
'<td title="最后使用: ' + fmt(a.lastUsedAt) + '">' + rel(a.lastUsedAt) + '</td>' +
|
|
769
770
|
'<td>' + (a.useCount || 0) + '</td>' +
|
|
770
771
|
'<td' + (expired ? ' class="warn"' : '') + ' title="sessionid 到期: ' + fmt(a.sessionExpiresAt) + '(cookie 承诺寿命,非实时有效性)">' + day(a.sessionExpiresAt) + (expired ? '(已过)' : '') + '</td>' +
|
|
771
772
|
'<td title="上次检测: ' + fmt(a.lastHealthAt) + (a.lastHealth === 'dead' ? '\\n死号已标疑似,可重登录复活' : a.lastHealth === 'alive' ? '\\n登录态存活' : '') + '">' + health + ' <span class="muted">' + rel(a.lastHealthAt) + '</span></td>' +
|
|
@@ -792,9 +793,10 @@ async function load() {
|
|
|
792
793
|
'<span class="chip">疑似 <b>' + nSus + '</b></span>' +
|
|
793
794
|
'<span class="chip">已停用 <b>' + nRet + '</b></span>';
|
|
794
795
|
document.getElementById('content').innerHTML =
|
|
795
|
-
'<table><tr><th>#</th><th>账号</th><th title="状态四态:使用中=租约在身 · 空闲=可被自动取号 · 疑似=检测判死暂不参与自动取号(可重登录复活)· 已停用=退出池">状态</th><th>使用者</th><th title="
|
|
796
|
+
'<table><tr><th>#</th><th>账号</th><th title="状态四态:使用中=租约在身 · 空闲=可被自动取号 · 疑似=检测判死暂不参与自动取号(可重登录复活)· 已停用=退出池">状态</th><th>使用者</th><th title="上次使用的程序:在租=当前取号的前一任(与使用者列对比看切换);空闲=最近一任。附最近换号原因×连续次数(仅显式上报才计数)">上次使用</th><th>最近使用</th><th>次数</th><th title="sessionid cookie 的承诺寿命,非实时有效性">有效期</th><th title="登录态探测(✓/✗/?/—);真实判据 = 业务出活">检测</th><th>操作</th><th>历史</th></tr>' + rows + '</table>' +
|
|
796
797
|
'<div style="padding: 10px 14px;"><p class="muted" style="margin:0">排序:使用中 → 空闲 → 疑似 → 已停用 · 每 15 秒自动刷新 · 有效期 = sessionid cookie 承诺寿命 · 检测 = 登录态探测(真实判据 = 业务出活)· cookies 明文不出服务端 · 「新增账号/重登录」一键拉起需本机装 tt-help 并执行过 <b>tt-help protocol register</b></p>' +
|
|
797
|
-
'<p class="muted" style="margin:6px 0 0">检测列图例:✓ 存活 · ✗ 已死(自动标疑似)· ? 探测异常(网络/超时,不代表号死)· — 未检测 · 时间 =
|
|
798
|
+
'<p class="muted" style="margin:6px 0 0">检测列图例:✓ 存活 · ✗ 已死(自动标疑似)· ? 探测异常(网络/超时,不代表号死)· — 未检测 · 时间 = 最近探测</p>' +
|
|
799
|
+
'<p class="muted" style="margin:6px 0 0">面标注图例:🚫 该面残缺禁用(派号跳过) · ✅ 验证通过 · — 未验证默认可用 · 角标「探」= 探针标注;「标挂/恢复」选面(explore/search),不影响状态与当前租约</p></div>';
|
|
798
800
|
} catch (e) { msg('加载失败: ' + e.message, true); }
|
|
799
801
|
}
|
|
800
802
|
function copyCmd(cmd) {
|
|
@@ -1773,11 +1775,19 @@ export function startWatchServer(
|
|
|
1773
1775
|
ret = updateAccount(body);
|
|
1774
1776
|
console.error(`[登录池 ${ts}] UPDATE: ${ret.username} status=${ret.status}`);
|
|
1775
1777
|
} else if (lpAction === "clear-history") {
|
|
1776
|
-
// 清空日志(账号中心页面按钮):history 清成一条留痕 + useCount
|
|
1778
|
+
// 清空日志(账号中心页面按钮):history 清成一条留痕 + useCount/换号原因清零
|
|
1777
1779
|
ret = clearAccountHistory(body);
|
|
1778
1780
|
console.error(`[登录池 ${ts}] CLEAR-LOGS: ${ret.username} (by ${body.by || "page"})`);
|
|
1781
|
+
} else if (lpAction === "face") {
|
|
1782
|
+
// 面标注(2026-11-02 faceBans):explore/search 按面标挂/恢复,探针与人工同入口;
|
|
1783
|
+
// leased 号也可标(不动租约,下次派发生效)
|
|
1784
|
+
ret = setAccountFace(body);
|
|
1785
|
+
const fb = ret.faceBans && ret.faceBans[body.face];
|
|
1786
|
+
console.error(
|
|
1787
|
+
`[登录池 ${ts}] FACE: ${ret.username} ${body.face}=${fb && fb.ban ? "禁用" : "恢复"} (${body.source || "manual"}${body.by ? " · " + body.by : ""})`,
|
|
1788
|
+
);
|
|
1779
1789
|
} else if (lpAction === "clear-history-all") {
|
|
1780
|
-
// 一键清空全部账号日志(页面批量按钮):各账号留痕一条 + useCount
|
|
1790
|
+
// 一键清空全部账号日志(页面批量按钮):各账号留痕一条 + useCount/换号原因清零
|
|
1781
1791
|
const sum = clearAllAccountHistory(body);
|
|
1782
1792
|
console.error(`[登录池 ${ts}] CLEAR-LOGS-ALL: ${sum.cleared} 个账号 (by ${body.by || "page"})`);
|
|
1783
1793
|
sendJSON(res, 200, { ok: true, cleared: sum.cleared });
|
|
@@ -2503,6 +2513,8 @@ export function startWatchServer(
|
|
|
2503
2513
|
body.errorStack || "",
|
|
2504
2514
|
body.hostname || req.headers["x-client-hostname"] || "",
|
|
2505
2515
|
body.clientId || req.headers["x-watchdog-client-id"] || "",
|
|
2516
|
+
body.poolUsername || "",
|
|
2517
|
+
body.proxyNode || "",
|
|
2506
2518
|
);
|
|
2507
2519
|
sendJSON(res, 200, { ok: true });
|
|
2508
2520
|
} else {
|
|
@@ -3655,6 +3667,39 @@ export function startWatchServer(
|
|
|
3655
3667
|
`[job] 无国家数据自动回炉已启动: 间隔 ${NO_COUNTRY_MOVE_INTERVAL_MS / 3600000} 小时`,
|
|
3656
3668
|
);
|
|
3657
3669
|
|
|
3670
|
+
// 错误任务自动恢复(B 方案:无次数上限全自动):
|
|
3671
|
+
// explore 失败任务 status='error' 躺平在 jobs 表,此前全靠页面「批量重新处理」人工重置。
|
|
3672
|
+
// 定时扫龄期超阈的 error 行复用 resetJob 回流(与按钮同语义,含三表迁移/缓存失效/统计置脏)。
|
|
3673
|
+
// 龄期防抖:避开错误风暴进行中 + 客户端可能仍在处理原任务(慢节点单用户可超 10 分钟)。
|
|
3674
|
+
const ERROR_AUTO_RESET_INTERVAL_MS = 5 * 60 * 1000; // 扫描间隔
|
|
3675
|
+
const ERROR_AUTO_RESET_AGE_MS = 10 * 60 * 1000; // error 龄期阈值(「恢复 10 分钟前的错误任务」)
|
|
3676
|
+
const ERROR_AUTO_RESET_BATCH = 500; // 每轮上限,积压大时分轮消化
|
|
3677
|
+
const autoResetStaleErrors = () => {
|
|
3678
|
+
try {
|
|
3679
|
+
const ids = store.listStaleErrorJobs(
|
|
3680
|
+
ERROR_AUTO_RESET_AGE_MS,
|
|
3681
|
+
ERROR_AUTO_RESET_BATCH,
|
|
3682
|
+
);
|
|
3683
|
+
if (ids.length === 0) return;
|
|
3684
|
+
let reset = 0;
|
|
3685
|
+
for (const uid of ids) {
|
|
3686
|
+
const ret = store.resetJob(uid);
|
|
3687
|
+
if (ret && ret.saved) reset++;
|
|
3688
|
+
}
|
|
3689
|
+
const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
3690
|
+
console.error(
|
|
3691
|
+
`[JOB ${ts}] AUTO-RESET-ERROR: reset=${reset}/${ids.length}(龄期>${ERROR_AUTO_RESET_AGE_MS / 60000}min,无次数上限)`,
|
|
3692
|
+
);
|
|
3693
|
+
} catch (e) {
|
|
3694
|
+
console.error(`[JOB] AUTO-RESET-ERROR 定时任务异常: ${e.message}`);
|
|
3695
|
+
}
|
|
3696
|
+
};
|
|
3697
|
+
setInterval(autoResetStaleErrors, ERROR_AUTO_RESET_INTERVAL_MS);
|
|
3698
|
+
setTimeout(autoResetStaleErrors, 60 * 1000); // 启动 1 分钟后先清一轮积压
|
|
3699
|
+
console.error(
|
|
3700
|
+
`[job] 错误任务自动恢复已启动: 间隔 ${ERROR_AUTO_RESET_INTERVAL_MS / 60000} 分钟, 龄期 ${ERROR_AUTO_RESET_AGE_MS / 60000} 分钟, 每轮上限 ${ERROR_AUTO_RESET_BATCH}(重启后 1 分钟首扫)`,
|
|
3701
|
+
);
|
|
3702
|
+
|
|
3658
3703
|
_resolve({ server, port });
|
|
3659
3704
|
});
|
|
3660
3705
|
|