tt-help-cli-ycl 1.4.115 → 1.4.119
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-clash-discover.mjs +126 -0
- package/scripts/test-clash-multichannel.mjs +120 -0
- package/src/clash/api.js +29 -3
- package/src/clash/benchmark/lib/clash-api.js +3 -232
- package/src/clash/checker.js +63 -30
- package/src/clash/daemon.js +285 -108
- package/src/clash/discover.js +451 -0
- package/src/clash/ipc.js +65 -20
- package/src/cli/clash-daemon.js +10 -1
- package/src/cli/login-check.js +309 -0
- package/src/cli/refresh-mini.js +36 -24
- package/src/cli/watchdog.js +2 -0
- package/src/lib/args.js +81 -0
- package/src/lib/constants.js +17 -1
- package/src/main.js +3 -0
- package/src/npm-main.js +3 -0
- package/src/watch/data-store.js +34 -3
- package/src/watch/public/app.js +218 -21
- package/src/watch/public/index.html +20 -1
- package/src/watch/watchdog-server.js +90 -4
- package/src/watchdog/agent.js +166 -7
- package/src/watchdog/reporter.js +32 -1
package/src/watch/data-store.js
CHANGED
|
@@ -668,7 +668,8 @@ function getTargetRefreshStats(hours = 24) {
|
|
|
668
668
|
)
|
|
669
669
|
.all(...targetLocations);
|
|
670
670
|
|
|
671
|
-
if (!totalRows.length)
|
|
671
|
+
if (!totalRows.length)
|
|
672
|
+
return { countries: [], total: 0, refreshed: 0, pendingRefresh: 0 };
|
|
672
673
|
|
|
673
674
|
// 按国家统计最近 N 小时内刷新过的数量
|
|
674
675
|
const threshold = Date.now() - hours * 3600 * 1000;
|
|
@@ -688,17 +689,47 @@ function getTargetRefreshStats(hours = 24) {
|
|
|
688
689
|
const refreshedMap = new Map();
|
|
689
690
|
for (const r of refreshedRows) refreshedMap.set(r.country, r.cnt);
|
|
690
691
|
|
|
692
|
+
// 按国家统计待 Refresh 数量(pending_refresh = 1,等待 refresh 客户端处理)
|
|
693
|
+
const pendingRows = getDb()
|
|
694
|
+
.prepare(
|
|
695
|
+
`
|
|
696
|
+
SELECT location_created as country, COUNT(*) as cnt
|
|
697
|
+
FROM jobs
|
|
698
|
+
WHERE tt_seller = 1
|
|
699
|
+
AND verified = 0
|
|
700
|
+
AND pending_refresh = 1
|
|
701
|
+
AND location_created IN (${placeholders})
|
|
702
|
+
GROUP BY location_created
|
|
703
|
+
`,
|
|
704
|
+
)
|
|
705
|
+
.all(...targetLocations);
|
|
706
|
+
const pendingMap = new Map();
|
|
707
|
+
for (const r of pendingRows) pendingMap.set(r.country, r.cnt);
|
|
708
|
+
|
|
691
709
|
const results = [];
|
|
692
710
|
let totalAll = 0;
|
|
693
711
|
let refreshedAll = 0;
|
|
712
|
+
let pendingAll = 0;
|
|
694
713
|
for (const r of totalRows) {
|
|
695
714
|
const refreshed = refreshedMap.get(r.country) || 0;
|
|
715
|
+
const pendingRefresh = pendingMap.get(r.country) || 0;
|
|
696
716
|
totalAll += r.cnt;
|
|
697
717
|
refreshedAll += refreshed;
|
|
698
|
-
|
|
718
|
+
pendingAll += pendingRefresh;
|
|
719
|
+
results.push({
|
|
720
|
+
country: r.country,
|
|
721
|
+
total: r.cnt,
|
|
722
|
+
refreshed,
|
|
723
|
+
pendingRefresh,
|
|
724
|
+
});
|
|
699
725
|
}
|
|
700
726
|
|
|
701
|
-
return {
|
|
727
|
+
return {
|
|
728
|
+
countries: results,
|
|
729
|
+
total: totalAll,
|
|
730
|
+
refreshed: refreshedAll,
|
|
731
|
+
pendingRefresh: pendingAll,
|
|
732
|
+
};
|
|
702
733
|
}
|
|
703
734
|
|
|
704
735
|
export function createStore(filePath, options = {}) {
|
package/src/watch/public/app.js
CHANGED
|
@@ -1706,7 +1706,7 @@ let currentTagsSeq = 0;
|
|
|
1706
1706
|
const TAGS_PAGE_SIZE = 200;
|
|
1707
1707
|
let cachedTagCountries = [];
|
|
1708
1708
|
let currentRefreshHours = 24;
|
|
1709
|
-
let currentTargetRefreshHours =
|
|
1709
|
+
let currentTargetRefreshHours = 48;
|
|
1710
1710
|
|
|
1711
1711
|
function showTagsPage() {
|
|
1712
1712
|
document.getElementById("mainPage").classList.add("hidden");
|
|
@@ -2087,6 +2087,7 @@ function renderTargetRefreshStats(data) {
|
|
|
2087
2087
|
<thead><tr style="color:#888;text-align:left">
|
|
2088
2088
|
<th style="padding:4px 8px">国家</th>
|
|
2089
2089
|
<th style="padding:4px 8px;text-align:right">商家数</th>
|
|
2090
|
+
<th style="padding:4px 8px;text-align:right">待Refresh</th>
|
|
2090
2091
|
<th style="padding:4px 8px;text-align:right">${currentTargetRefreshHours}h已刷</th>
|
|
2091
2092
|
<th style="padding:4px 8px;width:120px">进度</th>
|
|
2092
2093
|
</tr></thead>
|
|
@@ -2098,6 +2099,7 @@ function renderTargetRefreshStats(data) {
|
|
|
2098
2099
|
html += `<tr style="color:#ccc;border-top:1px solid #222">
|
|
2099
2100
|
<td style="padding:4px 8px">${c.country}</td>
|
|
2100
2101
|
<td style="padding:4px 8px;text-align:right">${c.total}</td>
|
|
2102
|
+
<td style="padding:4px 8px;text-align:right;color:${c.pendingRefresh > 0 ? "#fbbf24" : "#9ca3af"}">${c.pendingRefresh || 0}</td>
|
|
2101
2103
|
<td style="padding:4px 8px;text-align:right;color:${cpct >= 50 ? "#22c55e" : "#facc15"}">${c.refreshed}</td>
|
|
2102
2104
|
<td style="padding:4px 8px">
|
|
2103
2105
|
<div style="display:flex;align-items:center;gap:6px">
|
|
@@ -3436,6 +3438,7 @@ async function fetchWatchdogData() {
|
|
|
3436
3438
|
_wdData.clientCommands || {},
|
|
3437
3439
|
_wdData.clashCountries || {},
|
|
3438
3440
|
_wdData.errorTriggers || {},
|
|
3441
|
+
_wdData.loginPorts || {},
|
|
3439
3442
|
);
|
|
3440
3443
|
} catch (e) {
|
|
3441
3444
|
document.getElementById("wdClientsList").textContent =
|
|
@@ -3488,6 +3491,7 @@ function renderWatchdogDashboard(
|
|
|
3488
3491
|
clientCommands,
|
|
3489
3492
|
clashCountries,
|
|
3490
3493
|
errorTriggers,
|
|
3494
|
+
loginPorts,
|
|
3491
3495
|
) {
|
|
3492
3496
|
// 顶栏统计始终更新(textContent 不闪烁)
|
|
3493
3497
|
var onlineCount = clients.filter(function (c) {
|
|
@@ -3546,7 +3550,13 @@ function renderWatchdogDashboard(
|
|
|
3546
3550
|
var allCards = [];
|
|
3547
3551
|
sortedClients.forEach(function (c) {
|
|
3548
3552
|
allCards.push(
|
|
3549
|
-
renderClientCard(
|
|
3553
|
+
renderClientCard(
|
|
3554
|
+
c,
|
|
3555
|
+
clientCommands,
|
|
3556
|
+
clashCountries,
|
|
3557
|
+
errorTriggers,
|
|
3558
|
+
loginPorts,
|
|
3559
|
+
),
|
|
3550
3560
|
);
|
|
3551
3561
|
});
|
|
3552
3562
|
// 离线卡也按名称排序
|
|
@@ -3570,6 +3580,7 @@ function renderWatchdogDashboard(
|
|
|
3570
3580
|
clientCommands[uid],
|
|
3571
3581
|
clashCountries,
|
|
3572
3582
|
errorTriggers,
|
|
3583
|
+
loginPorts,
|
|
3573
3584
|
),
|
|
3574
3585
|
);
|
|
3575
3586
|
});
|
|
@@ -3615,7 +3626,13 @@ function renderWatchdogDashboard(
|
|
|
3615
3626
|
}
|
|
3616
3627
|
}
|
|
3617
3628
|
|
|
3618
|
-
function renderOfflineCard(
|
|
3629
|
+
function renderOfflineCard(
|
|
3630
|
+
userId,
|
|
3631
|
+
cmds,
|
|
3632
|
+
clashCountries,
|
|
3633
|
+
errorTriggers,
|
|
3634
|
+
loginPorts,
|
|
3635
|
+
) {
|
|
3619
3636
|
var cmdCount = cmds ? cmds.length : 0;
|
|
3620
3637
|
return styled(
|
|
3621
3638
|
"div",
|
|
@@ -3686,6 +3703,7 @@ function renderOfflineCard(userId, cmds, clashCountries, errorTriggers) {
|
|
|
3686
3703
|
) +
|
|
3687
3704
|
renderCountrySection({}, userId, clashCountries) +
|
|
3688
3705
|
renderErrorTriggerSection({}, userId, errorTriggers) +
|
|
3706
|
+
renderLoginSection({}, userId, loginPorts) +
|
|
3689
3707
|
renderCmdRows(cmds || [], userId, [], false),
|
|
3690
3708
|
);
|
|
3691
3709
|
}
|
|
@@ -3720,7 +3738,13 @@ function findAnalysis(llmAnalysis, cmdId) {
|
|
|
3720
3738
|
return null;
|
|
3721
3739
|
}
|
|
3722
3740
|
|
|
3723
|
-
function renderClientCard(
|
|
3741
|
+
function renderClientCard(
|
|
3742
|
+
c,
|
|
3743
|
+
clientCommands,
|
|
3744
|
+
clashCountries,
|
|
3745
|
+
errorTriggers,
|
|
3746
|
+
loginPorts,
|
|
3747
|
+
) {
|
|
3724
3748
|
var os = c.osInfo || {},
|
|
3725
3749
|
cfg = c.config || {},
|
|
3726
3750
|
procs = c.processes || [];
|
|
@@ -3833,6 +3857,7 @@ function renderClientCard(c, clientCommands, clashCountries, errorTriggers) {
|
|
|
3833
3857
|
styled("div", "color:#888;font-size:12px;margin-bottom:6px", infoLine) +
|
|
3834
3858
|
renderCountrySection(c, userId, clashCountries) +
|
|
3835
3859
|
renderErrorTriggerSection(c, userId, errorTriggers) +
|
|
3860
|
+
renderLoginSection(c, userId, loginPorts) +
|
|
3836
3861
|
renderProxySection(c, userId) +
|
|
3837
3862
|
renderCmdRows(myCmds, userId, procs, online, c.llmAnalysis),
|
|
3838
3863
|
);
|
|
@@ -3962,6 +3987,137 @@ async function wdSetCountries() {
|
|
|
3962
3987
|
}
|
|
3963
3988
|
}
|
|
3964
3989
|
|
|
3990
|
+
// ── 登录检测端口配置区域(看门狗面板中每个客户端卡片) ──
|
|
3991
|
+
|
|
3992
|
+
function renderLoginSection(c, userId, loginPorts) {
|
|
3993
|
+
if (!userId) return "";
|
|
3994
|
+
var cfgEntry = (loginPorts && loginPorts[userId]) || {};
|
|
3995
|
+
var configured =
|
|
3996
|
+
cfgEntry.ports && cfgEntry.ports.length ? cfgEntry.ports : [];
|
|
3997
|
+
var report = c.loginReport || null;
|
|
3998
|
+
var reportHtml;
|
|
3999
|
+
if (!report) {
|
|
4000
|
+
reportHtml = styled(
|
|
4001
|
+
"span",
|
|
4002
|
+
"color:#555;margin-left:auto",
|
|
4003
|
+
configured.length ? "等待客户端上报..." : "",
|
|
4004
|
+
);
|
|
4005
|
+
} else if (report.error) {
|
|
4006
|
+
var et = report.checkedAt
|
|
4007
|
+
? new Date(report.checkedAt).toLocaleTimeString("zh-CN")
|
|
4008
|
+
: "?";
|
|
4009
|
+
reportHtml =
|
|
4010
|
+
styled("span", "color:#888;margin-left:auto", "最近检测(" + et + "): ") +
|
|
4011
|
+
styled("span", "color:#f87171", "失败");
|
|
4012
|
+
} else if (report.ports && report.ports.length) {
|
|
4013
|
+
var et2 = report.checkedAt
|
|
4014
|
+
? new Date(report.checkedAt).toLocaleTimeString("zh-CN")
|
|
4015
|
+
: "?";
|
|
4016
|
+
var badges = report.ports
|
|
4017
|
+
.map(function (r) {
|
|
4018
|
+
var color =
|
|
4019
|
+
r.status === "logged_in"
|
|
4020
|
+
? "#4ade80"
|
|
4021
|
+
: r.status === "expired"
|
|
4022
|
+
? "#fbbf24"
|
|
4023
|
+
: r.status === "error"
|
|
4024
|
+
? "#f87171"
|
|
4025
|
+
: "#888";
|
|
4026
|
+
var label =
|
|
4027
|
+
r.status === "logged_in"
|
|
4028
|
+
? "已登录"
|
|
4029
|
+
: r.status === "expired"
|
|
4030
|
+
? "已过期"
|
|
4031
|
+
: r.status === "error"
|
|
4032
|
+
? "失败"
|
|
4033
|
+
: "未登录";
|
|
4034
|
+
var txt =
|
|
4035
|
+
r.port +
|
|
4036
|
+
" " +
|
|
4037
|
+
label +
|
|
4038
|
+
(r.status === "logged_in" && r.account ? " @" + r.account : "");
|
|
4039
|
+
return styled(
|
|
4040
|
+
"span",
|
|
4041
|
+
"color:" +
|
|
4042
|
+
color +
|
|
4043
|
+
";background:#0d1117;border:1px solid #333;padding:1px 6px;border-radius:4px",
|
|
4044
|
+
txt,
|
|
4045
|
+
);
|
|
4046
|
+
})
|
|
4047
|
+
.join(" ");
|
|
4048
|
+
reportHtml =
|
|
4049
|
+
styled("span", "color:#888;margin-left:auto", "最近检测(" + et2 + "): ") +
|
|
4050
|
+
badges;
|
|
4051
|
+
} else {
|
|
4052
|
+
reportHtml = styled("span", "color:#555;margin-left:auto", "无结果");
|
|
4053
|
+
}
|
|
4054
|
+
return styled(
|
|
4055
|
+
"div",
|
|
4056
|
+
"margin-top:6px;padding:6px 8px;background:#111;border-radius:6px;" +
|
|
4057
|
+
"display:flex;align-items:center;gap:8px;font-size:12px;flex-wrap:wrap",
|
|
4058
|
+
styled("span", "font-size:14px", "🔑") +
|
|
4059
|
+
styled("span", "color:#888", "登录检测:") +
|
|
4060
|
+
tag(
|
|
4061
|
+
"button",
|
|
4062
|
+
{
|
|
4063
|
+
onclick: "wdShowLoginPortsModal('" + esc(userId) + "')",
|
|
4064
|
+
style:
|
|
4065
|
+
"padding:2px 8px;font-size:11px;background:#333;color:#4ade80;" +
|
|
4066
|
+
"border:1px solid #444;border-radius:3px;cursor:pointer",
|
|
4067
|
+
title: "配置要定期检测登录状态的 CDP 端口(逗号分隔,留空清空)",
|
|
4068
|
+
},
|
|
4069
|
+
(configured.length ? configured.join(",") : "未配置") + " ⚙️",
|
|
4070
|
+
) +
|
|
4071
|
+
reportHtml,
|
|
4072
|
+
);
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
// ── 登录检测端口配置弹窗 ──
|
|
4076
|
+
|
|
4077
|
+
var _wdLoginPortsUserId = "";
|
|
4078
|
+
|
|
4079
|
+
function wdShowLoginPortsModal(userId) {
|
|
4080
|
+
_wdLoginPortsUserId = userId;
|
|
4081
|
+
document.getElementById("wdLoginPortsUserId").textContent = userId;
|
|
4082
|
+
var configured = [];
|
|
4083
|
+
if (_wdData && _wdData.loginPorts && _wdData.loginPorts[userId]) {
|
|
4084
|
+
configured = _wdData.loginPorts[userId].ports || [];
|
|
4085
|
+
}
|
|
4086
|
+
document.getElementById("wdLoginPortsInput").value = configured.join(",");
|
|
4087
|
+
document.getElementById("wdLoginPortsModal").style.display = "flex";
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4090
|
+
async function wdSetLoginPorts() {
|
|
4091
|
+
var raw = document.getElementById("wdLoginPortsInput").value;
|
|
4092
|
+
var ports = raw
|
|
4093
|
+
.split(",")
|
|
4094
|
+
.map(function (s) {
|
|
4095
|
+
return s.trim();
|
|
4096
|
+
})
|
|
4097
|
+
.filter(Boolean);
|
|
4098
|
+
try {
|
|
4099
|
+
var res = await fetch("/api/watchdog/login-ports", {
|
|
4100
|
+
method: "POST",
|
|
4101
|
+
headers: { "Content-Type": "application/json" },
|
|
4102
|
+
body: JSON.stringify({ userId: _wdLoginPortsUserId, ports: ports }),
|
|
4103
|
+
});
|
|
4104
|
+
var d = await res.json();
|
|
4105
|
+
document.getElementById("wdLoginPortsModal").style.display = "none";
|
|
4106
|
+
if (d.ok) {
|
|
4107
|
+
alert(
|
|
4108
|
+
"已配置检测端口: " +
|
|
4109
|
+
(d.ports.length ? d.ports.join(",") : "无") +
|
|
4110
|
+
"\n\n客户端将在下次心跳时生效,并立即触发一次检测。",
|
|
4111
|
+
);
|
|
4112
|
+
} else {
|
|
4113
|
+
alert("失败: " + (d.error || "未知错误"));
|
|
4114
|
+
}
|
|
4115
|
+
fetchWatchdogData();
|
|
4116
|
+
} catch (e) {
|
|
4117
|
+
alert("保存失败: " + e.message);
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
|
|
3965
4121
|
// ── 错误自动切节点配置区域(看门狗面板中每个客户端卡片) ──
|
|
3966
4122
|
|
|
3967
4123
|
function renderErrorTriggerSection(c, userId, errorTriggers) {
|
|
@@ -4055,23 +4211,41 @@ async function wdSetErrorTrigger() {
|
|
|
4055
4211
|
}
|
|
4056
4212
|
}
|
|
4057
4213
|
|
|
4058
|
-
// ──
|
|
4214
|
+
// ── 代理节点区域(看门狗面板中每个客户端卡片,支持多控制端通道) ──
|
|
4059
4215
|
|
|
4060
4216
|
function renderProxySection(c, userId) {
|
|
4217
|
+
var channels = c.clashHealthChannels || [];
|
|
4218
|
+
if (channels.length > 0) {
|
|
4219
|
+
var html = "";
|
|
4220
|
+
for (var i = 0; i < channels.length; i++) {
|
|
4221
|
+
html += renderProxyChannel(channels[i], userId);
|
|
4222
|
+
}
|
|
4223
|
+
return html;
|
|
4224
|
+
}
|
|
4225
|
+
// 兼容旧版:仅主控制端(legacy clashHealth)
|
|
4061
4226
|
var h = c.clashHealth;
|
|
4062
4227
|
if (!h || !h.node) return "";
|
|
4063
|
-
|
|
4064
4228
|
var nodes = (h.meta && h.meta.healthyNodes) || [];
|
|
4065
4229
|
var hasNodes = nodes.length > 0;
|
|
4230
|
+
return (
|
|
4231
|
+
renderProxyLine(h, nodes, userId, 0, hasNodes) +
|
|
4232
|
+
renderNodeList(nodes, h.node, userId, 0)
|
|
4233
|
+
);
|
|
4234
|
+
}
|
|
4066
4235
|
|
|
4236
|
+
/** 单个控制端通道的代理区域 */
|
|
4237
|
+
function renderProxyChannel(ch, userId) {
|
|
4238
|
+
if (!ch || !ch.node) return "";
|
|
4239
|
+
var nodes = (ch.meta && ch.meta.healthyNodes) || [];
|
|
4240
|
+
var hasNodes = nodes.length > 0;
|
|
4067
4241
|
return (
|
|
4068
|
-
renderProxyLine(
|
|
4069
|
-
renderNodeList(nodes,
|
|
4242
|
+
renderProxyLine(ch, nodes, userId, ch.port || 0, hasNodes) +
|
|
4243
|
+
renderNodeList(nodes, ch.node, userId, ch.port || 0)
|
|
4070
4244
|
);
|
|
4071
4245
|
}
|
|
4072
4246
|
|
|
4073
4247
|
/** 代理状态行:当前节点 + 健康状态 + 展开按钮 */
|
|
4074
|
-
function renderProxyLine(h, nodes, userId, hasNodes) {
|
|
4248
|
+
function renderProxyLine(h, nodes, userId, port, hasNodes) {
|
|
4075
4249
|
var dot = h.healthy ? "#4ade80" : "#f87171";
|
|
4076
4250
|
var status = h.healthy
|
|
4077
4251
|
? (h.timeTotal || 0).toFixed(1) +
|
|
@@ -4079,19 +4253,31 @@ function renderProxyLine(h, nodes, userId, hasNodes) {
|
|
|
4079
4253
|
((h.sizeDownload || 0) / 1024).toFixed(0) +
|
|
4080
4254
|
"KB"
|
|
4081
4255
|
: h.error || "异常";
|
|
4256
|
+
var clientTag = port
|
|
4257
|
+
? h.meta && h.meta.clientName
|
|
4258
|
+
? h.meta.clientName + " :" + port
|
|
4259
|
+
: "代理 :" + port
|
|
4260
|
+
: "";
|
|
4082
4261
|
|
|
4083
4262
|
return styled(
|
|
4084
4263
|
"div",
|
|
4085
4264
|
"margin-top:6px;padding:6px 8px;background:#111;border-radius:6px;" +
|
|
4086
4265
|
"display:flex;align-items:center;gap:8px;font-size:12px",
|
|
4087
4266
|
styled("span", "color:" + dot + ";font-size:14px", "●") +
|
|
4267
|
+
(clientTag
|
|
4268
|
+
? styled(
|
|
4269
|
+
"span",
|
|
4270
|
+
"color:#888;padding:0 4px;border:1px solid #333;border-radius:3px;font-size:10px",
|
|
4271
|
+
clientTag,
|
|
4272
|
+
)
|
|
4273
|
+
: "") +
|
|
4088
4274
|
styled("span", "color:#e0e0e0;font-weight:bold", h.node) +
|
|
4089
4275
|
styled("span", "color:#666", "| " + status) +
|
|
4090
4276
|
(hasNodes
|
|
4091
4277
|
? tag(
|
|
4092
4278
|
"button",
|
|
4093
4279
|
{
|
|
4094
|
-
onclick: "wdToggleNodes('" + esc(userId) + "')",
|
|
4280
|
+
onclick: "wdToggleNodes('" + esc(userId) + "'," + port + ")",
|
|
4095
4281
|
style:
|
|
4096
4282
|
"margin-left:auto;padding:2px 8px;font-size:11px;background:#333;" +
|
|
4097
4283
|
"color:#60a5fa;border:1px solid #444;border-radius:3px;cursor:pointer",
|
|
@@ -4104,15 +4290,15 @@ function renderProxyLine(h, nodes, userId, hasNodes) {
|
|
|
4104
4290
|
}
|
|
4105
4291
|
|
|
4106
4292
|
/** 节点列表面板(默认隐藏) */
|
|
4107
|
-
function renderNodeList(nodes, currentNode, userId,
|
|
4108
|
-
if (!
|
|
4293
|
+
function renderNodeList(nodes, currentNode, userId, port) {
|
|
4294
|
+
if (!nodes || !nodes.length) return "";
|
|
4109
4295
|
|
|
4110
4296
|
var items = "";
|
|
4111
4297
|
for (var i = 0; i < nodes.length; i++) {
|
|
4112
|
-
items += renderNodeItem(nodes[i], nodes[i] === currentNode, userId);
|
|
4298
|
+
items += renderNodeItem(nodes[i], nodes[i] === currentNode, userId, port);
|
|
4113
4299
|
}
|
|
4114
4300
|
|
|
4115
|
-
var id = "wdNodes-" + wdEscId(userId);
|
|
4301
|
+
var id = "wdNodes-" + wdEscId(userId) + "-" + port;
|
|
4116
4302
|
return (
|
|
4117
4303
|
'<div id="' +
|
|
4118
4304
|
id +
|
|
@@ -4124,7 +4310,7 @@ function renderNodeList(nodes, currentNode, userId, hasNodes) {
|
|
|
4124
4310
|
}
|
|
4125
4311
|
|
|
4126
4312
|
/** 单个节点行 */
|
|
4127
|
-
function renderNodeItem(name, isActive, userId) {
|
|
4313
|
+
function renderNodeItem(name, isActive, userId, port) {
|
|
4128
4314
|
var color = isActive ? "#4ade80" : "#888";
|
|
4129
4315
|
var bg = isActive ? "background:#1a3a2a;" : "";
|
|
4130
4316
|
var dot = isActive ? "●" : "○";
|
|
@@ -4135,7 +4321,9 @@ function renderNodeItem(name, isActive, userId) {
|
|
|
4135
4321
|
esc(userId) +
|
|
4136
4322
|
"','" +
|
|
4137
4323
|
esc(name) +
|
|
4138
|
-
"'
|
|
4324
|
+
"'," +
|
|
4325
|
+
port +
|
|
4326
|
+
')" ' +
|
|
4139
4327
|
'style="margin-left:auto;padding:1px 6px;font-size:10px;background:#2563eb;' +
|
|
4140
4328
|
'color:#fff;border:none;border-radius:2px;cursor:pointer">切换</button>';
|
|
4141
4329
|
|
|
@@ -4164,18 +4352,23 @@ function wdEscId(s) {
|
|
|
4164
4352
|
return (s || "").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
4165
4353
|
}
|
|
4166
4354
|
|
|
4167
|
-
function wdToggleNodes(userId) {
|
|
4168
|
-
var el = document.getElementById(
|
|
4355
|
+
function wdToggleNodes(userId, port) {
|
|
4356
|
+
var el = document.getElementById(
|
|
4357
|
+
"wdNodes-" + wdEscId(userId) + "-" + (port || 0),
|
|
4358
|
+
);
|
|
4169
4359
|
if (!el) return;
|
|
4170
4360
|
el.style.display = el.style.display === "none" ? "block" : "none";
|
|
4171
4361
|
}
|
|
4172
4362
|
|
|
4173
|
-
function wdSwitchNode(userId, node) {
|
|
4363
|
+
function wdSwitchNode(userId, node, port) {
|
|
4364
|
+
var portLabel = port ? "(代理通道 " + port + ")" : "";
|
|
4174
4365
|
if (
|
|
4175
4366
|
!confirm(
|
|
4176
4367
|
"确定要将 " +
|
|
4177
4368
|
userId +
|
|
4178
|
-
"
|
|
4369
|
+
" 的代理" +
|
|
4370
|
+
portLabel +
|
|
4371
|
+
"切换到: " +
|
|
4179
4372
|
node +
|
|
4180
4373
|
" ?\n\n切换后自动巡检会继续运行。",
|
|
4181
4374
|
)
|
|
@@ -4185,7 +4378,11 @@ function wdSwitchNode(userId, node) {
|
|
|
4185
4378
|
fetch("/api/watchdog/switch-node", {
|
|
4186
4379
|
method: "POST",
|
|
4187
4380
|
headers: { "Content-Type": "application/json" },
|
|
4188
|
-
body: JSON.stringify({
|
|
4381
|
+
body: JSON.stringify({
|
|
4382
|
+
userId: userId,
|
|
4383
|
+
node: node,
|
|
4384
|
+
proxyPort: port || undefined,
|
|
4385
|
+
}),
|
|
4189
4386
|
})
|
|
4190
4387
|
.then(function (r) {
|
|
4191
4388
|
return r.json();
|
|
@@ -667,7 +667,26 @@
|
|
|
667
667
|
</div>
|
|
668
668
|
</div>
|
|
669
669
|
|
|
670
|
-
|
|
670
|
+
<!-- 登录检测端口配置弹窗 -->
|
|
671
|
+
<div id="wdLoginPortsModal"
|
|
672
|
+
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
|
+
<div style="background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:24px;width:90%;max-width:480px">
|
|
674
|
+
<div style="font-size:16px;color:#4ade80;margin-bottom:4px">🔑 登录检测端口 — <span id="wdLoginPortsUserId"></span>
|
|
675
|
+
</div>
|
|
676
|
+
<div style="color:#888;font-size:12px;margin-bottom:12px">配置该客户端要定期检测登录状态的 CDP 端口(逗号分隔,如 <b
|
|
677
|
+
style="color:#4ade80">9222,9223,9225</b>)。<br>客户端每隔一段时间自动检测并上报结果,留空保存可清空配置。</div>
|
|
678
|
+
<input id="wdLoginPortsInput" placeholder="9222,9223,9225"
|
|
679
|
+
style="width:100%;padding:8px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:6px;font-size:13px;box-sizing:border-box">
|
|
680
|
+
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px">
|
|
681
|
+
<button onclick="document.getElementById('wdLoginPortsModal').style.display='none'"
|
|
682
|
+
style="padding:8px 16px;background:#333;color:#ccc;border:none;border-radius:6px;cursor:pointer;font-size:12px">取消</button>
|
|
683
|
+
<button onclick="wdSetLoginPorts()"
|
|
684
|
+
style="padding:8px 16px;background:#4ade80;color:#000;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600">保存并下发</button>
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
</div>
|
|
688
|
+
|
|
689
|
+
<script src="app.js?v=20260816b"></script>
|
|
671
690
|
</body>
|
|
672
691
|
|
|
673
692
|
</html>
|
|
@@ -120,6 +120,8 @@ function getOnlineClients() {
|
|
|
120
120
|
processes,
|
|
121
121
|
llmAnalysis,
|
|
122
122
|
clashHealth: info.clashHealth || null,
|
|
123
|
+
clashHealthChannels: info.clashHealthChannels || [],
|
|
124
|
+
loginReport: info.loginReport || null,
|
|
123
125
|
lastHeartbeat: info.lastHeartbeat,
|
|
124
126
|
registeredAt: info.registeredAt,
|
|
125
127
|
online,
|
|
@@ -242,6 +244,43 @@ function getAllErrorTriggers() {
|
|
|
242
244
|
return cfg.errorTriggers || {};
|
|
243
245
|
}
|
|
244
246
|
|
|
247
|
+
// ── 登录端口检测配置管理(per agent,后端配置要检查的 CDP 端口列表) ──
|
|
248
|
+
|
|
249
|
+
function getLoginPortsStored(userId) {
|
|
250
|
+
if (!userId) return [];
|
|
251
|
+
const cfg = loadConfig();
|
|
252
|
+
const entry = cfg.loginPorts && cfg.loginPorts[userId];
|
|
253
|
+
if (entry && Array.isArray(entry.ports)) return entry.ports;
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function setLoginPortsStored(userId, ports) {
|
|
258
|
+
const cleaned = [
|
|
259
|
+
...new Set(
|
|
260
|
+
(Array.isArray(ports) ? ports : [])
|
|
261
|
+
.map((p) => parseInt(p, 10))
|
|
262
|
+
.filter((p) => Number.isInteger(p) && p > 0 && p < 65536),
|
|
263
|
+
),
|
|
264
|
+
];
|
|
265
|
+
const cfg = loadConfig();
|
|
266
|
+
if (!cfg.loginPorts) cfg.loginPorts = {};
|
|
267
|
+
if (cleaned.length === 0) {
|
|
268
|
+
delete cfg.loginPorts[userId];
|
|
269
|
+
} else {
|
|
270
|
+
cfg.loginPorts[userId] = {
|
|
271
|
+
ports: cleaned,
|
|
272
|
+
updatedAt: new Date().toISOString(),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
saveConfig(cfg);
|
|
276
|
+
return cleaned;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function getAllLoginPorts() {
|
|
280
|
+
const cfg = loadConfig();
|
|
281
|
+
return cfg.loginPorts || {};
|
|
282
|
+
}
|
|
283
|
+
|
|
245
284
|
// ── 节点国家配置管理(per agent,默认 US,幂等下发) ──
|
|
246
285
|
|
|
247
286
|
/** 读取某 agent 的国家配置,未配置时默认 ["US"] */
|
|
@@ -324,6 +363,10 @@ async function handleHeartbeat(params, body, res) {
|
|
|
324
363
|
config: body.config || {},
|
|
325
364
|
processes: body.processes || [],
|
|
326
365
|
clashHealth: body.clashHealth || null,
|
|
366
|
+
clashHealthChannels: Array.isArray(body.clashHealthChannels)
|
|
367
|
+
? body.clashHealthChannels
|
|
368
|
+
: [],
|
|
369
|
+
loginReport: body.loginReport || null,
|
|
327
370
|
});
|
|
328
371
|
|
|
329
372
|
// 按 userId 返回该客户端的专属命令列表
|
|
@@ -362,6 +405,7 @@ async function handleHeartbeat(params, body, res) {
|
|
|
362
405
|
clashCommands: clashCommands.length > 0 ? clashCommands : undefined,
|
|
363
406
|
clientConfig: getClientConfigStored(userId),
|
|
364
407
|
errorTrigger: getErrorTriggerStored(userId),
|
|
408
|
+
loginPorts: getLoginPortsStored(userId),
|
|
365
409
|
serverTime: Date.now(),
|
|
366
410
|
});
|
|
367
411
|
}
|
|
@@ -375,6 +419,7 @@ async function handleGetClients(params, body, res) {
|
|
|
375
419
|
clashCountries: getAllClientCountries(),
|
|
376
420
|
supportedCountries: SUPPORTED_COUNTRIES,
|
|
377
421
|
errorTriggers: getAllErrorTriggers(),
|
|
422
|
+
loginPorts: getAllLoginPorts(),
|
|
378
423
|
configPath: WATCHDOG_CONFIG_PATH,
|
|
379
424
|
});
|
|
380
425
|
}
|
|
@@ -445,6 +490,7 @@ async function handleDeleteClient(params, body, res) {
|
|
|
445
490
|
if (cfg.clientConfigs) delete cfg.clientConfigs[userId];
|
|
446
491
|
if (cfg.clashCountries) delete cfg.clashCountries[userId];
|
|
447
492
|
if (cfg.errorTriggers) delete cfg.errorTriggers[userId];
|
|
493
|
+
if (cfg.loginPorts) delete cfg.loginPorts[userId];
|
|
448
494
|
saveConfig(cfg);
|
|
449
495
|
lastSentCountries.delete(userId);
|
|
450
496
|
clashCmdQueue.delete(userId);
|
|
@@ -492,16 +538,22 @@ async function handleClearLlmAnalysis(params, body, res) {
|
|
|
492
538
|
}
|
|
493
539
|
|
|
494
540
|
async function handleSwitchNode(params, body, res) {
|
|
495
|
-
const { userId, node } = body || {};
|
|
541
|
+
const { userId, node, proxyPort } = body || {};
|
|
496
542
|
if (!userId || !node) {
|
|
497
543
|
sendJSON(res, 400, { error: "缺少 userId 或 node" });
|
|
498
544
|
return;
|
|
499
545
|
}
|
|
500
|
-
// 加入 Clash
|
|
546
|
+
// 加入 Clash 命令队列,客户端下次心跳时取走(proxyPort 用于路由到对应控制端)
|
|
501
547
|
const queue = clashCmdQueue.get(userId) || [];
|
|
502
|
-
queue.push({
|
|
548
|
+
queue.push({
|
|
549
|
+
action: "switchNode",
|
|
550
|
+
node,
|
|
551
|
+
proxyPort: proxyPort ? Number(proxyPort) : undefined,
|
|
552
|
+
});
|
|
503
553
|
clashCmdQueue.set(userId, queue);
|
|
504
|
-
console.error(
|
|
554
|
+
console.error(
|
|
555
|
+
`[watchdog] ${userId} 节点切换已入队: → ${node}${proxyPort ? ` (通道 ${proxyPort})` : ""}`,
|
|
556
|
+
);
|
|
505
557
|
sendJSON(res, 200, { ok: true, message: `将在下次心跳时切换到 ${node}` });
|
|
506
558
|
}
|
|
507
559
|
|
|
@@ -558,6 +610,36 @@ async function handleSetErrorTrigger(params, body, res) {
|
|
|
558
610
|
sendJSON(res, 200, { ok: true, config: cleaned });
|
|
559
611
|
}
|
|
560
612
|
|
|
613
|
+
/** 配置某 agent 要检测登录状态的 CDP 端口列表(逗号分隔或数组) */
|
|
614
|
+
async function handleSetLoginPorts(params, body, res) {
|
|
615
|
+
const { userId, ports } = body || {};
|
|
616
|
+
if (!userId) {
|
|
617
|
+
sendJSON(res, 400, { error: "缺少 userId" });
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
// 前端可能传字符串 "9222,9223",统一转数组
|
|
621
|
+
let portArr = ports;
|
|
622
|
+
if (typeof ports === "string") {
|
|
623
|
+
portArr = ports
|
|
624
|
+
.split(",")
|
|
625
|
+
.map((s) => s.trim())
|
|
626
|
+
.filter(Boolean)
|
|
627
|
+
.map((s) => parseInt(s, 10));
|
|
628
|
+
}
|
|
629
|
+
const cleaned = setLoginPortsStored(userId, portArr);
|
|
630
|
+
const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
631
|
+
console.error(
|
|
632
|
+
`[watchdog ${ts}] ${userId} 登录检测端口已更新: ${cleaned.join(",") || "已清空"}`,
|
|
633
|
+
);
|
|
634
|
+
sendJSON(res, 200, {
|
|
635
|
+
ok: true,
|
|
636
|
+
ports: cleaned,
|
|
637
|
+
message: cleaned.length
|
|
638
|
+
? `已配置检测端口 ${cleaned.join(",")},客户端将在下次心跳时生效`
|
|
639
|
+
: "已清空检测端口",
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
561
643
|
// ── 公开 ──
|
|
562
644
|
|
|
563
645
|
export function registerWatchdogRoutes(httpServer) {
|
|
@@ -635,5 +717,9 @@ export async function tryHandleWatchdogRoute(
|
|
|
635
717
|
await handleSetErrorTrigger(params, body, res);
|
|
636
718
|
return true;
|
|
637
719
|
}
|
|
720
|
+
if (method === "POST" && routePath === "/api/watchdog/login-ports") {
|
|
721
|
+
await handleSetLoginPorts(params, body, res);
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
638
724
|
return false;
|
|
639
725
|
}
|