mihomo-cli 2.0.1 → 2.2.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,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.2.0] - 2026-05-01
4
+
5
+ ### 新增
6
+
7
+ - **节点测速**:`sub test [name]` 测试订阅节点连通性,支持 `-t` 超时和 `-j` 并发参数
8
+ - **节点清理**:`sub clean [name]` 测速后自动清理不可用节点,移除空分组
9
+ - **启动自动清理**:`start` / `start tun` 启动时,节点数超过 100 自动执行清理
10
+
11
+ ### 安全
12
+
13
+ - **强制端口配置**:HTTP 端口固定 7890,SOCKS5 端口固定 7891,忽略订阅中的 `mixed-port` 配置
14
+
15
+ ---
16
+
17
+ ## [2.1.0] - 2026-05-01
18
+
19
+ ### 新增
20
+
21
+ - **删除订阅**:`sub remove <name>` 删除订阅(别名 `rm`/`delete`),同时清理缓存和配置文件
22
+ - 删除当前使用中的订阅时自动切换到第一个剩余订阅
23
+ - **添加即切换**:`sub add` 添加订阅后自动切换为当前使用的订阅
24
+
25
+ ### 安全
26
+
27
+ - **强制 `allow-lan: false`**:无论订阅配置如何,始终禁止局域网访问
28
+ - **强制 `external-controller: 127.0.0.1:9090`**:控制面板仅监听本地,防止不可信订阅暴露控制接口
29
+ - **剥离 `external-ui` 相关字段**:构建配置时强制删除 `external-ui`/`external-ui-name`/`external-ui-url`,防止订阅触发额外下载
30
+
31
+ ### 优化
32
+
33
+ - **TUN DNS 劫持**:`dns-hijack` 从 `['0.0.0.0:53']` 改为 `['any:53', 'tcp://any:53']`,同时劫持 UDP 和 TCP DNS,覆盖 IPv4/IPv6
34
+ - **帮助顺序统一**:订阅子命令统一为 use → add → update → remove → web 顺序
35
+ - **`removeSubscription` 返回切换信息**:返回自动切换到的订阅名,避免调用方重复读取状态
36
+ - **`setDefaultSubscription` 跳过冗余写入**:已是同值时直接返回
37
+ - **删除后跳过自动更新**:`sub remove` 后列出订阅时不触发网络更新
38
+
39
+ ---
40
+
3
41
  ## [2.0.1] - 2026-04-22
4
42
 
5
43
  ### 修复
package/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  - 🌐 **订阅管理** - 添加/更新订阅,支持流量统计和到期时间显示
8
8
  - 🔄 **自动更新** - 启动时自动检查并更新过期订阅
9
9
  - 🔍 **模糊匹配** - `sub use` / `sub web` 支持订阅名称模糊匹配
10
+ - 🧹 **节点测速清理** - `sub test` 测试连通性,`sub clean` 自动清理失败节点,启动时超过 100 节点自动清理
10
11
  - 📝 **覆写配置** - 在订阅基础上进行自定义覆写,支持强制覆盖、数组合并
11
12
  - 🔄 **智能重启** - `sub use` 切换订阅、`ow on/off` 切换覆写后自动重启
12
13
  - 🚀 **进程管理** - 启动/停止/切换模式,自动清理残留进程
@@ -94,11 +95,14 @@ mihomo ui yacd # YACD
94
95
  | 命令 | 说明 |
95
96
  | ----------------------------- | -------------------------------------- |
96
97
  | `mihomo sub` | 列出所有订阅(含流量、到期时间) |
97
- | `mihomo sub add <url> [name]` | 添加订阅 |
98
+ | `mihomo sub use <name>` | 切换当前订阅(支持模糊匹配,自动重启) |
99
+ | `mihomo sub add <url> [name]` | 添加订阅并自动切换 |
98
100
  | `mihomo sub update` | 更新所有订阅 |
99
101
  | `mihomo sub update <name>` | 更新指定订阅(支持模糊匹配) |
100
- | `mihomo sub use <name>` | 切换当前订阅(支持模糊匹配,自动重启) |
102
+ | `mihomo sub remove <name>` | 删除订阅(支持模糊匹配) |
101
103
  | `mihomo sub web [name]` | 打开订阅页面(无参打开默认) |
104
+ | `mihomo sub test [name]` | 测试节点连通性(`-t` 超时,`-j` 并发) |
105
+ | `mihomo sub clean [name]` | 测速并清理失败节点 |
102
106
 
103
107
  ### 覆写配置
104
108
 
@@ -289,9 +293,10 @@ sudo pkill -9 mihomo
289
293
 
290
294
  ### 端口被占用
291
295
 
292
- 默认端口(取决于订阅配置):
296
+ 默认端口(系统强制,不受订阅配置影响):
293
297
 
294
- - Mixed 端口: `7890`
298
+ - HTTP 端口: `7890`
299
+ - SOCKS5 端口: `7891`
295
300
  - 外部控制器: `127.0.0.1:9090`
296
301
 
297
302
  ## 安全特性
package/dist/index.js CHANGED
@@ -2695,13 +2695,17 @@ var TUN_CONFIG = {
2695
2695
  tun: {
2696
2696
  enable: true,
2697
2697
  stack: "mixed",
2698
- "dns-hijack": ["0.0.0.0:53"],
2698
+ "dns-hijack": ["any:53", "tcp://any:53"],
2699
2699
  "auto-route": true,
2700
2700
  "auto-detect-interface": true,
2701
2701
  "strict-route": true
2702
2702
  }
2703
2703
  };
