tt-help-cli-ycl 1.4.85 → 1.4.87
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 -2
- package/src/clash/checker.js +53 -6
- package/src/clash/daemon.js +123 -17
- package/src/clash/ipc.js +7 -4
- package/src/cli/explore.js +69 -8
- package/src/cli/refresh.js +78 -10
- package/src/cli/search.js +69 -8
- package/src/watch/public/app.js +242 -5
- package/src/watch/public/index.html +56 -1
- package/src/watch/watchdog-server.js +157 -1
- package/src/watchdog/agent.js +286 -17
- package/src/watchdog/process-manager.js +23 -1
- package/src/watchdog/upgrader.js +39 -2
- package/src/webserver/server.mjs +32 -0
package/src/watch/public/app.js
CHANGED
|
@@ -3432,6 +3432,8 @@ async function fetchWatchdogData() {
|
|
|
3432
3432
|
renderWatchdogDashboard(
|
|
3433
3433
|
_wdData.clients || [],
|
|
3434
3434
|
_wdData.clientCommands || {},
|
|
3435
|
+
_wdData.clashCountries || {},
|
|
3436
|
+
_wdData.errorTriggers || {},
|
|
3435
3437
|
);
|
|
3436
3438
|
} catch (e) {
|
|
3437
3439
|
document.getElementById("wdClientsList").textContent =
|
|
@@ -3479,7 +3481,12 @@ function esc(s) {
|
|
|
3479
3481
|
|
|
3480
3482
|
var _wdLastRendered = "";
|
|
3481
3483
|
|
|
3482
|
-
function renderWatchdogDashboard(
|
|
3484
|
+
function renderWatchdogDashboard(
|
|
3485
|
+
clients,
|
|
3486
|
+
clientCommands,
|
|
3487
|
+
clashCountries,
|
|
3488
|
+
errorTriggers,
|
|
3489
|
+
) {
|
|
3483
3490
|
// 顶栏统计始终更新(textContent 不闪烁)
|
|
3484
3491
|
var onlineCount = clients.filter(function (c) {
|
|
3485
3492
|
return c.online;
|
|
@@ -3536,7 +3543,9 @@ function renderWatchdogDashboard(clients, clientCommands) {
|
|
|
3536
3543
|
// 收集所有卡片:全部注册客户端 + 未上线但有命令配置的 userId
|
|
3537
3544
|
var allCards = [];
|
|
3538
3545
|
sortedClients.forEach(function (c) {
|
|
3539
|
-
allCards.push(
|
|
3546
|
+
allCards.push(
|
|
3547
|
+
renderClientCard(c, clientCommands, clashCountries, errorTriggers),
|
|
3548
|
+
);
|
|
3540
3549
|
});
|
|
3541
3550
|
// 离线卡也按名称排序
|
|
3542
3551
|
var offlineUids = [];
|
|
@@ -3553,7 +3562,14 @@ function renderWatchdogDashboard(clients, clientCommands) {
|
|
|
3553
3562
|
return a.toLowerCase().localeCompare(b.toLowerCase());
|
|
3554
3563
|
});
|
|
3555
3564
|
offlineUids.forEach(function (uid) {
|
|
3556
|
-
allCards.push(
|
|
3565
|
+
allCards.push(
|
|
3566
|
+
renderOfflineCard(
|
|
3567
|
+
uid,
|
|
3568
|
+
clientCommands[uid],
|
|
3569
|
+
clashCountries,
|
|
3570
|
+
errorTriggers,
|
|
3571
|
+
),
|
|
3572
|
+
);
|
|
3557
3573
|
});
|
|
3558
3574
|
|
|
3559
3575
|
// 数据指纹:只有变化时才重建卡片 HTML
|
|
@@ -3597,7 +3613,7 @@ function renderWatchdogDashboard(clients, clientCommands) {
|
|
|
3597
3613
|
}
|
|
3598
3614
|
}
|
|
3599
3615
|
|
|
3600
|
-
function renderOfflineCard(userId, cmds) {
|
|
3616
|
+
function renderOfflineCard(userId, cmds, clashCountries, errorTriggers) {
|
|
3601
3617
|
var cmdCount = cmds ? cmds.length : 0;
|
|
3602
3618
|
return styled(
|
|
3603
3619
|
"div",
|
|
@@ -3666,6 +3682,8 @@ function renderOfflineCard(userId, cmds) {
|
|
|
3666
3682
|
"color:#555;font-size:12px",
|
|
3667
3683
|
"该机器尚未连接看门狗,配置了 " + cmdCount + " 条命令",
|
|
3668
3684
|
) +
|
|
3685
|
+
renderCountrySection({}, userId, clashCountries) +
|
|
3686
|
+
renderErrorTriggerSection({}, userId, errorTriggers) +
|
|
3669
3687
|
renderCmdRows(cmds || [], userId, [], false),
|
|
3670
3688
|
);
|
|
3671
3689
|
}
|
|
@@ -3700,7 +3718,7 @@ function findAnalysis(llmAnalysis, cmdId) {
|
|
|
3700
3718
|
return null;
|
|
3701
3719
|
}
|
|
3702
3720
|
|
|
3703
|
-
function renderClientCard(c, clientCommands) {
|
|
3721
|
+
function renderClientCard(c, clientCommands, clashCountries, errorTriggers) {
|
|
3704
3722
|
var os = c.osInfo || {},
|
|
3705
3723
|
cfg = c.config || {},
|
|
3706
3724
|
procs = c.processes || [];
|
|
@@ -3811,11 +3829,230 @@ function renderClientCard(c, clientCommands) {
|
|
|
3811
3829
|
header +
|
|
3812
3830
|
styled("div", "color:#888;font-size:12px", osStr) +
|
|
3813
3831
|
styled("div", "color:#888;font-size:12px;margin-bottom:6px", infoLine) +
|
|
3832
|
+
renderCountrySection(c, userId, clashCountries) +
|
|
3833
|
+
renderErrorTriggerSection(c, userId, errorTriggers) +
|
|
3814
3834
|
renderProxySection(c, userId) +
|
|
3815
3835
|
renderCmdRows(myCmds, userId, procs, online, c.llmAnalysis),
|
|
3816
3836
|
);
|
|
3817
3837
|
}
|
|
3818
3838
|
|
|
3839
|
+
// ── 节点国家配置区域(看门狗面板中每个客户端卡片) ──
|
|
3840
|
+
|
|
3841
|
+
function fmtCountriesArr(v) {
|
|
3842
|
+
if (!v) return "无";
|
|
3843
|
+
var arr = Array.isArray(v) ? v : [v];
|
|
3844
|
+
return arr.length ? arr.join(",") : "无";
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
function renderCountrySection(c, userId, clashCountries) {
|
|
3848
|
+
if (!userId) return "";
|
|
3849
|
+
var cfgEntry = (clashCountries && clashCountries[userId]) || {};
|
|
3850
|
+
var configured =
|
|
3851
|
+
cfgEntry.countries && cfgEntry.countries.length
|
|
3852
|
+
? cfgEntry.countries
|
|
3853
|
+
: ["US"];
|
|
3854
|
+
var h = c.clashHealth || {};
|
|
3855
|
+
var meta = h.meta || {};
|
|
3856
|
+
var activeStr = fmtCountriesArr(meta.countryFilter);
|
|
3857
|
+
return styled(
|
|
3858
|
+
"div",
|
|
3859
|
+
"margin-top:6px;padding:6px 8px;background:#111;border-radius:6px;" +
|
|
3860
|
+
"display:flex;align-items:center;gap:8px;font-size:12px",
|
|
3861
|
+
styled("span", "font-size:14px", "🌍") +
|
|
3862
|
+
styled("span", "color:#888", "节点国家:") +
|
|
3863
|
+
tag(
|
|
3864
|
+
"button",
|
|
3865
|
+
{
|
|
3866
|
+
onclick: "wdShowCountryModal('" + esc(userId) + "')",
|
|
3867
|
+
style:
|
|
3868
|
+
"padding:2px 8px;font-size:11px;background:#333;color:#fbbf24;" +
|
|
3869
|
+
"border:1px solid #444;border-radius:3px;cursor:pointer",
|
|
3870
|
+
title: "配置此客户端的节点国家(多选,未配置默认 US)",
|
|
3871
|
+
},
|
|
3872
|
+
fmtCountriesArr(configured) + " ⚙️",
|
|
3873
|
+
) +
|
|
3874
|
+
styled(
|
|
3875
|
+
"span",
|
|
3876
|
+
"color:#666;margin-left:auto",
|
|
3877
|
+
"生效: " + (activeStr !== "无" ? activeStr : "—"),
|
|
3878
|
+
),
|
|
3879
|
+
);
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3882
|
+
// ── 节点国家配置弹窗 ──
|
|
3883
|
+
|
|
3884
|
+
var _wdCountryUserId = "";
|
|
3885
|
+
|
|
3886
|
+
function wdShowCountryModal(userId) {
|
|
3887
|
+
_wdCountryUserId = userId;
|
|
3888
|
+
document.getElementById("wdCountryUserId").textContent = userId;
|
|
3889
|
+
// 预填当前配置
|
|
3890
|
+
var configured = ["US"];
|
|
3891
|
+
if (_wdData && _wdData.clashCountries && _wdData.clashCountries[userId]) {
|
|
3892
|
+
var entry = _wdData.clashCountries[userId];
|
|
3893
|
+
if (entry.countries && entry.countries.length) configured = entry.countries;
|
|
3894
|
+
}
|
|
3895
|
+
var supported = (_wdData && _wdData.supportedCountries) || [
|
|
3896
|
+
"US",
|
|
3897
|
+
"GB",
|
|
3898
|
+
"DE",
|
|
3899
|
+
"FR",
|
|
3900
|
+
"JP",
|
|
3901
|
+
"KR",
|
|
3902
|
+
"SG",
|
|
3903
|
+
"HK",
|
|
3904
|
+
"TW",
|
|
3905
|
+
"RU",
|
|
3906
|
+
"NL",
|
|
3907
|
+
"IN",
|
|
3908
|
+
"CA",
|
|
3909
|
+
"AU",
|
|
3910
|
+
"MY",
|
|
3911
|
+
];
|
|
3912
|
+
var html = "";
|
|
3913
|
+
for (var i = 0; i < supported.length; i++) {
|
|
3914
|
+
var cc = supported[i];
|
|
3915
|
+
var checked = configured.indexOf(cc) >= 0 ? "checked" : "";
|
|
3916
|
+
html +=
|
|
3917
|
+
'<label style="display:inline-flex;align-items:center;gap:4px;' +
|
|
3918
|
+
"margin:4px;padding:4px 8px;background:#111;border:1px solid #333;" +
|
|
3919
|
+
'border-radius:6px;cursor:pointer;font-size:12px;color:#e0e0e0">' +
|
|
3920
|
+
'<input type="checkbox" class="wd-country-cb" value="' +
|
|
3921
|
+
cc +
|
|
3922
|
+
'" ' +
|
|
3923
|
+
checked +
|
|
3924
|
+
"/> " +
|
|
3925
|
+
cc +
|
|
3926
|
+
"</label>";
|
|
3927
|
+
}
|
|
3928
|
+
document.getElementById("wdCountryList").innerHTML = html;
|
|
3929
|
+
document.getElementById("wdCountryModal").style.display = "flex";
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
async function wdSetCountries() {
|
|
3933
|
+
var cbs = document.querySelectorAll(".wd-country-cb:checked");
|
|
3934
|
+
var countries = [];
|
|
3935
|
+
for (var i = 0; i < cbs.length; i++) countries.push(cbs[i].value);
|
|
3936
|
+
if (countries.length === 0) {
|
|
3937
|
+
alert("请至少选择一个国家");
|
|
3938
|
+
return;
|
|
3939
|
+
}
|
|
3940
|
+
try {
|
|
3941
|
+
var res = await fetch("/api/watchdog/set-country-filter", {
|
|
3942
|
+
method: "POST",
|
|
3943
|
+
headers: { "Content-Type": "application/json" },
|
|
3944
|
+
body: JSON.stringify({ userId: _wdCountryUserId, countries: countries }),
|
|
3945
|
+
});
|
|
3946
|
+
var d = await res.json();
|
|
3947
|
+
document.getElementById("wdCountryModal").style.display = "none";
|
|
3948
|
+
if (d.ok) {
|
|
3949
|
+
alert(
|
|
3950
|
+
"已配置: " +
|
|
3951
|
+
d.countries.join(",") +
|
|
3952
|
+
"\n\n客户端将在下次心跳时生效(约15秒内)。",
|
|
3953
|
+
);
|
|
3954
|
+
} else {
|
|
3955
|
+
alert("失败: " + (d.error || "未知错误"));
|
|
3956
|
+
}
|
|
3957
|
+
fetchWatchdogData();
|
|
3958
|
+
} catch (e) {
|
|
3959
|
+
alert("保存失败: " + e.message);
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
// ── 错误自动切节点配置区域(看门狗面板中每个客户端卡片) ──
|
|
3964
|
+
|
|
3965
|
+
function renderErrorTriggerSection(c, userId, errorTriggers) {
|
|
3966
|
+
if (!userId) return "";
|
|
3967
|
+
var entry = (errorTriggers && errorTriggers[userId]) || null;
|
|
3968
|
+
var enabled = entry ? !!entry.enabled : false;
|
|
3969
|
+
var label = entry
|
|
3970
|
+
? (enabled ? "已启用" : "已禁用") +
|
|
3971
|
+
" · " +
|
|
3972
|
+
(entry.triggerCount || 10) +
|
|
3973
|
+
"次/" +
|
|
3974
|
+
(entry.minMinutes || 5) +
|
|
3975
|
+
"min"
|
|
3976
|
+
: "未配置(默认禁用)";
|
|
3977
|
+
var btnColor = enabled ? "#4ade80" : "#888";
|
|
3978
|
+
return styled(
|
|
3979
|
+
"div",
|
|
3980
|
+
"margin-top:6px;padding:6px 8px;background:#111;border-radius:6px;" +
|
|
3981
|
+
"display:flex;align-items:center;gap:8px;font-size:12px",
|
|
3982
|
+
styled("span", "font-size:14px", "🚨") +
|
|
3983
|
+
styled("span", "color:#888", "错误切换:") +
|
|
3984
|
+
tag(
|
|
3985
|
+
"button",
|
|
3986
|
+
{
|
|
3987
|
+
onclick: "wdShowErrorTriggerModal('" + esc(userId) + "')",
|
|
3988
|
+
style:
|
|
3989
|
+
"padding:2px 8px;font-size:11px;background:#333;color:" +
|
|
3990
|
+
btnColor +
|
|
3991
|
+
";border:1px solid #444;border-radius:3px;cursor:pointer",
|
|
3992
|
+
title: "配置业务日志连续错误自动切节点(默认关闭)",
|
|
3993
|
+
},
|
|
3994
|
+
(enabled ? "开 " : "关 ") + "⚙️",
|
|
3995
|
+
) +
|
|
3996
|
+
styled("span", "color:#666;margin-left:auto", label),
|
|
3997
|
+
);
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
// ── 错误自动切节点配置弹窗 ──
|
|
4001
|
+
|
|
4002
|
+
var _wdErrUserId = "";
|
|
4003
|
+
|
|
4004
|
+
function wdShowErrorTriggerModal(userId) {
|
|
4005
|
+
_wdErrUserId = userId;
|
|
4006
|
+
document.getElementById("wdErrUserId").textContent = userId;
|
|
4007
|
+
var entry =
|
|
4008
|
+
_wdData && _wdData.errorTriggers && _wdData.errorTriggers[userId]
|
|
4009
|
+
? _wdData.errorTriggers[userId]
|
|
4010
|
+
: null;
|
|
4011
|
+
document.getElementById("wdErrEnabled").checked = entry
|
|
4012
|
+
? !!entry.enabled
|
|
4013
|
+
: false;
|
|
4014
|
+
document.getElementById("wdErrCount").value =
|
|
4015
|
+
(entry && entry.triggerCount) || 10;
|
|
4016
|
+
document.getElementById("wdErrMin").value = (entry && entry.minMinutes) || 5;
|
|
4017
|
+
document.getElementById("wdErrBreak").value =
|
|
4018
|
+
(entry && entry.breakSeconds) || 120;
|
|
4019
|
+
document.getElementById("wdErrorTriggerModal").style.display = "flex";
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
async function wdSetErrorTrigger() {
|
|
4023
|
+
var config = {
|
|
4024
|
+
enabled: document.getElementById("wdErrEnabled").checked,
|
|
4025
|
+
triggerCount: parseInt(document.getElementById("wdErrCount").value) || 10,
|
|
4026
|
+
minMinutes: parseInt(document.getElementById("wdErrMin").value) || 5,
|
|
4027
|
+
breakSeconds: parseInt(document.getElementById("wdErrBreak").value) || 120,
|
|
4028
|
+
};
|
|
4029
|
+
try {
|
|
4030
|
+
var res = await fetch("/api/watchdog/set-error-trigger", {
|
|
4031
|
+
method: "POST",
|
|
4032
|
+
headers: { "Content-Type": "application/json" },
|
|
4033
|
+
body: JSON.stringify({ userId: _wdErrUserId, config: config }),
|
|
4034
|
+
});
|
|
4035
|
+
var d = await res.json();
|
|
4036
|
+
document.getElementById("wdErrorTriggerModal").style.display = "none";
|
|
4037
|
+
if (d.ok) {
|
|
4038
|
+
alert(
|
|
4039
|
+
config.enabled
|
|
4040
|
+
? "已开启错误自动切节点\n连续 " +
|
|
4041
|
+
config.triggerCount +
|
|
4042
|
+
" 次 / " +
|
|
4043
|
+
config.minMinutes +
|
|
4044
|
+
" min\n\n客户端将在下次心跳时生效(约15秒内)。"
|
|
4045
|
+
: "已关闭错误自动切节点\n\n客户端将在下次心跳时生效(约15秒内)。",
|
|
4046
|
+
);
|
|
4047
|
+
} else {
|
|
4048
|
+
alert("失败: " + (d.error || "未知错误"));
|
|
4049
|
+
}
|
|
4050
|
+
fetchWatchdogData();
|
|
4051
|
+
} catch (e) {
|
|
4052
|
+
alert("保存失败: " + e.message);
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
|
|
3819
4056
|
// ── 代理节点区域(看门狗面板中每个客户端卡片) ──
|
|
3820
4057
|
|
|
3821
4058
|
function renderProxySection(c, userId) {
|
|
@@ -612,7 +612,62 @@
|
|
|
612
612
|
</div>
|
|
613
613
|
</div>
|
|
614
614
|
|
|
615
|
-
|
|
615
|
+
<!-- 节点国家配置弹窗 -->
|
|
616
|
+
<div id="wdCountryModal"
|
|
617
|
+
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">
|
|
618
|
+
<div style="background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:24px;width:90%;max-width:520px">
|
|
619
|
+
<div style="font-size:16px;color:#fbbf24;margin-bottom:4px">🌍 节点国家配置 — <span id="wdCountryUserId"></span></div>
|
|
620
|
+
<div style="color:#888;font-size:12px;margin-bottom:12px">选择该客户端可用的代理节点国家(可多选)。<br>未配置时默认使用 <b
|
|
621
|
+
style="color:#fbbf24">US</b>。修改后客户端将在下次心跳时生效。</div>
|
|
622
|
+
<div id="wdCountryList"
|
|
623
|
+
style="padding:10px;background:#16161e;border:1px solid #333;border-radius:8px;min-height:120px">加载中...</div>
|
|
624
|
+
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px">
|
|
625
|
+
<button onclick="document.getElementById('wdCountryModal').style.display='none'"
|
|
626
|
+
style="padding:8px 16px;background:#333;color:#ccc;border:none;border-radius:6px;cursor:pointer;font-size:12px">取消</button>
|
|
627
|
+
<button onclick="wdSetCountries()"
|
|
628
|
+
style="padding:8px 16px;background:#fbbf24;color:#000;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600">保存并下发</button>
|
|
629
|
+
</div>
|
|
630
|
+
</div>
|
|
631
|
+
</div>
|
|
632
|
+
|
|
633
|
+
<!-- 错误自动切节点配置弹窗 -->
|
|
634
|
+
<div id="wdErrorTriggerModal"
|
|
635
|
+
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">
|
|
636
|
+
<div style="background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:24px;width:90%;max-width:480px">
|
|
637
|
+
<div style="font-size:16px;color:#f87171;margin-bottom:4px">🚨 错误自动切节点 — <span id="wdErrUserId"></span></div>
|
|
638
|
+
<div style="color:#888;font-size:12px;margin-bottom:12px">业务程序(explore/auto/refresh 等)连续报错(CDN限流 / 网络异常 / net:: /
|
|
639
|
+
被封 等)时,自动切换出口节点。<br><b style="color:#f87171">默认关闭</b>,需手动开启。</div>
|
|
640
|
+
<label
|
|
641
|
+
style="display:flex;align-items:center;gap:8px;margin-bottom:12px;color:#e0e0e0;font-size:13px;cursor:pointer">
|
|
642
|
+
<input type="checkbox" id="wdErrEnabled" style="width:16px;height:16px"> 启用(开启后连续错误达标自动切节点)
|
|
643
|
+
</label>
|
|
644
|
+
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px">
|
|
645
|
+
<div>
|
|
646
|
+
<label style="display:block;color:#888;font-size:10px;margin-bottom:2px">连续次数</label>
|
|
647
|
+
<input id="wdErrCount" type="number" min="1"
|
|
648
|
+
style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box">
|
|
649
|
+
</div>
|
|
650
|
+
<div>
|
|
651
|
+
<label style="display:block;color:#888;font-size:10px;margin-bottom:2px">最小分钟</label>
|
|
652
|
+
<input id="wdErrMin" type="number" min="1"
|
|
653
|
+
style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box">
|
|
654
|
+
</div>
|
|
655
|
+
<div>
|
|
656
|
+
<label style="display:block;color:#888;font-size:10px;margin-bottom:2px">中断秒数</label>
|
|
657
|
+
<input id="wdErrBreak" type="number" min="1"
|
|
658
|
+
style="width:100%;padding:6px;background:#111;border:1px solid #333;color:#e0e0e0;border-radius:4px;font-size:11px;box-sizing:border-box">
|
|
659
|
+
</div>
|
|
660
|
+
</div>
|
|
661
|
+
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px">
|
|
662
|
+
<button onclick="document.getElementById('wdErrorTriggerModal').style.display='none'"
|
|
663
|
+
style="padding:8px 16px;background:#333;color:#ccc;border:none;border-radius:6px;cursor:pointer;font-size:12px">取消</button>
|
|
664
|
+
<button onclick="wdSetErrorTrigger()"
|
|
665
|
+
style="padding:8px 16px;background:#f87171;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600">保存并下发</button>
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
</div>
|
|
669
|
+
|
|
670
|
+
<script src="app.js?v=20260811b"></script>
|
|
616
671
|
</body>
|
|
617
672
|
|
|
618
673
|
</html>
|
|
@@ -30,6 +30,29 @@ const LLM_CACHE_TTL = 15 * 60 * 1000; // 15 分钟过期
|
|
|
30
30
|
// 看门狗客户端下次心跳时取走,取走后清空
|
|
31
31
|
const clashCmdQueue = new Map();
|
|
32
32
|
|
|
33
|
+
// 国家过滤配置幂等下发记录:userId → JSON.stringify(countries)
|
|
34
|
+
// 仅当配置变化时才入队,避免每次心跳都触发 daemon 刷新候选集
|
|
35
|
+
const lastSentCountries = new Map();
|
|
36
|
+
|
|
37
|
+
// 支持的节点国家(与 src/clash/checker.js COUNTRY_PATTERNS 保持一致)
|
|
38
|
+
const SUPPORTED_COUNTRIES = [
|
|
39
|
+
"US",
|
|
40
|
+
"GB",
|
|
41
|
+
"DE",
|
|
42
|
+
"FR",
|
|
43
|
+
"JP",
|
|
44
|
+
"KR",
|
|
45
|
+
"SG",
|
|
46
|
+
"HK",
|
|
47
|
+
"TW",
|
|
48
|
+
"RU",
|
|
49
|
+
"NL",
|
|
50
|
+
"IN",
|
|
51
|
+
"CA",
|
|
52
|
+
"AU",
|
|
53
|
+
"MY",
|
|
54
|
+
];
|
|
55
|
+
|
|
33
56
|
// ── 配置文件读写 ──
|
|
34
57
|
|
|
35
58
|
function loadConfig() {
|
|
@@ -199,6 +222,61 @@ function setClientConfigStored(userId, config) {
|
|
|
199
222
|
saveConfig(cfg);
|
|
200
223
|
}
|
|
201
224
|
|
|
225
|
+
// ── 错误触发切换配置管理(per agent,默认不启用,后端下发控制) ──
|
|
226
|
+
|
|
227
|
+
function getErrorTriggerStored(userId) {
|
|
228
|
+
if (!userId) return null;
|
|
229
|
+
const cfg = loadConfig();
|
|
230
|
+
return (cfg.errorTriggers && cfg.errorTriggers[userId]) || null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function setErrorTriggerStored(userId, config) {
|
|
234
|
+
const cfg = loadConfig();
|
|
235
|
+
if (!cfg.errorTriggers) cfg.errorTriggers = {};
|
|
236
|
+
cfg.errorTriggers[userId] = config;
|
|
237
|
+
saveConfig(cfg);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function getAllErrorTriggers() {
|
|
241
|
+
const cfg = loadConfig();
|
|
242
|
+
return cfg.errorTriggers || {};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── 节点国家配置管理(per agent,默认 US,幂等下发) ──
|
|
246
|
+
|
|
247
|
+
/** 读取某 agent 的国家配置,未配置时默认 ["US"] */
|
|
248
|
+
function getClientCountries(userId) {
|
|
249
|
+
if (!userId) return ["US"];
|
|
250
|
+
const cfg = loadConfig();
|
|
251
|
+
const entry = cfg.clashCountries && cfg.clashCountries[userId];
|
|
252
|
+
if (entry && Array.isArray(entry.countries) && entry.countries.length > 0) {
|
|
253
|
+
return entry.countries;
|
|
254
|
+
}
|
|
255
|
+
return ["US"];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 设置某 agent 的国家配置(去重、转大写、仅保留白名单) */
|
|
259
|
+
function setClientCountries(userId, countries) {
|
|
260
|
+
const cleaned = [
|
|
261
|
+
...new Set(countries.map((c) => String(c).trim().toUpperCase())),
|
|
262
|
+
].filter((c) => SUPPORTED_COUNTRIES.includes(c));
|
|
263
|
+
if (cleaned.length === 0) cleaned.push("US");
|
|
264
|
+
const cfg = loadConfig();
|
|
265
|
+
if (!cfg.clashCountries) cfg.clashCountries = {};
|
|
266
|
+
cfg.clashCountries[userId] = {
|
|
267
|
+
countries: cleaned,
|
|
268
|
+
updatedAt: new Date().toISOString(),
|
|
269
|
+
};
|
|
270
|
+
saveConfig(cfg);
|
|
271
|
+
return cleaned;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** 返回全部国家配置(供前端展示) */
|
|
275
|
+
function getAllClientCountries() {
|
|
276
|
+
const cfg = loadConfig();
|
|
277
|
+
return cfg.clashCountries || {};
|
|
278
|
+
}
|
|
279
|
+
|
|
202
280
|
// ── 工具函数 ──
|
|
203
281
|
|
|
204
282
|
function readBody(req) {
|
|
@@ -252,8 +330,17 @@ async function handleHeartbeat(params, body, res) {
|
|
|
252
330
|
const userId = (body.config && body.config.userId) || "";
|
|
253
331
|
const commands = getClientCommands(userId);
|
|
254
332
|
|
|
333
|
+
// 幂等下发节点国家配置(默认 US;配置变化时才入队,daemon 端相同则忽略)
|
|
334
|
+
const countries = getClientCountries(userId);
|
|
335
|
+
const countryKey = JSON.stringify(countries);
|
|
336
|
+
const pending = clashCmdQueue.get(userId) || [];
|
|
337
|
+
if (countryKey !== lastSentCountries.get(userId)) {
|
|
338
|
+
pending.push({ action: "setCountryFilter", countries });
|
|
339
|
+
lastSentCountries.set(userId, countryKey);
|
|
340
|
+
}
|
|
341
|
+
|
|
255
342
|
// 取出待下发的 Clash 命令(一次性,取走即清空)
|
|
256
|
-
const clashCommands =
|
|
343
|
+
const clashCommands = pending;
|
|
257
344
|
if (clashCommands.length > 0) {
|
|
258
345
|
clashCmdQueue.delete(userId);
|
|
259
346
|
}
|
|
@@ -268,6 +355,7 @@ async function handleHeartbeat(params, body, res) {
|
|
|
268
355
|
commands,
|
|
269
356
|
clashCommands: clashCommands.length > 0 ? clashCommands : undefined,
|
|
270
357
|
clientConfig: getClientConfigStored(userId),
|
|
358
|
+
errorTrigger: getErrorTriggerStored(userId),
|
|
271
359
|
serverTime: Date.now(),
|
|
272
360
|
});
|
|
273
361
|
}
|
|
@@ -278,6 +366,9 @@ async function handleGetClients(params, body, res) {
|
|
|
278
366
|
sendJSON(res, 200, {
|
|
279
367
|
clients: onlineClients,
|
|
280
368
|
clientCommands: allCmds,
|
|
369
|
+
clashCountries: getAllClientCountries(),
|
|
370
|
+
supportedCountries: SUPPORTED_COUNTRIES,
|
|
371
|
+
errorTriggers: getAllErrorTriggers(),
|
|
281
372
|
configPath: WATCHDOG_CONFIG_PATH,
|
|
282
373
|
});
|
|
283
374
|
}
|
|
@@ -346,7 +437,11 @@ async function handleDeleteClient(params, body, res) {
|
|
|
346
437
|
const cfg = loadConfig();
|
|
347
438
|
if (cfg.clientCommands) delete cfg.clientCommands[userId];
|
|
348
439
|
if (cfg.clientConfigs) delete cfg.clientConfigs[userId];
|
|
440
|
+
if (cfg.clashCountries) delete cfg.clashCountries[userId];
|
|
441
|
+
if (cfg.errorTriggers) delete cfg.errorTriggers[userId];
|
|
349
442
|
saveConfig(cfg);
|
|
443
|
+
lastSentCountries.delete(userId);
|
|
444
|
+
clashCmdQueue.delete(userId);
|
|
350
445
|
// 2. 从内存客户端列表中移除所有匹配的客户端
|
|
351
446
|
let removed = 0;
|
|
352
447
|
for (const [clientId, info] of clients) {
|
|
@@ -404,6 +499,59 @@ async function handleSwitchNode(params, body, res) {
|
|
|
404
499
|
sendJSON(res, 200, { ok: true, message: `将在下次心跳时切换到 ${node}` });
|
|
405
500
|
}
|
|
406
501
|
|
|
502
|
+
/** 配置某 agent 的节点国家过滤(持久化 + 立即入队下发) */
|
|
503
|
+
async function handleSetCountryFilter(params, body, res) {
|
|
504
|
+
const { userId, countries } = body || {};
|
|
505
|
+
if (!userId) {
|
|
506
|
+
sendJSON(res, 400, { error: "缺少 userId" });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (!Array.isArray(countries) || countries.length === 0) {
|
|
510
|
+
sendJSON(res, 400, { error: 'countries 必须是非空数组,例如 ["US","DE"]' });
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const cleaned = setClientCountries(userId, countries);
|
|
514
|
+
// 立即入队下发,并同步幂等记录(避免下次心跳重复补发)
|
|
515
|
+
const queue = clashCmdQueue.get(userId) || [];
|
|
516
|
+
queue.push({ action: "setCountryFilter", countries: cleaned });
|
|
517
|
+
clashCmdQueue.set(userId, queue);
|
|
518
|
+
lastSentCountries.set(userId, JSON.stringify(cleaned));
|
|
519
|
+
const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
520
|
+
console.error(
|
|
521
|
+
`[watchdog ${ts}] ${userId} 国家配置已更新: ${cleaned.join(",")}(已入队下发)`,
|
|
522
|
+
);
|
|
523
|
+
sendJSON(res, 200, {
|
|
524
|
+
ok: true,
|
|
525
|
+
countries: cleaned,
|
|
526
|
+
message: `已配置 ${cleaned.join(",")},客户端将在下次心跳时生效`,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** 配置某 agent 的错误触发切换(业务日志连续错误达标时自动切节点;默认不启用) */
|
|
531
|
+
async function handleSetErrorTrigger(params, body, res) {
|
|
532
|
+
const { userId, config } = body || {};
|
|
533
|
+
if (!userId) {
|
|
534
|
+
sendJSON(res, 400, { error: "缺少 userId" });
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (!config || typeof config !== "object") {
|
|
538
|
+
sendJSON(res, 400, { error: "缺少 config 对象" });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const cleaned = {
|
|
542
|
+
enabled: !!config.enabled,
|
|
543
|
+
triggerCount: parseInt(config.triggerCount) || 10,
|
|
544
|
+
minMinutes: parseInt(config.minMinutes) || 5,
|
|
545
|
+
breakSeconds: parseInt(config.breakSeconds) || 120,
|
|
546
|
+
};
|
|
547
|
+
setErrorTriggerStored(userId, cleaned);
|
|
548
|
+
const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
549
|
+
console.error(
|
|
550
|
+
`[watchdog ${ts}] ${userId} 错误触发切换配置已更新: ${JSON.stringify(cleaned)}`,
|
|
551
|
+
);
|
|
552
|
+
sendJSON(res, 200, { ok: true, config: cleaned });
|
|
553
|
+
}
|
|
554
|
+
|
|
407
555
|
// ── 公开 ──
|
|
408
556
|
|
|
409
557
|
export function registerWatchdogRoutes(httpServer) {
|
|
@@ -473,5 +621,13 @@ export async function tryHandleWatchdogRoute(
|
|
|
473
621
|
await handleSwitchNode(params, body, res);
|
|
474
622
|
return true;
|
|
475
623
|
}
|
|
624
|
+
if (method === "POST" && routePath === "/api/watchdog/set-country-filter") {
|
|
625
|
+
await handleSetCountryFilter(params, body, res);
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
if (method === "POST" && routePath === "/api/watchdog/set-error-trigger") {
|
|
629
|
+
await handleSetErrorTrigger(params, body, res);
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
476
632
|
return false;
|
|
477
633
|
}
|