mihomo-cli 2.4.1 → 2.5.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.5.0] - 2026-05-03
4
+
5
+ ### 新功能
6
+
7
+ - **test 命令** - `mihomo test` 快速测试当前运行实例的节点连通性
8
+ - **clean 命令** - `mihomo clean` 清理失败节点并自动重启
9
+
10
+ ### 改进
11
+
12
+ - `sub test` / `sub clean` 改用独立临时进程测试,不影响当前代理,支持测试任意订阅(不限于活跃订阅)
13
+ - 启动时 auto-clean 使用当前运行实例直接测速,提升启动速度
14
+
15
+ ### 移除
16
+
17
+ - 移除 `sub best` 命令
18
+
19
+ ---
20
+
21
+ ## [2.4.2] - 2026-05-02
22
+
23
+ ### 改进
24
+
25
+ - 自动清理阈值统一为 50 个节点(不再区分订阅类型)
26
+ - 订阅默认更新间隔从 12 小时缩短为 4 小时
27
+
28
+ ---
29
+
3
30
  ## [2.4.1] - 2026-05-02
4
31
 
5
32
  ### 修复
package/README.md CHANGED
@@ -7,10 +7,9 @@
7
7
  - 🌐 **订阅管理** - 添加/更新订阅,支持流量统计和到期时间显示
8
8
  - 🔄 **自动更新** - 启动时自动检查并更新过期订阅
9
9
  - 🔍 **模糊匹配** - `sub use` / `sub web` 支持订阅名称模糊匹配
10
- - 🧹 **节点测速清理** - `sub test` 测试连通性,`sub clean` 自动清理失败节点,启动时自动清理
10
+ - 🧹 **节点测速清理** - `test` 快速测试、`clean` 清理并重启;`sub test/clean` 独立进程测试任意订阅
11
11
  - 📊 **免费订阅基准测试** - `bench` 命令一键测试 20 个内置免费订阅源质量排名
12
12
  - 🆓 **快速添加免费订阅** - `sub free <id>` 一键添加内置免费订阅源
13
- - 🏆 **聚合订阅** - `sub best <id>` 一键添加自动更新的聚合订阅(每小时更新、去重、测活)
14
13
  - 📝 **覆写配置** - 在订阅基础上进行自定义覆写,支持强制覆盖、数组合并
15
14
  - 🔄 **智能重启** - `sub use` 切换订阅、`ow on/off` 切换覆写后自动重启
16
15
  - 🚀 **进程管理** - 启动/停止/切换模式,自动清理残留进程
@@ -101,13 +100,14 @@ mihomo ui yacd # YACD
101
100
  | `mihomo sub use <name>` | 切换当前订阅(支持模糊匹配,自动重启) |
102
101
  | `mihomo sub add <url> [name]` | 添加订阅并自动切换(支持逗号分隔多 URL 合并) |
103
102
  | `mihomo sub free <id>` | 添加内置免费订阅(`0`=合并 #1+#2,`sub free` 列出可用源)|
104
- | `mihomo sub best <id>` | 添加聚合订阅(`sub best` 列出可用源)|
105
103
  | `mihomo sub update` | 更新所有订阅 |
106
104
  | `mihomo sub update <name>` | 更新指定订阅(支持模糊匹配) |
107
105
  | `mihomo sub remove <name>` | 删除订阅(支持模糊匹配) |
108
106
  | `mihomo sub web [name]` | 打开订阅页面(无参打开默认) |
109
107
  | `mihomo sub test [name]` | 测试节点连通性(`-t` 超时,`-j` 并发) |
110
108
  | `mihomo sub clean [name]` | 测速并清理失败节点 |
109
+ | `mihomo test` | 快速测试当前节点连通性(`-t` 超时,`-j` 并发) |
110
+ | `mihomo clean` | 清理失败节点并自动重启(`-t` 超时,`-j` 并发) |
111
111
 
112
112
  ### 覆写配置
113
113
 
@@ -198,7 +198,7 @@ mihomo kernel --mirror-all hk.gh-proxy.org
198
198
 
199
199
  ## 订阅自动更新
200
200
 
201
- - 默认更新间隔:12 小时(或订阅服务端指定的 `profile-update-interval`)
201
+ - 默认更新间隔:4 小时(或订阅服务端指定的 `profile-update-interval`)
202
202
  - 触发时机:`start` 命令、`sub list` 命令
203
203
  - 更新失败时继续使用本地缓存,不影响使用
204
204
 
package/dist/index.js CHANGED
@@ -2687,14 +2687,6 @@ function getFreeSubscriptionSources() {
2687
2687
  { name: "NoMoreWalls", url: "https://gh-proxy.org/raw.githubusercontent.com/peasoft/NoMoreWalls/master/list.meta.yml" }
2688
2688
  ];
2689
2689
  }