2704
2704
  var BASE_CONFIG = {
2705
+ "allow-lan": false,
2706
+ "external-controller": "127.0.0.1:9090",
2707
+ port: 7890,
2708
+ "socks-port": 7891,
2705
2709
  "log-level": "warning",
2706
2710
  "geodata-mode": true,
2707
2711
  "geo-update-interval": 24,
@@ -2819,11 +2823,33 @@ function addSubscription(url, name = "default") {
2819
2823
  }
2820
2824
  writeSettings(updates);
2821
2825
  }
2826
+ function removeSubscription(name) {
2827
+ const settings = readSettings();
2828
+ const subs = settings.subscriptions || [];
2829
+ const idx = subs.findIndex((s) => s.name === name);
2830
+ if (idx < 0) return null;
2831
+ subs.splice(idx, 1);
2832
+ const updates = { subscriptions: subs };
2833
+ let switchedTo = null;
2834
+ if (settings.active_subscription === name) {
2835
+ switchedTo = subs.length > 0 ? subs[0].name : null;
2836
+ updates.active_subscription = switchedTo ?? void 0;
2837
+ }
2838
+ writeSettings(updates);
2839
+ const cache = readSubscriptionCache();
2840
+ if (cache[name]) {
2841
+ delete cache[name];
2842
+ writeSubscriptionCache(cache);
2843
+ }
2844
+ fs2.rmSync(getSubscriptionRawConfigPath(name), { force: true });
2845
+ return switchedTo;
2846
+ }
2822
2847
  function setDefaultSubscription(name) {
2823
2848
  const settings = readSettings();
2824
2849
  const subs = settings.subscriptions || [];
2825
2850
  const idx = subs.findIndex((s) => s.name === name);
2826
2851
  if (idx < 0) return false;
2852
+ if (settings.active_subscription === name) return true;
2827
2853
  writeSettings({ active_subscription: name });
2828
2854
  return true;
2829
2855
  }
@@ -3003,6 +3029,14 @@ function buildConfig(subRawContent, mode) {
3003
3029
  systemConfig[key] = value;
3004
3030
  }
3005
3031
  }
