tt-help-cli-ycl 1.4.115 → 1.4.117
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/refresh-mini.js +36 -24
- package/src/lib/args.js +17 -0
- package/src/lib/constants.js +6 -1
- package/src/watch/public/app.js +58 -17
- package/src/watch/watchdog-server.js +14 -4
- package/src/watchdog/agent.js +16 -6
- package/src/watchdog/reporter.js +32 -1
package/src/cli/refresh-mini.js
CHANGED
|
@@ -37,7 +37,6 @@ import {
|
|
|
37
37
|
killEdgeProcesses,
|
|
38
38
|
} from "../lib/browser/cdp.js";
|
|
39
39
|
import { createApiClient } from "../lib/api-client.js";
|
|
40
|
-
import { requireClashOrExit } from "../clash/checker.js";
|
|
41
40
|
import { readHealth, writeCommand } from "../clash/ipc.js";
|
|
42
41
|
import { spawn } from "child_process";
|
|
43
42
|
import { fileURLToPath } from "url";
|
|
@@ -83,6 +82,19 @@ async function withRetry(label, fn) {
|
|
|
83
82
|
*/
|
|
84
83
|
let spawnedDaemonPid = null;
|
|
85
84
|
|
|
85
|
+
/** 从代理 URL 解析端口(http://127.0.0.1:7890 → 7890),无法解析返回 0(默认通道) */
|
|
86
|
+
function proxyPortOf(url) {
|
|
87
|
+
if (!url) return 0;
|
|
88
|
+
try {
|
|
89
|
+
const u = new URL(url);
|
|
90
|
+
const p = parseInt(u.port, 10);
|
|
91
|
+
return p > 0 ? p : 0;
|
|
92
|
+
} catch {
|
|
93
|
+
const m = String(url).match(/:(\d+)/);
|
|
94
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
86
98
|
function spawnClashDaemon() {
|
|
87
99
|
try {
|
|
88
100
|
const mainJs = path.join(__dirname, "..", "main.js");
|
|
@@ -115,10 +127,10 @@ function stopSpawnedDaemon() {
|
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
/**
|
|
118
|
-
* 用 daemon 的 IPC 健康状态判断其是否存活(黑盒接口:clash-health.json
|
|
130
|
+
* 用 daemon 的 IPC 健康状态判断其是否存活(黑盒接口:clash-health.json,按通道端口)
|
|
119
131
|
*/
|
|
120
|
-
function isDaemonAlive() {
|
|
121
|
-
const h = readHealth();
|
|
132
|
+
function isDaemonAlive(port = 0) {
|
|
133
|
+
const h = readHealth(port);
|
|
122
134
|
return !!(h && !h.stale);
|
|
123
135
|
}
|
|
124
136
|
|
|
@@ -140,20 +152,13 @@ export async function handleRefreshMini(options) {
|
|
|
140
152
|
ensureDaemon,
|
|
141
153
|
} = options;
|
|
142
154
|
|
|
155
|
+
// 配置了什么代理就用什么代理(--proxy > ~/.tt-help.json 的 proxy);
|
|
156
|
+
// 未配置则不注入 proxyServer,浏览器走系统默认代理。
|
|
157
|
+
// 节点切换仍由 clash-daemon 负责(触发通知模式),不在此处自动解析。
|
|
143
158
|
const effectiveProxy = exploreProxy || configuredProxy;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
effectiveProxy.includes("127.0.0.1") ||
|
|
148
|
-
effectiveProxy.includes("localhost")
|
|
149
|
-
) {
|
|
150
|
-
// refresh-mini 必须走本地 Clash:未配置代理时自动发现,节点轮换才有意义
|
|
151
|
-
resolvedProxy = await requireClashOrExit("refresh-mini");
|
|
152
|
-
} else {
|
|
153
|
-
console.error(
|
|
154
|
-
`[警告] 使用非本地代理 ${effectiveProxy},Clash 节点轮换将无效`,
|
|
155
|
-
);
|
|
156
|
-
}
|
|
159
|
+
const resolvedProxy = effectiveProxy || null;
|
|
160
|
+
// 切换通知通道:按本实例的代理端口路由到 daemon 对应控制端
|
|
161
|
+
const proxyPort = proxyPortOf(resolvedProxy);
|
|
157
162
|
|
|
158
163
|
let userId = exploreUserId || configuredUserId;
|
|
159
164
|
let browser = null;
|
|
@@ -196,7 +201,7 @@ export async function handleRefreshMini(options) {
|
|
|
196
201
|
if (exploreJobLocations) console.error(`任务国家: ${exploreJobLocations}`);
|
|
197
202
|
console.error(`空闲间隔: ${exploreInterval || 30} 秒`);
|
|
198
203
|
console.error(
|
|
199
|
-
`代理: ${resolvedProxy || "
|
|
204
|
+
`代理: ${resolvedProxy || "系统默认"}${
|
|
200
205
|
rotateMin > 0
|
|
201
206
|
? `(每 ${rotateMin} 分钟轮换节点)`
|
|
202
207
|
: "(定时轮换已关闭)"
|
|
@@ -310,8 +315,8 @@ export async function handleRefreshMini(options) {
|
|
|
310
315
|
|
|
311
316
|
// --ensure-daemon:检查 clash-daemon 是否存活,未存活则拉起(默认不检测)
|
|
312
317
|
if (ensureDaemon) {
|
|
313
|
-
if (isDaemonAlive()) {
|
|
314
|
-
const h = readHealth();
|
|
318
|
+
if (isDaemonAlive(proxyPort)) {
|
|
319
|
+
const h = readHealth(proxyPort);
|
|
315
320
|
console.error(
|
|
316
321
|
`[daemon] 已运行(上次巡检 ${h?.ageSeconds ?? "?"}s 前)`,
|
|
317
322
|
);
|
|
@@ -339,16 +344,23 @@ export async function handleRefreshMini(options) {
|
|
|
339
344
|
proxySwitchCount++;
|
|
340
345
|
lastProxyTriggerTime = Date.now();
|
|
341
346
|
consecutiveTaskErrors = 0;
|
|
342
|
-
const before = readHealth()?.lastSwitch || "";
|
|
347
|
+
const before = readHealth(proxyPort)?.lastSwitch || "";
|
|
343
348
|
console.error(
|
|
344
|
-
`[代理] 触发切换通知(${reason},累计 ${proxySwitchCount} 次),等待 daemon 回执...`,
|
|
349
|
+
`[代理] 触发切换通知(${reason}${proxyPort ? `,通道 ${proxyPort}` : ""},累计 ${proxySwitchCount} 次),等待 daemon 回执...`,
|
|
350
|
+
);
|
|
351
|
+
writeCommand(
|
|
352
|
+
{
|
|
353
|
+
action: "failover",
|
|
354
|
+
reason: `refresh-mini: ${reason}`,
|
|
355
|
+
proxyPort: proxyPort || undefined,
|
|
356
|
+
},
|
|
357
|
+
proxyPort,
|
|
345
358
|
);
|
|
346
|
-
writeCommand({ action: "failover", reason: `refresh-mini: ${reason}` });
|
|
347
359
|
|
|
348
360
|
const deadline = Date.now() + SWITCH_RECEIPT_TIMEOUT_MS;
|
|
349
361
|
while (Date.now() < deadline) {
|
|
350
362
|
await sleep(3000);
|
|
351
|
-
const h = readHealth();
|
|
363
|
+
const h = readHealth(proxyPort);
|
|
352
364
|
if (
|
|
353
365
|
h &&
|
|
354
366
|
h.lastSwitch &&
|
package/src/lib/args.js
CHANGED
|
@@ -758,6 +758,10 @@ function parseClashDaemonArgs(args) {
|
|
|
758
758
|
let benchmark = 10;
|
|
759
759
|
let dryRun = false;
|
|
760
760
|
let country = null;
|
|
761
|
+
let apiPort = null;
|
|
762
|
+
let secret = null;
|
|
763
|
+
let selection = null;
|
|
764
|
+
const configPaths = [];
|
|
761
765
|
|
|
762
766
|
for (let i = 0; i < args.length; i++) {
|
|
763
767
|
const arg = args[i];
|
|
@@ -769,6 +773,15 @@ function parseClashDaemonArgs(args) {
|
|
|
769
773
|
country = args[++i] || null;
|
|
770
774
|
} else if (arg === "--dry-run") {
|
|
771
775
|
dryRun = true;
|
|
776
|
+
} else if (arg === "--api-port") {
|
|
777
|
+
apiPort = parseInt(args[++i]) || 0;
|
|
778
|
+
} else if (arg === "--secret") {
|
|
779
|
+
secret = args[++i] || "";
|
|
780
|
+
} else if (arg === "--selection") {
|
|
781
|
+
selection = args[++i] || null;
|
|
782
|
+
} else if (arg === "--config-path") {
|
|
783
|
+
const p = args[++i];
|
|
784
|
+
if (p) configPaths.push(p);
|
|
772
785
|
}
|
|
773
786
|
}
|
|
774
787
|
|
|
@@ -778,6 +791,10 @@ function parseClashDaemonArgs(args) {
|
|
|
778
791
|
benchmark,
|
|
779
792
|
country,
|
|
780
793
|
"dry-run": dryRun,
|
|
794
|
+
"api-port": apiPort,
|
|
795
|
+
secret,
|
|
796
|
+
selection,
|
|
797
|
+
"config-paths": configPaths,
|
|
781
798
|
urls: [],
|
|
782
799
|
outputFormat: "json",
|
|
783
800
|
exploreCount: 0,
|
package/src/lib/constants.js
CHANGED
|
@@ -420,7 +420,7 @@ const HELP_TEXT = [
|
|
|
420
420
|
" tt-help watchdog -i 30 --client-id my-server-01",
|
|
421
421
|
"",
|
|
422
422
|
" tt-help clash-daemon [选项]",
|
|
423
|
-
" Clash Verge 代理节点健康守护:定时巡检 TikTok 可达性,异常时自动切换节点",
|
|
423
|
+
" Clash Verge / mihomo 代理节点健康守护:定时巡检 TikTok 可达性,异常时自动切换节点",
|
|
424
424
|
" 看门狗启动时会自动拉起此守护进程",
|
|
425
425
|
" 选项:",
|
|
426
426
|
" --interval <秒> 巡检间隔,默认 120 秒(2分钟)",
|
|
@@ -428,6 +428,11 @@ const HELP_TEXT = [
|
|
|
428
428
|
" --country <国家代码> 只保留指定国家节点,默认不过滤",
|
|
429
429
|
" 支持: US, GB, DE, FR, JP, KR, SG, HK, TW, RU 等",
|
|
430
430
|
" --dry-run 只检测不切换(调试用)",
|
|
431
|
+
" --api-port <端口> 控制 API 端口(默认自动发现;显式指定验证失败时报错)",
|
|
432
|
+
" --secret <secret> 控制 API 密钥(默认自动获取)",
|
|
433
|
+
" --selection <策略> 多控制端选择策略: system-proxy / preference / first-alive",
|
|
434
|
+
" --config-path <路径> 追加自定义 config.yaml 探测路径(可重复)",
|
|
435
|
+
" 控制端自动发现: 依次探测 Clash Verge / 69cloud 配置 → 端口枚举,按策略选定",
|
|
431
436
|
" 看门狗运行时可通过 /tmp/clash-cmd.json 发命令动态调整:",
|
|
432
437
|
' {"action":"switchNode","node":"节点名"} 一次性切换到指定节点(不干扰自检)',
|
|
433
438
|
' {"action":"setCountryFilter","country":"US"} 切换国家过滤',
|
package/src/watch/public/app.js
CHANGED
|
@@ -4055,23 +4055,41 @@ async function wdSetErrorTrigger() {
|
|
|
4055
4055
|
}
|
|
4056
4056
|
}
|
|
4057
4057
|
|
|
4058
|
-
// ──
|
|
4058
|
+
// ── 代理节点区域(看门狗面板中每个客户端卡片,支持多控制端通道) ──
|
|
4059
4059
|
|
|
4060
4060
|
function renderProxySection(c, userId) {
|
|
4061
|
+
var channels = c.clashHealthChannels || [];
|
|
4062
|
+
if (channels.length > 0) {
|
|
4063
|
+
var html = "";
|
|
4064
|
+
for (var i = 0; i < channels.length; i++) {
|
|
4065
|
+
html += renderProxyChannel(channels[i], userId);
|
|
4066
|
+
}
|
|
4067
|
+
return html;
|
|
4068
|
+
}
|
|
4069
|
+
// 兼容旧版:仅主控制端(legacy clashHealth)
|
|
4061
4070
|
var h = c.clashHealth;
|
|
4062
4071
|
if (!h || !h.node) return "";
|
|
4063
|
-
|
|
4064
4072
|
var nodes = (h.meta && h.meta.healthyNodes) || [];
|
|
4065
4073
|
var hasNodes = nodes.length > 0;
|
|
4074
|
+
return (
|
|
4075
|
+
renderProxyLine(h, nodes, userId, 0, hasNodes) +
|
|
4076
|
+
renderNodeList(nodes, h.node, userId, 0)
|
|
4077
|
+
);
|
|
4078
|
+
}
|
|
4066
4079
|
|
|
4080
|
+
/** 单个控制端通道的代理区域 */
|
|
4081
|
+
function renderProxyChannel(ch, userId) {
|
|
4082
|
+
if (!ch || !ch.node) return "";
|
|
4083
|
+
var nodes = (ch.meta && ch.meta.healthyNodes) || [];
|
|
4084
|
+
var hasNodes = nodes.length > 0;
|
|
4067
4085
|
return (
|
|
4068
|
-
renderProxyLine(
|
|
4069
|
-
renderNodeList(nodes,
|
|
4086
|
+
renderProxyLine(ch, nodes, userId, ch.port || 0, hasNodes) +
|
|
4087
|
+
renderNodeList(nodes, ch.node, userId, ch.port || 0)
|
|
4070
4088
|
);
|
|
4071
4089
|
}
|
|
4072
4090
|
|
|
4073
4091
|
/** 代理状态行:当前节点 + 健康状态 + 展开按钮 */
|
|
4074
|
-
function renderProxyLine(h, nodes, userId, hasNodes) {
|
|
4092
|
+
function renderProxyLine(h, nodes, userId, port, hasNodes) {
|
|
4075
4093
|
var dot = h.healthy ? "#4ade80" : "#f87171";
|
|
4076
4094
|
var status = h.healthy
|
|
4077
4095
|
? (h.timeTotal || 0).toFixed(1) +
|
|
@@ -4079,19 +4097,31 @@ function renderProxyLine(h, nodes, userId, hasNodes) {
|
|
|
4079
4097
|
((h.sizeDownload || 0) / 1024).toFixed(0) +
|
|
4080
4098
|
"KB"
|
|
4081
4099
|
: h.error || "异常";
|
|
4100
|
+
var clientTag = port
|
|
4101
|
+
? h.meta && h.meta.clientName
|
|
4102
|
+
? h.meta.clientName + " :" + port
|
|
4103
|
+
: "代理 :" + port
|
|
4104
|
+
: "";
|
|
4082
4105
|
|
|
4083
4106
|
return styled(
|
|
4084
4107
|
"div",
|
|
4085
4108
|
"margin-top:6px;padding:6px 8px;background:#111;border-radius:6px;" +
|
|
4086
4109
|
"display:flex;align-items:center;gap:8px;font-size:12px",
|
|
4087
4110
|
styled("span", "color:" + dot + ";font-size:14px", "●") +
|
|
4111
|
+
(clientTag
|
|
4112
|
+
? styled(
|
|
4113
|
+
"span",
|
|
4114
|
+
"color:#888;padding:0 4px;border:1px solid #333;border-radius:3px;font-size:10px",
|
|
4115
|
+
clientTag,
|
|
4116
|
+
)
|
|
4117
|
+
: "") +
|
|
4088
4118
|
styled("span", "color:#e0e0e0;font-weight:bold", h.node) +
|
|
4089
4119
|
styled("span", "color:#666", "| " + status) +
|
|
4090
4120
|
(hasNodes
|
|
4091
4121
|
? tag(
|
|
4092
4122
|
"button",
|
|
4093
4123
|
{
|
|
4094
|
-
onclick: "wdToggleNodes('" + esc(userId) + "')",
|
|
4124
|
+
onclick: "wdToggleNodes('" + esc(userId) + "'," + port + ")",
|
|
4095
4125
|
style:
|
|
4096
4126
|
"margin-left:auto;padding:2px 8px;font-size:11px;background:#333;" +
|
|
4097
4127
|
"color:#60a5fa;border:1px solid #444;border-radius:3px;cursor:pointer",
|
|
@@ -4104,15 +4134,15 @@ function renderProxyLine(h, nodes, userId, hasNodes) {
|
|
|
4104
4134
|
}
|
|
4105
4135
|
|
|
4106
4136
|
/** 节点列表面板(默认隐藏) */
|
|
4107
|
-
function renderNodeList(nodes, currentNode, userId,
|
|
4108
|
-
if (!
|
|
4137
|
+
function renderNodeList(nodes, currentNode, userId, port) {
|
|
4138
|
+
if (!nodes || !nodes.length) return "";
|
|
4109
4139
|
|
|
4110
4140
|
var items = "";
|
|
4111
4141
|
for (var i = 0; i < nodes.length; i++) {
|
|
4112
|
-
items += renderNodeItem(nodes[i], nodes[i] === currentNode, userId);
|
|
4142
|
+
items += renderNodeItem(nodes[i], nodes[i] === currentNode, userId, port);
|
|
4113
4143
|
}
|
|
4114
4144
|
|
|
4115
|
-
var id = "wdNodes-" + wdEscId(userId);
|
|
4145
|
+
var id = "wdNodes-" + wdEscId(userId) + "-" + port;
|
|
4116
4146
|
return (
|
|
4117
4147
|
'<div id="' +
|
|
4118
4148
|
id +
|
|
@@ -4124,7 +4154,7 @@ function renderNodeList(nodes, currentNode, userId, hasNodes) {
|
|
|
4124
4154
|
}
|
|
4125
4155
|
|
|
4126
4156
|
/** 单个节点行 */
|
|
4127
|
-
function renderNodeItem(name, isActive, userId) {
|
|
4157
|
+
function renderNodeItem(name, isActive, userId, port) {
|
|
4128
4158
|
var color = isActive ? "#4ade80" : "#888";
|
|
4129
4159
|
var bg = isActive ? "background:#1a3a2a;" : "";
|
|
4130
4160
|
var dot = isActive ? "●" : "○";
|
|
@@ -4135,7 +4165,9 @@ function renderNodeItem(name, isActive, userId) {
|
|
|
4135
4165
|
esc(userId) +
|
|
4136
4166
|
"','" +
|
|
4137
4167
|
esc(name) +
|
|
4138
|
-
"'
|
|
4168
|
+
"'," +
|
|
4169
|
+
port +
|
|
4170
|
+
')" ' +
|
|
4139
4171
|
'style="margin-left:auto;padding:1px 6px;font-size:10px;background:#2563eb;' +
|
|
4140
4172
|
'color:#fff;border:none;border-radius:2px;cursor:pointer">切换</button>';
|
|
4141
4173
|
|
|
@@ -4164,18 +4196,23 @@ function wdEscId(s) {
|
|
|
4164
4196
|
return (s || "").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
4165
4197
|
}
|
|
4166
4198
|
|
|
4167
|
-
function wdToggleNodes(userId) {
|
|
4168
|
-
var el = document.getElementById(
|
|
4199
|
+
function wdToggleNodes(userId, port) {
|
|
4200
|
+
var el = document.getElementById(
|
|
4201
|
+
"wdNodes-" + wdEscId(userId) + "-" + (port || 0),
|
|
4202
|
+
);
|
|
4169
4203
|
if (!el) return;
|
|
4170
4204
|
el.style.display = el.style.display === "none" ? "block" : "none";
|
|
4171
4205
|
}
|
|
4172
4206
|
|
|
4173
|
-
function wdSwitchNode(userId, node) {
|
|
4207
|
+
function wdSwitchNode(userId, node, port) {
|
|
4208
|
+
var portLabel = port ? "(代理通道 " + port + ")" : "";
|
|
4174
4209
|
if (
|
|
4175
4210
|
!confirm(
|
|
4176
4211
|
"确定要将 " +
|
|
4177
4212
|
userId +
|
|
4178
|
-
"
|
|
4213
|
+
" 的代理" +
|
|
4214
|
+
portLabel +
|
|
4215
|
+
"切换到: " +
|
|
4179
4216
|
node +
|
|
4180
4217
|
" ?\n\n切换后自动巡检会继续运行。",
|
|
4181
4218
|
)
|
|
@@ -4185,7 +4222,11 @@ function wdSwitchNode(userId, node) {
|
|
|
4185
4222
|
fetch("/api/watchdog/switch-node", {
|
|
4186
4223
|
method: "POST",
|
|
4187
4224
|
headers: { "Content-Type": "application/json" },
|
|
4188
|
-
body: JSON.stringify({
|
|
4225
|
+
body: JSON.stringify({
|
|
4226
|
+
userId: userId,
|
|
4227
|
+
node: node,
|
|
4228
|
+
proxyPort: port || undefined,
|
|
4229
|
+
}),
|
|
4189
4230
|
})
|
|
4190
4231
|
.then(function (r) {
|
|
4191
4232
|
return r.json();
|
|
@@ -120,6 +120,7 @@ function getOnlineClients() {
|
|
|
120
120
|
processes,
|
|
121
121
|
llmAnalysis,
|
|
122
122
|
clashHealth: info.clashHealth || null,
|
|
123
|
+
clashHealthChannels: info.clashHealthChannels || [],
|
|
123
124
|
lastHeartbeat: info.lastHeartbeat,
|
|
124
125
|
registeredAt: info.registeredAt,
|
|
125
126
|
online,
|
|
@@ -324,6 +325,9 @@ async function handleHeartbeat(params, body, res) {
|
|
|
324
325
|
config: body.config || {},
|
|
325
326
|
processes: body.processes || [],
|
|
326
327
|
clashHealth: body.clashHealth || null,
|
|
328
|
+
clashHealthChannels: Array.isArray(body.clashHealthChannels)
|
|
329
|
+
? body.clashHealthChannels
|
|
330
|
+
: [],
|
|
327
331
|
});
|
|
328
332
|
|
|
329
333
|
// 按 userId 返回该客户端的专属命令列表
|
|
@@ -492,16 +496,22 @@ async function handleClearLlmAnalysis(params, body, res) {
|
|
|
492
496
|
}
|
|
493
497
|
|
|
494
498
|
async function handleSwitchNode(params, body, res) {
|
|
495
|
-
const { userId, node } = body || {};
|
|
499
|
+
const { userId, node, proxyPort } = body || {};
|
|
496
500
|
if (!userId || !node) {
|
|
497
501
|
sendJSON(res, 400, { error: "缺少 userId 或 node" });
|
|
498
502
|
return;
|
|
499
503
|
}
|
|
500
|
-
// 加入 Clash
|
|
504
|
+
// 加入 Clash 命令队列,客户端下次心跳时取走(proxyPort 用于路由到对应控制端)
|
|
501
505
|
const queue = clashCmdQueue.get(userId) || [];
|
|
502
|
-
queue.push({
|
|
506
|
+
queue.push({
|
|
507
|
+
action: "switchNode",
|
|
508
|
+
node,
|
|
509
|
+
proxyPort: proxyPort ? Number(proxyPort) : undefined,
|
|
510
|
+
});
|
|
503
511
|
clashCmdQueue.set(userId, queue);
|
|
504
|
-
console.error(
|
|
512
|
+
console.error(
|
|
513
|
+
`[watchdog] ${userId} 节点切换已入队: → ${node}${proxyPort ? ` (通道 ${proxyPort})` : ""}`,
|
|
514
|
+
);
|
|
505
515
|
sendJSON(res, 200, { ok: true, message: `将在下次心跳时切换到 ${node}` });
|
|
506
516
|
}
|
|
507
517
|
|
package/src/watchdog/agent.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getClientConfig,
|
|
13
13
|
getLocalIP,
|
|
14
14
|
getClashHealth,
|
|
15
|
+
getClashHealthChannels,
|
|
15
16
|
} from "./reporter.js";
|
|
16
17
|
import { ProcessManager } from "./process-manager.js";
|
|
17
18
|
import {
|
|
@@ -414,7 +415,8 @@ export class WatchdogAgent {
|
|
|
414
415
|
config: getClientConfig(),
|
|
415
416
|
processes: this.processManager.getProcessesStatus(),
|
|
416
417
|
llmAnalysis: this._llmResults,
|
|
417
|
-
clashHealth: getClashHealth(), // daemon
|
|
418
|
+
clashHealth: getClashHealth(), // daemon 写入的主控制端健康状态(兼容旧版页面)
|
|
419
|
+
clashHealthChannels: getClashHealthChannels(), // 多控制端各通道健康状态
|
|
418
420
|
};
|
|
419
421
|
}
|
|
420
422
|
|
|
@@ -458,13 +460,21 @@ export class WatchdogAgent {
|
|
|
458
460
|
}
|
|
459
461
|
}
|
|
460
462
|
|
|
461
|
-
// 处理 Clash daemon
|
|
463
|
+
// 处理 Clash daemon 命令(按 proxyPort 分组写入对应通道,无端口走默认通道)
|
|
462
464
|
if (Array.isArray(data.clashCommands) && data.clashCommands.length > 0) {
|
|
463
465
|
const ipc = await import("../clash/ipc.js");
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
466
|
+
const byPort = new Map();
|
|
467
|
+
for (const cmd of data.clashCommands) {
|
|
468
|
+
const port = cmd && cmd.proxyPort ? Number(cmd.proxyPort) : 0;
|
|
469
|
+
if (!byPort.has(port)) byPort.set(port, []);
|
|
470
|
+
byPort.get(port).push(cmd);
|
|
471
|
+
}
|
|
472
|
+
for (const [port, cmds] of byPort) {
|
|
473
|
+
ipc.writeCommand(cmds, port);
|
|
474
|
+
console.error(
|
|
475
|
+
`[watchdog] 下发 Clash 命令${port ? ` (通道 ${port})` : ""}: ${JSON.stringify(cmds)}`,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
468
478
|
}
|
|
469
479
|
|
|
470
480
|
// 服务端下发的客户端配置(远程修改 ~/.tt-help.json)
|
package/src/watchdog/reporter.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import os from "os";
|
|
7
|
-
import { existsSync, readFileSync } from "fs";
|
|
7
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
8
8
|
import { join } from "path";
|
|
9
9
|
import { homedir, tmpdir } from "os";
|
|
10
10
|
|
|
@@ -91,3 +91,34 @@ export function getClashHealth() {
|
|
|
91
91
|
}
|
|
92
92
|
return null;
|
|
93
93
|
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 读取多控制端健康状态(daemon 多通道模式写入的 clash-health-{port}.json)
|
|
97
|
+
* @returns {Array<{port:number, stale:boolean, ageSeconds:number, ...health}>}
|
|
98
|
+
*/
|
|
99
|
+
export function getClashHealthChannels() {
|
|
100
|
+
const out = [];
|
|
101
|
+
try {
|
|
102
|
+
for (const name of readdirSync(tmpdir())) {
|
|
103
|
+
const m = /^clash-health-(\d+)\.json$/.exec(name);
|
|
104
|
+
if (!m) continue;
|
|
105
|
+
const port = parseInt(m[1], 10);
|
|
106
|
+
if (!port) continue;
|
|
107
|
+
try {
|
|
108
|
+
const data = JSON.parse(readFileSync(join(tmpdir(), name), "utf-8"));
|
|
109
|
+
const age = Date.now() - new Date(data.lastCheck || 0).getTime();
|
|
110
|
+
out.push({
|
|
111
|
+
...data,
|
|
112
|
+
port,
|
|
113
|
+
stale: age > 120_000,
|
|
114
|
+
ageSeconds: Math.round(age / 1000),
|
|
115
|
+
});
|
|
116
|
+
} catch {
|
|
117
|
+
/* 单个文件解析失败跳过 */
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
/* 目录不可读 */
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|