mihomo-cli 2.4.2 → 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,23 @@
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
+
3
21
  ## [2.4.2] - 2026-05-02
4
22
 
5
23
  ### 改进
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
 
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);
@@ -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();
@@ -4741,9 +4867,9 @@ function printTestResult(result, index, total) {
4741
4867
  const prefix = `[${index + 1}/${total}]`;
4742
4868
  if (result.delay !== null) {
4743
4869
  const delayColor = result.delay < 300 ? colors.green : result.delay < 800 ? colors.yellow : colors.red;
4744
- 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`)}`);
4745
4871
  } else {
4746
- 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")}`);
4747
4873
  }
4748
4874
  }
4749
4875
  function formatCleanSummary(result) {
@@ -4760,7 +4886,7 @@ function githubRepoUrl(rawUrl) {
4760
4886
  if (match) return `https://github.com/${match[1]}`;
4761
4887
  return null;
4762
4888
  }
4763
- function resolveActiveTestTarget(args) {
4889
+ function resolveTestTarget(args) {
4764
4890
  const subs = getSubscriptions();
4765
4891
  if (subs.length === 0) {
4766
4892
  console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
@@ -4769,28 +4895,18 @@ function resolveActiveTestTarget(args) {
4769
4895
  const nameArg = getNonFlagArg(args, 2);
4770
4896
  const timeout = parseIntArg(args, "-t", "--timeout", 2e3);
4771
4897
  const concurrency = parseIntArg(args, "-j", "--concurrency", 100);
4772
- const activeSub = getActiveSubscription();
4773
4898
  let target;
4774
4899
  if (nameArg) {
4775
4900
  const matches = findSubscriptionFuzzy(subs, nameArg);
4776
4901
  target = pickSingleSubscription(matches, nameArg);
4777
4902
  } else {
4903
+ const activeSub = getActiveSubscription();
4778
4904
  if (!activeSub) {
4779
- 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");
4780
4906
  process.exit(1);
4781
4907
  }
4782
4908
  target = activeSub;
4783
4909
  }
4784
- const status = getStatus();
4785
- if (!status.running) {
4786
- console.error("\u9519\u8BEF: mihomo \u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 (mihomo start)");
4787
- process.exit(1);
4788
- }
4789
- if (!activeSub || activeSub.name !== target.name) {
4790
- console.error(`\u9519\u8BEF: \u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605\u662F "${activeSub?.name}"\uFF0C\u4E0D\u662F "${target.name}"`);
4791
- console.log(`\u8BF7\u5148\u5207\u6362: mihomo sub use ${target.name}`);
4792
- process.exit(1);
4793
- }
4794
4910
  return { target, timeout, concurrency };
4795
4911
  }
4796
4912
  async function printSubscriptionList(options) {
@@ -4899,36 +5015,6 @@ async function addFreeSubscription(freeId) {
4899
5015
  console.log("");
4900
5016
  await printSubscriptionList();
4901
5017
  }
4902
- function printBestSourceList() {
4903
- const bestSources = getBestSubscriptionSources();
4904
- for (let i = 0; i < bestSources.length; i++) {
4905
- console.log(` ${i + 1} ${bestSources[i].name} \u2014 ${bestSources[i].description}`);
4906
- }
4907
- }
4908
- async function addBestSubscription(bestId) {
4909
- const bestSources = getBestSubscriptionSources();
4910
- if (bestId < 1 || bestId > bestSources.length) {
4911
- console.error(`\u9519\u8BEF: best \u8BA2\u9605 ID \u8303\u56F4 1-${bestSources.length}`);
4912
- console.log("\n\u53EF\u7528\u6E90:");
4913
- printBestSourceList();
4914
- process.exit(1);
4915
- }
4916
- const source = bestSources[bestId - 1];
4917
- const name = `best${bestId}`;
4918
- console.log(`\u6DFB\u52A0 best \u8BA2\u9605: ${name} (${source.description})`);
4919
- try {
4920
- addSubscription(source.url, name);
4921
- const info = await downloadSubscription(source.url, name);
4922
- setDefaultSubscription(name);
4923
- saveSubscriptionCache(name, { web_page_url: "https://github.com/imaex/mihomo-free-sub" });
4924
- console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
4925
- } catch (e) {
4926
- console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
4927
- process.exit(1);
4928
- }
4929
- console.log("");
4930
- await printSubscriptionList();
4931
- }
4932
5018
  async function cmdSubscription(args) {
4933
5019
  const action = args[1];
4934
5020
  if (!action || action === "list") {
@@ -4946,17 +5032,6 @@ async function cmdSubscription(args) {
4946
5032
  await addFreeSubscription(id);
4947
5033
  return;
4948
5034
  }
4949
- if (action === "best") {
4950
- const id = parseInt(args[2], 10);
4951
- if (Number.isNaN(id)) {
4952
- console.log("\u7528\u6CD5: mihomo sub best <id>\n");
4953
- console.log("\u53EF\u7528\u6E90:");
4954
- printBestSourceList();
4955
- process.exit(1);
4956
- }
4957
- await addBestSubscription(id);
4958
- return;
4959
- }
4960
5035
  if (action === "add") {
4961
5036
  const freeId = parseIntArg(args, "--free", "--free", -1);
4962
5037
  if (freeId > 0) {
@@ -5155,14 +5230,17 @@ async function cmdSubscription(args) {
5155
5230
  return;
5156
5231
  }
5157
5232
  if (action === "clean") {
5158
- const { target, timeout, concurrency } = resolveActiveTestTarget(args);
5233
+ const { target, timeout, concurrency } = resolveTestTarget(args);
5159
5234
  console.log(`\u6E05\u7406\u8BA2\u9605 "${target.name}"...`);
5160
5235
  console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
5161
5236
  console.log("");
5162
- const result = await autoCleanSubscription(target.name, {
5163
- timeout,
5164
- concurrency,
5165
- 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
+ });
5166
5244
  });
5167
5245
  console.log("");
5168
5246
  console.log(formatTestSummary(result.summary));
@@ -5171,20 +5249,26 @@ async function cmdSubscription(args) {
5171
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"));
5172
5250
  } else if (result.removedProxies > 0) {
5173
5251
  console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
5174
- console.log("");
5175
- 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
+ }
5176
5257
  }
5177
5258
  return;
5178
5259
  }
5179
5260
  if (action === "test") {
5180
- const { target, timeout, concurrency } = resolveActiveTestTarget(args);
5261
+ const { target, timeout, concurrency } = resolveTestTarget(args);
5181
5262
  console.log(`\u6D4B\u8BD5\u8BA2\u9605 "${target.name}" \u7684\u8282\u70B9\u8FDE\u901A\u6027...`);
5182
5263
  console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
5183
5264
  console.log("");
5184
- const summary = await testSubscriptionProxies(target.name, {
5185
- timeout,
5186
- concurrency,
5187
- 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
+ });
5188
5272
  });
5189
5273
  console.log("");
5190
5274
  console.log(formatTestSummary(summary));
@@ -5431,6 +5515,8 @@ ${colors.cyan("\u8BA2\u9605:")}
5431
5515
  ${colors.bold("subscription")} web [name] \u6253\u5F00\u8BA2\u9605\u9875\u9762
5432
5516
  ${colors.bold("subscription")} test [name] \u6D4B\u8BD5\u8282\u70B9\u8FDE\u901A\u6027
5433
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
5434
5520
  ${colors.bold("bench")} [name] [-t ms] [-j N] \u6D4B\u8BD5\u514D\u8D39\u8BA2\u9605\u6E90\u8D28\u91CF\u6392\u540D
5435
5521
 
5436
5522
  ${colors.cyan("\u914D\u7F6E:")}
@@ -5472,8 +5558,8 @@ function printVersion() {
5472
5558
 
5473
5559
  // src/kernel.ts
5474
5560
  import { execSync as execSync4, spawnSync } from "child_process";
5475
- import fs7 from "fs";
5476
- import path5 from "path";
5561
+ import fs8 from "fs";
5562
+ import path6 from "path";
5477
5563
 
5478
5564
  // node_modules/compare-versions/lib/esm/utils.js
5479
5565
  var semver = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
@@ -5598,10 +5684,10 @@ async function checkUpdate(mirror) {
5598
5684
  };
5599
5685
  }
5600
5686
  function findBinaryInDir(dir) {
5601
- const files = fs7.readdirSync(dir);
5687
+ const files = fs8.readdirSync(dir);
5602
5688
  for (const f of files) {
5603
- const fullPath = path5.join(dir, f);
5604
- const stat = fs7.statSync(fullPath);
5689
+ const fullPath = path6.join(dir, f);
5690
+ const stat = fs8.statSync(fullPath);
5605
5691
  if (stat.isDirectory()) {
5606
5692
  const found = findBinaryInDir(fullPath);
5607
5693
  if (found) return found;
@@ -5627,7 +5713,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5627
5713
  \u5E73\u53F0: ${platform}, \u67B6\u6784: ${arch}${hint}`);
5628
5714
  }
5629
5715
  const downloadUrl = withMirror(asset.browser_download_url, mirror);
5630
- const tempPath = path5.join(DIRS.kernel, asset.name);
5716
+ const tempPath = path6.join(DIRS.kernel, asset.name);
5631
5717
  const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
5632
5718
  if (progressCallback) {
5633
5719
  progressCallback(`\u4E0B\u8F7D\u5185\u6838: ${asset.name} (${sizeMB} MB)`);
@@ -5639,12 +5725,12 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5639
5725
  );
5640
5726
  if (curlResult.status !== 0) {
5641
5727
  try {
5642
- fs7.unlinkSync(tempPath);
5728
+ fs8.unlinkSync(tempPath);
5643
5729
  } catch {
5644
5730
  }
5645
5731
  throw new Error(`\u4E0B\u8F7D\u5931\u8D25 (curl \u9000\u51FA\u7801 ${curlResult.status})`);
5646
5732
  }
5647
- if (!fs7.existsSync(tempPath)) {
5733
+ if (!fs8.existsSync(tempPath)) {
5648
5734
  throw new Error("\u4E0B\u8F7D\u5931\u8D25: \u6587\u4EF6\u672A\u751F\u6210");
5649
5735
  }
5650
5736
  if (progressCallback) {
@@ -5656,14 +5742,14 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5656
5742
  if (tempPath.endsWith(".tar.gz") || tempPath.endsWith(".tgz")) {
5657
5743
  execSync4(`tar -xzf "${tempPath}" -C "${extractPath}"`, { stdio: ["pipe", "pipe", "inherit"] });
5658
5744
  } else if (tempPath.endsWith(".gz")) {
5659
- const baseName = path5.basename(tempPath, ".gz");
5660
- const outputPath = path5.join(extractPath, baseName);
5745
+ const baseName = path6.basename(tempPath, ".gz");
5746
+ const outputPath = path6.join(extractPath, baseName);
5661
5747
  execSync4(`gzip -dc "${tempPath}" > "${outputPath}"`, { stdio: ["pipe", "pipe", "inherit"] });
5662
5748
  extractedBinary = outputPath;
5663
5749
  }
5664
5750
  } catch (e) {
5665
5751
  try {
5666
- fs7.unlinkSync(tempPath);
5752
+ fs8.unlinkSync(tempPath);
5667
5753
  } catch {
5668
5754
  }
5669
5755
  throw new Error(`\u89E3\u538B\u5931\u8D25: ${e.message}`);
@@ -5671,25 +5757,25 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5671
5757
  const foundBinary = extractedBinary || findBinaryInDir(extractPath);
5672
5758
  if (!foundBinary) {
5673
5759
  try {
5674
- fs7.unlinkSync(tempPath);
5760
+ fs8.unlinkSync(tempPath);
5675
5761
  } catch {
5676
5762
  }
5677
5763
  throw new Error("\u89E3\u538B\u540E\u672A\u627E\u5230\u53EF\u6267\u884C\u6587\u4EF6");
5678
5764
  }
5679
5765
  const targetPath = PATHS.mihomoBinary;
5680
5766
  if (foundBinary !== targetPath) {
5681
- if (fs7.existsSync(targetPath)) {
5682
- fs7.chmodSync(targetPath, 493);
5767
+ if (fs8.existsSync(targetPath)) {
5768
+ fs8.chmodSync(targetPath, 493);
5683
5769
  try {
5684
- fs7.unlinkSync(targetPath);
5770
+ fs8.unlinkSync(targetPath);
5685
5771
  } catch {
5686
5772
  }
5687
5773
  }
5688
- fs7.renameSync(foundBinary, targetPath);
5774
+ fs8.renameSync(foundBinary, targetPath);
5689
5775
  }
5690
- fs7.chmodSync(targetPath, 493);
5776
+ fs8.chmodSync(targetPath, 493);
5691
5777
  try {
5692
- fs7.unlinkSync(tempPath);
5778
+ fs8.unlinkSync(tempPath);
5693
5779
  } catch {
5694
5780
  }
5695
5781
  clearKernelVersionCache();
@@ -5830,7 +5916,7 @@ function cmdLogs(args) {
5830
5916
  }
5831
5917
 
5832
5918
  // src/commands/overwrite.ts
5833
- import path6 from "path";
5919
+ import path7 from "path";
5834
5920
  function printOverwriteList() {
5835
5921
  const info = listOverwriteFile();
5836
5922
  const statusText = info.enabled ? colors.green("\u5DF2\u542F\u7528") : colors.yellow("\u5DF2\u7981\u7528");
@@ -5840,8 +5926,8 @@ function printOverwriteList() {
5840
5926
  if (info.files.length === 0) {
5841
5927
  console.log("\u6682\u65E0\u8986\u5199\u6587\u4EF6");
5842
5928
  console.log("");
5843
- console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path6.join(info.dir, "overwrite.yaml")}`);
5844
- 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")}`);
5845
5931
  console.log("");
5846
5932
  } else {
5847
5933
  console.log(`${colors.cyan("\u8986\u5199\u6587\u4EF6")} (${info.files.length} \u4E2A\uFF0C\u6309\u987A\u5E8F\u52A0\u8F7D):`);
@@ -5905,7 +5991,7 @@ async function cmdOverwrite(args) {
5905
5991
  }
5906
5992
 
5907
5993
  // src/commands/reset.ts
5908
- import fs8 from "fs";
5994
+ import fs9 from "fs";
5909
5995
  import readline from "readline";
5910
5996
  var RESET_TARGETS = [
5911
5997
  {
@@ -5960,8 +6046,8 @@ var RESET_TARGETS = [
5960
6046
  label: "\u8986\u5199",
5961
6047
  paths: () => {
5962
6048
  const dir = USER_DATA_DIR;
5963
- if (!fs8.existsSync(dir)) return [];
5964
- 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}`);
5965
6051
  },
5966
6052
  needsStop: false
5967
6053
  }
@@ -6077,6 +6163,69 @@ async function cmdStop() {
6077
6163
  console.log(colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B"));
6078
6164
  }
6079
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
+
6080
6229
  // src/commands/ui.ts
6081
6230
  function cmdUI(args) {
6082
6231
  const uiName = args[1] || "zash";
@@ -6095,7 +6244,7 @@ function cmdUI(args) {
6095
6244
  }
6096
6245
 
6097
6246
  // src/commands/update.ts
6098
- import { exec, spawn as spawn3 } from "child_process";
6247
+ import { exec, spawn as spawn4 } from "child_process";
6099
6248
  import { promisify } from "util";
6100
6249
  var execAsync = promisify(exec);
6101
6250
  async function cmdUpdate() {
@@ -6104,7 +6253,7 @@ async function cmdUpdate() {
6104
6253
  console.log("\u6B63\u5728\u66F4\u65B0 mihomo-cli...");
6105
6254
  console.log("");
6106
6255
  await new Promise((resolve) => {
6107
- const npm = spawn3("npm", ["install", "-g", "mihomo-cli"], { stdio: "inherit" });
6256
+ const npm = spawn4("npm", ["install", "-g", "mihomo-cli"], { stdio: "inherit" });
6108
6257
  npm.on("close", (code) => {
6109
6258
  if (code === 0) {
6110
6259
  resolve();
@@ -6252,6 +6401,12 @@ async function main() {
6252
6401
  case "bench":
6253
6402
  await cmdBench(args);
6254
6403
  break;
6404
+ case "test":
6405
+ await cmdTest(args);
6406
+ break;
6407
+ case "clean":
6408
+ await cmdClean(args);
6409
+ break;
6255
6410
  default:
6256
6411
  console.error(`\u672A\u77E5\u547D\u4EE4: ${cmd}`);
6257
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.2",
3
+ "version": "2.5.0",
4
4
  "type": "module",
5
5
  "description": "A terminal-based mihomo (Clash.Meta) client for macOS",
6
6
  "bin": {