2690
- var GH_SUB = "https://gh-proxy.org/raw.githubusercontent.com/imaex/mihomo-free-sub/sub";
2691
- function getBestSubscriptionSources() {
2692
- return [
2693
- { name: "curated", url: `${GH_SUB}/curated.yaml`, description: "\u7CBE\u9009 29 \u7EC4\uFF08\u4EC5\u6D4B\u901F\u6E90\uFF09" },
2694
- { name: "acl4ssr", url: `${GH_SUB}/acl4ssr.yaml`, description: "ACL4SSR 29 \u7EC4\uFF08\u5168\u90E8\u6E90\uFF09" },
2695
- { name: "freesub", url: `${GH_SUB}/freesub.yaml`, description: "freeSub 24 \u7EC4" }
2696
- ];
2697
- }
2698
2690
  var BENCH_CONFIG = {
2699
2691
  "allow-lan": false,
2700
2692
  "external-controller": "127.0.0.1:19090",
@@ -2703,6 +2695,14 @@ var BENCH_CONFIG = {
2703
2695
  "log-level": "error",
2704
2696
  "geodata-mode": true
2705
2697
  };
2698
+ var TEST_CONFIG = {
2699
+ "allow-lan": false,
2700
+ "external-controller": "127.0.0.1:29090",
2701
+ port: 27890,
2702
+ "socks-port": 27891,
2703
+ "log-level": "error",
2704
+ "geodata-mode": true
2705
+ };
2706
2706
  var BASE_CONFIG = {
2707
2707
  "allow-lan": false,
2708
2708
  "external-controller": "127.0.0.1:9090",
@@ -3397,6 +3397,15 @@ function parseMirrorArg(args) {
3397
3397
  }
3398
3398
  return { mirror: null, isOverride: false, type: "download" };
3399
3399
  }
3400
+ function isProxyValid(proxy) {
3401
+ if (!proxy.name || !proxy.server || !proxy.port) return false;
3402
+ if (!proxy.type) return false;
3403
+ if (proxy.type === "ss" && typeof proxy.cipher === "string" && proxy.cipher.startsWith("2022-blake3")) {
3404
+ const pw = String(proxy.password || "");
3405
+ if (!/^[A-Za-z0-9+/]+=*$/.test(pw) || pw.length < 20) return false;
3406
+ }
3407
+ return true;
3408
+ }
3400
3409
 
3401
3410
  // src/bench.ts
3402
3411
  var BENCH_DIR = path3.join(USER_DATA_DIR, "bench");
@@ -3541,15 +3550,6 @@ async function downloadAllSources(sources, onProgress) {
3541
3550
  });
3542
3551
  return await Promise.all(tasks);
3543
3552
  }
