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/src/cli/comments.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "../lib/target-locations.js";
|
|
23
23
|
import { getTargetLocations } from "../lib/app-config.js";
|
|
24
24
|
import { createApiClient } from "../lib/api-client.js";
|
|
25
|
+
import { syncTargetLocationsFromServer } from "../lib/client-locations.js";
|
|
25
26
|
import { ensureBrowserReady, killEdgeProcesses } from "../lib/browser/cdp.js";
|
|
26
27
|
import { delay } from "../lib/delay.js";
|
|
27
28
|
import {
|
|
@@ -248,6 +249,9 @@ async function runAutoMode(options) {
|
|
|
248
249
|
locations,
|
|
249
250
|
} = options;
|
|
250
251
|
|
|
252
|
+
// 目标国家以服务端配置为准(单一事实来源;拉取失败沿用本地列表)
|
|
253
|
+
await syncTargetLocationsFromServer(serverUrl);
|
|
254
|
+
|
|
251
255
|
const actualParallel = Math.max(1, parallel || CFG.parallel);
|
|
252
256
|
const actualInterval = interval || CFG.interval;
|
|
253
257
|
const actualMaxComments = maxComments || CFG.maxComments;
|
|
@@ -434,6 +438,9 @@ async function runAutoMode(options) {
|
|
|
434
438
|
// ════════════════════════════════════════
|
|
435
439
|
try {
|
|
436
440
|
while (!shuttingDown) {
|
|
441
|
+
// 周期性同步服务端目标国家(内部节流,几乎零开销)
|
|
442
|
+
await syncTargetLocationsFromServer(serverUrl);
|
|
443
|
+
|
|
437
444
|
// 浏览器窗口被手动关闭(CDP 断开)→ 自动退出
|
|
438
445
|
if (isBrowserDisconnected(browser)) {
|
|
439
446
|
console.error("🚪 检测到浏览器窗口已关闭,自动退出");
|
package/src/cli/explore.js
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
proxy as configuredProxy,
|
|
20
20
|
} from "../lib/constants.js";
|
|
21
21
|
import { getMacOrUuid } from "../lib/mac-or-uuid.js";
|
|
22
|
+
import { syncTargetLocationsFromServer } from "../lib/client-locations.js";
|
|
22
23
|
import {
|
|
23
24
|
ensureBrowserReady as ensureBrowserReadyCDP,
|
|
24
25
|
killEdgeProcesses,
|
|
@@ -62,7 +63,6 @@ export async function handleExplore(options) {
|
|
|
62
63
|
exploreEnableFollow,
|
|
63
64
|
exploreMaxFollowing,
|
|
64
65
|
exploreMaxFollowers,
|
|
65
|
-
exploreLocation,
|
|
66
66
|
exploreJobLocations,
|
|
67
67
|
exploreMaxUsers,
|
|
68
68
|
serverUrl,
|
|
@@ -127,6 +127,12 @@ export async function handleExplore(options) {
|
|
|
127
127
|
);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
// 未显式传 --location 时,以服务端目标国家为准(周期同步,失败回退本地)
|
|
131
|
+
let exploreLocation = options.exploreLocation;
|
|
132
|
+
if (!options.locationExplicit) {
|
|
133
|
+
const synced = await syncTargetLocationsFromServer(serverUrl);
|
|
134
|
+
if (synced) exploreLocation = synced.join(",");
|
|
135
|
+
}
|
|
130
136
|
console.error(`\n国家筛选: ${exploreLocation}`);
|
|
131
137
|
if (exploreJobLocations) console.error(`任务国家: ${exploreJobLocations}`);
|
|
132
138
|
console.error(`视频采集: ${exploreMaxVideos || 1}`);
|
|
@@ -401,6 +407,11 @@ export async function handleExplore(options) {
|
|
|
401
407
|
|
|
402
408
|
while (!shuttingDown) {
|
|
403
409
|
try {
|
|
410
|
+
// 周期性同步服务端目标国家(内部节流,几乎零开销)
|
|
411
|
+
if (!options.locationExplicit) {
|
|
412
|
+
const synced = await syncTargetLocationsFromServer(serverUrl);
|
|
413
|
+
if (synced) exploreLocation = synced.join(",");
|
|
414
|
+
}
|
|
404
415
|
const jobQuery = exploreJobLocations
|
|
405
416
|
? `${serverUrl}/api/job?userId=${encodeURIComponent(userId)}&locations=${encodeURIComponent(exploreJobLocations)}&loggedIn=${loggedIn}`
|
|
406
417
|
: `${serverUrl}/api/job?userId=${encodeURIComponent(userId)}&loggedIn=${loggedIn}`;
|
package/src/cli/refresh-mini.js
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
saveUserId,
|
|
33
33
|
} from "../lib/constants.js";
|
|
34
34
|
import { getMacOrUuid } from "../lib/mac-or-uuid.js";
|
|
35
|
+
import { syncTargetLocationsFromServer } from "../lib/client-locations.js";
|
|
35
36
|
import {
|
|
36
37
|
ensureBrowserReady as ensureBrowserReadyCDP,
|
|
37
38
|
killEdgeProcesses,
|
|
@@ -138,7 +139,6 @@ export async function handleRefreshMini(options) {
|
|
|
138
139
|
const {
|
|
139
140
|
explorePreset,
|
|
140
141
|
exploreInterval,
|
|
141
|
-
exploreLocation,
|
|
142
142
|
exploreJobLocations,
|
|
143
143
|
exploreMaxUsers,
|
|
144
144
|
explorePort,
|
|
@@ -210,6 +210,12 @@ export async function handleRefreshMini(options) {
|
|
|
210
210
|
console.error(`CDP 端口: ${explorePort || DEFAULT_PORT}(固定)`);
|
|
211
211
|
console.error(`采集范围: mini(仅视频及视频相关信息)`);
|
|
212
212
|
console.error(`视频采集: ${exploreMaxVideos || 16}`);
|
|
213
|
+
// 未显式传 --location 时,以服务端目标国家为准(周期同步,失败回退本地)
|
|
214
|
+
let exploreLocation = options.exploreLocation;
|
|
215
|
+
if (!options.locationExplicit) {
|
|
216
|
+
const synced = await syncTargetLocationsFromServer(serverUrl);
|
|
217
|
+
if (synced) exploreLocation = synced.join(",");
|
|
218
|
+
}
|
|
213
219
|
console.error(`国家筛选: ${exploreLocation}`);
|
|
214
220
|
if (exploreJobLocations) console.error(`任务国家: ${exploreJobLocations}`);
|
|
215
221
|
console.error(`空闲间隔: ${exploreInterval || 30} 秒`);
|
|
@@ -468,6 +474,12 @@ export async function handleRefreshMini(options) {
|
|
|
468
474
|
while (true) {
|
|
469
475
|
if (shuttingDown) break;
|
|
470
476
|
|
|
477
|
+
// 周期性同步服务端目标国家(内部节流,几乎零开销)
|
|
478
|
+
if (!options.locationExplicit) {
|
|
479
|
+
const synced = await syncTargetLocationsFromServer(serverUrl);
|
|
480
|
+
if (synced) exploreLocation = synced.join(",");
|
|
481
|
+
}
|
|
482
|
+
|
|
471
483
|
// 定时轮换(任务间隙,预防性换 IP,等回执后再继续)
|
|
472
484
|
if (rotateMin > 0) {
|
|
473
485
|
const elapsedMin = (Date.now() - lastProxyTriggerTime) / 60000;
|
package/src/cli/refresh.js
CHANGED
|
@@ -28,6 +28,7 @@ import { createApiClient } from "../lib/api-client.js";
|
|
|
28
28
|
import { createCDNHandler } from "../lib/cdn-handler.js";
|
|
29
29
|
import { proxyPortOf, triggerProxySwitch } from "../clash/proxy-switch.js";
|
|
30
30
|
import path from "path";
|
|
31
|
+
import { syncTargetLocationsFromServer } from "../lib/client-locations.js";
|
|
31
32
|
import os from "os";
|
|
32
33
|
|
|
33
34
|
const MAX_RETRY_WAIT = 5 * 60 * 1000;
|
|
@@ -57,7 +58,6 @@ export async function handleRefresh(options) {
|
|
|
57
58
|
exploreEnableFollow,
|
|
58
59
|
exploreMaxFollowing,
|
|
59
60
|
exploreMaxFollowers,
|
|
60
|
-
exploreLocation,
|
|
61
61
|
exploreJobLocations,
|
|
62
62
|
exploreMaxUsers,
|
|
63
63
|
serverUrl,
|
|
@@ -142,6 +142,12 @@ export async function handleRefresh(options) {
|
|
|
142
142
|
console.error(
|
|
143
143
|
`关注/粉丝: ${followEnabled ? "启用" : "禁用"} (${exploreMaxFollowing}/${exploreMaxFollowers})`,
|
|
144
144
|
);
|
|
145
|
+
// 未显式传 --location 时,以服务端目标国家为准(周期同步,失败回退本地)
|
|
146
|
+
let exploreLocation = options.exploreLocation;
|
|
147
|
+
if (!options.locationExplicit) {
|
|
148
|
+
const synced = await syncTargetLocationsFromServer(serverUrl);
|
|
149
|
+
if (synced) exploreLocation = synced.join(",");
|
|
150
|
+
}
|
|
145
151
|
console.error(`国家筛选: ${exploreLocation}`);
|
|
146
152
|
if (exploreJobLocations) console.error(`任务国家: ${exploreJobLocations}`);
|
|
147
153
|
console.error(`空闲间隔: ${exploreInterval || 30} 秒`);
|
|
@@ -379,6 +385,12 @@ export async function handleRefresh(options) {
|
|
|
379
385
|
while (true) {
|
|
380
386
|
if (shuttingDown) break;
|
|
381
387
|
|
|
388
|
+
// 周期性同步服务端目标国家(内部节流,几乎零开销)
|
|
389
|
+
if (!options.locationExplicit) {
|
|
390
|
+
const synced = await syncTargetLocationsFromServer(serverUrl);
|
|
391
|
+
if (synced) exploreLocation = synced.join(",");
|
|
392
|
+
}
|
|
393
|
+
|
|
382
394
|
// 健康检查
|
|
383
395
|
const checkResult = healthChecker.check();
|
|
384
396
|
if (checkResult.shouldSwitch) {
|
package/src/lib/app-config.js
CHANGED
|
@@ -20,6 +20,29 @@ const CONFIG_FILE = join(homedir(), ".tt-help-config.json");
|
|
|
20
20
|
|
|
21
21
|
let _cache = null;
|
|
22
22
|
|
|
23
|
+
// 服务端同步的运行时目标国家覆盖(优先于本地文件与内置默认,不落盘)
|
|
24
|
+
let _runtimeTargetLocations = null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 设置运行时目标国家(通常来自 Watch 服务端同步)。
|
|
28
|
+
* 传空/非法值 = 清除覆盖,回退本地配置。
|
|
29
|
+
* 返回生效后的列表。
|
|
30
|
+
*/
|
|
31
|
+
export function setRuntimeTargetLocations(locations) {
|
|
32
|
+
const normalized = Array.isArray(locations)
|
|
33
|
+
? [
|
|
34
|
+
...new Set(
|
|
35
|
+
locations
|
|
36
|
+
.map((l) => String(l).trim().toUpperCase())
|
|
37
|
+
.filter(Boolean),
|
|
38
|
+
),
|
|
39
|
+
]
|
|
40
|
+
: [];
|
|
41
|
+
_runtimeTargetLocations =
|
|
42
|
+
normalized.length > 0 ? [...normalized] : null;
|
|
43
|
+
return getTargetLocations();
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
export function getConfigFilePath() {
|
|
24
47
|
return CONFIG_FILE;
|
|
25
48
|
}
|
|
@@ -94,8 +117,9 @@ function normalizeLocations(locations) {
|
|
|
94
117
|
];
|
|
95
118
|
}
|
|
96
119
|
|
|
97
|
-
/**
|
|
120
|
+
/** 读取当前目标国家:服务端同步覆盖 > 配置文件 > 内置默认 */
|
|
98
121
|
export function getTargetLocations() {
|
|
122
|
+
if (_runtimeTargetLocations) return [..._runtimeTargetLocations];
|
|
99
123
|
const raw = getConfig("targetLocations", []);
|
|
100
124
|
const normalized = normalizeLocations(raw);
|
|
101
125
|
return normalized.length > 0 ? normalized : [...DEFAULT_TARGET_LOCATIONS];
|
package/src/lib/args.js
CHANGED
|
@@ -175,6 +175,7 @@ function parseExploreArgs(args) {
|
|
|
175
175
|
let exploreMaxFollowing = 50;
|
|
176
176
|
let exploreMaxFollowers = 50;
|
|
177
177
|
let exploreLocation = DEFAULT_TARGET_LOCATIONS_CSV;
|
|
178
|
+
let locationExplicit = false; // 是否显式传了 --location(显式时跳过服务端同步)
|
|
178
179
|
let exploreJobLocations = null;
|
|
179
180
|
let exploreMaxUsers = 0;
|
|
180
181
|
let explorePort = null;
|
|
@@ -204,6 +205,7 @@ function parseExploreArgs(args) {
|
|
|
204
205
|
exploreMaxGuess = parseInt(args[++i]) || 0;
|
|
205
206
|
} else if (arg === "--location") {
|
|
206
207
|
exploreLocation = args[++i];
|
|
208
|
+
locationExplicit = true;
|
|
207
209
|
} else if (arg === "--job-locations") {
|
|
208
210
|
exploreJobLocations = args[++i];
|
|
209
211
|
} else if (arg === "--enable-follow") {
|
|
@@ -275,6 +277,7 @@ function parseExploreArgs(args) {
|
|
|
275
277
|
exploreMaxFollowing,
|
|
276
278
|
exploreMaxFollowers,
|
|
277
279
|
exploreLocation,
|
|
280
|
+
locationExplicit,
|
|
278
281
|
exploreJobLocations,
|
|
279
282
|
serverUrl,
|
|
280
283
|
exploreMaxUsers,
|
|
@@ -543,6 +546,7 @@ function parseRefreshArgs(args) {
|
|
|
543
546
|
let exploreMaxFollowing = 100;
|
|
544
547
|
let exploreMaxFollowers = 100;
|
|
545
548
|
let exploreLocation = DEFAULT_TARGET_LOCATIONS_CSV;
|
|
549
|
+
let locationExplicit = false; // 是否显式传了 --location(显式时跳过服务端同步)
|
|
546
550
|
let exploreMaxUsers = 0;
|
|
547
551
|
let explorePort = null;
|
|
548
552
|
let exploreBasePort = null;
|
|
@@ -599,6 +603,7 @@ function parseRefreshArgs(args) {
|
|
|
599
603
|
exploreNoProduct = true;
|
|
600
604
|
} else if (arg === "--location") {
|
|
601
605
|
exploreLocation = args[++i];
|
|
606
|
+
locationExplicit = true;
|
|
602
607
|
} else if (arg === "--job-locations") {
|
|
603
608
|
exploreJobLocations = args[++i];
|
|
604
609
|
} else if (arg === "--enable-follow") {
|
|
@@ -620,6 +625,7 @@ function parseRefreshArgs(args) {
|
|
|
620
625
|
exploreMaxFollowing,
|
|
621
626
|
exploreMaxFollowers,
|
|
622
627
|
exploreLocation,
|
|
628
|
+
locationExplicit,
|
|
623
629
|
exploreJobLocations,
|
|
624
630
|
exploreMaxUsers,
|
|
625
631
|
explorePort,
|
|
@@ -652,6 +658,7 @@ function parseRefreshMiniArgs(args) {
|
|
|
652
658
|
let explorePreset = "normal";
|
|
653
659
|
let exploreInterval = 30;
|
|
654
660
|
let exploreLocation = DEFAULT_TARGET_LOCATIONS_CSV;
|
|
661
|
+
let locationExplicit = false; // 是否显式传了 --location(显式时跳过服务端同步)
|
|
655
662
|
let exploreMaxUsers = 0;
|
|
656
663
|
let explorePort = 4222;
|
|
657
664
|
let exploreUserId = null;
|
|
@@ -689,6 +696,7 @@ function parseRefreshMiniArgs(args) {
|
|
|
689
696
|
exploreProxy = args[++i];
|
|
690
697
|
} else if (arg === "--location") {
|
|
691
698
|
exploreLocation = args[++i];
|
|
699
|
+
locationExplicit = true;
|
|
692
700
|
} else if (arg === "--job-locations") {
|
|
693
701
|
exploreJobLocations = args[++i];
|
|
694
702
|
} else if (arg === "--max-users") {
|
|
@@ -709,6 +717,7 @@ function parseRefreshMiniArgs(args) {
|
|
|
709
717
|
exploreCommentDelay,
|
|
710
718
|
exploreInterval,
|
|
711
719
|
exploreLocation,
|
|
720
|
+
locationExplicit,
|
|
712
721
|
exploreJobLocations,
|
|
713
722
|
exploreMaxUsers,
|
|
714
723
|
explorePort,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 客户端目标国家同步:以 Watch 服务端 /api/settings/target-locations 为单一事实来源。
|
|
3
|
+
*
|
|
4
|
+
* 设计:
|
|
5
|
+
* - 采集客户端(explore / refresh / refresh-mini / comments)启动时及任务循环中调用,
|
|
6
|
+
* 成功后覆盖运行时 getTargetLocations()(含 locationRank 等派生逻辑,不写本地盘)
|
|
7
|
+
* - 内部节流:成功后 SYNC_INTERVAL_MS 内直接返回缓存,循环里每轮调用也几乎零开销
|
|
8
|
+
* - 失败回退:拉取失败时沿用本地配置/内置默认,不中断采集
|
|
9
|
+
* - 显式传 --location 的客户端不调用本模块,用户参数始终优先
|
|
10
|
+
*/
|
|
11
|
+
import { setRuntimeTargetLocations } from "./app-config.js";
|
|
12
|
+
|
|
13
|
+
const SYNC_INTERVAL_MS = 10 * 60 * 1000; // 同步成功后 10 分钟内复用缓存
|
|
14
|
+
const FETCH_TIMEOUT_MS = 8000;
|
|
15
|
+
|
|
16
|
+
let _lastSyncAt = 0;
|
|
17
|
+
let _lastList = null; // 最近一次同步成功的列表
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 从服务端同步目标国家(节流)。
|
|
21
|
+
* @param {string} serverUrl Watch 服务端地址
|
|
22
|
+
* @param {{force?: boolean}} options force=true 跳过节流
|
|
23
|
+
* @returns {Promise<string[]|null>} 成功返回国家数组;失败返回 null(调用方沿用本地列表)
|
|
24
|
+
*/
|
|
25
|
+
export async function syncTargetLocationsFromServer(
|
|
26
|
+
serverUrl,
|
|
27
|
+
{ force = false } = {},
|
|
28
|
+
) {
|
|
29
|
+
if (!serverUrl) return _lastList;
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
if (
|
|
32
|
+
!force &&
|
|
33
|
+
_lastList &&
|
|
34
|
+
now - _lastSyncAt < SYNC_INTERVAL_MS
|
|
35
|
+
) {
|
|
36
|
+
return _lastList;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const base = String(serverUrl).replace(/\/+$/, "");
|
|
40
|
+
const res = await fetch(`${base}/api/settings/target-locations`, {
|
|
41
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
const list = Array.isArray(data.locations) ? data.locations : [];
|
|
46
|
+
const normalized = [
|
|
47
|
+
...new Set(
|
|
48
|
+
list.map((l) => String(l).trim().toUpperCase()).filter(Boolean),
|
|
49
|
+
),
|
|
50
|
+
];
|
|
51
|
+
if (normalized.length === 0) throw new Error("服务端返回空列表");
|
|
52
|
+
|
|
53
|
+
const changed =
|
|
54
|
+
!_lastList || normalized.join(",") !== _lastList.join(",");
|
|
55
|
+
setRuntimeTargetLocations(normalized);
|
|
56
|
+
_lastSyncAt = now;
|
|
57
|
+
_lastList = [...normalized];
|
|
58
|
+
if (changed) {
|
|
59
|
+
console.error(
|
|
60
|
+
`[locations] 已同步服务端目标国家(${normalized.length}): ${normalized.join(",")}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return _lastList;
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error(
|
|
66
|
+
`[locations] 同步服务端目标国家失败(${e.message}),沿用本地列表`,
|
|
67
|
+
);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/watch/public/app.js
CHANGED
|
@@ -4749,6 +4749,59 @@ async function wdSetCountries() {
|
|
|
4749
4749
|
}
|
|
4750
4750
|
}
|
|
4751
4751
|
|
|
4752
|
+
// ── 代理订阅配置弹窗(保存后心跳下发 → daemon 热应用) ──
|
|
4753
|
+
|
|
4754
|
+
var _wdProxyUserId = "";
|
|
4755
|
+
|
|
4756
|
+
function wdShowProxyModal(userId) {
|
|
4757
|
+
_wdProxyUserId = userId;
|
|
4758
|
+
document.getElementById("wdProxyUserId").textContent = userId;
|
|
4759
|
+
var ta = document.getElementById("wdProxySubsText");
|
|
4760
|
+
var configured = [];
|
|
4761
|
+
if (_wdData && _wdData.proxySettings && _wdData.proxySettings[userId]) {
|
|
4762
|
+
configured = (_wdData.proxySettings[userId].subscriptions || []).map(function (s) {
|
|
4763
|
+
return typeof s === "string" ? s : s.url;
|
|
4764
|
+
});
|
|
4765
|
+
}
|
|
4766
|
+
ta.value = configured.join("\n");
|
|
4767
|
+
document.getElementById("wdProxyModal").style.display = "flex";
|
|
4768
|
+
}
|
|
4769
|
+
|
|
4770
|
+
function wdCloseProxyModal() {
|
|
4771
|
+
document.getElementById("wdProxyModal").style.display = "none";
|
|
4772
|
+
}
|
|
4773
|
+
|
|
4774
|
+
async function wdSaveProxySettings() {
|
|
4775
|
+
var userId = _wdProxyUserId;
|
|
4776
|
+
var lines = (document.getElementById("wdProxySubsText").value || "")
|
|
4777
|
+
.split("\n")
|
|
4778
|
+
.map(function (s) {
|
|
4779
|
+
return s.trim();
|
|
4780
|
+
})
|
|
4781
|
+
.filter(Boolean);
|
|
4782
|
+
try {
|
|
4783
|
+
var res = await fetch("/api/watchdog/set-proxy-settings", {
|
|
4784
|
+
method: "POST",
|
|
4785
|
+
headers: { "Content-Type": "application/json" },
|
|
4786
|
+
body: JSON.stringify({ userId: userId, subscriptions: lines }),
|
|
4787
|
+
});
|
|
4788
|
+
var d = await res.json();
|
|
4789
|
+
if (!res.ok || !d.ok) throw new Error(d.error || "HTTP " + res.status);
|
|
4790
|
+
if (_wdData) {
|
|
4791
|
+
_wdData.proxySettings = _wdData.proxySettings || {};
|
|
4792
|
+
_wdData.proxySettings[userId] = { subscriptions: d.subscriptions || [] };
|
|
4793
|
+
}
|
|
4794
|
+
wdCloseProxyModal();
|
|
4795
|
+
alert(
|
|
4796
|
+
"已保存 " +
|
|
4797
|
+
(d.subscriptions ? d.subscriptions.length : lines.length) +
|
|
4798
|
+
" 条订阅\n\n客户端将在下次心跳时热应用(无需重启,约 15~30 秒后刷新页面可见订阅用量)。",
|
|
4799
|
+
);
|
|
4800
|
+
} catch (e) {
|
|
4801
|
+
alert("保存失败: " + e.message);
|
|
4802
|
+
}
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4752
4805
|
// ── 登录检测端口配置区域(看门狗面板中每个客户端卡片) ──
|
|
4753
4806
|
|
|
4754
4807
|
// userId -> 详情展开状态(跨刷新保留)
|
|
@@ -5242,7 +5295,8 @@ function renderProxySection(c, userId) {
|
|
|
5242
5295
|
var hasNodes = nodes.length > 0;
|
|
5243
5296
|
return (
|
|
5244
5297
|
renderProxyLine(h, nodes, userId, 0, hasNodes) +
|
|
5245
|
-
renderNodeList(nodes, h.node, userId, 0, delays)
|
|
5298
|
+
renderNodeList(nodes, h.node, userId, 0, delays) +
|
|
5299
|
+
renderSubscriptions(h.meta && h.meta.proxy, userId)
|
|
5246
5300
|
);
|
|
5247
5301
|
}
|
|
5248
5302
|
|
|
@@ -5254,10 +5308,117 @@ function renderProxyChannel(ch, userId) {
|
|
|
5254
5308
|
var hasNodes = nodes.length > 0;
|
|
5255
5309
|
return (
|
|
5256
5310
|
renderProxyLine(ch, nodes, userId, ch.port || 0, hasNodes) +
|
|
5257
|
-
renderNodeList(nodes, ch.node, userId, ch.port || 0, delays)
|
|
5311
|
+
renderNodeList(nodes, ch.node, userId, ch.port || 0, delays) +
|
|
5312
|
+
renderSubscriptions(ch.meta && ch.meta.proxy, userId)
|
|
5258
5313
|
);
|
|
5259
5314
|
}
|
|
5260
5315
|
|
|
5316
|
+
function fmtGB(bytes) {
|
|
5317
|
+
if (!bytes || bytes <= 0) return "0";
|
|
5318
|
+
var gb = bytes / 1073741824;
|
|
5319
|
+
return gb >= 1024 ? (gb / 1024).toFixed(2) + " TB" : gb.toFixed(1) + " GB";
|
|
5320
|
+
}
|
|
5321
|
+
|
|
5322
|
+
function fmtSpeed(bytesPerSec) {
|
|
5323
|
+
if (!bytesPerSec || bytesPerSec <= 0) return "0 KB/s";
|
|
5324
|
+
if (bytesPerSec >= 1048576) return (bytesPerSec / 1048576).toFixed(1) + " MB/s";
|
|
5325
|
+
return (bytesPerSec / 1024).toFixed(0) + " KB/s";
|
|
5326
|
+
}
|
|
5327
|
+
|
|
5328
|
+
/**
|
|
5329
|
+
* 订阅用量/到期 + 实时速率 + 订阅配置入口(数据: clashHealth.meta.proxy)
|
|
5330
|
+
* 临期(<7天)或超量(剩余<10%)标红
|
|
5331
|
+
*/
|
|
5332
|
+
function renderSubscriptions(proxy, userId) {
|
|
5333
|
+
proxy = proxy || {};
|
|
5334
|
+
var subs = proxy.subscriptions || [];
|
|
5335
|
+
var rows = "";
|
|
5336
|
+
// 订阅应用结果(daemon 最近一次 setSubscription 执行反馈;随下次应用/daemon 重启刷新)
|
|
5337
|
+
var ar = proxy.applyResult;
|
|
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
|
+
}
|
|
5356
|
+
// 实时速率
|
|
5357
|
+
var t = proxy.traffic;
|
|
5358
|
+
if (t && (t.downRate > 0 || t.upRate > 0)) {
|
|
5359
|
+
rows +=
|
|
5360
|
+
styled(
|
|
5361
|
+
"div",
|
|
5362
|
+
"font-size:11px;color:#94a3b8;margin:2px 0 4px",
|
|
5363
|
+
"⚡ 速率: ↓" + fmtSpeed(t.downRate) + " / ↑" + fmtSpeed(t.upRate),
|
|
5364
|
+
);
|
|
5365
|
+
}
|
|
5366
|
+
for (var i = 0; i < subs.length; i++) {
|
|
5367
|
+
var s = subs[i];
|
|
5368
|
+
var used = (s.upload || 0) + (s.download || 0);
|
|
5369
|
+
var total = s.total || 0;
|
|
5370
|
+
var remain = total > 0 ? total - used : 0;
|
|
5371
|
+
var pct = total > 0 ? Math.round((remain / total) * 100) : null;
|
|
5372
|
+
var daysLeft = s.expire ? Math.round((s.expire * 1000 - Date.now()) / 86400000) : null;
|
|
5373
|
+
var danger = (pct !== null && pct < 10) || (daysLeft !== null && daysLeft < 7);
|
|
5374
|
+
var color = danger ? "#f87171" : "#94a3b8";
|
|
5375
|
+
var parts = [
|
|
5376
|
+
s.name + "(" + (s.proxies || 0) + "节点)",
|
|
5377
|
+
total > 0 ? "剩余 " + fmtGB(remain) + (pct !== null ? " (" + pct + "%)" : "") : "用量未知",
|
|
5378
|
+
s.expire ?
|
|
5379
|
+
"到期 " + new Date(s.expire * 1000).toLocaleDateString() +
|
|
5380
|
+
(daysLeft !== null ? "(剩 " + daysLeft + " 天)" : "")
|
|
5381
|
+
: "无到期",
|
|
5382
|
+
];
|
|
5383
|
+
rows +=
|
|
5384
|
+
styled(
|
|
5385
|
+
"div",
|
|
5386
|
+
"font-size:11px;margin:2px 0" + (danger ? ";background:#3b1d1d;border-radius:4px;padding:3px 6px" : ""),
|
|
5387
|
+
styled("span", "color:" + color, "📡 " + parts.join(" | ")) +
|
|
5388
|
+
(danger ? styled("span", "color:#f87171;margin-left:6px", "⚠️") : ""),
|
|
5389
|
+
);
|
|
5390
|
+
}
|
|
5391
|
+
// 配置入口(摘要双来源: 后台配置优先,回退 daemon 实际生效快照;显示条数+订阅名)
|
|
5392
|
+
var configured = (window._wdData && _wdData.proxySettings && _wdData.proxySettings[userId] && _wdData.proxySettings[userId].subscriptions) || proxy.subscriptions || [];
|
|
5393
|
+
var cfgNames = configured
|
|
5394
|
+
.map(function (s) {
|
|
5395
|
+
return s.name || String(s.url || "").replace(/^https?:\/\//, "").split("/")[0];
|
|
5396
|
+
})
|
|
5397
|
+
.filter(Boolean);
|
|
5398
|
+
var cfgSummary = configured.length
|
|
5399
|
+
? configured.length + " 条" + (cfgNames.length ? "(" + cfgNames.slice(0, 3).join("、") + (cfgNames.length > 3 ? " 等" : "") + ")" : "")
|
|
5400
|
+
: "未配置";
|
|
5401
|
+
rows +=
|
|
5402
|
+
styled(
|
|
5403
|
+
"div",
|
|
5404
|
+
"margin-top:4px;padding:5px 8px;background:#111;border-radius:6px;display:flex;align-items:center;gap:8px;font-size:12px",
|
|
5405
|
+
styled("span", "color:#888", "🛜 订阅配置:") +
|
|
5406
|
+
styled("span", "color:#e0e0e0", cfgSummary) +
|
|
5407
|
+
tag(
|
|
5408
|
+
"button",
|
|
5409
|
+
{
|
|
5410
|
+
onclick: "wdShowProxyModal('" + esc(userId) + "')",
|
|
5411
|
+
style:
|
|
5412
|
+
"padding:2px 8px;font-size:11px;background:#333;color:#fbbf24;" +
|
|
5413
|
+
"border:1px solid #444;border-radius:3px;cursor:pointer",
|
|
5414
|
+
title: "配置该客户端的代理订阅地址(保存后自动热应用,无需重启)",
|
|
5415
|
+
},
|
|
5416
|
+
"配置 ⚙️",
|
|
5417
|
+
),
|
|
5418
|
+
);
|
|
5419
|
+
return rows;
|
|
5420
|
+
}
|
|
5421
|
+
|
|
5261
5422
|
/** 代理状态行:当前节点 + 健康状态 + 展开按钮 */
|
|
5262
5423
|
function renderProxyLine(h, nodes, userId, port, hasNodes) {
|
|
5263
5424
|
var dot = h.healthy ? "#4ade80" : "#f87171";
|
|
@@ -781,6 +781,23 @@
|
|
|
781
781
|
</div>
|
|
782
782
|
</div>
|
|
783
783
|
|
|
784
|
+
<!-- 代理订阅配置弹窗 -->
|
|
785
|
+
<div id="wdProxyModal"
|
|
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
|
+
<div style="background:#1e1e2e;border:1px solid #333;border-radius:12px;padding:24px;width:90%;max-width:560px">
|
|
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)。保存后客户端下次心跳时自动热应用(无需重启 mihomo),<br>应用成功后此处可看到剩余流量与到期时间;临期(<7天)或超量(剩余<10%)会标红。</div>
|
|
790
|
+
<textarea id="wdProxySubsText" rows="6" placeholder="https://example.com/api/v1/client/subscribe?token=xxxx\nhttps://another-airport.com/sub?token=yyyy"
|
|
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
|
+
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px">
|
|
793
|
+
<button onclick="wdCloseProxyModal()"
|
|
794
|
+
style="padding:8px 16px;background:#333;color:#ccc;border:none;border-radius:6px;cursor:pointer;font-size:12px">取消</button>
|
|
795
|
+
<button onclick="wdSaveProxySettings()"
|
|
796
|
+
style="padding:8px 16px;background:#fbbf24;color:#000;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600">保存并热应用</button>
|
|
797
|
+
</div>
|
|
798
|
+
</div>
|
|
799
|
+
</div>
|
|
800
|
+
|
|
784
801
|
<!-- 节点国家配置弹窗 -->
|
|
785
802
|
<div id="wdCountryModal"
|
|
786
803
|
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">
|
|
@@ -315,6 +315,26 @@ function getAllSwitchRestarts() {
|
|
|
315
315
|
return cfg.switchRestarts || {};
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
// ── 代理订阅配置管理(per agent;页面改动 → 心跳下发 → daemon 热应用) ──
|
|
319
|
+
|
|
320
|
+
function getProxySettingsStored(userId) {
|
|
321
|
+
if (!userId) return null;
|
|
322
|
+
const cfg = loadConfig();
|
|
323
|
+
return (cfg.proxySettings && cfg.proxySettings[userId]) || null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function setProxySettingsStored(userId, config) {
|
|
327
|
+
const cfg = loadConfig();
|
|
328
|
+
if (!cfg.proxySettings) cfg.proxySettings = {};
|
|
329
|
+
cfg.proxySettings[userId] = config;
|
|
330
|
+
saveConfig(cfg);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function getAllProxySettings() {
|
|
334
|
+
const cfg = loadConfig();
|
|
335
|
+
return cfg.proxySettings || {};
|
|
336
|
+
}
|
|
337
|
+
|
|
318
338
|
// ── 自动升级开关配置管理(per agent,默认开启:未配置或 enabled=true 均开启,后端显式关闭才停用) ──
|
|
319
339
|
|
|
320
340
|
function getAutoUpgradeStored(userId) {
|
|
@@ -517,6 +537,7 @@ async function handleHeartbeat(params, body, res) {
|
|
|
517
537
|
clientConfig: getClientConfigStored(userId),
|
|
518
538
|
errorTrigger: getErrorTriggerStored(userId),
|
|
519
539
|
switchRestart: getSwitchRestartStored(userId),
|
|
540
|
+
proxySettings: getProxySettingsStored(userId),
|
|
520
541
|
autoUpgrade: getAutoUpgradeStored(userId),
|
|
521
542
|
loginPorts: getLoginPortsStored(userId),
|
|
522
543
|
serverTime: Date.now(),
|
|
@@ -533,6 +554,7 @@ async function handleGetClients(params, body, res) {
|
|
|
533
554
|
supportedCountries: SUPPORTED_COUNTRIES,
|
|
534
555
|
errorTriggers: getAllErrorTriggers(),
|
|
535
556
|
switchRestarts: getAllSwitchRestarts(),
|
|
557
|
+
proxySettings: getAllProxySettings(),
|
|
536
558
|
autoUpgrades: getAllAutoUpgrades(),
|
|
537
559
|
loginPorts: getAllLoginPorts(),
|
|
538
560
|
configPath: WATCHDOG_CONFIG_PATH,
|
|
@@ -809,6 +831,33 @@ async function handleSetErrorTrigger(params, body, res) {
|
|
|
809
831
|
sendJSON(res, 200, { ok: true, config: cleaned });
|
|
810
832
|
}
|
|
811
833
|
|
|
834
|
+
/** 配置某 agent 的代理订阅(心跳下发 → daemon 热应用;空列表表示清空) */
|
|
835
|
+
async function handleSetProxySettings(params, body, res) {
|
|
836
|
+
const { userId, subscriptions } = body || {};
|
|
837
|
+
if (!userId) {
|
|
838
|
+
sendJSON(res, 400, { error: "缺少 userId" });
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (!Array.isArray(subscriptions)) {
|
|
842
|
+
sendJSON(res, 400, { error: "subscriptions 须为数组" });
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
// 归一化: 字符串或 {name,url},去重去空
|
|
846
|
+
const seen = new Set();
|
|
847
|
+
const cleaned = [];
|
|
848
|
+
for (const it of subscriptions) {
|
|
849
|
+
const url = typeof it === "string" ? it.trim() : it && String(it.url || "").trim();
|
|
850
|
+
if (!url || !/^https?:\/\//.test(url) || seen.has(url)) continue;
|
|
851
|
+
seen.add(url);
|
|
852
|
+
const name = typeof it === "object" && it.name ? String(it.name).trim() : "";
|
|
853
|
+
cleaned.push({ name, url });
|
|
854
|
+
}
|
|
855
|
+
setProxySettingsStored(userId, { subscriptions: cleaned });
|
|
856
|
+
const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
857
|
+
console.error(`[watchdog ${ts}] ${userId} 代理订阅配置已更新: ${cleaned.length} 条`);
|
|
858
|
+
sendJSON(res, 200, { ok: true, subscriptions: cleaned });
|
|
859
|
+
}
|
|
860
|
+
|
|
812
861
|
/** 配置某 agent 的代理切换重启(代理切换后强制重启全部命令;默认不启用) */
|
|
813
862
|
async function handleSetSwitchRestart(params, body, res) {
|
|
814
863
|
const { userId, config } = body || {};
|
|
@@ -967,6 +1016,10 @@ export async function tryHandleWatchdogRoute(
|
|
|
967
1016
|
await handleSetSwitchRestart(params, body, res);
|
|
968
1017
|
return true;
|
|
969
1018
|
}
|
|
1019
|
+
if (method === "POST" && routePath === "/api/watchdog/set-proxy-settings") {
|
|
1020
|
+
await handleSetProxySettings(params, body, res);
|
|
1021
|
+
return true;
|
|
1022
|
+
}
|
|
970
1023
|
if (method === "POST" && routePath === "/api/watchdog/set-auto-upgrade") {
|
|
971
1024
|
await handleSetAutoUpgrade(params, body, res);
|
|
972
1025
|
return true;
|