tt-help-cli-ycl 1.4.171 → 1.4.175
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 +209 -25
- package/src/watch/public/index.html +19 -1
- 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 +89 -194
- package/src/watchdog/login-apply.js +232 -0
- package/src/watchdog/login-import.js +177 -0
- package/src/watchdog/upgrader.js +59 -5
package/src/watch/public/app.js
CHANGED
|
@@ -4192,6 +4192,49 @@ function esc(s) {
|
|
|
4192
4192
|
|
|
4193
4193
|
var _wdLastRendered = "";
|
|
4194
4194
|
|
|
4195
|
+
// ── 看门狗客户端卡片折叠(默认只显示头部,点击展开全部;内存态,刷新重置) ──
|
|
4196
|
+
var _wdCardExpandAll = false; // 全局默认态(顶栏「全部展开/收起」切换)
|
|
4197
|
+
var _wdCardExpanded = {}; // 单卡覆盖:userId → true/false(点过单卡按钮才有)
|
|
4198
|
+
|
|
4199
|
+
function wdCardIsExpanded(userId) {
|
|
4200
|
+
return _wdCardExpanded[userId] !== undefined
|
|
4201
|
+
? _wdCardExpanded[userId]
|
|
4202
|
+
: _wdCardExpandAll;
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
// 单卡展开/收起:直接切 DOM 不重渲染(数据变化重建时按此状态生成)
|
|
4206
|
+
function wdToggleCardBody(userId) {
|
|
4207
|
+
var body = document.getElementById("wdCardBody-" + wdEscId(userId));
|
|
4208
|
+
if (!body) return;
|
|
4209
|
+
var expand = body.style.display === "none";
|
|
4210
|
+
body.style.display = expand ? "" : "none";
|
|
4211
|
+
_wdCardExpanded[userId] = expand;
|
|
4212
|
+
var btn = document.getElementById("wdCardToggle-" + wdEscId(userId));
|
|
4213
|
+
if (btn) {
|
|
4214
|
+
btn.textContent = expand ? "▲ 收起" : "▼ 展开";
|
|
4215
|
+
btn.title = expand ? "收起卡片" : "展开卡片";
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
// 顶栏总开关:重置单卡覆盖,全部统一切换
|
|
4220
|
+
function wdToggleAllCards() {
|
|
4221
|
+
_wdCardExpandAll = !_wdCardExpandAll;
|
|
4222
|
+
_wdCardExpanded = {};
|
|
4223
|
+
var bodies = document.querySelectorAll("[id^='wdCardBody-']");
|
|
4224
|
+
for (var i = 0; i < bodies.length; i++) {
|
|
4225
|
+
bodies[i].style.display = _wdCardExpandAll ? "" : "none";
|
|
4226
|
+
var btn = document.getElementById(
|
|
4227
|
+
bodies[i].id.replace("wdCardBody-", "wdCardToggle-"),
|
|
4228
|
+
);
|
|
4229
|
+
if (btn) {
|
|
4230
|
+
btn.textContent = _wdCardExpandAll ? "▲ 收起" : "▼ 展开";
|
|
4231
|
+
btn.title = _wdCardExpandAll ? "收起卡片" : "展开卡片";
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
var label = document.getElementById("wdToggleAllLabel");
|
|
4235
|
+
if (label) label.textContent = _wdCardExpandAll ? "⊟ 全部收起" : "⊞ 全部展开";
|
|
4236
|
+
}
|
|
4237
|
+
|
|
4195
4238
|
function renderWatchdogDashboard(
|
|
4196
4239
|
clients,
|
|
4197
4240
|
clientCommands,
|
|
@@ -4242,9 +4285,9 @@ function renderWatchdogDashboard(
|
|
|
4242
4285
|
b.hostname ||
|
|
4243
4286
|
""
|
|
4244
4287
|
).toLowerCase();
|
|
4245
|
-
//
|
|
4288
|
+
// 在线优先;名称用自然数字序(F-2 排在 F-10 前)
|
|
4246
4289
|
if (a.online !== b.online) return a.online ? -1 : 1;
|
|
4247
|
-
return nameA.localeCompare(nameB);
|
|
4290
|
+
return nameA.localeCompare(nameB, undefined, { numeric: true });
|
|
4248
4291
|
});
|
|
4249
4292
|
|
|
4250
4293
|
// 构建 userId → 在线客户端 映射
|
|
@@ -4281,7 +4324,9 @@ function renderWatchdogDashboard(
|
|
|
4281
4324
|
}
|
|
4282
4325
|
}
|
|
4283
4326
|
offlineUids.sort(function (a, b) {
|
|
4284
|
-
return a
|
|
4327
|
+
return a
|
|
4328
|
+
.toLowerCase()
|
|
4329
|
+
.localeCompare(b.toLowerCase(), undefined, { numeric: true });
|
|
4285
4330
|
});
|
|
4286
4331
|
offlineUids.forEach(function (uid) {
|
|
4287
4332
|
allCards.push(
|
|
@@ -4348,6 +4393,29 @@ function renderOfflineCard(
|
|
|
4348
4393
|
autoUpgrades,
|
|
4349
4394
|
) {
|
|
4350
4395
|
var cmdCount = cmds ? cmds.length : 0;
|
|
4396
|
+
var expanded = wdCardIsExpanded(userId);
|
|
4397
|
+
// 第二行摘要:离线卡最关心的就是配置了多少命令,整行可点展开/收起
|
|
4398
|
+
var summaryLine = tag(
|
|
4399
|
+
"div",
|
|
4400
|
+
{
|
|
4401
|
+
style:
|
|
4402
|
+
"display:flex;gap:12px;align-items:center;flex-wrap:wrap;font-size:12px;margin-top:4px;cursor:pointer",
|
|
4403
|
+
onclick: "wdToggleCardBody('" + esc(userId) + "')",
|
|
4404
|
+
title: "点击展开/收起卡片",
|
|
4405
|
+
},
|
|
4406
|
+
styled("span", "color:#f59e0b", "配置 " + cmdCount + " 条命令"),
|
|
4407
|
+
);
|
|
4408
|
+
var toggleBtn = tag(
|
|
4409
|
+
"button",
|
|
4410
|
+
{
|
|
4411
|
+
id: "wdCardToggle-" + wdEscId(userId),
|
|
4412
|
+
onclick: "wdToggleCardBody('" + esc(userId) + "')",
|
|
4413
|
+
style:
|
|
4414
|
+
"padding:2px 8px;font-size:11px;background:#333;color:#888;border:1px solid #444;border-radius:3px;cursor:pointer",
|
|
4415
|
+
title: expanded ? "收起卡片" : "展开卡片",
|
|
4416
|
+
},
|
|
4417
|
+
expanded ? "▲ 收起" : "▼ 展开",
|
|
4418
|
+
);
|
|
4351
4419
|
return tag(
|
|
4352
4420
|
"div",
|
|
4353
4421
|
{
|
|
@@ -4411,20 +4479,24 @@ function renderOfflineCard(
|
|
|
4411
4479
|
title: "删除此客户端配置",
|
|
4412
4480
|
},
|
|
4413
4481
|
"删除",
|
|
4414
|
-
)
|
|
4482
|
+
) +
|
|
4483
|
+
toggleBtn,
|
|
4415
4484
|
),
|
|
4416
4485
|
) +
|
|
4417
|
-
|
|
4486
|
+
summaryLine +
|
|
4487
|
+
tag(
|
|
4418
4488
|
"div",
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4489
|
+
{
|
|
4490
|
+
id: "wdCardBody-" + wdEscId(userId),
|
|
4491
|
+
style: expanded ? "" : "display:none",
|
|
4492
|
+
},
|
|
4493
|
+
renderCountrySection({}, userId, clashCountries) +
|
|
4494
|
+
renderErrorTriggerSection({}, userId, errorTriggers) +
|
|
4495
|
+
renderSwitchRestartSection({}, userId, switchRestarts) +
|
|
4496
|
+
renderAutoUpgradeSection({}, userId, autoUpgrades) +
|
|
4497
|
+
renderLoginSection({}, userId, loginPorts) +
|
|
4498
|
+
renderCmdRows(cmds || [], userId, [], false),
|
|
4499
|
+
),
|
|
4428
4500
|
);
|
|
4429
4501
|
}
|
|
4430
4502
|
|
|
@@ -4474,6 +4546,78 @@ function renderClientCard(
|
|
|
4474
4546
|
var myCmds = userId && clientCommands[userId] ? clientCommands[userId] : [];
|
|
4475
4547
|
var online = c.online;
|
|
4476
4548
|
|
|
4549
|
+
var uptimeStr = os.uptime != null ? Math.round(os.uptime / 3600) + "h" : "?";
|
|
4550
|
+
// 第二行巡检摘要:版本/进程/登录/国家(异常项高亮),整行可点展开/收起
|
|
4551
|
+
var procRun = procs.filter(function (p) {
|
|
4552
|
+
return p.status === "running";
|
|
4553
|
+
}).length;
|
|
4554
|
+
var procColor = procRun < procs.length ? "#f87171" : "#888";
|
|
4555
|
+
var procTxt =
|
|
4556
|
+
"进程 " + procRun + "/" + procs.length + (procRun < procs.length ? " ⚠" : "");
|
|
4557
|
+
|
|
4558
|
+
var loginTxt = "登录 —";
|
|
4559
|
+
var loginColor = "#555";
|
|
4560
|
+
var rep = c.loginReport;
|
|
4561
|
+
if (rep && rep.error) {
|
|
4562
|
+
loginTxt = "登录 检测失败";
|
|
4563
|
+
loginColor = "#f87171";
|
|
4564
|
+
} else if (rep && Array.isArray(rep.ports) && rep.ports.length) {
|
|
4565
|
+
var loginOk = rep.ports.filter(function (r) {
|
|
4566
|
+
return r.status === "logged_in";
|
|
4567
|
+
}).length;
|
|
4568
|
+
loginTxt =
|
|
4569
|
+
"登录 " +
|
|
4570
|
+
loginOk +
|
|
4571
|
+
"/" +
|
|
4572
|
+
rep.ports.length +
|
|
4573
|
+
(loginOk === rep.ports.length ? " ✓" : " ⚠");
|
|
4574
|
+
loginColor = loginOk === rep.ports.length ? "#4ade80" : "#fbbf24";
|
|
4575
|
+
}
|
|
4576
|
+
|
|
4577
|
+
var ccEntry = (clashCountries && clashCountries[userId]) || {};
|
|
4578
|
+
var ccCfg =
|
|
4579
|
+
ccEntry.countries && ccEntry.countries.length ? ccEntry.countries : ["US"];
|
|
4580
|
+
var ccActive = c.clashHealth && c.clashHealth.meta
|
|
4581
|
+
? c.clashHealth.meta.countryFilter
|
|
4582
|
+
: null;
|
|
4583
|
+
var ccActiveStr = Array.isArray(ccActive) ? ccActive.join(",") : ccActive;
|
|
4584
|
+
var ccCfgStr = ccCfg.join(",");
|
|
4585
|
+
var ccColor = "#888";
|
|
4586
|
+
var ccTxt = "🌍 " + ccCfgStr;
|
|
4587
|
+
if (ccActiveStr && ccActiveStr !== ccCfgStr) {
|
|
4588
|
+
ccTxt = "🌍 " + ccCfgStr + "≠" + ccActiveStr + " ⚠";
|
|
4589
|
+
ccColor = "#fbbf24";
|
|
4590
|
+
}
|
|
4591
|
+
|
|
4592
|
+
var summaryLine = tag(
|
|
4593
|
+
"div",
|
|
4594
|
+
{
|
|
4595
|
+
style:
|
|
4596
|
+
"display:flex;gap:12px;align-items:center;flex-wrap:wrap;font-size:12px;margin-top:4px;cursor:pointer",
|
|
4597
|
+
onclick: "wdToggleCardBody('" + esc(userId) + "')",
|
|
4598
|
+
title: "点击展开/收起卡片",
|
|
4599
|
+
},
|
|
4600
|
+
styled("span", "color:#60a5fa", "v" + (c.ttHelpVersion || "?")) +
|
|
4601
|
+
styled("span", "color:" + procColor, procTxt) +
|
|
4602
|
+
styled("span", "color:" + loginColor, loginTxt) +
|
|
4603
|
+
styled("span", "color:" + ccColor, ccTxt) +
|
|
4604
|
+
styled("span", "color:#888", "运行 " + uptimeStr),
|
|
4605
|
+
);
|
|
4606
|
+
|
|
4607
|
+
// 折叠状态:默认只显示头部,点展开按钮看全部(需在 header 组装前定义)
|
|
4608
|
+
var expanded = wdCardIsExpanded(userId);
|
|
4609
|
+
var toggleBtn = tag(
|
|
4610
|
+
"button",
|
|
4611
|
+
{
|
|
4612
|
+
id: "wdCardToggle-" + wdEscId(userId),
|
|
4613
|
+
onclick: "wdToggleCardBody('" + esc(userId) + "')",
|
|
4614
|
+
style:
|
|
4615
|
+
"padding:2px 8px;font-size:11px;background:#333;color:#888;border:1px solid #444;border-radius:3px;cursor:pointer",
|
|
4616
|
+
title: expanded ? "收起卡片" : "展开卡片",
|
|
4617
|
+
},
|
|
4618
|
+
expanded ? "▲ 收起" : "▼ 展开",
|
|
4619
|
+
);
|
|
4620
|
+
|
|
4477
4621
|
var osStr = [
|
|
4478
4622
|
os.platform,
|
|
4479
4623
|
os.release,
|
|
@@ -4578,6 +4722,11 @@ function renderClientCard(
|
|
|
4578
4722
|
: "color:#fca5a5;font-size:12px",
|
|
4579
4723
|
"最后心跳 " + lastSeen,
|
|
4580
4724
|
) +
|
|
4725
|
+
styled(
|
|
4726
|
+
"span",
|
|
4727
|
+
"color:#888;font-size:13px",
|
|
4728
|
+
"进程 " + procs.length,
|
|
4729
|
+
) +
|
|
4581
4730
|
(!online && userId
|
|
4582
4731
|
? tag(
|
|
4583
4732
|
"button",
|
|
@@ -4589,7 +4738,8 @@ function renderClientCard(
|
|
|
4589
4738
|
},
|
|
4590
4739
|
"删除",
|
|
4591
4740
|
)
|
|
4592
|
-
: "")
|
|
4741
|
+
: "") +
|
|
4742
|
+
toggleBtn,
|
|
4593
4743
|
),
|
|
4594
4744
|
);
|
|
4595
4745
|
|
|
@@ -4603,15 +4753,34 @@ function renderClientCard(
|
|
|
4603
4753
|
";border-radius:12px;padding:16px",
|
|
4604
4754
|
},
|
|
4605
4755
|
header +
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4756
|
+
summaryLine +
|
|
4757
|
+
tag(
|
|
4758
|
+
"div",
|
|
4759
|
+
{
|
|
4760
|
+
id: "wdCardBody-" + wdEscId(userId),
|
|
4761
|
+
style: expanded ? "" : "display:none",
|
|
4762
|
+
},
|
|
4763
|
+
styled("div", "color:#888;font-size:12px", osStr) +
|
|
4764
|
+
styled(
|
|
4765
|
+
"div",
|
|
4766
|
+
"color:#888;font-size:12px;margin-bottom:6px",
|
|
4767
|
+
infoLine,
|
|
4768
|
+
) +
|
|
4769
|
+
renderCountrySection(c, userId, clashCountries) +
|
|
4770
|
+
renderErrorTriggerSection(c, userId, errorTriggers) +
|
|
4771
|
+
renderSwitchRestartSection(c, userId, switchRestarts) +
|
|
4772
|
+
renderAutoUpgradeSection(c, userId, autoUpgrades) +
|
|
4773
|
+
renderLoginSection(c, userId, loginPorts) +
|
|
4774
|
+
renderProxySection(c, userId) +
|
|
4775
|
+
renderCmdRows(
|
|
4776
|
+
myCmds,
|
|
4777
|
+
userId,
|
|
4778
|
+
procs,
|
|
4779
|
+
online,
|
|
4780
|
+
c.llmAnalysis,
|
|
4781
|
+
c.clientId,
|
|
4782
|
+
),
|
|
4783
|
+
),
|
|
4615
4784
|
);
|
|
4616
4785
|
}
|
|
4617
4786
|
|
|
@@ -5064,6 +5233,14 @@ function wdSubRefsCell(s) {
|
|
|
5064
5233
|
.replace("<span", '<span title="' + escHtml(names) + '"');
|
|
5065
5234
|
}
|
|
5066
5235
|
|
|
5236
|
+
function wdSubWeightCell(s) {
|
|
5237
|
+
var w = parseInt(s.weight, 10);
|
|
5238
|
+
if (!Number.isFinite(w) || w <= 1)
|
|
5239
|
+
return styled("span", "color:#555;font-size:11px", "×1");
|
|
5240
|
+
return styled("span", "color:#4ade80;font-weight:700;font-size:11px;cursor:help", "×" + w)
|
|
5241
|
+
.replace("<span", '<span title="自动分配权重:占比≈权重比(修改后下次心跳生效)"');
|
|
5242
|
+
}
|
|
5243
|
+
|
|
5067
5244
|
function wdSubSourceCell(s) {
|
|
5068
5245
|
if (s.source === "probe") {
|
|
5069
5246
|
var p = s.lastProbe || {};
|
|
@@ -5179,6 +5356,7 @@ function renderSubscriptionsCenter(subs) {
|
|
|
5179
5356
|
styled("div", "flex:0 0 160px", wdSubTrafficCell(s)) +
|
|
5180
5357
|
styled("div", "flex:0 0 130px", wdSubExpireCell(s)) +
|
|
5181
5358
|
styled("div", "flex:0 0 80px;text-align:center", wdSubRefsCell(s)) +
|
|
5359
|
+
styled("div", "flex:0 0 48px;text-align:center;", wdSubWeightCell(s)) +
|
|
5182
5360
|
styled("div", "flex:1;overflow:hidden", wdSubSourceCell(s)) +
|
|
5183
5361
|
styled("div", "display:flex;gap:6px;flex:0 0 auto", actions));
|
|
5184
5362
|
});
|
|
@@ -5202,6 +5380,7 @@ function wdSubShowAddModal() {
|
|
|
5202
5380
|
document.getElementById("wdSubName").value = "";
|
|
5203
5381
|
document.getElementById("wdSubUrl").value = "";
|
|
5204
5382
|
document.getElementById("wdSubNote").value = "";
|
|
5383
|
+
document.getElementById("wdSubWeight").value = 1;
|
|
5205
5384
|
document.getElementById("wdSubModal").style.display = "flex";
|
|
5206
5385
|
}
|
|
5207
5386
|
|
|
@@ -5218,6 +5397,7 @@ function wdSubShowEditModal(id) {
|
|
|
5218
5397
|
document.getElementById("wdSubName").value = s.name || "";
|
|
5219
5398
|
document.getElementById("wdSubUrl").value = s.url || "";
|
|
5220
5399
|
document.getElementById("wdSubNote").value = s.note || "";
|
|
5400
|
+
document.getElementById("wdSubWeight").value = parseInt(s.weight, 10) || 1;
|
|
5221
5401
|
document.getElementById("wdSubModal").style.display = "flex";
|
|
5222
5402
|
}
|
|
5223
5403
|
|
|
@@ -5226,10 +5406,14 @@ function wdSubCloseModal() {
|
|
|
5226
5406
|
}
|
|
5227
5407
|
|
|
5228
5408
|
async function wdSubSave() {
|
|
5409
|
+
var w = parseInt(document.getElementById("wdSubWeight").value, 10);
|
|
5410
|
+
if (!Number.isFinite(w)) w = 1;
|
|
5411
|
+
w = Math.max(1, Math.min(10, w));
|
|
5229
5412
|
var payload = {
|
|
5230
5413
|
name: document.getElementById("wdSubName").value.trim(),
|
|
5231
5414
|
url: document.getElementById("wdSubUrl").value.trim(),
|
|
5232
5415
|
note: document.getElementById("wdSubNote").value.trim(),
|
|
5416
|
+
weight: w,
|
|
5233
5417
|
};
|
|
5234
5418
|
if (!payload.url) {
|
|
5235
5419
|
alert("请填写订阅地址");
|
|
@@ -5395,7 +5579,7 @@ function renderLoginSection(c, userId, loginPorts) {
|
|
|
5395
5579
|
style:
|
|
5396
5580
|
"padding:2px 8px;font-size:11px;background:#333;color:#4ade80;" +
|
|
5397
5581
|
"border:1px solid #444;border-radius:3px;cursor:pointer",
|
|
5398
|
-
title: "
|
|
5582
|
+
title: "立即重检上次未通过的端口(全部已登录则跳过;结果随心跳自动刷新)",
|
|
5399
5583
|
},
|
|
5400
5584
|
"重检",
|
|
5401
5585
|
);
|
|
@@ -644,6 +644,10 @@
|
|
|
644
644
|
<div class="label">命令池</div>
|
|
645
645
|
<div class="value slate" id="wdCmdCount">0</div>
|
|
646
646
|
</div>
|
|
647
|
+
<div class="stat-card clickable" onclick="wdToggleAllCards()" style="background:rgba(96,165,250,0.05)">
|
|
648
|
+
<div class="label" id="wdToggleAllLabel" style="font-size:18px">⊞</div>
|
|
649
|
+
<div class="value" style="font-size:11px;color:#60a5fa">全部展开</div>
|
|
650
|
+
</div>
|
|
647
651
|
<div class="stat-card clickable" onclick="fetchWatchdogData()" style="background:rgba(96,165,250,0.05)">
|
|
648
652
|
<div class="label" style="font-size:18px">🔄</div>
|
|
649
653
|
<div class="value" style="font-size:11px;color:#60a5fa">刷新</div>
|
|
@@ -664,10 +668,18 @@
|
|
|
664
668
|
</div>
|
|
665
669
|
<div id="wdSubsList" style="color:#666;font-size:12px">加载中...</div>
|
|
666
670
|
</div>
|
|
671
|
+
<!-- 账号中心入口(独立页 /login-pool,同登录门禁) -->
|
|
672
|
+
<div style="margin-bottom:16px;border:1px solid #2a2a3d;border-radius:10px;background:#16161f;padding:12px;display:flex;align-items:center;justify-content:space-between">
|
|
673
|
+
<div>
|
|
674
|
+
<span style="font-size:14px;color:#34d399">🧑💼 账户中心(登录池)</span>
|
|
675
|
+
<span style="color:#666;font-size:11px;margin-left:8px">账号在用状态 / 使用者 / 使用次数 / 停用启用</span>
|
|
676
|
+
</div>
|
|
677
|
+
<a href="/login-pool" target="_blank"
|
|
678
|
+
style="padding:5px 14px;font-size:12px;background:#34d399;color:#000;border-radius:6px;font-weight:600;text-decoration:none">打开账号中心 →</a>
|
|
679
|
+
</div>
|
|
667
680
|
<div id="wdClientsList" style="color:#666;font-size:12px">加载中...</div>
|
|
668
681
|
</div>
|
|
669
682
|
|
|
670
|
-
<!-- 订阅中心 新增/编辑弹窗 -->
|
|
671
683
|
<div id="wdSubModal"
|
|
672
684
|
style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:9999;align-items:center;justify-content:center">
|
|
673
685
|
<div style="background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:24px;width:90%;max-width:560px">
|
|
@@ -688,6 +700,12 @@
|
|
|
688
700
|
<input id="wdSubNote" placeholder="如:每月1号重置流量"
|
|
689
701
|
style="width:100%;box-sizing:border-box;padding:8px 10px;background:#16161e;border:1px solid #333;border-radius:8px;color:#e0e0e0;font-size:12px">
|
|
690
702
|
</div>
|
|
703
|
+
<div style="margin-bottom:10px">
|
|
704
|
+
<div style="color:#94a3b8;font-size:12px;margin-bottom:4px">自动分配权重(1-10,默认 1)</div>
|
|
705
|
+
<input id="wdSubWeight" type="number" min="1" max="10" step="1" value="1"
|
|
706
|
+
style="width:120px;box-sizing:border-box;padding:8px 10px;background:#16161e;border:1px solid #333;border-radius:8px;color:#e0e0e0;font-size:12px">
|
|
707
|
+
<div style="color:#666;font-size:11px;margin-top:4px">仅对「⚡自动分配」的节点生效:占比 ≈ 权重比,如设 2 则用量 ≈ 普通订阅的 2 倍;全部为 1 时平均摊开。修改后下次心跳生效。</div>
|
|
708
|
+
</div>
|
|
691
709
|
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px">
|
|
692
710
|
<button onclick="wdSubCloseModal()"
|
|
693
711
|
style="padding:8px 16px;background:#333;color:#ccc;border:none;border-radius:6px;cursor:pointer;font-size:12px">取消</button>
|
package/src/watch/server.js
CHANGED
|
@@ -10,6 +10,17 @@ import { createStore } from "./data-store.js";
|
|
|
10
10
|
import { getTagDb } from "./db-schema.js";
|
|
11
11
|
import { getTargetLocations } from "../lib/app-config.js";
|
|
12
12
|
import { tryHandleWatchdogRoute } from "./watchdog-server.js";
|
|
13
|
+
import {
|
|
14
|
+
getLoginPoolView,
|
|
15
|
+
addAccount,
|
|
16
|
+
updateAccount,
|
|
17
|
+
deleteAccount,
|
|
18
|
+
assignAccount,
|
|
19
|
+
releaseAccount,
|
|
20
|
+
takeAccount,
|
|
21
|
+
acquireAccount,
|
|
22
|
+
importAccount,
|
|
23
|
+
} from "./login-pool.js";
|
|
13
24
|
|
|
14
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
26
|
|
|
@@ -624,6 +635,174 @@ ${err}
|
|
|
624
635
|
res.end(html);
|
|
625
636
|
}
|
|
626
637
|
|
|
638
|
+
/** 账号中心(登录池)页面:自包含,无外部依赖,脱敏展示 + 基础管理动作 */
|
|
639
|
+
function serveLoginPoolPage(res) {
|
|
640
|
+
const html = `<!DOCTYPE html>
|
|
641
|
+
<html><head>
|
|
642
|
+
<meta charset="utf-8">
|
|
643
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
644
|
+
<title>账号中心 · 登录池</title>
|
|
645
|
+
<style>
|
|
646
|
+
body { font-family: system-ui, sans-serif; margin: 20px; background: #f7f8fa; color: #1f2329; }
|
|
647
|
+
h1 { font-size: 20px; }
|
|
648
|
+
.bar { margin: 12px 0; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
649
|
+
button { padding: 4px 10px; border: 1px solid #d0d3d9; border-radius: 6px; background: #fff; cursor: pointer; font-size: 12px; }
|
|
650
|
+
button:hover { background: #f0f2f5; }
|
|
651
|
+
button:disabled { color: #c0c4cc; cursor: not-allowed; background: #fafafa; }
|
|
652
|
+
table { border-collapse: collapse; width: 100%; background: #fff; }
|
|
653
|
+
th, td { border: 1px solid #e5e7eb; padding: 6px 10px; font-size: 13px; text-align: left; vertical-align: top; }
|
|
654
|
+
th { background: #f2f3f5; white-space: nowrap; }
|
|
655
|
+
.badge { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 12px; font-weight: 600; }
|
|
656
|
+
.b-leased { background: #dbeafe; color: #2563eb; }
|
|
657
|
+
.b-pool { background: #dcfce7; color: #16a34a; }
|
|
658
|
+
.b-suspect { background: #fef3c7; color: #d97706; }
|
|
659
|
+
.b-retired { background: #e5e7eb; color: #6b7280; }
|
|
660
|
+
.muted { color: #9ca3af; font-size: 12px; }
|
|
661
|
+
.warn { color: #dc2626; }
|
|
662
|
+
#msg { margin: 0 8px; }
|
|
663
|
+
details { margin-top: 2px; }
|
|
664
|
+
summary { cursor: pointer; }
|
|
665
|
+
</style>
|
|
666
|
+
</head>
|
|
667
|
+
<body>
|
|
668
|
+
<h1>账号中心 · 登录池</h1>
|
|
669
|
+
<div class="bar">
|
|
670
|
+
<button onclick="load()">刷新</button>
|
|
671
|
+
<label><input type="checkbox" id="auto" checked> 自动刷新(15s)</label>
|
|
672
|
+
<button onclick="addDlg()">入池</button>
|
|
673
|
+
<button onclick="takeDlg()" title="调试:走命令队列的 take(watchdog 注入流)">模拟取号</button>
|
|
674
|
+
<span id="msg"></span>
|
|
675
|
+
</div>
|
|
676
|
+
<div id="content">加载中…</div>
|
|
677
|
+
<script>
|
|
678
|
+
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"','\\'':'''}[c]));
|
|
679
|
+
const fmt = t => t ? new Date(t).toLocaleString('zh-CN', {hour12:false}) : '—';
|
|
680
|
+
const day = t => t ? new Date(t).toLocaleDateString('zh-CN') : '—';
|
|
681
|
+
const ST = { leased: ['使用中','b-leased'], pool: ['空闲','b-pool'], suspect: ['疑似异常','b-suspect'], retired: ['已停用','b-retired'] };
|
|
682
|
+
const ORDER = { leased: 0, pool: 1, suspect: 2, retired: 3 };
|
|
683
|
+
const rel = t => {
|
|
684
|
+
if (!t) return '—';
|
|
685
|
+
const d = Date.now() - t;
|
|
686
|
+
if (d < 60000) return '刚刚';
|
|
687
|
+
if (d < 3600000) return Math.floor(d / 60000) + '分钟前';
|
|
688
|
+
if (d < 86400000) return Math.floor(d / 3600000) + '小时前';
|
|
689
|
+
return Math.floor(d / 86400000) + '天前';
|
|
690
|
+
};
|
|
691
|
+
async function api(p, body) {
|
|
692
|
+
const r = await fetch(p, body ? {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)} : undefined);
|
|
693
|
+
const j = await r.json().catch(() => ({}));
|
|
694
|
+
if (!r.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
|
695
|
+
return j;
|
|
696
|
+
}
|
|
697
|
+
function msg(t, err) { const el = document.getElementById('msg'); el.textContent = t; el.style.color = err ? '#dc2626' : '#16a34a'; }
|
|
698
|
+
const openHist = new Set(); // 展开了历史明细的账号 id(跨自动刷新保留)
|
|
699
|
+
function onHistToggle(el) {
|
|
700
|
+
const id = el.dataset.acc;
|
|
701
|
+
if (el.open) openHist.add(id);
|
|
702
|
+
else openHist.delete(id);
|
|
703
|
+
}
|
|
704
|
+
async function load() {
|
|
705
|
+
try {
|
|
706
|
+
const j = await api('/api/login-pool');
|
|
707
|
+
const list = j.accounts.slice().sort((a, b) =>
|
|
708
|
+
((ORDER[a.status] ?? 9) - (ORDER[b.status] ?? 9)) ||
|
|
709
|
+
((b.lastUsedAt || b.createdAt || 0) - (a.lastUsedAt || a.createdAt || 0)));
|
|
710
|
+
const rows = list.map((a, idx) => {
|
|
711
|
+
const l = a.lease;
|
|
712
|
+
const biz = a.lastBusinessOkAt ? fmt(a.lastBusinessOkAt) : '无记录';
|
|
713
|
+
const expired = a.sessionExpiresAt && a.sessionExpiresAt < Date.now();
|
|
714
|
+
const st = ST[a.status] || [a.status, ''];
|
|
715
|
+
const who = l
|
|
716
|
+
? esc(l.userId) + (l.port ? ':' + esc(l.port) : '') + (l.appName ? ' <span class="muted">· ' + esc(l.appName) + '</span>' : '')
|
|
717
|
+
: '<span class="muted">—</span>';
|
|
718
|
+
const meta = [];
|
|
719
|
+
if (a.note) meta.push(esc(a.note));
|
|
720
|
+
if (a.proxyHint) meta.push('出口 ' + esc(a.proxyHint));
|
|
721
|
+
if (a.cookieCount) meta.push('cookies ' + a.cookieCount + ' 条');
|
|
722
|
+
return '<tr>' +
|
|
723
|
+
'<td class="muted">' + (idx + 1) + '</td>' +
|
|
724
|
+
'<td><b>' + esc(a.username) + '</b>' +
|
|
725
|
+
(meta.length ? '<div class="muted">' + meta.join(' · ') + '</div>' : '') + '</td>' +
|
|
726
|
+
'<td><span class="badge ' + st[1] + '">' + st[0] + '</span></td>' +
|
|
727
|
+
'<td>' + who + '</td>' +
|
|
728
|
+
'<td title="最后使用: ' + fmt(a.lastUsedAt) + '\\n最后出活: ' + biz + '">' + rel(a.lastUsedAt) + '</td>' +
|
|
729
|
+
'<td>' + (a.useCount || 0) + '</td>' +
|
|
730
|
+
'<td' + (expired ? ' class="warn"' : '') + ' title="sessionid 到期: ' + fmt(a.sessionExpiresAt) + '(cookie 承诺寿命,非实时有效性)">' + day(a.sessionExpiresAt) + (expired ? '(已过)' : '') + '</td>' +
|
|
731
|
+
'<td>' +
|
|
732
|
+
(a.status === 'leased' ? '<button onclick="releaseDlg(\\'' + a.id + '\\',\\'' + esc(a.username) + '\\')">释放</button> ' : '') +
|
|
733
|
+
(a.status === 'pool' ? '<button onclick="assignDlg(\\'' + a.id + '\\',\\'' + esc(a.username) + '\\')" title="手动指派给节点端口(watchdog 注入流)">指派</button> ' : '') +
|
|
734
|
+
(a.status === 'suspect' ? '<button onclick="enable(\\'' + a.id + '\\',\\'' + esc(a.username) + '\\',true)">启用</button> ' : '') +
|
|
735
|
+
(a.status === 'retired' ? '<button onclick="enable(\\'' + a.id + '\\',\\'' + esc(a.username) + '\\',false)">启用</button> ' : '') +
|
|
736
|
+
(a.status !== 'retired' ? '<button onclick="disable(\\'' + a.id + '\\',\\'' + esc(a.username) + '\\',' + (a.status === 'leased') + ')">停用</button> ' : '') +
|
|
737
|
+
(a.status === 'retired' ? '<button onclick="del(\\'' + a.id + '\\')">删除</button> ' : '') +
|
|
738
|
+
'<button disabled title="开发中:引导重新登录并更新池内登录态">重登录</button>' +
|
|
739
|
+
'</td>' +
|
|
740
|
+
'<td><details data-acc="' + esc(a.id) + '" ' + (openHist.has(a.id) ? 'open' : '') + ' ontoggle="onHistToggle(this)"><summary class="muted">' + (a.history ? a.history.length : 0) + ' 条</summary>' +
|
|
741
|
+
(a.history || []).slice(0, 8).map(h => '<div class="muted">' + fmt(h.at) + ' ' + esc(h.event) + ' ' + esc(h.detail) + '</div>').join('') + '</details></td>' +
|
|
742
|
+
'</tr>';
|
|
743
|
+
}).join('');
|
|
744
|
+
const nLeased = j.accounts.filter(a => a.status === 'leased').length;
|
|
745
|
+
const nFree = j.accounts.filter(a => a.status === 'pool').length;
|
|
746
|
+
document.getElementById('content').innerHTML =
|
|
747
|
+
'<table><tr><th>#</th><th>账号</th><th>状态</th><th>使用者</th><th>最近使用</th><th>次数</th><th>有效期</th><th>操作</th><th>历史</th></tr>' + rows + '</table>' +
|
|
748
|
+
'<p class="muted">共 ' + j.total + ' 个 · 使用中 ' + nLeased + ' · 空闲 ' + nFree + ' · 排序:使用中 → 空闲 → 疑似 → 已停用 · 有效期 = sessionid cookie 承诺寿命(非实时有效性,真实判据 = 业务出活)· cookies 明文不出服务端</p>';
|
|
749
|
+
} catch (e) { msg('加载失败: ' + e.message, true); }
|
|
750
|
+
}
|
|
751
|
+
async function addDlg() {
|
|
752
|
+
const username = prompt('账号名(uniqueId,可留空自动从 cookies 提取):'); if (username === null) return;
|
|
753
|
+
const cookiesRaw = prompt('cookies JSON 数组(可直接粘 export.mjs 导出的整个 JSON,会自动取 cookies 字段):'); if (!cookiesRaw) return;
|
|
754
|
+
try {
|
|
755
|
+
let cookies = JSON.parse(cookiesRaw);
|
|
756
|
+
if (cookies && !Array.isArray(cookies) && Array.isArray(cookies.cookies)) cookies = cookies.cookies;
|
|
757
|
+
const note = prompt('备注(可空,如来源 F-11:9223):');
|
|
758
|
+
await api('/api/login-pool/add', { username: username || undefined, cookies, note: note || undefined });
|
|
759
|
+
msg('入池成功'); load();
|
|
760
|
+
} catch (e) { msg('入池失败: ' + e.message, true); }
|
|
761
|
+
}
|
|
762
|
+
async function takeDlg() {
|
|
763
|
+
const userId = prompt('节点 userId(如 F-11):'); if (!userId) return;
|
|
764
|
+
const port = prompt('端口:'); if (!port) return;
|
|
765
|
+
try { const j = await api('/api/login-pool/take', { userId, port: Number(port) }); msg('已取号: ' + j.account.username); load(); }
|
|
766
|
+
catch (e) { msg('取号失败: ' + e.message, true); }
|
|
767
|
+
}
|
|
768
|
+
async function assignDlg(id, name) {
|
|
769
|
+
const userId = prompt('指派给节点 userId(如 F-11):'); if (!userId) return;
|
|
770
|
+
const port = prompt('端口:'); if (!port) return;
|
|
771
|
+
try { await api('/api/login-pool/assign', { id, userId, port: Number(port) }); msg('已指派 ' + name); load(); }
|
|
772
|
+
catch (e) { msg('指派失败: ' + e.message, true); }
|
|
773
|
+
}
|
|
774
|
+
async function releaseDlg(id, name) {
|
|
775
|
+
const clearOnNode = confirm('释放 ' + name + '。\\n确定 = 同时下发 clearLogin 清理节点登录态;取消 = 仅释放租约(节点保留当前登录态)');
|
|
776
|
+
try { await api('/api/login-pool/release', { id, clearOnNode }); msg('已释放 ' + name); load(); }
|
|
777
|
+
catch (e) { msg('释放失败: ' + e.message, true); }
|
|
778
|
+
}
|
|
779
|
+
async function enable(id, name, suspect) {
|
|
780
|
+
if (suspect && !confirm(name + ' 为疑似异常状态,确认启用回池?')) return;
|
|
781
|
+
try { await api('/api/login-pool/update', { id, status: 'pool' }); msg('已启用 ' + name); load(); }
|
|
782
|
+
catch (e) { msg('失败: ' + e.message, true); }
|
|
783
|
+
}
|
|
784
|
+
async function disable(id, name, leased) {
|
|
785
|
+
const q = leased
|
|
786
|
+
? name + ' 使用中:停用将同时释放租约(节点本地登录态保留)。确认停用?'
|
|
787
|
+
: '确认停用 ' + name + '?停用后不参与自动取号';
|
|
788
|
+
if (!confirm(q)) return;
|
|
789
|
+
try { await api('/api/login-pool/update', { id, status: 'retired' }); msg('已停用 ' + name); load(); }
|
|
790
|
+
catch (e) { msg('失败: ' + e.message, true); }
|
|
791
|
+
}
|
|
792
|
+
async function del(id) {
|
|
793
|
+
if (!confirm('确认删除该账号?不可恢复')) return;
|
|
794
|
+
try { await api('/api/login-pool/delete', { id }); msg('已删除'); load(); }
|
|
795
|
+
catch (e) { msg('失败: ' + e.message, true); }
|
|
796
|
+
}
|
|
797
|
+
load();
|
|
798
|
+
setInterval(() => { if (document.getElementById('auto').checked) load(); }, 15000);
|
|
799
|
+
</script>
|
|
800
|
+
</body>
|
|
801
|
+
</html>`;
|
|
802
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
803
|
+
res.end(html);
|
|
804
|
+
}
|
|
805
|
+
|
|
627
806
|
export function startWatchServer(
|
|
628
807
|
dataAnchor,
|
|
629
808
|
port = 3000,
|
|
@@ -1405,6 +1584,61 @@ export function startWatchServer(
|
|
|
1405
1584
|
return;
|
|
1406
1585
|
}
|
|
1407
1586
|
|
|
1587
|
+
// ── 登录池(账号中心)端点:输出一律脱敏,cookies 明文仅存服务端/心跳下发通道 ──
|
|
1588
|
+
if (req.method === "GET" && routePath === "/api/login-pool") {
|
|
1589
|
+
sendJSON(res, 200, getLoginPoolView());
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
if (req.method === "POST" && routePath.startsWith("/api/login-pool/")) {
|
|
1593
|
+
const lpAction = routePath.slice("/api/login-pool/".length);
|
|
1594
|
+
const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
1595
|
+
try {
|
|
1596
|
+
const body = await readBody(req);
|
|
1597
|
+
let ret;
|
|
1598
|
+
if (lpAction === "add") {
|
|
1599
|
+
ret = addAccount(body);
|
|
1600
|
+
console.error(`[登录池 ${ts}] ADD: ${ret.username} (${ret.cookieCount} 条 cookies)`);
|
|
1601
|
+
} else if (lpAction === "take") {
|
|
1602
|
+
ret = takeAccount(body);
|
|
1603
|
+
console.error(`[登录池 ${ts}] TAKE: ${ret.username} → ${body.userId}:${body.port}`);
|
|
1604
|
+
} else if (lpAction === "acquire") {
|
|
1605
|
+
// Phase 2 直取(以号为主键):响应含明文 cookies(业务进程自注入,不经心跳命令队列)
|
|
1606
|
+
ret = acquireAccount(body);
|
|
1607
|
+
console.error(
|
|
1608
|
+
`[登录池 ${ts}] ACQUIRE: ${ret.username} → ${body.userId}${body.port ? ":" + body.port : ""} (${ret.cookieCount} 条 cookies)`,
|
|
1609
|
+
);
|
|
1610
|
+
} else if (lpAction === "import") {
|
|
1611
|
+
// 节点登录态导入(以号为主键,导入=入池登记为空闲,不自动租):按 username 去重
|
|
1612
|
+
const imp = importAccount(body);
|
|
1613
|
+
ret = imp.account;
|
|
1614
|
+
console.error(
|
|
1615
|
+
`[登录池 ${ts}] IMPORT: ${ret.username} ← ${body.userId}${body.port ? ":" + body.port : ""}${imp.dedup ? "(已在池,幂等跳过)" : "(新入池·空闲)"} (${ret.cookieCount} 条 cookies)`,
|
|
1616
|
+
);
|
|
1617
|
+
sendJSON(res, 200, { ok: true, dedup: imp.dedup, account: ret });
|
|
1618
|
+
return;
|
|
1619
|
+
} else if (lpAction === "assign") {
|
|
1620
|
+
ret = assignAccount(body);
|
|
1621
|
+
console.error(`[登录池 ${ts}] ASSIGN: ${ret.username} → ${body.userId}:${body.port}`);
|
|
1622
|
+
} else if (lpAction === "release") {
|
|
1623
|
+
ret = releaseAccount(body);
|
|
1624
|
+
console.error(`[登录池 ${ts}] RELEASE: ${ret.username} → pool`);
|
|
1625
|
+
} else if (lpAction === "update") {
|
|
1626
|
+
ret = updateAccount(body);
|
|
1627
|
+
console.error(`[登录池 ${ts}] UPDATE: ${ret.username} status=${ret.status}`);
|
|
1628
|
+
} else if (lpAction === "delete") {
|
|
1629
|
+
ret = deleteAccount(body.id);
|
|
1630
|
+
console.error(`[登录池 ${ts}] DELETE: ${ret.username}`);
|
|
1631
|
+
} else {
|
|
1632
|
+
sendJSON(res, 404, { error: `未知登录池操作: ${lpAction}` });
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
sendJSON(res, 200, { ok: true, account: ret });
|
|
1636
|
+
} catch (e) {
|
|
1637
|
+
sendJSON(res, 400, { error: e.message });
|
|
1638
|
+
}
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1408
1642
|
// ── recheck-jobs 端点 ──
|
|
1409
1643
|
if (req.method === "GET" && routePath === "/api/recheck-jobs") {
|
|
1410
1644
|
const limit = params.limit;
|
|
@@ -3100,6 +3334,24 @@ export function startWatchServer(
|
|
|
3100
3334
|
return;
|
|
3101
3335
|
}
|
|
3102
3336
|
|
|
3337
|
+
// ── 账号中心(登录池)独立页面:与主页同一登录门禁 ──
|
|
3338
|
+
if (req.method === "GET" && routePath === "/login-pool") {
|
|
3339
|
+
if (PASSWORD_ENABLED) {
|
|
3340
|
+
const token = parseAuthToken(req, params);
|
|
3341
|
+
const valid =
|
|
3342
|
+
token &&
|
|
3343
|
+
authSessions.has(token) &&
|
|
3344
|
+
authSessions.get(token) > Date.now();
|
|
3345
|
+
if (!valid) {
|
|
3346
|
+
serveLoginPage(res);
|
|
3347
|
+
return;
|
|
3348
|
+
}
|
|
3349
|
+
authSessions.set(token, Date.now() + AUTH_MAX_AGE * 1000);
|
|
3350
|
+
}
|
|
3351
|
+
serveLoginPoolPage(res);
|
|
3352
|
+
return;
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3103
3355
|
// ====== 静态资源(HTML 页面需登录) ======
|
|
3104
3356
|
|
|
3105
3357
|
if (
|