3544
- function isProxyValid(proxy) {
3545
- if (!proxy.name || !proxy.server || !proxy.port) return false;
3546
- if (!proxy.type) return false;
3547
- if (proxy.type === "ss" && typeof proxy.cipher === "string" && proxy.cipher.startsWith("2022-blake3")) {
3548
- const pw = String(proxy.password || "");
3549
- if (!/^[A-Za-z0-9+/]+=*$/.test(pw) || pw.length < 20) return false;
3550
- }
3551
- return true;
3552
- }
3553
3553
  function buildMergedBenchConfig(allProxies) {
3554
3554
  ensureBenchDirs();
3555
3555
  const validProxies = allProxies.filter(isProxyValid);
@@ -4211,7 +4211,7 @@ function viewLogWithTail(logPath, options) {
4211
4211
  }
4212
4212
 
4213
4213
  // src/subscription.ts
4214
- var DEFAULT_UPDATE_INTERVAL_HOURS = 12;
4214
+ var DEFAULT_UPDATE_INTERVAL_HOURS = 4;
4215
4215
  var YAML_DUMP_OPTS = { indent: 2, lineWidth: -1, noCompatMode: true };
4216
4216
  var HTTP_CLIENT = createHttpClient({ timeout: 6e4 });
4217
4217
  function isMultiUrl(url) {
@@ -4484,9 +4484,9 @@ async function autoUpdateStaleSubscription() {
4484
4484
  }
4485
4485
  var API_BASE = `http://${BASE_CONFIG["external-controller"]}`;
4486
4486
  var DEFAULT_TEST_URL = "http://www.gstatic.com/generate_204";
4487
- async function testProxyDelay(proxyName, timeout, testUrl, client) {
4487
+ async function testProxyDelay(proxyName, timeout, testUrl, client, apiBase = API_BASE) {
4488
4488
  const encodedName = encodeURIComponent(proxyName);
4489
- const url = `${API_BASE}/proxies/${encodedName}/delay?timeout=${timeout}&url=${encodeURIComponent(testUrl)}`;
4489
+ const url = `${apiBase}/proxies/${encodedName}/delay?timeout=${timeout}&url=${encodeURIComponent(testUrl)}`;
4490
4490
  try {
4491
4491
  const response = await client.get(url);
4492
4492
  const data = JSON.parse(response.data);
@@ -4506,7 +4506,7 @@ async function testProxyDelay(proxyName, timeout, testUrl, client) {
4506
4506
  }
4507
4507
  }
4508
4508
  async function testSubscriptionProxies(subName, options = {}) {
4509
- const { timeout = 2e3, concurrency = 100, testUrl = DEFAULT_TEST_URL, onResult } = options;
4509
+ const { timeout = 2e3, concurrency = 100, testUrl = DEFAULT_TEST_URL, apiBase = API_BASE, onResult } = options;
4510
4510
  const { proxies } = options.parsed || loadSubscriptionConfig(subName);
4511
4511
  if (proxies.length === 0) {
4512
4512
  return { total: 0, alive: 0, dead: 0, results: [] };
@@ -4516,7 +4516,7 @@ async function testSubscriptionProxies(subName, options = {}) {
4516
4516
  let completedCount = 0;
4517
4517
  for (let i = 0; i < proxies.length; i += concurrency) {
4518
4518
  const batch = proxies.slice(i, i + concurrency);
4519
- const batchResults = await Promise.all(batch.map((proxy) => testProxyDelay(proxy.name, timeout, testUrl, client)));
4519
+ const batchResults = await Promise.all(batch.map((proxy) => testProxyDelay(proxy.name, timeout, testUrl, client, apiBase)));
4520
4520
  for (const result of batchResults) {
4521
4521
  results.push(result);
4522
4522
  onResult?.(result, completedCount, proxies.length);
@@ -4598,12 +4598,138 @@ async function autoCleanSubscription(subName, options = {}) {
4598
4598
  removedGroups = cleanResult.removedGroups;
4599
4599
  }
4600
4600
  }
4601
- if (!skipped) {
4601
+ if (!skipped && removedProxies > 0) {
4602
4602
  saveSubscriptionConfig(subName, parsed);
4603
4603
  }
4604
4604
  return { summary, removedProxies, updatedGroups, removedGroups, skipped };
4605
4605
  }
4606
4606
 
4607
+ // src/test-instance.ts
4608
+ import { spawn as spawn3 } from "child_process";
4609
+ import fs7 from "fs";
4610
+ import path5 from "path";
4611
+ var TEST_DIR = path5.join(USER_DATA_DIR, "test");
4612
+ var TEST_DIRS = {
4613
+ data: path5.join(TEST_DIR, "data"),
4614
+ runtime: path5.join(TEST_DIR, "runtime")
4615
+ };
4616
+ var TEST_PATHS = {
4617
+ configFile: path5.join(TEST_DIRS.runtime, "config.yaml"),
4618
+ pidFile: path5.join(TEST_DIRS.runtime, "pid"),
4619
+ logFile: path5.join(TEST_DIR, "test.log")
4620
+ };
4621
+ var TEST_API = `http://${TEST_CONFIG["external-controller"]}`;
4622
+ function ensureTestDirs() {
4623
+ for (const dir of Object.values(TEST_DIRS)) {
4624
+ fs7.mkdirSync(dir, { recursive: true, mode: 448 });
4625
+ }
4626
+ }
4627
+ function cleanupTestDir() {
4628
+ rmrf(TEST_DIR);
4629
+ }
4630
+ function buildTestConfig(subName) {
4631
+ ensureTestDirs();
4632
+ const rawContent = readSubscriptionRawConfig(subName);
4633
+ if (!rawContent) {
4634
+ throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
4635
+ }
4636
+ const parsed = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
4637
+ const proxies = (parsed.proxies || []).filter(isProxyValid);
4638
+ if (proxies.length === 0) {
4639
+ throw new Error(`\u8BA2\u9605 "${subName}" \u6CA1\u6709\u6709\u6548\u8282\u70B9`);
4640
+ }
4641
+ const nameCount = /* @__PURE__ */ new Map();
4642
+ for (const proxy of proxies) {
4643
+ const count = (nameCount.get(proxy.name) || 0) + 1;
4644
+ nameCount.set(proxy.name, count);
4645
+ if (count > 1) {
4646
+ proxy.name = `${proxy.name} #${count}`;
4647
+ }
4648
+ }
4649
+ const config = {
4650
+ ...TEST_CONFIG,
4651
+ proxies,
4652
+ "proxy-groups": [
4653
+ {
4654
+ name: "PROXY",
4655
+ type: "select",
4656
+ proxies: proxies.map((p) => p.name)
4657
+ }
4658
+ ],
4659
+ rules: ["MATCH,PROXY"]
4660
+ };
4661
+ const content = jsYaml.dump(config, { indent: 2, lineWidth: -1, noCompatMode: true });
4662
+ fs7.writeFileSync(TEST_PATHS.configFile, content, { mode: 384 });
4663
+ }
4664
+ async function startTestInstance() {
4665
+ const binary2 = PATHS.mihomoBinary;
4666
+ if (!fs7.existsSync(binary2)) throw new Error("\u672A\u627E\u5230 mihomo \u5185\u6838");
4667
+ stopTestInstance();
4668
+ const logFd = fs7.openSync(TEST_PATHS.logFile, "a");
4669
+ const child = spawn3(binary2, ["-d", TEST_DIRS.data, "-f", TEST_PATHS.configFile], {
4670
+ detached: true,
4671
+ stdio: ["ignore", logFd, logFd]
4672
+ });
4673
+ fs7.closeSync(logFd);
4674
+ child.unref();
4675
+ const pid = child.pid;
4676
+ fs7.writeFileSync(TEST_PATHS.pidFile, pid.toString(), { mode: 384 });
4677
+ const client = createHttpClient({ timeout: 2e3 });
4678
+ let ready = false;
4679
+ for (let i = 0; i < 60; i++) {
4680
+ if (!isProcessRunning(pid)) break;
4681
+ try {
4682
+ await client.get(`${TEST_API}/version`);
4683
+ ready = true;
4684
+ break;
4685
+ } catch {
4686
+ await sleep(500);
4687
+ }
4688
+ }
4689
+ if (!isProcessRunning(pid)) {
4690
+ let errorDetail = "";
4691
+ try {
4692
+ errorDetail = fs7.readFileSync(TEST_PATHS.logFile, "utf8").slice(-1e3);
4693
+ } catch {
4694
+ }
4695
+ throw new Error(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
4696
+ ${errorDetail}` : ""}`);
4697
+ }
4698
+ if (!ready) {
4699
+ throw new Error("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u8D85\u65F6\uFF0CAPI \u672A\u54CD\u5E94");
4700
+ }
4701
+ }
4702
+ function stopTestInstance() {
4703
+ let pid;
4704
+ try {
4705
+ pid = parseInt(fs7.readFileSync(TEST_PATHS.pidFile, "utf8").trim(), 10);
4706
+ } catch {
4707
+ return;
4708
+ }
4709
+ if (pid > 0 && isProcessRunning(pid)) {
4710
+ process.kill(pid, "SIGKILL");
4711
+ for (let i = 0; i < 20; i++) {
4712
+ if (!isProcessRunning(pid)) break;
4713
+ sleepSync(100);
4714
+ }
4715
+ }
4716
+ try {
4717
+ fs7.unlinkSync(TEST_PATHS.pidFile);
4718
+ } catch {
4719
+ }
4720
+ }
4721
+ async function withTestInstance(subName, fn) {
4722
+ cleanupTestDir();
4723
+ buildTestConfig(subName);
4724
+ try {
4725
+ await startTestInstance();
4726
+ return await fn(TEST_API);
4727
+ } finally {
4728
+ stopTestInstance();
4729
+ cleanupTestDir();
4730
+ }
4731
+ }
4732
+
4607
4733
  // src/commands/status.ts
4608
4734
  function printStatus() {
4609
4735
  const status = getStatus();
@@ -4656,8 +4782,7 @@ function printStatus() {
4656
4782
  }
4657
4783
 
4658
4784
  // src/commands/start.ts
4659
- var AUTO_CLEAN_THRESHOLD = 100;
4660
- var AUTO_CLEAN_THRESHOLD_FREE = 50;
4785
+ var AUTO_CLEAN_THRESHOLD = 50;
4661
4786
  function handleStopResult(result) {
4662
4787
  if (result.remaining && result.remaining.length > 0) {
4663
4788
  console.error(`${colors.red("\u90E8\u5206\u8FDB\u7A0B\u672A\u7EC8\u6B62:")} ${result.remaining.join(", ")}`);
@@ -4709,10 +4834,9 @@ async function cmdStart(args) {
4709
4834
  }
4710
4835
  process.exit(1);
4711
4836
  }
4712
- const cleanThreshold = sub.name.startsWith("free") ? AUTO_CLEAN_THRESHOLD_FREE : AUTO_CLEAN_THRESHOLD;
4713
- if (configInfo.proxies > cleanThreshold) {
4837
+ if (configInfo.proxies > AUTO_CLEAN_THRESHOLD) {
4714
4838
  console.log("");
4715
- console.log(`\u8282\u70B9\u6570 ${configInfo.proxies} \u8D85\u8FC7 ${cleanThreshold}\uFF0C\u81EA\u52A8\u6E05\u7406...`);
4839
+ console.log(`\u8282\u70B9\u6570 ${configInfo.proxies} \u8D85\u8FC7 ${AUTO_CLEAN_THRESHOLD}\uFF0C\u81EA\u52A8\u6E05\u7406...`);
4716
4840
  console.log("");
4717
4841
  await sleep(1e3);
4718
4842
  const cleanResult = await autoCleanSubscription(sub.name, { onResult: printTestResult });
@@ -4743,9 +4867,9 @@ function printTestResult(result, index, total) {
4743
4867
  const prefix = `[${index + 1}/${total}]`;
4744
4868
  if (result.delay !== null) {
4745
4869
  const delayColor = result.delay < 300 ? colors.green : result.delay < 800 ? colors.yellow : colors.red;
4746
- console.log(` ${prefix} ${colors.green("\u2713")} ${result.name} ${delayColor(`${result.delay}ms`)}`);
4870
+ console.log(`${prefix} ${colors.green("\u2713")} ${result.name} ${delayColor(`${result.delay}ms`)}`);
4747
4871
  } else {
4748
- console.log(` ${prefix} ${colors.red("\u2717")} ${result.name} ${colors.gray(result.error || "timeout")}`);
4872
+ console.log(`${prefix} ${colors.red("\u2717")} ${result.name} ${colors.gray(result.error || "timeout")}`);
4749
4873
  }
4750
4874
  }
4751
4875
  function formatCleanSummary(result) {
@@ -4762,7 +4886,7 @@ function githubRepoUrl(rawUrl) {
4762
4886
  if (match) return `https://github.com/${match[1]}`;
4763
4887
  return null;
4764
4888
  }
4765
- function resolveActiveTestTarget(args) {
4889
+ function resolveTestTarget(args) {
4766
4890
  const subs = getSubscriptions();
4767
4891
  if (subs.length === 0) {
4768
4892
  console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
@@ -4771,28 +4895,18 @@ function resolveActiveTestTarget(args) {
4771
4895
  const nameArg = getNonFlagArg(args, 2);
4772
4896
  const timeout = parseIntArg(args, "-t", "--timeout", 2e3);
4773
4897
  const concurrency = parseIntArg(args, "-j", "--concurrency", 100);
4774
- const activeSub = getActiveSubscription();
4775
4898
  let target;
4776
4899
  if (nameArg) {
4777
4900
  const matches = findSubscriptionFuzzy(subs, nameArg);
4778
4901
  target = pickSingleSubscription(matches, nameArg);
4779
4902
  } else {
4903
+ const activeSub = getActiveSubscription();
4780
4904
  if (!activeSub) {
4781
- console.error("\u9519\u8BEF: \u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
4905
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605\uFF0C\u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
4782
4906
  process.exit(1);
4783
4907
  }
4784
4908
  target = activeSub;
4785
4909
  }
4786
- const status = getStatus();
4787
- if (!status.running) {
4788
- console.error("\u9519\u8BEF: mihomo \u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 (mihomo start)");
4789
- process.exit(1);
4790
- }
4791
- if (!activeSub || activeSub.name !== target.name) {
4792
- console.error(`\u9519\u8BEF: \u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605\u662F "${activeSub?.name}"\uFF0C\u4E0D\u662F "${target.name}"`);
4793
- console.log(`\u8BF7\u5148\u5207\u6362: mihomo sub use ${target.name}`);
4794
- process.exit(1);
4795
- }
4796
4910
  return { target, timeout, concurrency };
4797
4911
  }
4798
4912
  async function printSubscriptionList(options) {
@@ -4901,36 +5015,6 @@ async function addFreeSubscription(freeId) {
4901
5015
  console.log("");
4902
5016
  await printSubscriptionList();
4903
5017
  }
4904
- function printBestSourceList() {
4905
- const bestSources = getBestSubscriptionSources();
4906
- for (let i = 0; i < bestSources.length; i++) {
4907
- console.log(` ${i + 1} ${bestSources[i].name} \u2014 ${bestSources[i].description}`);
4908
- }
4909
- }
4910
- async function addBestSubscription(bestId) {
4911
- const bestSources = getBestSubscriptionSources();
4912
- if (bestId < 1 || bestId > bestSources.length) {
4913
- console.error(`\u9519\u8BEF: best \u8BA2\u9605 ID \u8303\u56F4 1-${bestSources.length}`);
4914
- console.log("\n\u53EF\u7528\u6E90:");
4915
- printBestSourceList();
4916
- process.exit(1);
4917
- }
4918
- const source = bestSources[bestId - 1];
4919
- const name = `best${bestId}`;
4920
- console.log(`\u6DFB\u52A0 best \u8BA2\u9605: ${name} (${source.description})`);
4921
- try {
4922
- addSubscription(source.url, name);
4923
- const info = await downloadSubscription(source.url, name);
4924
- setDefaultSubscription(name);
4925
- saveSubscriptionCache(name, { web_page_url: "https://github.com/imaex/mihomo-free-sub" });
4926
- console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
4927
- } catch (e) {
4928
- console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
4929
- process.exit(1);
4930
- }
4931
- console.log("");
4932
- await printSubscriptionList();
4933
- }
4934
5018
  async function cmdSubscription(args) {
4935
5019
  const action = args[1];
4936
5020
  if (!action || action === "list") {
@@ -4948,17 +5032,6 @@ async function cmdSubscription(args) {
4948
5032
  await addFreeSubscription(id);
4949
5033
  return;
4950
5034
  }
4951
- if (action === "best") {
4952
- const id = parseInt(args[2], 10);
4953
- if (Number.isNaN(id)) {
4954
- console.log("\u7528\u6CD5: mihomo sub best <id>\n");
4955
- console.log("\u53EF\u7528\u6E90:");
4956
- printBestSourceList();
4957
- process.exit(1);
4958
- }
4959
- await addBestSubscription(id);
4960
- return;
4961
- }
4962
5035
  if (action === "add") {
4963
5036
  const freeId = parseIntArg(args, "--free", "--free", -1);
4964
5037
  if (freeId > 0) {
@@ -5157,14 +5230,17 @@ async function cmdSubscription(args) {
5157
5230
  return;
5158
5231
  }
5159
5232
  if (action === "clean") {
5160
- const { target, timeout, concurrency } = resolveActiveTestTarget(args);
5233
+ const { target, timeout, concurrency } = resolveTestTarget(args);
5161
5234
  console.log(`\u6E05\u7406\u8BA2\u9605 "${target.name}"...`);
5162
5235
  console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
5163
5236
  console.log("");
5164
- const result = await autoCleanSubscription(target.name, {
5165
- timeout,
5166
- concurrency,
5167
- onResult: printTestResult
5237
+ const result = await withTestInstance(target.name, async (apiBase) => {
5238
+ return autoCleanSubscription(target.name, {
5239
+ timeout,
5240
+ concurrency,
5241
+ apiBase,
5242
+ onResult: printTestResult
5243
+ });
5168
5244
  });
5169
5245
  console.log("");
5170
5246
  console.log(formatTestSummary(result.summary));
@@ -5173,20 +5249,26 @@ async function cmdSubscription(args) {
5173
5249
  console.log(colors.yellow("\u5B58\u6D3B\u8282\u70B9\u4E0D\u8DB3 1%\uFF0C\u8DF3\u8FC7\u6E05\u7406\u3002\u8BF7\u68C0\u67E5\u539F\u59CB\u8BA2\u9605\u662F\u5426\u6709\u6548"));
5174
5250
  } else if (result.removedProxies > 0) {
5175
5251
  console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
5176
- console.log("");
5177
- console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
5252
+ const status = getStatus();
5253
+ if (status.running) {
5254
+ console.log("");
5255
+ console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
5256
+ }
5178
5257
  }
5179
5258
  return;
5180
5259
  }
5181
5260
  if (action === "test") {
5182
- const { target, timeout, concurrency } = resolveActiveTestTarget(args);
5261
+ const { target, timeout, concurrency } = resolveTestTarget(args);
5183
5262
  console.log(`\u6D4B\u8BD5\u8BA2\u9605 "${target.name}" \u7684\u8282\u70B9\u8FDE\u901A\u6027...`);
5184
5263
  console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
5185
5264
  console.log("");
5186
- const summary = await testSubscriptionProxies(target.name, {
5187
- timeout,
5188
- concurrency,
5189
- onResult: printTestResult
5265
+ const summary = await withTestInstance(target.name, async (apiBase) => {
5266
+ return testSubscriptionProxies(target.name, {
5267
+ timeout,
5268
+ concurrency,
5269
+ apiBase,
5270
+ onResult: printTestResult
5271
+ });
5190
5272
  });
5191
5273
  console.log("");
5192
5274
  console.log(formatTestSummary(summary));
@@ -5433,6 +5515,8 @@ ${colors.cyan("\u8BA2\u9605:")}
5433
5515
  ${colors.bold("subscription")} web [name] \u6253\u5F00\u8BA2\u9605\u9875\u9762
5434
5516
  ${colors.bold("subscription")} test [name] \u6D4B\u8BD5\u8282\u70B9\u8FDE\u901A\u6027
5435
5517
  ${colors.bold("subscription")} clean [name] \u6D4B\u901F\u5E76\u6E05\u7406\u5931\u8D25\u8282\u70B9
5518
+ ${colors.bold("test")} [-t ms] [-j N] \u5FEB\u901F\u6D4B\u8BD5\u5F53\u524D\u8282\u70B9\u8FDE\u901A\u6027
5519
+ ${colors.bold("clean")} [-t ms] [-j N] \u6E05\u7406\u5931\u8D25\u8282\u70B9\u5E76\u81EA\u52A8\u91CD\u542F
5436
5520
  ${colors.bold("bench")} [name] [-t ms] [-j N] \u6D4B\u8BD5\u514D\u8D39\u8BA2\u9605\u6E90\u8D28\u91CF\u6392\u540D
5437
5521
 
5438
5522
  ${colors.cyan("\u914D\u7F6E:")}
@@ -5474,8 +5558,8 @@ function printVersion() {
5474
5558
 
5475
5559
  // src/kernel.ts
5476
5560
  import { execSync as execSync4, spawnSync } from "child_process";
5477
- import fs7 from "fs";
5478
- import path5 from "path";
5561
+ import fs8 from "fs";
5562
+ import path6 from "path";
5479
5563
 
5480
5564
  // node_modules/compare-versions/lib/esm/utils.js
5481
5565
  var semver = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
@@ -5600,10 +5684,10 @@ async function checkUpdate(mirror) {
5600
5684
  };
5601
5685
  }
5602
5686
  function findBinaryInDir(dir) {
5603
- const files = fs7.readdirSync(dir);
5687
+ const files = fs8.readdirSync(dir);
5604
5688
  for (const f of files) {
5605
- const fullPath = path5.join(dir, f);
5606
- const stat = fs7.statSync(fullPath);
5689
+ const fullPath = path6.join(dir, f);
5690
+ const stat = fs8.statSync(fullPath);
5607
5691
  if (stat.isDirectory()) {
5608
5692
  const found = findBinaryInDir(fullPath);
5609
5693
  if (found) return found;
@@ -5629,7 +5713,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5629
5713
  \u5E73\u53F0: ${platform}, \u67B6\u6784: ${arch}${hint}`);
5630
5714
  }
5631
5715
  const downloadUrl = withMirror(asset.browser_download_url, mirror);
5632
- const tempPath = path5.join(DIRS.kernel, asset.name);
5716
+ const tempPath = path6.join(DIRS.kernel, asset.name);
5633
5717
  const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
5634
5718
  if (progressCallback) {
5635
5719
  progressCallback(`\u4E0B\u8F7D\u5185\u6838: ${asset.name} (${sizeMB} MB)`);
@@ -5641,12 +5725,12 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5641
5725
  );
5642
5726
  if (curlResult.status !== 0) {
5643
5727
  try {
5644
- fs7.unlinkSync(tempPath);
5728
+ fs8.unlinkSync(tempPath);
5645
5729
  } catch {
5646
5730
  }
5647
5731
  throw new Error(`\u4E0B\u8F7D\u5931\u8D25 (curl \u9000\u51FA\u7801 ${curlResult.status})`);
5648
5732
  }
5649
- if (!fs7.existsSync(tempPath)) {
5733
+ if (!fs8.existsSync(tempPath)) {
5650
5734
  throw new Error("\u4E0B\u8F7D\u5931\u8D25: \u6587\u4EF6\u672A\u751F\u6210");
5651
5735
  }
5652
5736
  if (progressCallback) {
@@ -5658,14 +5742,14 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5658
5742
  if (tempPath.endsWith(".tar.gz") || tempPath.endsWith(".tgz")) {
5659
5743
  execSync4(`tar -xzf "${tempPath}" -C "${extractPath}"`, { stdio: ["pipe", "pipe", "inherit"] });
5660
5744
  } else if (tempPath.endsWith(".gz")) {
5661
- const baseName = path5.basename(tempPath, ".gz");
5662
- const outputPath = path5.join(extractPath, baseName);
5745
+ const baseName = path6.basename(tempPath, ".gz");
5746
+ const outputPath = path6.join(extractPath, baseName);
5663
5747
  execSync4(`gzip -dc "${tempPath}" > "${outputPath}"`, { stdio: ["pipe", "pipe", "inherit"] });
5664
5748
  extractedBinary = outputPath;
5665
5749
  }
5666
5750
  } catch (e) {
5667
5751
  try {
5668
- fs7.unlinkSync(tempPath);
5752
+ fs8.unlinkSync(tempPath);
5669
5753
  } catch {
5670
5754
  }
5671
5755
  throw new Error(`\u89E3\u538B\u5931\u8D25: ${e.message}`);
@@ -5673,25 +5757,25 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5673
5757
  const foundBinary = extractedBinary || findBinaryInDir(extractPath);
5674
5758
  if (!foundBinary) {
5675
5759
  try {
5676
- fs7.unlinkSync(tempPath);
5760
+ fs8.unlinkSync(tempPath);
5677
5761
  } catch {
5678
5762
  }
5679
5763
  throw new Error("\u89E3\u538B\u540E\u672A\u627E\u5230\u53EF\u6267\u884C\u6587\u4EF6");
5680
5764
  }
5681
5765
  const targetPath = PATHS.mihomoBinary;
5682
5766
  if (foundBinary !== targetPath) {
5683
- if (fs7.existsSync(targetPath)) {
5684
- fs7.chmodSync(targetPath, 493);
5767
+ if (fs8.existsSync(targetPath)) {
5768
+ fs8.chmodSync(targetPath, 493);
5685
5769
  try {
5686
- fs7.unlinkSync(targetPath);
5770
+ fs8.unlinkSync(targetPath);
5687
5771
  } catch {
5688
5772
  }
5689
5773
  }
5690
- fs7.renameSync(foundBinary, targetPath);
5774
+ fs8.renameSync(foundBinary, targetPath);
5691
5775
  }
5692
- fs7.chmodSync(targetPath, 493);
5776
+ fs8.chmodSync(targetPath, 493);
5693
5777
  try {
5694
- fs7.unlinkSync(tempPath);
5778
+ fs8.unlinkSync(tempPath);
5695
5779
  } catch {
5696
5780
  }
5697
5781
  clearKernelVersionCache();
@@ -5832,7 +5916,7 @@ function cmdLogs(args) {
5832
5916
  }
5833
5917
 
5834
5918
  // src/commands/overwrite.ts
5835
- import path6 from "path";
5919
+ import path7 from "path";
5836
5920
  function printOverwriteList() {
5837
5921
  const info = listOverwriteFile();
5838
5922
  const statusText = info.enabled ? colors.green("\u5DF2\u542F\u7528") : colors.yellow("\u5DF2\u7981\u7528");
@@ -5842,8 +5926,8 @@ function printOverwriteList() {
5842
5926
  if (info.files.length === 0) {
5843
5927
  console.log("\u6682\u65E0\u8986\u5199\u6587\u4EF6");
5844
5928
  console.log("");
5845
- console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path6.join(info.dir, "overwrite.yaml")}`);
5846
- console.log(` \u6216 ${path6.join(info.dir, "overwrite.dns.yaml")}`);
5929
+ console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path7.join(info.dir, "overwrite.yaml")}`);
5930
+ console.log(` \u6216 ${path7.join(info.dir, "overwrite.dns.yaml")}`);
5847
5931
  console.log("");
5848
5932
  } else {
5849
5933
  console.log(`${colors.cyan("\u8986\u5199\u6587\u4EF6")} (${info.files.length} \u4E2A\uFF0C\u6309\u987A\u5E8F\u52A0\u8F7D):`);
@@ -5907,7 +5991,7 @@ async function cmdOverwrite(args) {
5907
5991
  }
5908
5992
 
5909
5993
  // src/commands/reset.ts
5910
- import fs8 from "fs";
5994
+ import fs9 from "fs";
5911
5995
  import readline from "readline";
5912
5996
  var RESET_TARGETS = [
5913
5997
  {
@@ -5962,8 +6046,8 @@ var RESET_TARGETS = [
5962
6046
  label: "\u8986\u5199",
5963
6047
  paths: () => {
5964
6048
  const dir = USER_DATA_DIR;
5965
- if (!fs8.existsSync(dir)) return [];
5966
- return fs8.readdirSync(dir).filter((f) => f === "overwrite.yaml" || /^overwrite\..+\.ya?ml$/.test(f)).map((f) => `${dir}/${f}`);
6049
+ if (!fs9.existsSync(dir)) return [];
6050
+ return fs9.readdirSync(dir).filter((f) => f === "overwrite.yaml" || /^overwrite\..+\.ya?ml$/.test(f)).map((f) => `${dir}/${f}`);
5967
6051
  },
5968
6052
  needsStop: false
5969
6053
  }
@@ -6079,6 +6163,69 @@ async function cmdStop() {
6079
6163
  console.log(colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B"));
6080
6164
  }
6081
6165
 
6166
+ // src/commands/test.ts
6167
+ function requireRunning() {
6168
+ const status = getStatus();
6169
+ if (!status.running) {
6170
+ console.error("\u9519\u8BEF: mihomo \u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 (mihomo start)");
6171
+ process.exit(1);
6172
+ }
6173
+ }
6174
+ function requireActiveSub() {
6175
+ const activeSub = getActiveSubscription();
6176
+ if (!activeSub) {
6177
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
6178
+ process.exit(1);
6179
+ }
6180
+ return activeSub;
6181
+ }
6182
+ async function cmdTest(args) {
6183
+ requireRunning();
6184
+ const activeSub = requireActiveSub();
6185
+ const timeout = parseIntArg(args, "-t", "--timeout", 1500);
6186
+ const concurrency = parseIntArg(args, "-j", "--concurrency", 100);
6187
+ console.log(`\u6D4B\u8BD5 "${activeSub.name}" \u8282\u70B9\u8FDE\u901A\u6027...`);
6188
+ console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6189
+ console.log("");
6190
+ const summary = await testSubscriptionProxies(activeSub.name, {
6191
+ timeout,
6192
+ concurrency,
6193
+ onResult: printTestResult
6194
+ });
6195
+ console.log("");
6196
+ console.log(formatTestSummary(summary));
6197
+ }
6198
+ async function cmdClean(args) {
6199
+ requireRunning();
6200
+ const activeSub = requireActiveSub();
6201
+ const timeout = parseIntArg(args, "-t", "--timeout", 1500);
6202
+ const concurrency = parseIntArg(args, "-j", "--concurrency", 100);
6203
+ console.log(`\u6E05\u7406 "${activeSub.name}" \u5931\u8D25\u8282\u70B9...`);
6204
+ console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6205
+ console.log("");
6206
+ const result = await autoCleanSubscription(activeSub.name, {
6207
+ timeout,
6208
+ concurrency,
6209
+ onResult: printTestResult
6210
+ });
6211
+ console.log("");
6212
+ console.log(formatTestSummary(result.summary));
6213
+ if (result.skipped) {
6214
+ console.log("");
6215
+ console.log("\u5B58\u6D3B\u8282\u70B9\u4E0D\u8DB3 1%\uFF0C\u8DF3\u8FC7\u6E05\u7406\u3002\u8BF7\u68C0\u67E5\u539F\u59CB\u8BA2\u9605\u662F\u5426\u6709\u6548");
6216
+ } else if (result.removedProxies === 0) {
6217
+ console.log("\u6240\u6709\u8282\u70B9\u6B63\u5E38\uFF0C\u65E0\u9700\u6E05\u7406");
6218
+ } else {
6219
+ console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
6220
+ console.log("");
6221
+ console.log("\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548...");
6222
+ stop();
6223
+ const configInfo = prepareConfigForStart("mixed", activeSub.name);
6224
+ const startResult = await start("mixed");
6225
+ console.log(`${colors.green("\u5DF2\u91CD\u542F")} (PID ${startResult.pid}) \xB7 ${formatProxySummary(configInfo)}`);
6226
+ }
6227
+ }
6228
+
6082
6229
  // src/commands/ui.ts
6083
6230
  function cmdUI(args) {
6084
6231
  const uiName = args[1] || "zash";
@@ -6097,7 +6244,7 @@ function cmdUI(args) {
6097
6244
  }
6098
6245
 
6099
6246
  // src/commands/update.ts
6100
- import { exec, spawn as spawn3 } from "child_process";
6247
+ import { exec, spawn as spawn4 } from "child_process";
6101
6248
  import { promisify } from "util";
6102
6249
  var execAsync = promisify(exec);
6103
6250
  async function cmdUpdate() {
@@ -6106,7 +6253,7 @@ async function cmdUpdate() {
6106
6253
  console.log("\u6B63\u5728\u66F4\u65B0 mihomo-cli...");
6107
6254
  console.log("");
6108
6255
  await new Promise((resolve) => {
6109
- const npm = spawn3("npm", ["install", "-g", "mihomo-cli"], { stdio: "inherit" });
6256
+ const npm = spawn4("npm", ["install", "-g", "mihomo-cli"], { stdio: "inherit" });
6110
6257
  npm.on("close", (code) => {
6111
6258
  if (code === 0) {
6112
6259
  resolve();
@@ -6254,6 +6401,12 @@ async function main() {
6254
6401
  case "bench":
6255
6402
  await cmdBench(args);
6256
6403
  break;
6404
+ case "test":
6405
+ await cmdTest(args);
6406
+ break;
6407
+ case "clean":
6408
+ await cmdClean(args);
6409
+ break;
6257
6410
  default:
6258
6411
  console.error(`\u672A\u77E5\u547D\u4EE4: ${cmd}`);
6259
6412
  console.error('\u4F7F\u7528 "mihomo help" \u67E5\u770B\u5E2E\u52A9');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mihomo-cli",
3
- "version": "2.4.1",
3
+ "version": "2.5.0",
4
4
  "type": "module",
5
5
  "description": "A terminal-based mihomo (Clash.Meta) client for macOS",
6
6
  "bin": {