tt-help-cli-ycl 1.4.78 → 1.4.80

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.
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * ClashDaemon — 代理节点健康守护
3
3
  *
4
- * 定时巡检当前节点 TikTok 可达性,异常时自动故障切换。
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
- function findBenchmarkScript() {
19
- const candidates = [
20
- join(
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 = findBenchmarkScript();
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
- if (list.nodes.length) {
58
- this._log(
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({ node: "?", healthy: false, error: "Clash 未运行" });
93
- return;
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 api.switchProxy(checker.CONFIG.groupName, target);
130
- await new Promise((r) => setTimeout(r, 600));
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({ node: "?", healthy: false, error: e.message });
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 === 0) {
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
- // ---- helpers ----
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,80 @@ export class ClashDaemon {
221
322
  }
222
323
 
223
324
  _setupSignals() {
224
- const s = (sig) => {
325
+ const shutdown = (sig) => {
225
326
  this._log(`收到 ${sig},退出`);
226
327
  this.stop();
227
328
  process.exit(0);
228
329
  };
229
- process.on("SIGINT", () => s("SIGINT"));
230
- process.on("SIGTERM", () => s("SIGTERM"));
330
+ process.on("SIGINT", () => shutdown("SIGINT"));
331
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
231
332
  }
232
333
  }
233
334
 
234
- // 直接运行时(watchdog spawn 或 node daemon.js),自动启动
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
+ join(
377
+ __dirname,
378
+ "..",
379
+ "..",
380
+ "..",
381
+ "clash-verge-explore",
382
+ "speedtest-tiktok.mjs",
383
+ ),
384
+ join(__dirname, "speedtest-tiktok.mjs"),
385
+ ])
386
+ if (existsSync(p)) return p;
387
+ return null;
388
+ }
389
+
390
+ // 直接运行时自动启动
235
391
  if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
236
392
  const args = process.argv.slice(2);
393
+ const getArg = (flag) =>
394
+ args.includes(flag) ? args[args.indexOf(flag) + 1] : undefined;
237
395
  new ClashDaemon({
238
- intervalSec: parseInt(args[args.indexOf("--interval") + 1]) || undefined,
239
- benchmarkMin: parseInt(args[args.indexOf("--benchmark") + 1]) || undefined,
396
+ intervalSec: parseInt(getArg("--interval")) || undefined,
397
+ benchmarkMin: parseInt(getArg("--benchmark")) || undefined,
398
+ country: getArg("--country") || undefined,
240
399
  dryRun: args.includes("--dry-run"),
241
400
  }).start();
242
401
  }
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
- return JSON.parse(readFileSync(CMD_FILE, "utf-8"));
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
  }
@@ -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 ||
@@ -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
- cdpOptions.proxyServer = effectiveProxy;
144
- console.error(
145
- `代理: ${effectiveProxy}${exploreProxy ? " (命令行指定)" : " (配置文件)"}`,
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
  }
@@ -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 (effectiveProxy) {
129
+ if (resolvedProxy) {
121
130
  console.error(
122
- `代理: ${effectiveProxy}${exploreProxy ? " (命令行指定)" : " (配置文件)"}`,
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 (effectiveProxy) {
156
- cdpOptions.proxyServer = effectiveProxy;
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 (effectiveProxy) {
234
- cdpOptions.proxyServer = effectiveProxy;
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("║ 代理: " + (effectiveProxy || "无"));
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 (effectiveProxy) {
114
- cdpOptions.proxyServer = effectiveProxy;
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
- ...(effectiveProxy ? { proxyServer: effectiveProxy } : {}),
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
- effectiveProxy,
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
- ...(effectiveProxy ? { proxyServer: effectiveProxy } : {}),
284
+ ...(resolvedProxy ? { proxyServer: resolvedProxy } : {}),
276
285
  });
277
286
  log(
278
- `[健康检查] 已切换到端口 ${nextAccount.port}${effectiveProxy ? ", 代理: " + effectiveProxy : ""}`,
287
+ `[健康检查] 已切换到端口 ${nextAccount.port}${resolvedProxy ? ", 代理: " + resolvedProxy : ""}`,
279
288
  );
280
289
  // 重新导航并检测登录
281
290
  await withRetry("账户切换后页面导航", async () => {
@@ -5,14 +5,20 @@ import {
5
5
  } from "../webserver/server.mjs";
6
6
 
7
7
  export async function handleWebserver(options) {
8
- const { webserverPort, webserverPoolSize, webserverProxy, webserverNotify } =
9
- options;
8
+ const {
9
+ webserverPort,
10
+ webserverPoolSize,
11
+ webserverProxy,
12
+ webserverLog,
13
+ webserverNotify,
14
+ } = options;
10
15
 
11
16
  const { server, port } = await startWebserver(
12
17
  webserverPort,
13
18
  webserverPoolSize,
14
19
  webserverProxy,
15
20
  webserverNotify,
21
+ webserverLog,
16
22
  );
17
23
 
18
24
  let shuttingDown = false;