tt-help-cli-ycl 1.4.79 → 1.4.82
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 +17 -0
- package/src/clash/benchmark/lib/clash-api.js +234 -0
- package/src/clash/benchmark/speedtest-result.json +431 -0
- package/src/clash/benchmark/speedtest-tiktok.mjs +384 -0
- package/src/clash/checker.js +218 -94
- package/src/clash/daemon.js +230 -68
- package/src/clash/ipc.js +23 -4
- package/src/cli/clash-daemon.js +2 -1
- package/src/cli/explore.js +15 -4
- package/src/cli/refresh.js +15 -6
- package/src/cli/search.js +16 -7
- package/src/cli/watchdog.js +8 -1
- package/src/cli/webserver.js +8 -2
- package/src/lib/args.js +12 -0
- package/src/lib/constants.js +24 -0
- package/src/lib/shop-scraper.mjs +34 -17
- package/src/watch/public/app.js +150 -0
- package/src/watch/public/index.html +1 -1
- package/src/watch/watchdog-server.js +32 -1
- package/src/watchdog/agent.js +72 -15
- package/src/watchdog/process-manager.js +57 -0
- package/src/webserver/server.mjs +37 -14
- package/src/clash/health-checker.js +0 -248
package/src/clash/daemon.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ClashDaemon — 代理节点健康守护
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 通过 IPC 文件向 watchdog 上报状态。
|
|
4
|
+
* 职责: 定时巡检 → 异常切换 → 基准测速 → IPC 上报
|
|
5
|
+
* 看门狗通过 /tmp/clash-cmd.json 下发命令,每 10s 轮询
|
|
7
6
|
*/
|
|
7
|
+
|
|
8
8
|
import { spawn } from "child_process";
|
|
9
9
|
import { join, dirname } from "path";
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
@@ -15,52 +15,61 @@ import * as ipc from "./ipc.js";
|
|
|
15
15
|
|
|
16
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
__dirname,
|
|
22
|
-
"..",
|
|
23
|
-
"..",
|
|
24
|
-
"..",
|
|
25
|
-
"clash-verge-explore",
|
|
26
|
-
"speedtest-tiktok.mjs",
|
|
27
|
-
),
|
|
28
|
-
join(__dirname, "speedtest-tiktok.mjs"),
|
|
29
|
-
];
|
|
30
|
-
for (const p of candidates) if (existsSync(p)) return p;
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
18
|
+
// ══════════════════════════════════════════════
|
|
19
|
+
// ClashDaemon
|
|
20
|
+
// ══════════════════════════════════════════════
|
|
33
21
|
|
|
34
22
|
export class ClashDaemon {
|
|
35
23
|
constructor(opts = {}) {
|
|
36
24
|
this.intervalMs = (opts.intervalSec || 30) * 1000;
|
|
37
25
|
this.benchmarkMs = (opts.benchmarkMin || 40) * 60 * 1000;
|
|
38
26
|
this.dryRun = !!opts.dryRun;
|
|
39
|
-
this.benchmarkScript =
|
|
27
|
+
this.benchmarkScript = _findBenchmark();
|
|
40
28
|
this._running = false;
|
|
41
29
|
this._timers = [];
|
|
42
30
|
this._benchmarkRunning = false;
|
|
31
|
+
if (opts.country) checker.CONFIG.nodeCountryFilter = opts.country;
|
|
32
|
+
this._meta = {
|
|
33
|
+
groupName: checker.CONFIG.groupName,
|
|
34
|
+
proxyUrl: checker.CONFIG.proxyUrl,
|
|
35
|
+
testUrl: checker.CONFIG.testUrl,
|
|
36
|
+
countryFilter: checker.CONFIG.nodeCountryFilter,
|
|
37
|
+
healthyNodesCount: 0,
|
|
38
|
+
healthyNodesTotal: 0,
|
|
39
|
+
lastBenchmark: null,
|
|
40
|
+
};
|
|
41
|
+
this._cmdHandlers = _buildCmdHandlers(this);
|
|
43
42
|
}
|
|
44
43
|
|
|
44
|
+
// ── 生命周期 ──
|
|
45
|
+
|
|
45
46
|
async start() {
|
|
46
47
|
if (this._running) return;
|
|
47
48
|
this._running = true;
|
|
48
49
|
this._logBanner();
|
|
49
50
|
this._setupSignals();
|
|
50
51
|
|
|
52
|
+
const { alive, reason } = checker.checkClashAliveWithReason();
|
|
53
|
+
if (!alive) {
|
|
54
|
+
this._log(
|
|
55
|
+
`⚠ 代理不可用:${reason}\n请先启动 Clash Verge 并确保 API 端口 9097 已开启`,
|
|
56
|
+
);
|
|
57
|
+
this._running = false;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const proxyUrl = await checker.resolveAndSetProxyUrl();
|
|
61
|
+
this._log(
|
|
62
|
+
`✓ Clash 已连接 (${reason}), 代理端口: ${proxyUrl.replace("http://", "")}`,
|
|
63
|
+
);
|
|
64
|
+
|
|
51
65
|
const list = checker.loadHealthyList();
|
|
52
66
|
const ageMin = list.updated
|
|
53
67
|
? Math.round((Date.now() - new Date(list.updated).getTime()) / 60000)
|
|
54
68
|
: Infinity;
|
|
55
69
|
const isStale = !list.nodes.length || ageMin >= this.benchmarkMs / 60000;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
`已加载 ${list.nodes.length} 个健康节点${isStale ? ` (已过期 ${ageMin}min,立即刷新)` : ""}`,
|
|
60
|
-
);
|
|
61
|
-
} else {
|
|
62
|
-
this._log("健康列表为空,将立即执行基准测速");
|
|
63
|
-
}
|
|
70
|
+
this._log(
|
|
71
|
+
`${list.nodes.length ? `已加载 ${list.nodes.length} 个健康节点${isStale ? ` (已过期 ${ageMin}min,立即刷新)` : ""}` : "健康列表为空,将立即执行基准测速"}`,
|
|
72
|
+
);
|
|
64
73
|
|
|
65
74
|
await this._healthCheck();
|
|
66
75
|
this._timers.push(setInterval(() => this._healthCheck(), this.intervalMs));
|
|
@@ -74,6 +83,8 @@ export class ClashDaemon {
|
|
|
74
83
|
}, initial);
|
|
75
84
|
|
|
76
85
|
this._log("守护进程已启动");
|
|
86
|
+
this._pollCommands();
|
|
87
|
+
this._timers.push(setInterval(() => this._pollCommands(), 10000));
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
async stop() {
|
|
@@ -83,14 +94,18 @@ export class ClashDaemon {
|
|
|
83
94
|
this._log("守护进程已停止");
|
|
84
95
|
}
|
|
85
96
|
|
|
86
|
-
//
|
|
97
|
+
// ── 巡检 ──
|
|
87
98
|
|
|
88
99
|
async _healthCheck() {
|
|
89
100
|
if (!this._running || this._benchmarkRunning) return;
|
|
90
101
|
try {
|
|
91
102
|
if (!checker.checkClashAlive()) {
|
|
92
|
-
ipc.writeHealth({
|
|
93
|
-
|
|
103
|
+
return ipc.writeHealth({
|
|
104
|
+
node: "?",
|
|
105
|
+
healthy: false,
|
|
106
|
+
error: "Clash 未运行",
|
|
107
|
+
meta: this._buildMeta(),
|
|
108
|
+
});
|
|
94
109
|
}
|
|
95
110
|
const node = await api
|
|
96
111
|
.getCurrentNode(checker.CONFIG.groupName)
|
|
@@ -103,6 +118,7 @@ export class ClashDaemon {
|
|
|
103
118
|
timeTotal: h.timeTotal,
|
|
104
119
|
sizeDownload: h.sizeDownload,
|
|
105
120
|
title: h.title,
|
|
121
|
+
meta: this._buildMeta(),
|
|
106
122
|
});
|
|
107
123
|
|
|
108
124
|
if (h.healthy) {
|
|
@@ -126,8 +142,12 @@ export class ClashDaemon {
|
|
|
126
142
|
return;
|
|
127
143
|
}
|
|
128
144
|
|
|
129
|
-
await
|
|
130
|
-
|
|
145
|
+
const switched = await checker.switchToNode(target);
|
|
146
|
+
if (!switched) {
|
|
147
|
+
this._log(`✗ ${reason} → 切换到 ${target} 失败`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
131
151
|
const h2 = checker.checkNodeHealth();
|
|
132
152
|
this._log(
|
|
133
153
|
`⚠ ${node} → ${target} (${h2.timeTotal.toFixed(1)}s ${(h2.sizeDownload / 1024).toFixed(0)}KB)`,
|
|
@@ -140,14 +160,20 @@ export class ClashDaemon {
|
|
|
140
160
|
sizeDownload: h2.sizeDownload,
|
|
141
161
|
title: h2.title,
|
|
142
162
|
lastSwitch: `${node} → ${target}`,
|
|
163
|
+
meta: this._buildMeta(),
|
|
143
164
|
});
|
|
144
165
|
} catch (e) {
|
|
145
166
|
this._log(`巡检异常: ${e.message}`);
|
|
146
|
-
ipc.writeHealth({
|
|
167
|
+
ipc.writeHealth({
|
|
168
|
+
node: "?",
|
|
169
|
+
healthy: false,
|
|
170
|
+
error: e.message,
|
|
171
|
+
meta: this._buildMeta(),
|
|
172
|
+
});
|
|
147
173
|
}
|
|
148
174
|
}
|
|
149
175
|
|
|
150
|
-
//
|
|
176
|
+
// ── 基准测速 ──
|
|
151
177
|
|
|
152
178
|
_runBenchmark() {
|
|
153
179
|
if (!this.benchmarkScript || this._benchmarkRunning) return;
|
|
@@ -157,11 +183,9 @@ export class ClashDaemon {
|
|
|
157
183
|
const child = spawn(
|
|
158
184
|
process.execPath,
|
|
159
185
|
[this.benchmarkScript, "--timeout", "12"],
|
|
160
|
-
{
|
|
161
|
-
cwd: dirname(this.benchmarkScript),
|
|
162
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
163
|
-
},
|
|
186
|
+
{ cwd: dirname(this.benchmarkScript), stdio: ["ignore", "pipe", "pipe"] },
|
|
164
187
|
);
|
|
188
|
+
|
|
165
189
|
let stderr = "";
|
|
166
190
|
child.stderr.on("data", (d) => {
|
|
167
191
|
stderr += d.toString();
|
|
@@ -169,29 +193,39 @@ export class ClashDaemon {
|
|
|
169
193
|
child.on("close", (code) => {
|
|
170
194
|
this._benchmarkRunning = false;
|
|
171
195
|
const elapsed = ((Date.now() - start) / 1000).toFixed(0);
|
|
172
|
-
if (code
|
|
173
|
-
try {
|
|
174
|
-
const rp = join(
|
|
175
|
-
dirname(this.benchmarkScript),
|
|
176
|
-
"speedtest-result.json",
|
|
177
|
-
);
|
|
178
|
-
if (existsSync(rp)) {
|
|
179
|
-
const nodes =
|
|
180
|
-
JSON.parse(readFileSync(rp, "utf-8")).realNodes?.map(
|
|
181
|
-
(r) => r.node,
|
|
182
|
-
) || [];
|
|
183
|
-
if (nodes.length) {
|
|
184
|
-
checker.saveHealthyList(nodes);
|
|
185
|
-
this._log(
|
|
186
|
-
`=== 基准测速完成 (${elapsed}s): ${nodes.length} 个健康节点 ===`,
|
|
187
|
-
);
|
|
188
|
-
} else this._log(`=== 基准测速完成: 0 个健康节点 ===`);
|
|
189
|
-
}
|
|
190
|
-
} catch (e) {
|
|
191
|
-
this._log(`解析结果失败: ${e.message}`);
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
196
|
+
if (code !== 0) {
|
|
194
197
|
this._log(`=== 基准测速失败 (exit ${code}, ${elapsed}s) ===`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
const rp = join(dirname(this.benchmarkScript), "speedtest-result.json");
|
|
202
|
+
if (!existsSync(rp)) {
|
|
203
|
+
this._log("=== 基准测速完成但未找到结果文件 ===");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const allNodes = (
|
|
207
|
+
JSON.parse(readFileSync(rp, "utf-8")).realNodes || []
|
|
208
|
+
).map((r) => r.node);
|
|
209
|
+
if (!allNodes.length) {
|
|
210
|
+
this._log("=== 基准测速完成: 0 个健康节点 ===");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
checker.saveHealthyList(allNodes);
|
|
214
|
+
const { filtered, excluded } = checker.filterNodesByCountry(
|
|
215
|
+
allNodes,
|
|
216
|
+
checker.CONFIG.nodeCountryFilter,
|
|
217
|
+
);
|
|
218
|
+
this._meta.lastBenchmark = new Date().toISOString();
|
|
219
|
+
this._meta.healthyNodesTotal = allNodes.length;
|
|
220
|
+
this._meta.healthyNodesCount = filtered.length;
|
|
221
|
+
const tag = checker.CONFIG.nodeCountryFilter
|
|
222
|
+
? ` (${checker.CONFIG.nodeCountryFilter}节点 ${filtered.length}个, 排除 ${excluded}个)`
|
|
223
|
+
: "";
|
|
224
|
+
this._log(
|
|
225
|
+
`=== 基准测速完成 (${elapsed}s): ${allNodes.length} 个健康节点${tag} ===`,
|
|
226
|
+
);
|
|
227
|
+
} catch (e) {
|
|
228
|
+
this._log(`解析结果失败: ${e.message}`);
|
|
195
229
|
}
|
|
196
230
|
});
|
|
197
231
|
child.on("error", (err) => {
|
|
@@ -200,7 +234,71 @@ export class ClashDaemon {
|
|
|
200
234
|
});
|
|
201
235
|
}
|
|
202
236
|
|
|
203
|
-
//
|
|
237
|
+
// ── 命令轮询 ──
|
|
238
|
+
|
|
239
|
+
_pollCommands() {
|
|
240
|
+
if (!this._running) return;
|
|
241
|
+
try {
|
|
242
|
+
const cmd = ipc.readCommand();
|
|
243
|
+
if (!cmd) return;
|
|
244
|
+
const handler = this._cmdHandlers[cmd.action];
|
|
245
|
+
if (handler) {
|
|
246
|
+
handler(cmd);
|
|
247
|
+
} else {
|
|
248
|
+
this._log(`📋 未知命令: ${JSON.stringify(cmd)}`);
|
|
249
|
+
}
|
|
250
|
+
ipc.clearCommand();
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async _handleSwitchNode(cmd) {
|
|
255
|
+
try {
|
|
256
|
+
const before = await api
|
|
257
|
+
.getCurrentNode(checker.CONFIG.groupName)
|
|
258
|
+
.catch(() => "?");
|
|
259
|
+
this._log(`🔄 看门狗指令: 切换节点 → ${cmd.node} (当前: ${before})`);
|
|
260
|
+
if (!(await checker.switchToNode(cmd.node))) {
|
|
261
|
+
this._log("🔄 看门狗指令: 切换失败");
|
|
262
|
+
return ipc.writeHealth({
|
|
263
|
+
node: before,
|
|
264
|
+
healthy: null,
|
|
265
|
+
lastSwitch: `manual → ${cmd.node} (失败)`,
|
|
266
|
+
meta: this._buildMeta(),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
const h = checker.checkNodeHealth();
|
|
270
|
+
this._log(
|
|
271
|
+
`🔄 看门狗指令: 切换完成 (${h.timeTotal.toFixed(1)}s ${(h.sizeDownload / 1024).toFixed(0)}KB, ${h.healthy ? "健康" : "异常"})`,
|
|
272
|
+
);
|
|
273
|
+
ipc.writeHealth({
|
|
274
|
+
node: cmd.node,
|
|
275
|
+
healthy: h.healthy,
|
|
276
|
+
httpCode: h.httpCode,
|
|
277
|
+
timeTotal: h.timeTotal,
|
|
278
|
+
sizeDownload: h.sizeDownload,
|
|
279
|
+
title: h.title,
|
|
280
|
+
lastSwitch: `manual: ${before} → ${cmd.node}`,
|
|
281
|
+
meta: this._buildMeta(),
|
|
282
|
+
});
|
|
283
|
+
} catch (e) {
|
|
284
|
+
this._log(`🔄 看门狗指令异常: ${e.message}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── helpers ──
|
|
289
|
+
|
|
290
|
+
_buildMeta() {
|
|
291
|
+
const list = checker.loadHealthyList();
|
|
292
|
+
Object.assign(this._meta, {
|
|
293
|
+
proxyUrl: checker.CONFIG.proxyUrl,
|
|
294
|
+
testUrl: checker.CONFIG.testUrl,
|
|
295
|
+
countryFilter: checker.CONFIG.nodeCountryFilter,
|
|
296
|
+
healthyNodesCount: list.nodes.length,
|
|
297
|
+
healthyNodesTotal: list.totalNodes,
|
|
298
|
+
healthyNodes: list.nodes.slice(0, 50),
|
|
299
|
+
});
|
|
300
|
+
return this._meta;
|
|
301
|
+
}
|
|
204
302
|
|
|
205
303
|
_log(msg) {
|
|
206
304
|
console.error(
|
|
@@ -209,10 +307,13 @@ export class ClashDaemon {
|
|
|
209
307
|
}
|
|
210
308
|
|
|
211
309
|
_logBanner() {
|
|
310
|
+
const filterTag = checker.CONFIG.nodeCountryFilter
|
|
311
|
+
? ` | 节点: ${checker.CONFIG.nodeCountryFilter}`
|
|
312
|
+
: "";
|
|
212
313
|
console.error("═".repeat(50));
|
|
213
314
|
console.error(" Clash Verge 节点健康守护");
|
|
214
315
|
console.error(
|
|
215
|
-
` 检测: ${checker.CONFIG.testUrl} | 巡检: ${this.intervalMs / 1000}s | 基准: ${this.benchmarkMs / 60000}min`,
|
|
316
|
+
` 检测: ${checker.CONFIG.testUrl} | 巡检: ${this.intervalMs / 1000}s | 基准: ${this.benchmarkMs / 60000}min${filterTag}`,
|
|
216
317
|
);
|
|
217
318
|
console.error(
|
|
218
319
|
` 基准脚本: ${this.benchmarkScript || "(不可用)"} | 模式: ${this.dryRun ? "DRY-RUN" : "正常"}`,
|
|
@@ -221,22 +322,83 @@ export class ClashDaemon {
|
|
|
221
322
|
}
|
|
222
323
|
|
|
223
324
|
_setupSignals() {
|
|
224
|
-
const
|
|
325
|
+
const shutdown = (sig) => {
|
|
225
326
|
this._log(`收到 ${sig},退出`);
|
|
226
327
|
this.stop();
|
|
227
328
|
process.exit(0);
|
|
228
329
|
};
|
|
229
|
-
process.on("SIGINT", () =>
|
|
230
|
-
process.on("SIGTERM", () =>
|
|
330
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
331
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
231
332
|
}
|
|
232
333
|
}
|
|
233
334
|
|
|
234
|
-
//
|
|
335
|
+
// ══════════════════════════════════════════════
|
|
336
|
+
// 命令处理器表
|
|
337
|
+
// ══════════════════════════════════════════════
|
|
338
|
+
|
|
339
|
+
function _buildCmdHandlers(daemon) {
|
|
340
|
+
return {
|
|
341
|
+
setCountryFilter(cmd) {
|
|
342
|
+
const prev = checker.CONFIG.nodeCountryFilter;
|
|
343
|
+
checker.CONFIG.nodeCountryFilter = cmd.country || null;
|
|
344
|
+
daemon._meta.countryFilter = checker.CONFIG.nodeCountryFilter;
|
|
345
|
+
daemon._log(
|
|
346
|
+
`📋 节点过滤: ${prev || "无"} → ${checker.CONFIG.nodeCountryFilter || "无"}`,
|
|
347
|
+
);
|
|
348
|
+
},
|
|
349
|
+
setTestUrl(cmd) {
|
|
350
|
+
if (cmd.url) {
|
|
351
|
+
checker.CONFIG.testUrl = cmd.url;
|
|
352
|
+
daemon._meta.testUrl = cmd.url;
|
|
353
|
+
daemon._log(`📋 检测URL: → ${cmd.url}`);
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
runBenchmark() {
|
|
357
|
+
daemon._log("📋 收到手动触发基准测速命令");
|
|
358
|
+
daemon._runBenchmark();
|
|
359
|
+
},
|
|
360
|
+
setInterval(cmd) {
|
|
361
|
+
if (cmd.seconds > 0) {
|
|
362
|
+
daemon.intervalMs = cmd.seconds * 1000;
|
|
363
|
+
daemon._log(`📋 巡检间隔: → ${cmd.seconds}s`);
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
switchNode: (cmd) => daemon._handleSwitchNode(cmd),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ══════════════════════════════════════════════
|
|
371
|
+
// 工具
|
|
372
|
+
// ══════════════════════════════════════════════
|
|
373
|
+
|
|
374
|
+
function _findBenchmark() {
|
|
375
|
+
for (const p of [
|
|
376
|
+
// 1. 包内 benchmark(npm publish 后 src/clash/benchmark/speedtest-tiktok.mjs)
|
|
377
|
+
join(__dirname, "benchmark", "speedtest-tiktok.mjs"),
|
|
378
|
+
// 2. 开发模式:clash-verge-explore 同级目录
|
|
379
|
+
join(
|
|
380
|
+
__dirname,
|
|
381
|
+
"..",
|
|
382
|
+
"..",
|
|
383
|
+
"..",
|
|
384
|
+
"clash-verge-explore",
|
|
385
|
+
"speedtest-tiktok.mjs",
|
|
386
|
+
),
|
|
387
|
+
join(__dirname, "speedtest-tiktok.mjs"),
|
|
388
|
+
])
|
|
389
|
+
if (existsSync(p)) return p;
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 直接运行时自动启动
|
|
235
394
|
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
236
395
|
const args = process.argv.slice(2);
|
|
396
|
+
const getArg = (flag) =>
|
|
397
|
+
args.includes(flag) ? args[args.indexOf(flag) + 1] : undefined;
|
|
237
398
|
new ClashDaemon({
|
|
238
|
-
intervalSec: parseInt(
|
|
239
|
-
benchmarkMin: parseInt(
|
|
399
|
+
intervalSec: parseInt(getArg("--interval")) || undefined,
|
|
400
|
+
benchmarkMin: parseInt(getArg("--benchmark")) || undefined,
|
|
401
|
+
country: getArg("--country") || undefined,
|
|
240
402
|
dryRun: args.includes("--dry-run"),
|
|
241
403
|
}).start();
|
|
242
404
|
}
|
package/src/clash/ipc.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 与 watchdog 进程间通信,通过 tmpdir 下的 JSON 文件。
|
|
5
5
|
*
|
|
6
|
-
* clash-health.json — daemon → watchdog
|
|
7
|
-
* clash-cmd.json — watchdog → daemon
|
|
6
|
+
* clash-health.json — daemon → watchdog(代理健康状态 + 运行元数据)
|
|
7
|
+
* clash-cmd.json — watchdog → daemon(远程命令)
|
|
8
8
|
*/
|
|
9
9
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
10
10
|
import { join } from "path";
|
|
@@ -15,6 +15,19 @@ const CMD_FILE = join(tmpdir(), "clash-cmd.json");
|
|
|
15
15
|
|
|
16
16
|
// ---- health(daemon → watchdog) ----
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* 写入健康状态到 IPC 文件
|
|
20
|
+
* @param {object} state
|
|
21
|
+
* @param {string} state.node - 当前节点名
|
|
22
|
+
* @param {boolean} state.healthy - 是否健康
|
|
23
|
+
* @param {number} [state.httpCode] - HTTP 状态码
|
|
24
|
+
* @param {number} [state.timeTotal] - 响应时间(秒)
|
|
25
|
+
* @param {number} [state.sizeDownload] - 下载大小(字节)
|
|
26
|
+
* @param {string} [state.title] - 页面标题
|
|
27
|
+
* @param {string} [state.error] - 错误信息
|
|
28
|
+
* @param {string} [state.lastSwitch] - 最近一次切换记录
|
|
29
|
+
* @param {object} [state.meta] - 运行元数据(groupName, proxyUrl, testUrl, healthyNodesCount 等)
|
|
30
|
+
*/
|
|
18
31
|
export function writeHealth(state) {
|
|
19
32
|
try {
|
|
20
33
|
writeFileSync(
|
|
@@ -49,8 +62,12 @@ export function readHealth() {
|
|
|
49
62
|
}
|
|
50
63
|
}
|
|
51
64
|
|
|
52
|
-
// ---- command(watchdog → daemon
|
|
65
|
+
// ---- command(watchdog → daemon) ----
|
|
53
66
|
|
|
67
|
+
/**
|
|
68
|
+
* 写入命令(watchdog 调用,daemon 轮询读取)
|
|
69
|
+
* @param {object} cmd - 命令对象,如 { action: "setCountryFilter", country: "US" }
|
|
70
|
+
*/
|
|
54
71
|
export function writeCommand(cmd) {
|
|
55
72
|
try {
|
|
56
73
|
writeFileSync(CMD_FILE, JSON.stringify(cmd, null, 2), "utf-8");
|
|
@@ -62,7 +79,9 @@ export function writeCommand(cmd) {
|
|
|
62
79
|
export function readCommand() {
|
|
63
80
|
try {
|
|
64
81
|
if (!existsSync(CMD_FILE)) return null;
|
|
65
|
-
|
|
82
|
+
const text = readFileSync(CMD_FILE, "utf-8").trim();
|
|
83
|
+
if (!text || text === "{}") return null;
|
|
84
|
+
return JSON.parse(text);
|
|
66
85
|
} catch {
|
|
67
86
|
return null;
|
|
68
87
|
}
|
package/src/cli/clash-daemon.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* clash-daemon CLI 命令入口
|
|
3
3
|
*
|
|
4
|
-
* 用法: tt-help clash-daemon [--interval 30] [--benchmark 40] [--dry-run]
|
|
4
|
+
* 用法: tt-help clash-daemon [--interval 30] [--benchmark 40] [--country US] [--dry-run]
|
|
5
5
|
*/
|
|
6
6
|
import { ClashDaemon } from "../clash/daemon.js";
|
|
7
7
|
|
|
@@ -9,6 +9,7 @@ export async function handleClashDaemon(options) {
|
|
|
9
9
|
const d = new ClashDaemon({
|
|
10
10
|
intervalSec: parseInt(options.interval) || undefined,
|
|
11
11
|
benchmarkMin: parseInt(options.benchmark) || undefined,
|
|
12
|
+
country: options.country || undefined,
|
|
12
13
|
dryRun:
|
|
13
14
|
options["dry-run"] === "" ||
|
|
14
15
|
options["dry-run"] === true ||
|
package/src/cli/explore.js
CHANGED
|
@@ -27,6 +27,7 @@ import { showResourceUsage } from "../utils/index.js";
|
|
|
27
27
|
import { HealthChecker } from "../lib/browser/health-checker.js";
|
|
28
28
|
import { createApiClient } from "../lib/api-client.js";
|
|
29
29
|
import { createCDNHandler } from "../lib/cdn-handler.js";
|
|
30
|
+
import { requireClashOrExit } from "../clash/checker.js";
|
|
30
31
|
import path from "path";
|
|
31
32
|
import os from "os";
|
|
32
33
|
|
|
@@ -140,10 +141,20 @@ export async function handleExplore(options) {
|
|
|
140
141
|
|
|
141
142
|
const effectiveProxy = exploreProxy || configuredProxy;
|
|
142
143
|
if (effectiveProxy) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
// 如果配置了本地代理,自动发现端口并确保 Clash 可用
|
|
145
|
+
if (
|
|
146
|
+
effectiveProxy.includes("127.0.0.1") ||
|
|
147
|
+
effectiveProxy.includes("localhost")
|
|
148
|
+
) {
|
|
149
|
+
const detected = await requireClashOrExit("Explore");
|
|
150
|
+
cdpOptions.proxyServer = detected;
|
|
151
|
+
console.error(`代理: ${detected} (自动发现)`);
|
|
152
|
+
} else {
|
|
153
|
+
cdpOptions.proxyServer = effectiveProxy;
|
|
154
|
+
console.error(
|
|
155
|
+
`代理: ${effectiveProxy}${exploreProxy ? " (命令行指定)" : " (配置文件)"}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
147
158
|
} else {
|
|
148
159
|
console.error(`代理: 未配置`);
|
|
149
160
|
}
|
package/src/cli/refresh.js
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { HealthChecker } from "../lib/browser/health-checker.js";
|
|
27
27
|
import { createApiClient } from "../lib/api-client.js";
|
|
28
28
|
import { createCDNHandler } from "../lib/cdn-handler.js";
|
|
29
|
+
import { requireClashOrExit } from "../clash/checker.js";
|
|
29
30
|
import path from "path";
|
|
30
31
|
import os from "os";
|
|
31
32
|
|
|
@@ -70,6 +71,14 @@ export async function handleRefresh(options) {
|
|
|
70
71
|
exploreWebserver,
|
|
71
72
|
} = options;
|
|
72
73
|
const effectiveProxy = exploreProxy || configuredProxy;
|
|
74
|
+
let resolvedProxy = effectiveProxy;
|
|
75
|
+
if (
|
|
76
|
+
effectiveProxy &&
|
|
77
|
+
(effectiveProxy.includes("127.0.0.1") ||
|
|
78
|
+
effectiveProxy.includes("localhost"))
|
|
79
|
+
) {
|
|
80
|
+
resolvedProxy = await requireClashOrExit("Refresh");
|
|
81
|
+
}
|
|
73
82
|
|
|
74
83
|
let userId = exploreUserId || configuredUserId;
|
|
75
84
|
let browser = null;
|
|
@@ -117,9 +126,9 @@ export async function handleRefresh(options) {
|
|
|
117
126
|
console.error(`国家筛选: ${exploreLocation}`);
|
|
118
127
|
if (exploreJobLocations) console.error(`任务国家: ${exploreJobLocations}`);
|
|
119
128
|
console.error(`空闲间隔: ${exploreInterval || 30} 秒`);
|
|
120
|
-
if (
|
|
129
|
+
if (resolvedProxy) {
|
|
121
130
|
console.error(
|
|
122
|
-
`代理: ${
|
|
131
|
+
`代理: ${resolvedProxy}${effectiveProxy !== resolvedProxy ? " (自动发现)" : exploreProxy ? " (命令行指定)" : " (配置文件)"}`,
|
|
123
132
|
);
|
|
124
133
|
}
|
|
125
134
|
if (exploreMaxUsers > 0) console.error(`上限: ${exploreMaxUsers} 个用户`);
|
|
@@ -152,8 +161,8 @@ export async function handleRefresh(options) {
|
|
|
152
161
|
cdpOptions.userDataDir = currentAccount.userDataDir;
|
|
153
162
|
}
|
|
154
163
|
|
|
155
|
-
if (
|
|
156
|
-
cdpOptions.proxyServer =
|
|
164
|
+
if (resolvedProxy) {
|
|
165
|
+
cdpOptions.proxyServer = resolvedProxy;
|
|
157
166
|
}
|
|
158
167
|
|
|
159
168
|
console.error(`CDP 端口: ${cdpOptions.port}, 用户编号: ${userId}`);
|
|
@@ -230,8 +239,8 @@ export async function handleRefresh(options) {
|
|
|
230
239
|
port: nextAccount.port,
|
|
231
240
|
userDataDir: nextAccount.userDataDir,
|
|
232
241
|
});
|
|
233
|
-
if (
|
|
234
|
-
cdpOptions.proxyServer =
|
|
242
|
+
if (resolvedProxy) {
|
|
243
|
+
cdpOptions.proxyServer = resolvedProxy;
|
|
235
244
|
}
|
|
236
245
|
console.error(`[健康检查] 已切换到端口 ${nextAccount.port}`);
|
|
237
246
|
await withRetry("账户切换后页面导航", async () => {
|
package/src/cli/search.js
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
} from "../scraper/modules/captcha-handler.js";
|
|
41
41
|
import { createApiClient } from "../lib/api-client.js";
|
|
42
42
|
import { HealthChecker } from "../lib/browser/health-checker.js";
|
|
43
|
+
import { requireClashOrExit } from "../clash/checker.js";
|
|
43
44
|
import {
|
|
44
45
|
server as cfgServer,
|
|
45
46
|
proxy as configuredProxy,
|
|
@@ -79,6 +80,14 @@ export async function handleSearch(parsed) {
|
|
|
79
80
|
} = searchOpts || {};
|
|
80
81
|
|
|
81
82
|
const effectiveProxy = cliProxy || configuredProxy;
|
|
83
|
+
let resolvedProxy = effectiveProxy;
|
|
84
|
+
if (
|
|
85
|
+
effectiveProxy &&
|
|
86
|
+
(effectiveProxy.includes("127.0.0.1") ||
|
|
87
|
+
effectiveProxy.includes("localhost"))
|
|
88
|
+
) {
|
|
89
|
+
resolvedProxy = await requireClashOrExit("Search");
|
|
90
|
+
}
|
|
82
91
|
|
|
83
92
|
const log = (...args) => process.stderr.write(args.join(" ") + "\n");
|
|
84
93
|
|
|
@@ -95,7 +104,7 @@ export async function handleSearch(parsed) {
|
|
|
95
104
|
portCount +
|
|
96
105
|
" 个)",
|
|
97
106
|
);
|
|
98
|
-
log("║ 代理: " + (
|
|
107
|
+
log("║ 代理: " + (resolvedProxy || "无"));
|
|
99
108
|
log("╚════════════════════════════════════╝");
|
|
100
109
|
log("");
|
|
101
110
|
|
|
@@ -110,8 +119,8 @@ export async function handleSearch(parsed) {
|
|
|
110
119
|
const cdpOptions = {};
|
|
111
120
|
cdpOptions.port = currentAccount.port;
|
|
112
121
|
cdpOptions.userDataDir = currentAccount.userDataDir;
|
|
113
|
-
if (
|
|
114
|
-
cdpOptions.proxyServer =
|
|
122
|
+
if (resolvedProxy) {
|
|
123
|
+
cdpOptions.proxyServer = resolvedProxy;
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
let browser = null;
|
|
@@ -161,7 +170,7 @@ export async function handleSearch(parsed) {
|
|
|
161
170
|
Object.assign(cdpOptions, {
|
|
162
171
|
port: currentAccount.port,
|
|
163
172
|
userDataDir: currentAccount.userDataDir,
|
|
164
|
-
...(
|
|
173
|
+
...(resolvedProxy ? { proxyServer: resolvedProxy } : {}),
|
|
165
174
|
});
|
|
166
175
|
}
|
|
167
176
|
|
|
@@ -264,7 +273,7 @@ export async function handleSearch(parsed) {
|
|
|
264
273
|
const newBrowser = await switchAccount(
|
|
265
274
|
{ port: oldAccount.port, userDataDir: oldAccount.userDataDir },
|
|
266
275
|
{ port: nextAccount.port, userDataDir: nextAccount.userDataDir },
|
|
267
|
-
|
|
276
|
+
resolvedProxy,
|
|
268
277
|
);
|
|
269
278
|
browser = newBrowser;
|
|
270
279
|
const newPage = await setupNewPage(browser);
|
|
@@ -272,10 +281,10 @@ export async function handleSearch(parsed) {
|
|
|
272
281
|
Object.assign(cdpOptions, {
|
|
273
282
|
port: nextAccount.port,
|
|
274
283
|
userDataDir: nextAccount.userDataDir,
|
|
275
|
-
...(
|
|
284
|
+
...(resolvedProxy ? { proxyServer: resolvedProxy } : {}),
|
|
276
285
|
});
|
|
277
286
|
log(
|
|
278
|
-
`[健康检查] 已切换到端口 ${nextAccount.port}${
|
|
287
|
+
`[健康检查] 已切换到端口 ${nextAccount.port}${resolvedProxy ? ", 代理: " + resolvedProxy : ""}`,
|
|
279
288
|
);
|
|
280
289
|
// 重新导航并检测登录
|
|
281
290
|
await withRetry("账户切换后页面导航", async () => {
|
package/src/cli/watchdog.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* --server <URL> 服务端地址(默认使用 ~/.tt-help.json 中的 server)
|
|
8
8
|
* --interval <秒> 心跳间隔,默认 15 秒
|
|
9
9
|
* --client-id <ID> 客户端标识(默认本机 hostname)
|
|
10
|
+
* --no-daemon 不启动 clash daemon(默认启动)
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import os from "os";
|
|
@@ -14,7 +15,12 @@ import { WatchdogAgent } from "../watchdog/agent.js";
|
|
|
14
15
|
import { getClientConfig } from "../watchdog/reporter.js";
|
|
15
16
|
|
|
16
17
|
export async function handleWatchdog(options) {
|
|
17
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
watchdogServer,
|
|
20
|
+
watchdogInterval,
|
|
21
|
+
watchdogClientId,
|
|
22
|
+
watchdogNoDaemon,
|
|
23
|
+
} = options;
|
|
18
24
|
|
|
19
25
|
// 服务端地址:--server 参数 > ~/.tt-help.json server > 默认值
|
|
20
26
|
const clientConfig = getClientConfig();
|
|
@@ -48,6 +54,7 @@ export async function handleWatchdog(options) {
|
|
|
48
54
|
serverUrl,
|
|
49
55
|
clientId,
|
|
50
56
|
intervalMs: intervalSec * 1000,
|
|
57
|
+
spawnDaemon: !watchdogNoDaemon,
|
|
51
58
|
});
|
|
52
59
|
|
|
53
60
|
await agent.start();
|