tt-help-cli-ycl 1.4.161 → 1.4.162
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/daemon.js +43 -10
- package/src/watch/public/app.js +324 -28
- package/src/watch/public/index.html +1 -1
- package/src/watchdog/env-init.js +13 -0
package/package.json
CHANGED
package/src/clash/daemon.js
CHANGED
|
@@ -443,6 +443,7 @@ export class ClashDaemon {
|
|
|
443
443
|
if (r.status === "started") {
|
|
444
444
|
started++;
|
|
445
445
|
this._log(`✓ 实例${idx} (${spec.apiPort}/${spec.mixedPort}) 自愈拉起成功,下轮发现将接管`);
|
|
446
|
+
await this._rediscover("实例自愈拉起");
|
|
446
447
|
} else if (r.status === "failed" || r.status === "no-vendor") {
|
|
447
448
|
failed++;
|
|
448
449
|
this._log(`⚠ 实例${idx} 自愈拉起失败: ${r.error || r.status}`);
|
|
@@ -689,33 +690,60 @@ export class ClashDaemon {
|
|
|
689
690
|
}
|
|
690
691
|
|
|
691
692
|
/**
|
|
692
|
-
* API
|
|
693
|
-
*
|
|
693
|
+
* 重新发现控制端(订阅应用/自愈拉起新实例后调用,或 API 连续失败时)
|
|
694
|
+
* 背景: controllers 是 start() 时的快照,正常运行期新拉起的内置实例(9098+)
|
|
695
|
+
* 不会自动进入巡检列表 —— 必须主动重发现才能接管(否则新实例永远不被巡检/上报)
|
|
696
|
+
* 防抖: 30s 内多次触发只执行一次(多实例同时拉起时避免重复探测)
|
|
694
697
|
*/
|
|
695
|
-
async
|
|
696
|
-
|
|
697
|
-
if (this.
|
|
698
|
-
this.
|
|
698
|
+
async _rediscover(reason) {
|
|
699
|
+
const now = Date.now();
|
|
700
|
+
if (now - (this._lastRediscoverAt || 0) < 30_000) return;
|
|
701
|
+
this._lastRediscoverAt = now;
|
|
699
702
|
invalidateDiscoverCache();
|
|
700
|
-
this._log(`API 连续失败,重新发现控制端... (${e?.message || e})`);
|
|
701
703
|
try {
|
|
702
704
|
const discs = await discoverAll(this._discoverOpts);
|
|
705
|
+
// 按端口迁移旧控制器运行状态(切换历史/冷却锚点/订阅快照等),
|
|
706
|
+
// 避免重发现后 failover 冷却锚点丢失导致反复切换
|
|
707
|
+
const prev = new Map(this._controllers.map((c) => [c.port, c]));
|
|
703
708
|
const seen = new Set();
|
|
704
709
|
this._controllers = discs.filter((d) => {
|
|
705
710
|
if (seen.has(d.port)) return false;
|
|
706
711
|
seen.add(d.port);
|
|
712
|
+
const old = prev.get(d.port);
|
|
713
|
+
if (old) {
|
|
714
|
+
d._meta = old._meta;
|
|
715
|
+
d._switchHistory = old._switchHistory;
|
|
716
|
+
d._lastSwitchAt = old._lastSwitchAt;
|
|
717
|
+
d._restartSeq = old._restartSeq;
|
|
718
|
+
d._trafficLast = old._trafficLast;
|
|
719
|
+
d._traffic = old._traffic;
|
|
720
|
+
d._subsSnapshot = old._subsSnapshot;
|
|
721
|
+
d._applyResult = old._applyResult;
|
|
722
|
+
d._lastProbeAt = old._lastProbeAt;
|
|
723
|
+
}
|
|
707
724
|
return true;
|
|
708
725
|
});
|
|
709
726
|
this._primary = this._controllers[0] || null;
|
|
710
|
-
this._lastDiscoverKey = "";
|
|
727
|
+
this._lastDiscoverKey = ""; // 强制下轮日志重新打印控制端列表
|
|
711
728
|
this._log(
|
|
712
|
-
`✓
|
|
729
|
+
`✓ ${reason}: 重新发现 ${this._controllers.length} 个控制端: ${this._controllers.map((c) => `${c.clientName}@${c.port}`).join(", ") || "无"}`,
|
|
713
730
|
);
|
|
714
731
|
} catch (err) {
|
|
715
|
-
this._log(
|
|
732
|
+
this._log(`${reason}: 重新发现控制端失败: ${err.message}`);
|
|
716
733
|
}
|
|
717
734
|
}
|
|
718
735
|
|
|
736
|
+
/**
|
|
737
|
+
* API 连续失败时的控制端重发现
|
|
738
|
+
* 连续 2 次巡检异常触发一次完整重发现,防止端口漂移后失联
|
|
739
|
+
*/
|
|
740
|
+
async _maybeRediscover(e) {
|
|
741
|
+
this._apiFailCount = (this._apiFailCount || 0) + 1;
|
|
742
|
+
if (this._apiFailCount < 2) return;
|
|
743
|
+
this._apiFailCount = 0;
|
|
744
|
+
await this._rediscover(`API 连续失败 (${e?.message || e})`);
|
|
745
|
+
}
|
|
746
|
+
|
|
719
747
|
/**
|
|
720
748
|
* 白名单守卫:当前 Clash Verge 正在使用的节点必须 ∈ 候选集(咱们节点列表)
|
|
721
749
|
*
|
|
@@ -1048,6 +1076,8 @@ export class ClashDaemon {
|
|
|
1048
1076
|
c._applyResult = { ok: true, changed: true, cleared: true, at: Date.now() };
|
|
1049
1077
|
this._log(`📋 订阅已清空: 回收内置 mihomo ${killed} 个进程,不再接管端口`);
|
|
1050
1078
|
this._writeHealth(c, { node: "?", healthy: null });
|
|
1079
|
+
// 实例已回收 → 重发现让死实例退出巡检列表(否则每轮巡检持续报"未运行")
|
|
1080
|
+
await this._rediscover("订阅清空");
|
|
1051
1081
|
return;
|
|
1052
1082
|
}
|
|
1053
1083
|
this._log(`📋 收到订阅配置: ${subs.length} 条,按条启动独立实例(9097+每条递增)...`);
|
|
@@ -1077,6 +1107,9 @@ export class ClashDaemon {
|
|
|
1077
1107
|
} else {
|
|
1078
1108
|
this._log(`⚠ ${failed.length}/${subs.length} 个实例应用失败(未持久化,待下轮心跳重试): ${failed.map((f) => `实例${f.index}`).join(",")}`);
|
|
1079
1109
|
}
|
|
1110
|
+
// 实例集可能已变化(成功拉起新实例/回收多余实例,含部分失败场景)→ 重发现同步巡检列表
|
|
1111
|
+
// (controllers 是启动快照;不重发现则新实例永远不被巡检/上报,214 实采踩坑)
|
|
1112
|
+
await this._rediscover("订阅应用");
|
|
1080
1113
|
c._subsSnapshot = await api.getSubscriptionSummaries().catch(() => null);
|
|
1081
1114
|
c._applyResult = {
|
|
1082
1115
|
ok: allOk,
|
package/src/watch/public/app.js
CHANGED
|
@@ -5278,16 +5278,53 @@ async function wdSetErrorTrigger() {
|
|
|
5278
5278
|
|
|
5279
5279
|
// ── 代理节点区域(看门狗面板中每个客户端卡片,支持多控制端通道) ──
|
|
5280
5280
|
|
|
5281
|
+
/** 内置实例端口段(与 discover.js RUNTIME_MAX_INSTANCES=8 对齐):代理 7897+n / API 9097+n */
|
|
5282
|
+
var _SUBS_BASE_PROXY_PORT = 7897;
|
|
5283
|
+
var _SUBS_BASE_API_PORT = 9097;
|
|
5284
|
+
var _SUBS_MAX_INSTANCES = 8;
|
|
5285
|
+
|
|
5286
|
+
/** 服务端配置的订阅列表(proxySettings[userId].subscriptions) */
|
|
5287
|
+
function wdSubsConfigured(userId) {
|
|
5288
|
+
if (_wdData && _wdData.proxySettings && _wdData.proxySettings[userId]) {
|
|
5289
|
+
return _wdData.proxySettings[userId].subscriptions || [];
|
|
5290
|
+
}
|
|
5291
|
+
return [];
|
|
5292
|
+
}
|
|
5293
|
+
|
|
5294
|
+
/**
|
|
5295
|
+
* 代理区域入口:
|
|
5296
|
+
* 内置实例通道(7897+n)→ 订阅总览表(一行 = 一条订阅 = 一个实例)
|
|
5297
|
+
* 外部通道(Verge 等) → 独立通道行(节点 + 自身订阅摘要)
|
|
5298
|
+
* 无通道 → 配置过订阅显示总览(实例离线态);否则单控制端旧视图
|
|
5299
|
+
*/
|
|
5281
5300
|
function renderProxySection(c, userId) {
|
|
5282
|
-
var channels = c.clashHealthChannels || [];
|
|
5301
|
+
var channels = (c.clashHealthChannels || []).slice();
|
|
5302
|
+
channels.sort(function (a, b) {
|
|
5303
|
+
return (a.port || 0) - (b.port || 0);
|
|
5304
|
+
});
|
|
5283
5305
|
if (channels.length > 0) {
|
|
5284
|
-
var
|
|
5306
|
+
var builtin = [];
|
|
5307
|
+
var external = [];
|
|
5285
5308
|
for (var i = 0; i < channels.length; i++) {
|
|
5286
|
-
|
|
5309
|
+
var idx = (channels[i].port || 0) - _SUBS_BASE_PROXY_PORT;
|
|
5310
|
+
if (idx >= 0 && idx < _SUBS_MAX_INSTANCES) builtin.push(channels[i]);
|
|
5311
|
+
else external.push(channels[i]);
|
|
5312
|
+
}
|
|
5313
|
+
var html = renderSubsOverview(c, userId, builtin, channels);
|
|
5314
|
+
for (var j = 0; j < external.length; j++) {
|
|
5315
|
+
// 外部通道的订阅配置入口:仅当没有总览区时显示(配置入口全局收敛为一个)
|
|
5316
|
+
html += renderExternalChannel(external[j], userId, !html);
|
|
5287
5317
|
}
|
|
5288
5318
|
return html;
|
|
5289
5319
|
}
|
|
5290
|
-
|
|
5320
|
+
if (wdSubsConfigured(userId).length > 0) {
|
|
5321
|
+
return renderSubsOverview(c, userId, [], []);
|
|
5322
|
+
}
|
|
5323
|
+
return renderProxyLegacy(c, userId);
|
|
5324
|
+
}
|
|
5325
|
+
|
|
5326
|
+
/** 兼容旧版:仅主控制端(legacy clashHealth) */
|
|
5327
|
+
function renderProxyLegacy(c, userId) {
|
|
5291
5328
|
var h = c.clashHealth;
|
|
5292
5329
|
if (!h || !h.node) return "";
|
|
5293
5330
|
var nodes = (h.meta && h.meta.healthyNodes) || [];
|
|
@@ -5300,8 +5337,8 @@ function renderProxySection(c, userId) {
|
|
|
5300
5337
|
);
|
|
5301
5338
|
}
|
|
5302
5339
|
|
|
5303
|
-
/**
|
|
5304
|
-
function
|
|
5340
|
+
/** 外部代理通道(Verge 等非内置实例):节点行 + 节点列表 + 自身订阅摘要 */
|
|
5341
|
+
function renderExternalChannel(ch, userId, withConfig) {
|
|
5305
5342
|
if (!ch || !ch.node) return "";
|
|
5306
5343
|
var nodes = (ch.meta && ch.meta.healthyNodes) || [];
|
|
5307
5344
|
var delays = (ch.meta && ch.meta.healthyNodeDelays) || {};
|
|
@@ -5309,10 +5346,284 @@ function renderProxyChannel(ch, userId) {
|
|
|
5309
5346
|
return (
|
|
5310
5347
|
renderProxyLine(ch, nodes, userId, ch.port || 0, hasNodes) +
|
|
5311
5348
|
renderNodeList(nodes, ch.node, userId, ch.port || 0, delays) +
|
|
5312
|
-
renderSubscriptions(ch.meta && ch.meta.proxy, userId)
|
|
5349
|
+
renderSubscriptions(ch.meta && ch.meta.proxy, userId, withConfig !== false)
|
|
5350
|
+
);
|
|
5351
|
+
}
|
|
5352
|
+
|
|
5353
|
+
// ── 订阅总览区(每客户端一个) ──
|
|
5354
|
+
|
|
5355
|
+
/** 订阅应用结果提示(daemon 最近一次 setSubscription 执行反馈) */
|
|
5356
|
+
function renderApplyResult(ar) {
|
|
5357
|
+
if (!ar || !ar.at) return "";
|
|
5358
|
+
if (ar.ok === false) {
|
|
5359
|
+
return styled(
|
|
5360
|
+
"div",
|
|
5361
|
+
"font-size:11px;margin:2px 0 4px;padding:4px 6px;background:#3b1d1d;border:1px solid #7f1d1d;border-radius:4px;color:#f87171;line-height:1.4;width:100%;box-sizing:border-box",
|
|
5362
|
+
"❌ 订阅应用失败: " + (ar.error || "未知错误") +
|
|
5363
|
+
styled("span", "color:#94a3b8;margin-left:6px;font-size:10px", new Date(ar.at).toLocaleTimeString()),
|
|
5364
|
+
);
|
|
5365
|
+
}
|
|
5366
|
+
if (ar.changed) {
|
|
5367
|
+
return styled(
|
|
5368
|
+
"div",
|
|
5369
|
+
"font-size:11px;color:#4ade80;width:100%",
|
|
5370
|
+
"✓ 订阅应用成功 (" + (ar.count || "?") + " 条," + new Date(ar.at).toLocaleTimeString() + ")",
|
|
5371
|
+
);
|
|
5372
|
+
}
|
|
5373
|
+
return "";
|
|
5374
|
+
}
|
|
5375
|
+
|
|
5376
|
+
|
|
5377
|
+
/**
|
|
5378
|
+
* 订阅总览表:
|
|
5379
|
+
* 订阅名 | 实例 | 代理端口(业务 --proxy 用) | 当前节点(可展开切换) | 用量/到期/速率
|
|
5380
|
+
* 一行 = 一条订阅 = 一个内置 mihomo 实例;通道离线时显示占位行(配置可见,实例自愈中)
|
|
5381
|
+
*/
|
|
5382
|
+
function renderSubsOverview(c, userId, builtinChannels, allChannels) {
|
|
5383
|
+
var configured = wdSubsConfigured(userId);
|
|
5384
|
+
var byPort = {};
|
|
5385
|
+
for (var i = 0; i < builtinChannels.length; i++) {
|
|
5386
|
+
byPort[builtinChannels[i].port] = builtinChannels[i];
|
|
5387
|
+
}
|
|
5388
|
+
var count = configured.length;
|
|
5389
|
+
for (var p in byPort) {
|
|
5390
|
+
var n = parseInt(p, 10) - _SUBS_BASE_PROXY_PORT + 1;
|
|
5391
|
+
if (n > count) count = n;
|
|
5392
|
+
}
|
|
5393
|
+
if (!count) return "";
|
|
5394
|
+
|
|
5395
|
+
// 订阅应用结果(主控制端默认健康文件优先,回退任一通道快照)
|
|
5396
|
+
var ar = null;
|
|
5397
|
+
if (c.clashHealth && c.clashHealth.meta && c.clashHealth.meta.proxy) {
|
|
5398
|
+
ar = c.clashHealth.meta.proxy.applyResult;
|
|
5399
|
+
}
|
|
5400
|
+
if (!ar || !ar.at) {
|
|
5401
|
+
for (var k = 0; k < (allChannels || []).length; k++) {
|
|
5402
|
+
var pp = allChannels[k].meta && allChannels[k].meta.proxy;
|
|
5403
|
+
if (pp && pp.applyResult && pp.applyResult.at) {
|
|
5404
|
+
ar = pp.applyResult;
|
|
5405
|
+
break;
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
}
|
|
5409
|
+
|
|
5410
|
+
// 标题行:入口收敛为全局唯一一个配置按钮
|
|
5411
|
+
var head = styled(
|
|
5412
|
+
"div",
|
|
5413
|
+
"padding:6px 8px;background:#111;display:flex;align-items:center;gap:8px;font-size:12px;flex-wrap:wrap",
|
|
5414
|
+
styled("span", "font-size:14px", "🛜") +
|
|
5415
|
+
styled("span", "color:#e0e0e0;font-weight:bold", "代理订阅") +
|
|
5416
|
+
styled("span", "color:#94a3b8", count + " 条") +
|
|
5417
|
+
tag(
|
|
5418
|
+
"button",
|
|
5419
|
+
{
|
|
5420
|
+
onclick: "wdShowProxyModal('" + esc(userId) + "')",
|
|
5421
|
+
style:
|
|
5422
|
+
"padding:2px 8px;font-size:11px;background:#333;color:#fbbf24;" +
|
|
5423
|
+
"border:1px solid #444;border-radius:3px;cursor:pointer",
|
|
5424
|
+
title: "配置该客户端的代理订阅(每行一个地址,保存后自动热应用,无需重启)",
|
|
5425
|
+
},
|
|
5426
|
+
"配置 ⚙️",
|
|
5427
|
+
) +
|
|
5428
|
+
styled(
|
|
5429
|
+
"span",
|
|
5430
|
+
"color:#555;font-size:10px;margin-left:auto",
|
|
5431
|
+
"一条订阅 = 一个独立 mihomo 实例",
|
|
5432
|
+
) +
|
|
5433
|
+
renderApplyResult(ar),
|
|
5434
|
+
);
|
|
5435
|
+
|
|
5436
|
+
// tab 栏(即概览: 状态点+订阅名+端口;临期/超量红标、离线灰)+ 详情区(仅激活显示)
|
|
5437
|
+
// 选中态跨刷新保留(_wdSubsActiveTab),点击纯 DOM 切换不重渲染
|
|
5438
|
+
var activePort = _wdSubsActiveTab[wdEscId(userId)] || _SUBS_BASE_PROXY_PORT;
|
|
5439
|
+
var tabsBar = styled(
|
|
5440
|
+
"div",
|
|
5441
|
+
"display:flex;gap:2px;padding:0 8px;background:#16161e;border-bottom:1px solid #2a2a3a;overflow-x:auto;flex-wrap:wrap",
|
|
5442
|
+
(function () {
|
|
5443
|
+
var out = "";
|
|
5444
|
+
for (var m = 1; m <= count; m++) {
|
|
5445
|
+
out += renderSubsTab(m, configured[m - 1], byPort[_SUBS_BASE_PROXY_PORT + m - 1], userId,
|
|
5446
|
+
_SUBS_BASE_PROXY_PORT + m - 1 === activePort);
|
|
5447
|
+
}
|
|
5448
|
+
return out;
|
|
5449
|
+
})(),
|
|
5450
|
+
);
|
|
5451
|
+
var bodies = "";
|
|
5452
|
+
for (var m2 = 1; m2 <= count; m2++) {
|
|
5453
|
+
bodies += renderSubsBody(m2, configured[m2 - 1], byPort[_SUBS_BASE_PROXY_PORT + m2 - 1], userId,
|
|
5454
|
+
_SUBS_BASE_PROXY_PORT + m2 - 1 === activePort);
|
|
5455
|
+
}
|
|
5456
|
+
return styled(
|
|
5457
|
+
"div",
|
|
5458
|
+
"margin-top:6px;border:1px solid #2a2a3a;border-radius:8px;overflow:hidden",
|
|
5459
|
+
head + tabsBar + bodies,
|
|
5460
|
+
);
|
|
5461
|
+
}
|
|
5462
|
+
|
|
5463
|
+
/** 订阅选中 tab(userId → 代理端口),跨刷新保留 */
|
|
5464
|
+
var _wdSubsActiveTab = {};
|
|
5465
|
+
|
|
5466
|
+
/** tab 点击切换(纯 DOM 操作,不重渲染;与节点展开/切换命令同用代理端口路由) */
|
|
5467
|
+
function wdSubsTab(userId, port) {
|
|
5468
|
+
_wdSubsActiveTab[wdEscId(userId)] = port;
|
|
5469
|
+
for (var i = 0; i < _SUBS_MAX_INSTANCES; i++) {
|
|
5470
|
+
var p = _SUBS_BASE_PROXY_PORT + i;
|
|
5471
|
+
var tab = document.getElementById("wdSubsTab-" + wdEscId(userId) + "-" + p);
|
|
5472
|
+
var body = document.getElementById("wdSubsBody-" + wdEscId(userId) + "-" + p);
|
|
5473
|
+
if (!tab || !body) continue;
|
|
5474
|
+
var active = p === port;
|
|
5475
|
+
tab.style.background = active ? "#2a2a3a" : "transparent";
|
|
5476
|
+
tab.style.color = active ? "#fff" : "#94a3b8";
|
|
5477
|
+
tab.style.borderBottom = active ? "2px solid #60a5fa" : "2px solid transparent";
|
|
5478
|
+
body.style.display = active ? "block" : "none";
|
|
5479
|
+
}
|
|
5480
|
+
}
|
|
5481
|
+
|
|
5482
|
+
/** tab 按钮(即概览条目): 状态点 + 订阅名 + 端口;临期/超量红标、离线灰 */
|
|
5483
|
+
function renderSubsTab(n, conf, ch, userId, active) {
|
|
5484
|
+
var port = _SUBS_BASE_PROXY_PORT + n - 1;
|
|
5485
|
+
var meta = (ch && ch.meta) || {};
|
|
5486
|
+
var sub = ((meta.proxy || {}).subscriptions || [])[0] || null;
|
|
5487
|
+
var online = !!(ch && ch.node);
|
|
5488
|
+
var name = (sub && sub.name) || (conf && conf.name) || "实例" + n;
|
|
5489
|
+
// 临期/超量判定(与详情区同口径)
|
|
5490
|
+
var danger = false;
|
|
5491
|
+
if (sub) {
|
|
5492
|
+
var used = (sub.upload || 0) + (sub.download || 0);
|
|
5493
|
+
var total = sub.total || 0;
|
|
5494
|
+
var pct = total > 0 ? Math.round(((total - used) / total) * 100) : null;
|
|
5495
|
+
var daysLeft = sub.expire ? Math.round((sub.expire * 1000 - Date.now()) / 86400000) : null;
|
|
5496
|
+
danger = (pct !== null && pct < 10) || (daysLeft !== null && daysLeft < 7);
|
|
5497
|
+
}
|
|
5498
|
+
var dot = !online ? "○" : ch.healthy ? "●" : "●";
|
|
5499
|
+
var dotColor = !online ? "#555" : ch.healthy ? "#4ade80" : "#f87171";
|
|
5500
|
+
return tag(
|
|
5501
|
+
"button",
|
|
5502
|
+
{
|
|
5503
|
+
id: "wdSubsTab-" + wdEscId(userId) + "-" + port,
|
|
5504
|
+
onclick: "wdSubsTab('" + esc(userId) + "'," + port + ")",
|
|
5505
|
+
style:
|
|
5506
|
+
"padding:5px 10px;font-size:11px;cursor:pointer;white-space:nowrap;border:none;flex-shrink:0;" +
|
|
5507
|
+
"background:" + (active ? "#2a2a3a" : "transparent") + ";" +
|
|
5508
|
+
"color:" + (active ? "#fff" : "#94a3b8") + ";" +
|
|
5509
|
+
"border-bottom:2px solid " + (active ? "#60a5fa" : "transparent") + ";" +
|
|
5510
|
+
(online ? "" : "opacity:0.6;"),
|
|
5511
|
+
title: "实例" + n + " · 代理 " + port + " · API " + (_SUBS_BASE_API_PORT + n - 1) + (online ? (ch.healthy ? " · 健康" : " · 异常") : " · 未上线"),
|
|
5512
|
+
},
|
|
5513
|
+
styled("span", "color:" + dotColor, dot) +
|
|
5514
|
+
" " + name + styled("span", "color:#60a5fa;font-size:10px", " :" + port) +
|
|
5515
|
+
(danger ? styled("span", "color:#f87171;margin-left:2px", "⚠️") : ""),
|
|
5313
5516
|
);
|
|
5314
5517
|
}
|
|
5315
5518
|
|
|
5519
|
+
/** tab 详情区: 当前节点 + 端口/实例 + 用量/到期 + 速率 + 节点列表(可展开) */
|
|
5520
|
+
function renderSubsBody(n, conf, ch, userId, active) {
|
|
5521
|
+
var port = _SUBS_BASE_PROXY_PORT + n - 1;
|
|
5522
|
+
var apiPort = _SUBS_BASE_API_PORT + n - 1;
|
|
5523
|
+
var meta = (ch && ch.meta) || {};
|
|
5524
|
+
var proxy = meta.proxy || {};
|
|
5525
|
+
var sub = (proxy.subscriptions || [])[0] || null;
|
|
5526
|
+
var online = !!(ch && ch.node);
|
|
5527
|
+
var nodes = meta.healthyNodes || [];
|
|
5528
|
+
var delays = meta.healthyNodeDelays || {};
|
|
5529
|
+
|
|
5530
|
+
// 行1: 当前节点 + 健康状态 + 节点列表展开
|
|
5531
|
+
var line1;
|
|
5532
|
+
if (online) {
|
|
5533
|
+
var dotColor = ch.healthy ? "#4ade80" : "#f87171";
|
|
5534
|
+
var status = ch.healthy
|
|
5535
|
+
? (ch.timeTotal || 0).toFixed(1) + "s " + ((ch.sizeDownload || 0) / 1024).toFixed(0) + "KB"
|
|
5536
|
+
: ch.error || "异常";
|
|
5537
|
+
line1 = styled(
|
|
5538
|
+
"div",
|
|
5539
|
+
"display:flex;align-items:center;gap:8px;padding:8px 10px 4px;font-size:13px;flex-wrap:wrap",
|
|
5540
|
+
styled("span", "color:" + dotColor, "●") +
|
|
5541
|
+
styled("span", "color:#e0e0e0;font-weight:bold;word-break:break-all", ch.node) +
|
|
5542
|
+
styled("span", "color:#666;font-size:11px", "| " + status) +
|
|
5543
|
+
(ch.stale ? styled("span", "color:#fbbf24;font-size:10px", "数据过期" + ch.ageSeconds + "s") : "") +
|
|
5544
|
+
(nodes.length
|
|
5545
|
+
? tag(
|
|
5546
|
+
"button",
|
|
5547
|
+
{
|
|
5548
|
+
onclick: "wdToggleNodes('" + esc(userId) + "'," + port + ")",
|
|
5549
|
+
style:
|
|
5550
|
+
"margin-left:auto;padding:2px 8px;font-size:11px;background:#333;color:#60a5fa;" +
|
|
5551
|
+
"border:1px solid #444;border-radius:3px;cursor:pointer;flex-shrink:0",
|
|
5552
|
+
title: "查看/切换该实例的可用节点",
|
|
5553
|
+
},
|
|
5554
|
+
nodes.length + " 个节点 ▸",
|
|
5555
|
+
)
|
|
5556
|
+
: ""),
|
|
5557
|
+
);
|
|
5558
|
+
} else {
|
|
5559
|
+
line1 = styled(
|
|
5560
|
+
"div",
|
|
5561
|
+
"display:flex;align-items:center;gap:8px;padding:8px 10px 4px;font-size:12px",
|
|
5562
|
+
styled("span", "color:#555", "○") +
|
|
5563
|
+
styled("span", "color:#888", "实例未运行(等待自愈拉起,配置保留)"),
|
|
5564
|
+
);
|
|
5565
|
+
}
|
|
5566
|
+
|
|
5567
|
+
// 行2: 实例/端口(业务 --proxy 用代理端口)
|
|
5568
|
+
var line2 = styled(
|
|
5569
|
+
"div",
|
|
5570
|
+
"padding:2px 10px;font-size:11px;color:#888;display:flex;gap:10px;flex-wrap:wrap",
|
|
5571
|
+
styled("span", null, "实例 #" + n) +
|
|
5572
|
+
tag(
|
|
5573
|
+
"span",
|
|
5574
|
+
{
|
|
5575
|
+
style: "color:#60a5fa;font-family:monospace",
|
|
5576
|
+
title: "业务命令 --proxy 用此端口",
|
|
5577
|
+
},
|
|
5578
|
+
"代理 127.0.0.1:" + port,
|
|
5579
|
+
) +
|
|
5580
|
+
styled("span", "color:#555;font-family:monospace", "API " + apiPort) +
|
|
5581
|
+
(proxy.runtime && proxy.runtime.status ? styled("span", "color:#555", "内核" + (proxy.runtime.status || "")) : ""),
|
|
5582
|
+
);
|
|
5583
|
+
|
|
5584
|
+
// 行3: 用量/到期 + 行4: 速率
|
|
5585
|
+
var line34 = "";
|
|
5586
|
+
if (sub) {
|
|
5587
|
+
var used = (sub.upload || 0) + (sub.download || 0);
|
|
5588
|
+
var total = sub.total || 0;
|
|
5589
|
+
var remain = total > 0 ? total - used : 0;
|
|
5590
|
+
var pct = total > 0 ? Math.round((remain / total) * 100) : null;
|
|
5591
|
+
var daysLeft = sub.expire ? Math.round((sub.expire * 1000 - Date.now()) / 86400000) : null;
|
|
5592
|
+
var danger = (pct !== null && pct < 10) || (daysLeft !== null && daysLeft < 7);
|
|
5593
|
+
var parts = [];
|
|
5594
|
+
if (sub.proxies) parts.push(sub.proxies + " 节点");
|
|
5595
|
+
parts.push(total > 0 ? "剩 " + fmtGB(remain) + (pct !== null ? " (" + pct + "%)" : "") : "用量未知");
|
|
5596
|
+
if (sub.expire) {
|
|
5597
|
+
parts.push("到 " + new Date(sub.expire * 1000).toLocaleDateString() + (daysLeft !== null ? "(剩" + daysLeft + "天)" : ""));
|
|
5598
|
+
}
|
|
5599
|
+
line34 = styled(
|
|
5600
|
+
"div",
|
|
5601
|
+
"padding:2px 10px 4px;font-size:11px" + (danger ? ";background:#3b1d1d" : ""),
|
|
5602
|
+
styled("span", "color:" + (danger ? "#f87171" : "#94a3b8"), "📡 " + parts.join(" · ") + (danger ? " ⚠️" : "")),
|
|
5603
|
+
);
|
|
5604
|
+
}
|
|
5605
|
+
var t = proxy.traffic;
|
|
5606
|
+
if (t && (t.downRate > 0 || t.upRate > 0)) {
|
|
5607
|
+
line34 += styled(
|
|
5608
|
+
"div",
|
|
5609
|
+
"padding:2px 10px 6px;font-size:11px;color:#666",
|
|
5610
|
+
"⚡ ↓" + fmtSpeed(t.downRate) + " / ↑" + fmtSpeed(t.upRate),
|
|
5611
|
+
);
|
|
5612
|
+
}
|
|
5613
|
+
|
|
5614
|
+
var bodyInner = line1 + line2 + line34 +
|
|
5615
|
+
(online && nodes.length ? renderNodeList(nodes, ch.node, userId, port, delays) : "");
|
|
5616
|
+
return tag(
|
|
5617
|
+
"div",
|
|
5618
|
+
{
|
|
5619
|
+
id: "wdSubsBody-" + wdEscId(userId) + "-" + port,
|
|
5620
|
+
style: "display:" + (active ? "block" : "none") + ";border-top:1px solid #22222f",
|
|
5621
|
+
},
|
|
5622
|
+
bodyInner,
|
|
5623
|
+
);
|
|
5624
|
+
}
|
|
5625
|
+
|
|
5626
|
+
|
|
5316
5627
|
function fmtGB(bytes) {
|
|
5317
5628
|
if (!bytes || bytes <= 0) return "0";
|
|
5318
5629
|
var gb = bytes / 1073741824;
|
|
@@ -5328,31 +5639,14 @@ function fmtSpeed(bytesPerSec) {
|
|
|
5328
5639
|
/**
|
|
5329
5640
|
* 订阅用量/到期 + 实时速率 + 订阅配置入口(数据: clashHealth.meta.proxy)
|
|
5330
5641
|
* 临期(<7天)或超量(剩余<10%)标红
|
|
5642
|
+
* @param {boolean} [withConfig] 是否渲染配置入口按钮(默认 true;多实例总览区已收敛唯一入口时传 false)
|
|
5331
5643
|
*/
|
|
5332
|
-
function renderSubscriptions(proxy, userId) {
|
|
5644
|
+
function renderSubscriptions(proxy, userId, withConfig) {
|
|
5333
5645
|
proxy = proxy || {};
|
|
5334
5646
|
var subs = proxy.subscriptions || [];
|
|
5335
5647
|
var rows = "";
|
|
5336
5648
|
// 订阅应用结果(daemon 最近一次 setSubscription 执行反馈;随下次应用/daemon 重启刷新)
|
|
5337
|
-
|
|
5338
|
-
if (ar && ar.at) {
|
|
5339
|
-
if (ar.ok === false) {
|
|
5340
|
-
rows +=
|
|
5341
|
-
styled(
|
|
5342
|
-
"div",
|
|
5343
|
-
"font-size:11px;margin:2px 0 4px;padding:4px 6px;background:#3b1d1d;border:1px solid #7f1d1d;border-radius:4px;color:#f87171;line-height:1.4",
|
|
5344
|
-
"❌ 订阅应用失败: " + esc(ar.error || "未知错误") +
|
|
5345
|
-
styled("span", "color:#94a3b8;margin-left:6px;font-size:10px", new Date(ar.at).toLocaleTimeString()),
|
|
5346
|
-
);
|
|
5347
|
-
} else if (ar.changed) {
|
|
5348
|
-
rows +=
|
|
5349
|
-
styled(
|
|
5350
|
-
"div",
|
|
5351
|
-
"font-size:11px;margin:2px 0 4px;color:#4ade80",
|
|
5352
|
-
"✓ 订阅应用成功 (" + (ar.count || "?") + " 条," + new Date(ar.at).toLocaleTimeString() + ")",
|
|
5353
|
-
);
|
|
5354
|
-
}
|
|
5355
|
-
}
|
|
5649
|
+
rows += renderApplyResult(proxy.applyResult);
|
|
5356
5650
|
// 实时速率
|
|
5357
5651
|
var t = proxy.traffic;
|
|
5358
5652
|
if (t && (t.downRate > 0 || t.upRate > 0)) {
|
|
@@ -5389,7 +5683,8 @@ function renderSubscriptions(proxy, userId) {
|
|
|
5389
5683
|
);
|
|
5390
5684
|
}
|
|
5391
5685
|
// 配置入口(摘要双来源: 后台配置优先,回退 daemon 实际生效快照;显示条数+订阅名)
|
|
5392
|
-
|
|
5686
|
+
if (withConfig !== false) {
|
|
5687
|
+
var configured = (window._wdData && _wdData.proxySettings && _wdData.proxySettings[userId] && _wdData.proxySettings[userId].subscriptions) || proxy.subscriptions || [];
|
|
5393
5688
|
var cfgNames = configured
|
|
5394
5689
|
.map(function (s) {
|
|
5395
5690
|
return s.name || String(s.url || "").replace(/^https?:\/\//, "").split("/")[0];
|
|
@@ -5416,6 +5711,7 @@ function renderSubscriptions(proxy, userId) {
|
|
|
5416
5711
|
"配置 ⚙️",
|
|
5417
5712
|
),
|
|
5418
5713
|
);
|
|
5714
|
+
}
|
|
5419
5715
|
return rows;
|
|
5420
5716
|
}
|
|
5421
5717
|
|
|
@@ -786,7 +786,7 @@
|
|
|
786
786
|
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">
|
|
787
787
|
<div style="background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:24px;width:90%;max-width:560px">
|
|
788
788
|
<div style="font-size:16px;color:#fbbf24;margin-bottom:4px">🛜 代理订阅配置 — <span id="wdProxyUserId"></span></div>
|
|
789
|
-
<div style="color:#888;font-size:12px;margin-bottom:12px">每行一个订阅地址(http/https),<b style="color:#fbbf24"
|
|
789
|
+
<div style="color:#888;font-size:12px;margin-bottom:12px">每行一个订阅地址(http/https),<b style="color:#fbbf24">一行 = 一个独立 mihomo 实例</b>:按行序分配实例 1/2/3…,代理端口依次 127.0.0.1:7897、7898、7899…(API 管理端口 9097 起,同步 +1),业务命令用 --proxy 指定使用哪个端口(客户端卡片“代理订阅”总览表可查映射)。<br>保存后客户端下次心跳时自动热应用(约 15~30 秒);订阅名按行序自动编号 sub-1、sub-2…,<b style="color:#f87171">在中间插入/删除行会使后续实例重新编号并重载配置</b>,建议只在末尾追加。临期(<7天)或超量(剩余<10%)在总览表中标红。</div>
|
|
790
790
|
<textarea id="wdProxySubsText" rows="6" placeholder="https://example.com/api/v1/client/subscribe?token=xxxx\nhttps://another-airport.com/sub?token=yyyy"
|
|
791
791
|
style="width:100%;box-sizing:border-box;padding:10px;background:#16161e;border:1px solid #333;border-radius:8px;color:#e0e0e0;font-size:12px;font-family:monospace;resize:vertical"></textarea>
|
|
792
792
|
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px">
|
package/src/watchdog/env-init.js
CHANGED
|
@@ -146,11 +146,24 @@ async function killTree(pid) {
|
|
|
146
146
|
*/
|
|
147
147
|
export async function takeoverEnvironment({ spawnDaemon, selfPid, log = () => {} } = {}) {
|
|
148
148
|
const procs = listProcesses();
|
|
149
|
+
// 自身祖先链一律跳过: 启动包装层(schtasks 的 cmd /c node cli.js watchdog…、
|
|
150
|
+
// Start-Process 的 powershell 等)命令行同样命中 agent 签名,若误判为"旧 agent"
|
|
151
|
+
// 会 taskkill /T 树杀连坐树内的自己(214 实采踩坑:watchdog 启动后即自杀)。
|
|
152
|
+
// 祖先链只可能是本次启动的包装层,绝不可能是需要接管的"旧实例"。
|
|
153
|
+
const byPid = new Map(procs.map((p) => [p.pid, p]));
|
|
154
|
+
const ancestors = new Set();
|
|
155
|
+
let cur = byPid.get(selfPid)?.ppid;
|
|
156
|
+
while (cur && cur > 1 && !ancestors.has(cur) && ancestors.size < 32) {
|
|
157
|
+
ancestors.add(cur);
|
|
158
|
+
cur = byPid.get(cur)?.ppid;
|
|
159
|
+
}
|
|
160
|
+
|
|
149
161
|
const agents = [];
|
|
150
162
|
const daemons = [];
|
|
151
163
|
|
|
152
164
|
for (const p of procs) {
|
|
153
165
|
if (p.pid === selfPid) continue;
|
|
166
|
+
if (ancestors.has(p.pid)) continue; // 祖先链跳过(防树杀自杀)
|
|
154
167
|
if (isAgentCmd(p.cmd)) agents.push(p);
|
|
155
168
|
else if (spawnDaemon && isDaemonCmd(p.cmd)) daemons.push(p);
|
|
156
169
|
}
|