3032
+ systemConfig["allow-lan"] = false;
3033
+ systemConfig["external-controller"] = BASE_CONFIG["external-controller"];
3034
+ systemConfig.port = BASE_CONFIG.port;
3035
+ systemConfig["socks-port"] = BASE_CONFIG["socks-port"];
3036
+ delete withOverwrites["mixed-port"];
3037
+ delete withOverwrites["external-ui"];
3038
+ delete withOverwrites["external-ui-name"];
3039
+ delete withOverwrites["external-ui-url"];
3006
3040
  if (mode === "tun") {
3007
3041
  systemConfig.tun = TUN_CONFIG.tun;
3008
3042
  const subDns = withOverwrites.dns || {};
@@ -3116,6 +3150,9 @@ var colors = {
3116
3150
  function sleepSync(ms) {
3117
3151
  Atomics.wait(sleepBuf, 0, 0, ms);
3118
3152
  }
3153
+ function sleep(ms) {
3154
+ return new Promise((resolve) => setTimeout(resolve, ms));
3155
+ }
3119
3156
  function formatBytes(bytes) {
3120
3157
  if (bytes === void 0 || bytes === null) return "\u672A\u77E5";
3121
3158
  const num = Number(bytes);
@@ -3833,10 +3870,13 @@ ${colors.cyan("\u754C\u9762:")}
3833
3870
 
3834
3871
  ${colors.cyan("\u8BA2\u9605:")}
3835
3872
  ${colors.bold("subscription")} \u5217\u51FA\u6240\u6709\u8BA2\u9605\uFF08\u522B\u540D sub\uFF09
3873
+ ${colors.bold("subscription")} use <name> \u5207\u6362\u5F53\u524D\u8BA2\u9605
3836
3874
  ${colors.bold("subscription")} add <url> [name] \u6DFB\u52A0\u8BA2\u9605
3837
3875
  ${colors.bold("subscription")} update [name] \u66F4\u65B0\u8BA2\u9605\uFF08\u65E0\u53C2\u66F4\u65B0\u6240\u6709\uFF09
3838
- ${colors.bold("subscription")} use <name> \u5207\u6362\u5F53\u524D\u8BA2\u9605
3876
+ ${colors.bold("subscription")} remove <name> \u5220\u9664\u8BA2\u9605
3839
3877
  ${colors.bold("subscription")} web [name] \u6253\u5F00\u8BA2\u9605\u9875\u9762
3878
+ ${colors.bold("subscription")} test [name] \u6D4B\u8BD5\u8282\u70B9\u8FDE\u901A\u6027
3879
+ ${colors.bold("subscription")} clean [name] \u6D4B\u901F\u5E76\u6E05\u7406\u5931\u8D25\u8282\u70B9
3840
3880
 
3841
3881
  ${colors.cyan("\u914D\u7F6E:")}
3842
3882
  ${colors.bold("overwrite")} \u67E5\u770B\u8986\u5199\u72B6\u6001\uFF08\u522B\u540D ow\uFF09
@@ -4238,7 +4278,25 @@ import path5 from "path";
4238
4278
 
4239
4279
  // src/subscription.ts
4240
4280
  var DEFAULT_UPDATE_INTERVAL_HOURS = 12;
4281
+ var YAML_DUMP_OPTS = { indent: 2, lineWidth: -1, noCompatMode: true };
4241
4282
  var HTTP_CLIENT2 = createHttpClient({ timeout: 6e4 });
4283
+ function loadSubscriptionConfig(subName) {
4284
+ const rawContent = readSubscriptionRawConfig(subName);
4285
+ if (!rawContent) {
4286
+ throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
4287
+ }
4288
+ const raw = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
4289
+ return {
4290
+ raw,
4291
+ proxies: raw.proxies || [],
4292
+ proxyGroups: raw["proxy-groups"] || []
4293
+ };
4294
+ }
4295
+ function saveSubscriptionConfig(subName, parsed) {
4296
+ parsed.raw.proxies = parsed.proxies;
4297
+ parsed.raw["proxy-groups"] = parsed.proxyGroups;
4298
+ saveSubscriptionRawConfig(subName, jsYaml.dump(parsed.raw, YAML_DUMP_OPTS));
4299
+ }
4242
4300
  function parseUserInfo(header) {
4243
4301
  if (!header) return null;
4244
4302
  const info = {};
@@ -4413,6 +4471,122 @@ async function autoUpdateStaleSubscription() {
4413
4471
  }
4414
4472
  return { total: staleSubs.length, updated: updatedCount, failed: staleSubs.length - updatedCount };
4415
4473
  }
4474
+ var API_BASE = `http://${BASE_CONFIG["external-controller"]}`;
4475
+ var DEFAULT_TEST_URL = "http://www.gstatic.com/generate_204";
4476
+ async function testProxyDelay(proxyName, timeout, testUrl, client) {
4477
+ const encodedName = encodeURIComponent(proxyName);
4478
+ const url = `${API_BASE}/proxies/${encodedName}/delay?timeout=${timeout}&url=${encodeURIComponent(testUrl)}`;
4479
+ try {
4480
+ const response = await client.get(url);
4481
+ const data = JSON.parse(response.data);
4482
+ if (data.delay && data.delay > 0) {
4483
+ return { name: proxyName, delay: data.delay };
4484
+ }
4485
+ return { name: proxyName, delay: null, error: data.message || "no delay" };
4486
+ } catch (e) {
4487
+ const err = e;
4488
+ let errorMsg = "timeout";
4489
+ if (err.response?.data?.message) {
4490
+ errorMsg = String(err.response.data.message);
4491
+ } else if (err.message) {
4492
+ errorMsg = err.message;
4493
+ }
4494
+ return { name: proxyName, delay: null, error: errorMsg };
4495
+ }
4496
+ }
4497
+ async function testSubscriptionProxies(subName, options = {}) {
4498
+ const { timeout = 2e3, concurrency = 100, testUrl = DEFAULT_TEST_URL, onResult } = options;
4499
+ const { proxies } = options.parsed || loadSubscriptionConfig(subName);
4500
+ if (proxies.length === 0) {
4501
+ return { total: 0, alive: 0, dead: 0, results: [] };
4502
+ }
4503
+ const client = createHttpClient({ timeout: timeout + 3e3 });
4504
+ const results = [];
4505
+ let completedCount = 0;
4506
+ for (let i = 0; i < proxies.length; i += concurrency) {
4507
+ const batch = proxies.slice(i, i + concurrency);
4508
+ const batchResults = await Promise.all(batch.map((proxy) => testProxyDelay(proxy.name, timeout, testUrl, client)));
4509
+ for (const result of batchResults) {
4510
+ results.push(result);
4511
+ onResult?.(result, completedCount, proxies.length);
4512
+ completedCount++;
4513
+ }
4514
+ }
4515
+ const alive = results.filter((r) => r.delay !== null).length;
4516
+ return { total: results.length, alive, dead: results.length - alive, results };
4517
+ }
4518
+ function shortenProxyNames(parsed) {
4519
+ const { proxies, proxyGroups } = parsed;
4520
+ const renameMap = /* @__PURE__ */ new Map();
4521
+ const usedNames = /* @__PURE__ */ new Set();
4522
+ for (const proxy of proxies) {
4523
+ const shortened = proxy.name.replace(/_github\.com\/[^_]+/, "");
4524
+ if (shortened !== proxy.name && !usedNames.has(shortened)) {
4525
+ renameMap.set(proxy.name, shortened);
4526
+ usedNames.add(shortened);
4527
+ } else {
4528
+ usedNames.add(proxy.name);
4529
+ }
4530
+ }
4531
+ if (renameMap.size === 0) return 0;
4532
+ for (const proxy of proxies) {
4533
+ const newName = renameMap.get(proxy.name);
4534
+ if (newName) proxy.name = newName;
4535
+ }
4536
+ for (const group of proxyGroups) {
4537
+ if (Array.isArray(group.proxies)) {
4538
+ group.proxies = group.proxies.map((name) => renameMap.get(name) || name);
4539
+ }
4540
+ }
4541
+ return renameMap.size;
4542
+ }
4543
+ function cleanDeadProxies(parsed, deadNames) {
4544
+ const { proxies, proxyGroups } = parsed;
4545
+ const originalCount = proxies.length;
4546
+ parsed.proxies = proxies.filter((p) => !deadNames.has(p.name));
4547
+ const removedProxies = originalCount - parsed.proxies.length;
4548
+ let updatedGroups = 0;
4549
+ const removedGroupNames = /* @__PURE__ */ new Set();
4550
+ for (const group of proxyGroups) {
4551
+ if (Array.isArray(group.proxies)) {
4552
+ const before = group.proxies.length;
4553
+ group.proxies = group.proxies.filter((name) => !deadNames.has(name));
4554
+ if (group.proxies.length < before) {
4555
+ updatedGroups++;
4556
+ }
4557
+ if (group.proxies.length === 0) {
4558
+ removedGroupNames.add(group.name);
4559
+ }
4560
+ }
4561
+ }
4562
+ if (removedGroupNames.size > 0) {
4563
+ parsed.proxyGroups = proxyGroups.filter((g) => !removedGroupNames.has(g.name));
4564
+ for (const group of parsed.proxyGroups) {
4565
+ if (Array.isArray(group.proxies)) {
4566
+ group.proxies = group.proxies.filter((name) => !removedGroupNames.has(name));
4567
+ }
4568
+ }
4569
+ }
4570
+ return { removedProxies, updatedGroups, removedGroups: removedGroupNames.size };
4571
+ }
4572
+ async function autoCleanSubscription(subName, options = {}) {
4573
+ const parsed = loadSubscriptionConfig(subName);
4574
+ shortenProxyNames(parsed);
4575
+ saveSubscriptionConfig(subName, parsed);
4576
+ const summary = await testSubscriptionProxies(subName, { ...options, parsed });
4577
+ let removedProxies = 0;
4578
+ let updatedGroups = 0;
4579
+ let removedGroups = 0;
4580
+ if (summary.dead > 0) {
4581
+ const deadNames = new Set(summary.results.filter((r) => r.delay === null).map((r) => r.name));
4582
+ const cleanResult = cleanDeadProxies(parsed, deadNames);
4583
+ removedProxies = cleanResult.removedProxies;
4584
+ updatedGroups = cleanResult.updatedGroups;
4585
+ removedGroups = cleanResult.removedGroups;
4586
+ saveSubscriptionConfig(subName, parsed);
4587
+ }
4588
+ return { summary, removedProxies, updatedGroups, removedGroups };
4589
+ }
4416
4590
 
4417
4591
  // src/commands/status.ts
4418
4592
  function printStatus() {
@@ -4465,7 +4639,316 @@ function printStatus() {
4465
4639
  console.log("");
4466
4640
  }
4467
4641
 
4642
+ // src/commands/subscription.ts
4643
+ function printTestResult(result, index, total) {
4644
+ const prefix = `[${index + 1}/${total}]`;
4645
+ if (result.delay !== null) {
4646
+ const delayColor = result.delay < 300 ? colors.green : result.delay < 800 ? colors.yellow : colors.red;
4647
+ console.log(` ${prefix} ${colors.green("\u2713")} ${result.name} ${delayColor(`${result.delay}ms`)}`);
4648
+ } else {
4649
+ console.log(` ${prefix} ${colors.red("\u2717")} ${result.name} ${colors.gray(result.error || "timeout")}`);
4650
+ }
4651
+ }
4652
+ function formatCleanSummary(result) {
4653
+ const parts = [`\u79FB\u9664 ${result.removedProxies} \u4E2A\u8282\u70B9`];
4654
+ if (result.removedGroups > 0) parts.push(`\u5220\u9664 ${result.removedGroups} \u4E2A\u7A7A\u5206\u7EC4`);
4655
+ if (result.updatedGroups > 0) parts.push(`\u66F4\u65B0 ${result.updatedGroups} \u4E2A\u5206\u7EC4`);
4656
+ return parts.join(", ");
4657
+ }
4658
+ function formatTestSummary(summary) {
4659
+ return `\u7ED3\u679C: ${colors.green(`${summary.alive} \u5B58\u6D3B`)} / ${colors.red(`${summary.dead} \u5931\u8D25`)} / ${summary.total} \u603B\u8BA1`;
4660
+ }
4661
+ function resolveActiveTestTarget(args) {
4662
+ const subs = getSubscriptions();
4663
+ if (subs.length === 0) {
4664
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
4665
+ process.exit(1);
4666
+ }
4667
+ const nameArg = getNonFlagArg(args, 2);
4668
+ const timeout = parseIntArg(args, "-t", "--timeout", 2e3);
4669
+ const concurrency = parseIntArg(args, "-j", "--concurrency", 100);
4670
+ const activeSub = getActiveSubscription();
4671
+ let target;
4672
+ if (nameArg) {
4673
+ const matches = findSubscriptionFuzzy(subs, nameArg);
4674
+ target = pickSingleSubscription(matches, nameArg);
4675
+ } else {
4676
+ if (!activeSub) {
4677
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
4678
+ process.exit(1);
4679
+ }
4680
+ target = activeSub;
4681
+ }
4682
+ const status = getStatus();
4683
+ if (!status.running) {
4684
+ console.error("\u9519\u8BEF: mihomo \u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 (mihomo start)");
4685
+ process.exit(1);
4686
+ }
4687
+ if (!activeSub || activeSub.name !== target.name) {
4688
+ console.error(`\u9519\u8BEF: \u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605\u662F "${activeSub?.name}"\uFF0C\u4E0D\u662F "${target.name}"`);
4689
+ console.log(`\u8BF7\u5148\u5207\u6362: mihomo sub use ${target.name}`);
4690
+ process.exit(1);
4691
+ }
4692
+ return { target, timeout, concurrency };
4693
+ }
4694
+ async function printSubscriptionList(options) {
4695
+ if (options?.autoUpdate !== false) {
4696
+ const updateResult = await autoUpdateStaleSubscription();
4697
+ if (updateResult.total > 0) console.log("");
4698
+ }
4699
+ const subs = getSubscriptionsWithCache();
4700
+ if (subs.length === 0) {
4701
+ console.log("\u6CA1\u6709\u8BA2\u9605");
4702
+ console.log("");
4703
+ console.log("\u6DFB\u52A0\u8BA2\u9605: mihomo sub add <url> [name]");
4704
+ console.log("");
4705
+ return;
4706
+ }
4707
+ const activeSub = getActiveSubscription();
4708
+ console.log(colors.cyan("\u8BA2\u9605\u5217\u8868:"));
4709
+ subs.forEach((s, i) => {
4710
+ const time = formatDate(s.updated_at);
4711
+ const defaultMark = activeSub && s.name === activeSub.name ? colors.green(" [\u4F7F\u7528\u4E2D]") : "";
4712
+ const interval = s.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
4713
+ console.log(` ${i + 1}. ${s.name}${defaultMark}`);
4714
+ console.log(` ${colors.gray("\u66F4\u65B0: ")}${time} (\u95F4\u9694: ${interval}h)`);
4715
+ if (s.username) {
4716
+ console.log(` ${colors.gray("\u7528\u6237: ")}${s.username}`);
4717
+ }
4718
+ if (s.download !== void 0 || s.total !== void 0) {
4719
+ const used = (s.upload || 0) + (s.download || 0);
4720
+ const usedStr = formatBytes(used);
4721
+ const totalStr = formatBytes(s.total);
4722
+ let percentStr = "";
4723
+ if (s.total && s.total > 0) {
4724
+ const percent = Math.min(used / s.total * 100, 100);
4725
+ percentStr = ` (${percent.toFixed(1)}%)`;
4726
+ }
4727
+ console.log(` ${colors.gray("\u6D41\u91CF: ")}${usedStr} / ${totalStr}${percentStr}`);
4728
+ }
4729
+ if (s.expire !== void 0) {
4730
+ console.log(` ${colors.gray("\u5230\u671F: ")}${formatTimestamp(s.expire)}`);
4731
+ }
4732
+ if (s.web_page_url) {
4733
+ console.log(` ${colors.gray("\u9875\u9762: ")}${s.web_page_url}`);
4734
+ }
4735
+ });
4736
+ console.log("");
4737
+ console.log("\u5207\u6362\u8BA2\u9605: mihomo sub use <name>");
4738
+ console.log("\u65B0\u589E\u8BA2\u9605: mihomo sub add <url> [name]");
4739
+ console.log("\u66F4\u65B0\u8BA2\u9605: mihomo sub update [name]");
4740
+ console.log("\u5220\u9664\u8BA2\u9605: mihomo sub remove <name>");
4741
+ console.log("\u6D4B\u8BD5\u8282\u70B9: mihomo sub test [name]");
4742
+ console.log("\u6E05\u7406\u8282\u70B9: mihomo sub clean [name]");
4743
+ console.log("\u6253\u5F00\u9875\u9762: mihomo sub web [name]");
4744
+ console.log("");
4745
+ }
4746
+ async function cmdSubscription(args) {
4747
+ const action = args[1];
4748
+ if (!action || action === "list") {
4749
+ await printSubscriptionList();
4750
+ return;
4751
+ }
4752
+ if (action === "add") {
4753
+ const url = args[2];
4754
+ const name = args[3] || "default";
4755
+ if (!url?.startsWith("http")) {
4756
+ console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
4757
+ process.exit(1);
4758
+ }
4759
+ console.log(`\u6DFB\u52A0\u8BA2\u9605: ${name}`);
4760
+ try {
4761
+ addSubscription(url, name);
4762
+ setDefaultSubscription(name);
4763
+ const info = await downloadSubscription(url, name);
4764
+ console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
4765
+ } catch (e) {
4766
+ console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
4767
+ process.exit(1);
4768
+ }
4769
+ console.log("");
4770
+ await printSubscriptionList();
4771
+ return;
4772
+ }
4773
+ if (action === "update") {
4774
+ const name = args[2];
4775
+ const subs = getSubscriptions();
4776
+ if (subs.length === 0) {
4777
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
4778
+ process.exit(1);
4779
+ }
4780
+ if (!name) {
4781
+ console.log(`\u66F4\u65B0\u6240\u6709 ${subs.length} \u4E2A\u8BA2\u9605...`);
4782
+ const results = await Promise.all(subs.map(tryUpdateOne));
4783
+ let ok = 0;
4784
+ for (const r of results) {
4785
+ if (r.success) {
4786
+ ok++;
4787
+ console.log(`${colors.green("\u2713")} ${r.name}: ${colors.green("\u5DF2\u66F4\u65B0")} (${formatProxySummary(r)})`);
4788
+ } else {
4789
+ console.log(`${colors.red("\u2717")} ${r.name}: ${colors.red("\u5931\u8D25")} (${(r.error || "").split("\n")[0]})`);
4790
+ }
4791
+ }
4792
+ if (ok === 0) process.exit(1);
4793
+ console.log("");
4794
+ await printSubscriptionList();
4795
+ return;
4796
+ }
4797
+ const matches = findSubscriptionFuzzy(subs, name);
4798
+ const target = pickSingleSubscription(matches, name);
4799
+ console.log(`\u66F4\u65B0\u8BA2\u9605: ${target.name}`);
4800
+ try {
4801
+ const info = await downloadSubscription(target.url, target.name);
4802
+ console.log(`\u5DF2\u66F4\u65B0 (${formatProxySummary(info)})`);
4803
+ } catch (e) {
4804
+ console.error(`\u66F4\u65B0\u5931\u8D25: ${e.message}`);
4805
+ process.exit(1);
4806
+ }
4807
+ console.log("");
4808
+ await printSubscriptionList();
4809
+ return;
4810
+ }
4811
+ if (action === "use") {
4812
+ const name = args[2];
4813
+ const subs = getSubscriptions();
4814
+ if (!name) {
4815
+ console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
4816
+ if (subs.length > 0) {
4817
+ console.log("\n\u53EF\u7528\u8BA2\u9605:");
4818
+ for (const s of subs) console.log(` ${s.name}`);
4819
+ }
4820
+ process.exit(1);
4821
+ }
4822
+ const matches = findSubscriptionFuzzy(subs, name);
4823
+ const target = pickSingleSubscription(matches, name);
4824
+ const currentDefault = getActiveSubscription();
4825
+ const isAlreadyDefault = currentDefault && currentDefault.name === target.name;
4826
+ if (isAlreadyDefault) {
4827
+ console.log(`"${target.name}" \u5DF2\u662F\u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605`);
4828
+ console.log("");
4829
+ await printSubscriptionList();
4830
+ return;
4831
+ }
4832
+ const status = getStatus();
4833
+ const configInfo = getConfigInfo();
4834
+ const currentMode = configInfo?.tun ? "tun" : "mixed";
4835
+ const success = setDefaultSubscription(target.name);
4836
+ if (success) {
4837
+ console.log(`\u5DF2\u5207\u6362\u5230 "${target.name}"`);
4838
+ } else {
4839
+ console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u8BA2\u9605 "${name}"`);
4840
+ process.exit(1);
4841
+ }
4842
+ if (status.running) {
4843
+ console.log("");
4844
+ await cmdStart(["start", currentMode]);
4845
+ return;
4846
+ }
4847
+ console.log("");
4848
+ await printSubscriptionList();
4849
+ return;
4850
+ }
4851
+ if (action === "web" || action === "open") {
4852
+ const name = args[2];
4853
+ const subs = getSubscriptionsWithCache();
4854
+ if (subs.length === 0) {
4855
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
4856
+ process.exit(1);
4857
+ }
4858
+ let target;
4859
+ if (name) {
4860
+ const matches = findSubscriptionFuzzy(subs, name);
4861
+ target = pickSingleSubscription(matches, name);
4862
+ } else {
4863
+ target = subs[0];
4864
+ }
4865
+ const cached = getSubscriptionsWithCache().find((s) => s.name === target.name);
4866
+ let webPageUrl = cached?.web_page_url;
4867
+ if (!webPageUrl) {
4868
+ console.log("\u8BA2\u9605\u4FE1\u606F\u4E2D\u7F3A\u5C11\u9875\u9762\u5730\u5740\uFF0C\u6B63\u5728\u66F4\u65B0\u8BA2\u9605...");
4869
+ try {
4870
+ await downloadSubscription(target.url, target.name);
4871
+ const cache = readSubscriptionCache();
4872
+ if (cache[target.name]?.web_page_url) {
4873
+ webPageUrl = cache[target.name].web_page_url;
4874
+ } else {
4875
+ console.error("\u9519\u8BEF: \u8BE5\u8BA2\u9605\u6CA1\u6709\u63D0\u4F9B\u9875\u9762\u5730\u5740");
4876
+ process.exit(1);
4877
+ }
4878
+ } catch (e) {
4879
+ console.error(`\u66F4\u65B0\u5931\u8D25: ${e.message}`);
4880
+ process.exit(1);
4881
+ }
4882
+ }
4883
+ console.log(`\u6253\u5F00\u8BA2\u9605\u9875\u9762: ${webPageUrl}`);
4884
+ const opened = openUrl(webPageUrl);
4885
+ if (!opened) {
4886
+ console.log("\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740");
4887
+ }
4888
+ return;
4889
+ }
4890
+ if (action === "remove" || action === "rm" || action === "delete") {
4891
+ const name = args[2];
4892
+ const subs = getSubscriptions();
4893
+ if (!name) {
4894
+ console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8981\u5220\u9664\u7684\u8BA2\u9605\u540D\u79F0");
4895
+ if (subs.length > 0) {
4896
+ console.log("\n\u53EF\u7528\u8BA2\u9605:");
4897
+ for (const s of subs) console.log(` ${s.name}`);
4898
+ }
4899
+ process.exit(1);
4900
+ }
4901
+ const matches = findSubscriptionFuzzy(subs, name);
4902
+ const target = pickSingleSubscription(matches, name);
4903
+ const switchedTo = removeSubscription(target.name);
4904
+ console.log(`\u5DF2\u5220\u9664\u8BA2\u9605 "${target.name}"`);
4905
+ if (switchedTo) {
4906
+ console.log(`\u5DF2\u81EA\u52A8\u5207\u6362\u5230 "${switchedTo}"`);
4907
+ }
4908
+ console.log("");
4909
+ await printSubscriptionList({ autoUpdate: false });
4910
+ return;
4911
+ }
4912
+ if (action === "clean") {
4913
+ const { target, timeout, concurrency } = resolveActiveTestTarget(args);
4914
+ console.log(`\u6E05\u7406\u8BA2\u9605 "${target.name}"...`);
4915
+ console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
4916
+ console.log("");
4917
+ const result = await autoCleanSubscription(target.name, {
4918
+ timeout,
4919
+ concurrency,
4920
+ onResult: printTestResult
4921
+ });
4922
+ console.log("");
4923
+ console.log(formatTestSummary(result.summary));
4924
+ if (result.removedProxies > 0) {
4925
+ console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
4926
+ }
4927
+ console.log("");
4928
+ console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
4929
+ return;
4930
+ }
4931
+ if (action === "test") {
4932
+ const { target, timeout, concurrency } = resolveActiveTestTarget(args);
4933
+ console.log(`\u6D4B\u8BD5\u8BA2\u9605 "${target.name}" \u7684\u8282\u70B9\u8FDE\u901A\u6027...`);
4934
+ console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
4935
+ console.log("");
4936
+ const summary = await testSubscriptionProxies(target.name, {
4937
+ timeout,
4938
+ concurrency,
4939
+ onResult: printTestResult
4940
+ });
4941
+ console.log("");
4942
+ console.log(formatTestSummary(summary));
4943
+ return;
4944
+ }
4945
+ console.error("\u9519\u8BEF: \u672A\u77E5\u7684\u8BA2\u9605\u547D\u4EE4");
4946
+ console.log("\u7528\u6CD5: mihomo sub [list|use|add|update|remove|web|test|clean]");
4947
+ process.exit(1);
4948
+ }
4949
+
4468
4950
  // src/commands/start.ts
4951
+ var AUTO_CLEAN_THRESHOLD = 100;
4469
4952
  function handleStopResult(result) {
4470
4953
  if (result.remaining && result.remaining.length > 0) {
4471
4954
  console.error(`${colors.red("\u90E8\u5206\u8FDB\u7A0B\u672A\u7EC8\u6B62:")} ${result.remaining.join(", ")}`);
@@ -4508,11 +4991,34 @@ async function cmdStart(args) {
4508
4991
  try {
4509
4992
  const result = await start(targetMode);
4510
4993
  console.log(`${colors.green("\u5DF2\u542F\u52A8")} (PID ${result.pid})`);
4511
- printStatus();
4512
4994
  } catch (e) {
4513
4995
  console.error(`${colors.red("\u542F\u52A8\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
4514
4996
  process.exit(1);
4515
4997
  }
4998
+ if (configInfo.proxies > AUTO_CLEAN_THRESHOLD) {
4999
+ console.log("");
5000
+ console.log(`\u8282\u70B9\u6570 ${configInfo.proxies} \u8D85\u8FC7 ${AUTO_CLEAN_THRESHOLD}\uFF0C\u81EA\u52A8\u6E05\u7406...`);
5001
+ console.log("");
5002
+ await sleep(1e3);
5003
+ const cleanResult = await autoCleanSubscription(sub.name, { onResult: printTestResult });
5004
+ console.log("");
5005
+ console.log(formatTestSummary(cleanResult.summary));
5006
+ if (cleanResult.removedProxies > 0) {
5007
+ console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(cleanResult)}`);
5008
+ console.log("");
5009
+ console.log("\u91CD\u65B0\u52A0\u8F7D\u914D\u7F6E...");
5010
+ handleStopResult(stop(true));
5011
+ try {
5012
+ configInfo = prepareConfigForStart(targetMode, sub.name);
5013
+ const result = await start(targetMode);
5014
+ console.log(`${colors.green("\u5DF2\u91CD\u542F")} (PID ${result.pid}) \xB7 ${formatProxySummary(configInfo)}`);
5015
+ } catch (e) {
5016
+ console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
5017
+ process.exit(1);
5018
+ }
5019
+ }
5020
+ }
5021
+ printStatus();
4516
5022
  }
4517
5023
 
4518
5024
  // src/commands/overwrite.ts
@@ -4762,202 +5268,6 @@ async function cmdStop() {
4762
5268
  console.log(colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B"));
4763
5269
  }
4764
5270
 
4765
- // src/commands/subscription.ts
4766
- async function printSubscriptionList() {
4767
- const updateResult = await autoUpdateStaleSubscription();
4768
- if (updateResult.total > 0) console.log("");
4769
- const subs = getSubscriptionsWithCache();
4770
- if (subs.length === 0) {
4771
- console.log("\u6CA1\u6709\u8BA2\u9605");
4772
- console.log("");
4773
- console.log("\u6DFB\u52A0\u8BA2\u9605: mihomo sub add <url> [name]");
4774
- console.log("");
4775
- return;
4776
- }
4777
- const activeSub = getActiveSubscription();
4778
- console.log(colors.cyan("\u8BA2\u9605\u5217\u8868:"));
4779
- subs.forEach((s, i) => {
4780
- const time = formatDate(s.updated_at);
4781
- const defaultMark = activeSub && s.name === activeSub.name ? colors.green(" [\u4F7F\u7528\u4E2D]") : "";
4782
- const interval = s.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
4783
- console.log(` ${i + 1}. ${s.name}${defaultMark}`);
4784
- console.log(` ${colors.gray("\u66F4\u65B0: ")}${time} (\u95F4\u9694: ${interval}h)`);
4785
- if (s.username) {
4786
- console.log(` ${colors.gray("\u7528\u6237: ")}${s.username}`);
4787
- }
4788
- if (s.download !== void 0 || s.total !== void 0) {
4789
- const used = (s.upload || 0) + (s.download || 0);
4790
- const usedStr = formatBytes(used);
4791
- const totalStr = formatBytes(s.total);
4792
- let percentStr = "";
4793
- if (s.total && s.total > 0) {
4794
- const percent = Math.min(used / s.total * 100, 100);
4795
- percentStr = ` (${percent.toFixed(1)}%)`;
4796
- }
4797
- console.log(` ${colors.gray("\u6D41\u91CF: ")}${usedStr} / ${totalStr}${percentStr}`);
4798
- }
4799
- if (s.expire !== void 0) {
4800
- console.log(` ${colors.gray("\u5230\u671F: ")}${formatTimestamp(s.expire)}`);
4801
- }
4802
- if (s.web_page_url) {
4803
- console.log(` ${colors.gray("\u9875\u9762: ")}${s.web_page_url}`);
4804
- }
4805
- });
4806
- console.log("");
4807
- console.log("\u5207\u6362\u8BA2\u9605: mihomo sub use <name>");
4808
- console.log("\u66F4\u65B0\u8BA2\u9605: mihomo sub update [name]");
4809
- console.log("\u6253\u5F00\u9875\u9762: mihomo sub web [name]");
4810
- console.log("\u65B0\u589E\u8BA2\u9605: mihomo sub add <url> [name]");
4811
- console.log("");
4812
- }
4813
- async function cmdSubscription(args) {
4814
- const action = args[1];
4815
- if (!action || action === "list") {
4816
- await printSubscriptionList();
4817
- return;
4818
- }
4819
- if (action === "add") {
4820
- const url = args[2];
4821
- const name = args[3] || "default";
4822
- if (!url?.startsWith("http")) {
4823
- console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
4824
- process.exit(1);
4825
- }
4826
- console.log(`\u6DFB\u52A0\u8BA2\u9605: ${name}`);
4827
- try {
4828
- addSubscription(url, name);
4829
- const info = await downloadSubscription(url, name);
4830
- console.log(`\u5DF2\u6DFB\u52A0 (${formatProxySummary(info)})`);
4831
- } catch (e) {
4832
- console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
4833
- process.exit(1);
4834
- }
4835
- console.log("");
4836
- await printSubscriptionList();
4837
- return;
4838
- }
4839
- if (action === "update") {
4840
- const name = args[2];
4841
- const subs = getSubscriptions();
4842
- if (subs.length === 0) {
4843
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
4844
- process.exit(1);
4845
- }
4846
- if (!name) {
4847
- console.log(`\u66F4\u65B0\u6240\u6709 ${subs.length} \u4E2A\u8BA2\u9605...`);
4848
- const results = await Promise.all(subs.map(tryUpdateOne));
4849
- let ok = 0;
4850
- for (const r of results) {
4851
- if (r.success) {
4852
- ok++;
4853
- console.log(`${colors.green("\u2713")} ${r.name}: ${colors.green("\u5DF2\u66F4\u65B0")} (${formatProxySummary(r)})`);
4854
- } else {
4855
- console.log(`${colors.red("\u2717")} ${r.name}: ${colors.red("\u5931\u8D25")} (${(r.error || "").split("\n")[0]})`);
4856
- }
4857
- }
4858
- if (ok === 0) process.exit(1);
4859
- console.log("");
4860
- await printSubscriptionList();
4861
- return;
4862
- }
4863
- const matches = findSubscriptionFuzzy(subs, name);
4864
- const target = pickSingleSubscription(matches, name);
4865
- console.log(`\u66F4\u65B0\u8BA2\u9605: ${target.name}`);
4866
- try {
4867
- const info = await downloadSubscription(target.url, target.name);
4868
- console.log(`\u5DF2\u66F4\u65B0 (${formatProxySummary(info)})`);
4869
- } catch (e) {
4870
- console.error(`\u66F4\u65B0\u5931\u8D25: ${e.message}`);
4871
- process.exit(1);
4872
- }
4873
- console.log("");
4874
- await printSubscriptionList();
4875
- return;
4876
- }
4877
- if (action === "use") {
4878
- const name = args[2];
4879
- const subs = getSubscriptions();
4880
- if (!name) {
4881
- console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
4882
- if (subs.length > 0) {
4883
- console.log("\n\u53EF\u7528\u8BA2\u9605:");
4884
- for (const s of subs) console.log(` ${s.name}`);
4885
- }
4886
- process.exit(1);
4887
- }
4888
- const matches = findSubscriptionFuzzy(subs, name);
4889
- const target = pickSingleSubscription(matches, name);
4890
- const currentDefault = getActiveSubscription();
4891
- const isAlreadyDefault = currentDefault && currentDefault.name === target.name;
4892
- if (isAlreadyDefault) {
4893
- console.log(`"${target.name}" \u5DF2\u662F\u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605`);
4894
- console.log("");
4895
- await printSubscriptionList();
4896
- return;
4897
- }
4898
- const status = getStatus();
4899
- const configInfo = getConfigInfo();
4900
- const currentMode = configInfo?.tun ? "tun" : "mixed";
4901
- const success = setDefaultSubscription(target.name);
4902
- if (success) {
4903
- console.log(`\u5DF2\u5207\u6362\u5230 "${target.name}"`);
4904
- } else {
4905
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u8BA2\u9605 "${name}"`);
4906
- process.exit(1);
4907
- }
4908
- if (status.running) {
4909
- console.log("");
4910
- await cmdStart(["start", currentMode]);
4911
- return;
4912
- }
4913
- console.log("");
4914
- await printSubscriptionList();
4915
- return;
4916
- }
4917
- if (action === "web" || action === "open") {
4918
- const name = args[2];
4919
- const subs = getSubscriptionsWithCache();
4920
- if (subs.length === 0) {
4921
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
4922
- process.exit(1);
4923
- }
4924
- let target;
4925
- if (name) {
4926
- const matches = findSubscriptionFuzzy(subs, name);
4927
- target = pickSingleSubscription(matches, name);
4928
- } else {
4929
- target = subs[0];
4930
- }
4931
- const cached = getSubscriptionsWithCache().find((s) => s.name === target.name);
4932
- let webPageUrl = cached?.web_page_url;
4933
- if (!webPageUrl) {
4934
- console.log("\u8BA2\u9605\u4FE1\u606F\u4E2D\u7F3A\u5C11\u9875\u9762\u5730\u5740\uFF0C\u6B63\u5728\u66F4\u65B0\u8BA2\u9605...");
4935
- try {
4936
- await downloadSubscription(target.url, target.name);
4937
- const cache = readSubscriptionCache();
4938
- if (cache[target.name]?.web_page_url) {
4939
- webPageUrl = cache[target.name].web_page_url;
4940
- } else {
4941
- console.error("\u9519\u8BEF: \u8BE5\u8BA2\u9605\u6CA1\u6709\u63D0\u4F9B\u9875\u9762\u5730\u5740");
4942
- process.exit(1);
4943
- }
4944
- } catch (e) {
4945
- console.error(`\u66F4\u65B0\u5931\u8D25: ${e.message}`);
4946
- process.exit(1);
4947
- }
4948
- }
4949
- console.log(`\u6253\u5F00\u8BA2\u9605\u9875\u9762: ${webPageUrl}`);
4950
- const opened = openUrl(webPageUrl);
4951
- if (!opened) {
4952
- console.log("\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740");
4953
- }
4954
- return;
4955
- }
4956
- console.error("\u9519\u8BEF: \u672A\u77E5\u7684\u8BA2\u9605\u547D\u4EE4");
4957
- console.log("\u7528\u6CD5: mihomo sub [list|add|update|use|web]");
4958
- process.exit(1);
4959
- }
4960
-
4961
5271
  // src/commands/ui.ts
4962
5272
  function cmdUI(args) {
4963
5273
  const uiName = args[1] || "zash";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mihomo-cli",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "description": "A terminal-based mihomo (Clash.Meta) client for macOS",
6
6
  "bin": {