tt-help-cli-ycl 1.4.155 → 1.4.157
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/src/clash/api.js +49 -0
- package/src/clash/checker.js +28 -11
- package/src/clash/daemon.js +112 -1
- package/src/clash/runtime.js +823 -0
- package/src/cli/comments.js +7 -0
- package/src/cli/explore.js +12 -1
- package/src/cli/refresh-mini.js +13 -1
- package/src/cli/refresh.js +13 -1
- package/src/lib/app-config.js +25 -1
- package/src/lib/args.js +9 -0
- package/src/lib/client-locations.js +70 -0
- package/src/watch/public/app.js +163 -2
- package/src/watch/public/index.html +17 -0
- package/src/watch/watchdog-server.js +53 -0
- package/src/watchdog/agent.js +39 -0
- package/src/watchdog/env-init.js +187 -0
- package/src/watchdog/process-manager.js +17 -6
package/package.json
CHANGED
package/src/clash/api.js
CHANGED
|
@@ -210,6 +210,29 @@ export async function getRules() {
|
|
|
210
210
|
export async function getProxyProviders() {
|
|
211
211
|
return request("/providers/proxies");
|
|
212
212
|
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 订阅摘要(仅 HTTP 类型 provider):
|
|
216
|
+
* 节点数 + subscriptionInfo(官方口径的已用/总量/到期,用于看门狗页面展示)
|
|
217
|
+
* @returns {Promise<Array<{name,proxies,upload,download,total,expire}>>}
|
|
218
|
+
*/
|
|
219
|
+
export async function getSubscriptionSummaries() {
|
|
220
|
+
const data = await getProxyProviders();
|
|
221
|
+
const out = [];
|
|
222
|
+
for (const [name, p] of Object.entries(data.providers || {})) {
|
|
223
|
+
if (p.vehicleType !== "HTTP") continue;
|
|
224
|
+
const si = p.subscriptionInfo || {};
|
|
225
|
+
out.push({
|
|
226
|
+
name,
|
|
227
|
+
proxies: (p.proxies || []).length,
|
|
228
|
+
upload: si.Upload || 0,
|
|
229
|
+
download: si.Download || 0,
|
|
230
|
+
total: si.Total || 0,
|
|
231
|
+
expire: si.Expire || 0,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
213
236
|
export async function updateProxyProvider(name) {
|
|
214
237
|
return request(`/providers/proxies/${encodeURIComponent(name)}`, {
|
|
215
238
|
method: "PUT",
|
|
@@ -279,3 +302,29 @@ export async function getProxyDelay(name, url, timeoutMs = 5000) {
|
|
|
279
302
|
return { delay: -1, timeout: true };
|
|
280
303
|
}
|
|
281
304
|
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 组批量延迟测试(一次测完全组成员,含 provider 节点)
|
|
308
|
+
*
|
|
309
|
+
* 背景: provider 模式下订阅节点不在 /proxies 表内,/proxies/{name}/delay 对其
|
|
310
|
+
* 返回 404(Resource not found),只能走本端点整组测速。
|
|
311
|
+
*
|
|
312
|
+
* @returns {Promise<Object<string,number>|null>} name → delay(ms),失败节点不在返回中;请求失败返回 null(调用方回退逐个探测)
|
|
313
|
+
*/
|
|
314
|
+
export async function getGroupDelay(groupName, url, timeoutMs = 5000) {
|
|
315
|
+
try {
|
|
316
|
+
const qs = `url=${encodeURIComponent(url)}&timeout=${timeoutMs}`;
|
|
317
|
+
const res = await fetch(
|
|
318
|
+
`${BASE_URL}/group/${encodeURIComponent(groupName)}/delay?${qs}`,
|
|
319
|
+
{
|
|
320
|
+
headers: headers(),
|
|
321
|
+
signal: AbortSignal.timeout(timeoutMs + 15000),
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
if (!res.ok) return null;
|
|
325
|
+
const data = await res.json();
|
|
326
|
+
return data && typeof data === "object" ? data : null;
|
|
327
|
+
} catch {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
package/src/clash/checker.js
CHANGED
|
@@ -395,19 +395,36 @@ export async function runDelayProbe({
|
|
|
395
395
|
const total = all.length;
|
|
396
396
|
|
|
397
397
|
const results = [];
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
398
|
+
// 批量优先: /group/{G}/delay 一次测完全组(含 provider 节点 —— 该类节点不在
|
|
399
|
+
// /proxies 表内,逐个探测必 404);批量不可用时回退逐个探测(兼容旧行为)
|
|
400
|
+
const batch = await api.getGroupDelay(CONFIG.groupName, CONFIG.testUrl, timeoutMs);
|
|
401
|
+
if (batch) {
|
|
402
|
+
for (const name of filtered) {
|
|
403
|
+
const delay = batch[name];
|
|
404
|
+
results.push({
|
|
405
|
+
node: name,
|
|
406
|
+
delay: typeof delay === "number" && delay > 0 ? delay : -1,
|
|
407
|
+
timeout: !(typeof delay === "number" && delay > 0),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
console.error(
|
|
412
|
+
"[checker] 批量测速不可用,回退逐个探测(provider 节点将不可见)",
|
|
413
|
+
);
|
|
414
|
+
const queue = [...filtered];
|
|
415
|
+
async function worker() {
|
|
416
|
+
while (queue.length) {
|
|
417
|
+
const name = queue.shift();
|
|
418
|
+
const r = await api.getProxyDelay(name, CONFIG.testUrl, timeoutMs);
|
|
419
|
+
results.push({ node: name, delay: r.delay, timeout: r.timeout });
|
|
420
|
+
}
|
|
404
421
|
}
|
|
422
|
+
const workers = Array.from(
|
|
423
|
+
{ length: Math.min(concurrency, Math.max(1, filtered.length)) },
|
|
424
|
+
() => worker(),
|
|
425
|
+
);
|
|
426
|
+
await Promise.all(workers);
|
|
405
427
|
}
|
|
406
|
-
const workers = Array.from(
|
|
407
|
-
{ length: Math.min(concurrency, Math.max(1, filtered.length)) },
|
|
408
|
-
() => worker(),
|
|
409
|
-
);
|
|
410
|
-
await Promise.all(workers);
|
|
411
428
|
|
|
412
429
|
const good = results
|
|
413
430
|
.filter((r) => !r.timeout && r.delay > 0 && r.delay <= thresholdMs)
|
package/src/clash/daemon.js
CHANGED
|
@@ -12,6 +12,7 @@ import * as api from "./api.js";
|
|
|
12
12
|
import * as checker from "./checker.js";
|
|
13
13
|
import * as ipc from "./ipc.js";
|
|
14
14
|
import { discoverAll, invalidateDiscoverCache } from "./discover.js";
|
|
15
|
+
import * as runtime from "./runtime.js";
|
|
15
16
|
|
|
16
17
|
// ── 国家过滤辅助(normalizeCountries 由 checker.js 提供单一来源) ──
|
|
17
18
|
/** 国家配置显示(数组 join) */
|
|
@@ -58,6 +59,8 @@ export class ClashDaemon {
|
|
|
58
59
|
};
|
|
59
60
|
this._apiFailCount = 0;
|
|
60
61
|
this._running = false;
|
|
62
|
+
// 内置 mihomo 运行时状态(ensureMihomoRunning 最近一次结果,透出到 health meta)
|
|
63
|
+
this._runtimeStatus = null;
|
|
61
64
|
this._timers = [];
|
|
62
65
|
this._waitTimer = null; // 等待控制端就绪的重试定时器
|
|
63
66
|
this._startedOnce = false; // 首次启动标记(banner/信号只打印注册一次)
|
|
@@ -293,6 +296,28 @@ export class ClashDaemon {
|
|
|
293
296
|
return false;
|
|
294
297
|
});
|
|
295
298
|
if (!aliveAny) {
|
|
299
|
+
// 内置内核自动拉起(先发现后拉起:Verge/已跑的 mihomo 会被发现并认领,
|
|
300
|
+
// 真没有才 spawn;端口被占/冷却期会跳过,详见 runtime.ensureMihomoRunning)
|
|
301
|
+
try {
|
|
302
|
+
const r = await runtime.ensureMihomoRunning({
|
|
303
|
+
subscriptions: runtime.readStoredSubscriptions(),
|
|
304
|
+
log: (m) => this._log(m),
|
|
305
|
+
});
|
|
306
|
+
this._runtimeStatus = { ...r, at: Date.now() };
|
|
307
|
+
if (r.status === "started") {
|
|
308
|
+
this._log(`✓ 内置 mihomo 已就绪,下轮发现将接管`);
|
|
309
|
+
} else if (r.status === "no-subscription") {
|
|
310
|
+
// 无订阅不接管(不拉起不占位),防刷屏降频打日志
|
|
311
|
+
if (Date.now() - (this._lastNoSubLogAt || 0) > 300000) {
|
|
312
|
+
this._log(`⏭ 未配置订阅,内置 mihomo 不参与(后台配置订阅后自动接管)`);
|
|
313
|
+
this._lastNoSubLogAt = Date.now();
|
|
314
|
+
}
|
|
315
|
+
} else if (r.status === "port-busy") {
|
|
316
|
+
this._log(`⚠ ${r.error}(不影响发现已有控制端)`);
|
|
317
|
+
}
|
|
318
|
+
} catch (e) {
|
|
319
|
+
this._log(`内置内核拉起异常: ${e.message}`);
|
|
320
|
+
}
|
|
296
321
|
this._waitRetry = (this._waitRetry || 0) + 1;
|
|
297
322
|
const shouldLog = this._waitRetry <= 3 || this._waitRetry % 6 === 0;
|
|
298
323
|
if (shouldLog) {
|
|
@@ -379,15 +404,49 @@ export class ClashDaemon {
|
|
|
379
404
|
this._useController(c);
|
|
380
405
|
try {
|
|
381
406
|
if (!checker.checkClashAlive()) {
|
|
407
|
+
// 控制端失联:尝试自动拉起内置内核(冷却保护,端口占用场景内部会跳过),
|
|
408
|
+
// 本轮先报不健康,拉起结果下轮巡检验证
|
|
409
|
+
const r = await runtime
|
|
410
|
+
.ensureMihomoRunning({
|
|
411
|
+
subscriptions: runtime.readStoredSubscriptions(),
|
|
412
|
+
log: (m) => this._log(m),
|
|
413
|
+
})
|
|
414
|
+
.catch((e) => ({ status: "failed", error: e.message }));
|
|
415
|
+
this._runtimeStatus = { ...r, at: Date.now() };
|
|
382
416
|
return this._writeHealth(c, {
|
|
383
417
|
node: "?",
|
|
384
418
|
healthy: false,
|
|
385
|
-
error: `${c.clientName}
|
|
419
|
+
error: `${c.clientName} 未运行${r.status === "started" ? "(内置内核已自动拉起)" : ""}`,
|
|
386
420
|
});
|
|
387
421
|
}
|
|
388
422
|
const node = await api
|
|
389
423
|
.getCurrentNode(checker.CONFIG.groupName)
|
|
390
424
|
.catch(() => "?");
|
|
425
|
+
|
|
426
|
+
// 订阅信息 + 流量采样(随巡检周期,差分算速率)
|
|
427
|
+
c._subsSnapshot = await api.getSubscriptionSummaries().catch(() => null);
|
|
428
|
+
const conn = await api.getConnections().catch(() => null);
|
|
429
|
+
if (conn) {
|
|
430
|
+
const now = Date.now();
|
|
431
|
+
const totals = {
|
|
432
|
+
down: conn.downloadTotal || 0,
|
|
433
|
+
up: conn.uploadTotal || 0,
|
|
434
|
+
at: now,
|
|
435
|
+
};
|
|
436
|
+
const last = c._trafficLast;
|
|
437
|
+
if (last && now > last.at) {
|
|
438
|
+
const sec = (now - last.at) / 1000;
|
|
439
|
+
c._traffic = {
|
|
440
|
+
downRate: Math.max(0, (totals.down - last.down) / sec),
|
|
441
|
+
upRate: Math.max(0, (totals.up - last.up) / sec),
|
|
442
|
+
totalDown: totals.down,
|
|
443
|
+
totalUp: totals.up,
|
|
444
|
+
sampledAt: now,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
c._trafficLast = totals;
|
|
448
|
+
}
|
|
449
|
+
|
|
391
450
|
let h = checker.checkNodeHealth();
|
|
392
451
|
this._writeHealth(c, {
|
|
393
452
|
node,
|
|
@@ -404,6 +463,13 @@ export class ClashDaemon {
|
|
|
404
463
|
try {
|
|
405
464
|
const proxies = await api.getProxies();
|
|
406
465
|
const valid = new Set(Object.keys(proxies?.proxies || {}));
|
|
466
|
+
// provider 模式: 订阅节点不在 /proxies 表内,并入 GLOBAL 全员作为合法全集,
|
|
467
|
+
// 否则每个候选都会被误判失效,导致每轮巡检都重复 delay 初测
|
|
468
|
+
try {
|
|
469
|
+
for (const n of await api.listGroupNodes(checker.CONFIG.groupName)) {
|
|
470
|
+
valid.add(n);
|
|
471
|
+
}
|
|
472
|
+
} catch {}
|
|
407
473
|
const pruned = checker.pruneHealthyListByCurrentProxies(valid);
|
|
408
474
|
if (pruned.removed > 0) {
|
|
409
475
|
if (pruned.allGone) {
|
|
@@ -885,6 +951,43 @@ export class ClashDaemon {
|
|
|
885
951
|
}
|
|
886
952
|
}
|
|
887
953
|
|
|
954
|
+
/**
|
|
955
|
+
* 看门狗指令: 应用订阅配置(服务端页面改动 → 心跳下发 → 此处热应用)
|
|
956
|
+
* 幂等: 与运行时配置比对,无变化不动作;成功后持久化到 ~/.tt-help.json
|
|
957
|
+
* 保证 daemon/机器重启后自动恢复,不依赖服务端可达
|
|
958
|
+
*/
|
|
959
|
+
async _handleSetSubscription(cmd, c) {
|
|
960
|
+
try {
|
|
961
|
+
const subs = runtime.normalizeSubscriptions(cmd.subscriptions);
|
|
962
|
+
if (!subs.length) {
|
|
963
|
+
// 清空订阅: 回收内置 mihomo + 清持久化 → 回到"无订阅不接管"态(用户可回退到 Verge)
|
|
964
|
+
const killed = await runtime.stopRuntimeMihomo({ log: (m) => this._log(m) });
|
|
965
|
+
runtime.storeSubscriptions([]);
|
|
966
|
+
c._subsSnapshot = [];
|
|
967
|
+
c._applyResult = { ok: true, changed: true, cleared: true, at: Date.now() };
|
|
968
|
+
this._log(`📋 订阅已清空: 回收内置 mihomo ${killed} 个进程,不再接管端口`);
|
|
969
|
+
this._writeHealth(c, { node: "?", healthy: null });
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
this._log(`📋 收到订阅配置: ${subs.length} 条,开始应用...`);
|
|
973
|
+
const r = await runtime.applyUserSubscriptions(subs, { log: (m) => this._log(m) });
|
|
974
|
+
if (r.ok) {
|
|
975
|
+
runtime.storeSubscriptions(subs);
|
|
976
|
+
if (r.providers) c._subsSnapshot = r.providers;
|
|
977
|
+
c._applyResult = { ok: true, changed: r.changed, at: Date.now(), count: subs.length };
|
|
978
|
+
this._log(`📋 订阅应用成功(${subs.length} 条)${r.changed ? "" : "(无变化)"}`);
|
|
979
|
+
} else {
|
|
980
|
+
c._applyResult = { ok: false, error: r.error, at: Date.now() };
|
|
981
|
+
this._log(`⚠ 订阅应用失败: ${r.error}`);
|
|
982
|
+
}
|
|
983
|
+
// 结果写健康文件,页面下次心跳可见
|
|
984
|
+
const node = await api.getCurrentNode(checker.CONFIG.groupName).catch(() => "?");
|
|
985
|
+
this._writeHealth(c, { node, healthy: null });
|
|
986
|
+
} catch (e) {
|
|
987
|
+
this._log(`📋 订阅应用异常: ${e.message}`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
888
991
|
// ── helpers ──
|
|
889
992
|
|
|
890
993
|
_buildMeta(c) {
|
|
@@ -900,6 +1003,13 @@ export class ClashDaemon {
|
|
|
900
1003
|
healthyNodesTotal: list.totalNodes,
|
|
901
1004
|
healthyNodes: list.nodes.slice(0, 50),
|
|
902
1005
|
healthyNodeDelays: list.delays || {},
|
|
1006
|
+
// 代理运行时:订阅用量/到期 + 实时速率(看门狗页面展示)
|
|
1007
|
+
proxy: {
|
|
1008
|
+
subscriptions: (c && c._subsSnapshot) || [],
|
|
1009
|
+
traffic: (c && c._traffic) || null,
|
|
1010
|
+
runtime: this._runtimeStatus || null,
|
|
1011
|
+
applyResult: (c && c._applyResult) || null,
|
|
1012
|
+
},
|
|
903
1013
|
});
|
|
904
1014
|
return base;
|
|
905
1015
|
}
|
|
@@ -983,6 +1093,7 @@ function _buildCmdHandlers(daemon) {
|
|
|
983
1093
|
},
|
|
984
1094
|
switchNode: (cmd, c) => daemon._handleSwitchNode(cmd, c),
|
|
985
1095
|
failover: (cmd, c) => daemon._handleFailover(cmd, c),
|
|
1096
|
+
setSubscription: (cmd, c) => daemon._handleSetSubscription(cmd, c),
|
|
986
1097
|
};
|
|
987
1098
|
}
|
|
988
1099
|
|