mihomo-cli 2.2.0 → 2.2.2

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,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.2.2] - 2026-05-01
4
+
5
+ ### 修复
6
+
7
+ - **文件描述符泄漏**:修复 `startMixedMode` 中 spawn 后未关闭 fd 的问题
8
+ - **forceSudo 参数失效**:修复 `cleanupAll` 忽略调用方传入的强制 sudo 参数
9
+ - **formatBytes 溢出**:修复超大字节值(>1PB)导致显示 `undefined` 单位
10
+ - **YAML 解析类型检查**:`parseYamlOrJson` 现在拒绝非对象类型的 YAML 内容
11
+ - **spawn 错误处理**:`openUrl` 添加 error 事件处理,防止未捕获异常
12
+ - **UserInfo 类型转换**:移除 `parseUserInfo` 中多余的 `as unknown` 双重转换
13
+
14
+ ### 安全
15
+
16
+ - **订阅名称校验**:新增文件名安全校验,防止路径穿越等不安全名称
17
+
18
+ ---
19
+
20
+ ## [2.2.1] - 2026-05-01
21
+
22
+ ### 修复
23
+
24
+ - **节点名称精简时序**:修复 `shortenProxyNames` 在测速前执行导致 API 返回 "Resource not found" 的问题,改为测速完成后再精简
25
+ - **清理安全阈值**:存活节点不足 1% 时跳过清理,提示用户检查原始订阅
26
+
27
+ ---
28
+
3
29
  ## [2.2.0] - 2026-05-01
4
30
 
5
31
  ### 新增
package/dist/index.js CHANGED
@@ -2808,7 +2808,14 @@ function getSubscriptionsWithCache() {
2808
2808
  ...cache[s.name] || {}
2809
2809
  }));
2810
2810
  }
2811
+ var SAFE_NAME_RE = /^[\w\-\p{Unified_Ideograph}]{1,64}$/u;
2812
+ function validateSubscriptionName(name) {
2813
+ if (!name || !SAFE_NAME_RE.test(name)) {
2814
+ throw new Error(`\u8BA2\u9605\u540D\u79F0\u65E0\u6548: "${name}"\uFF0C\u53EA\u5141\u8BB8\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u3001\u77ED\u6A2A\u7EBF\u548C\u4E2D\u6587\uFF08\u6700\u957F 64 \u5B57\u7B26\uFF09`);
2815
+ }
2816
+ }
2811
2817
  function addSubscription(url, name = "default") {
2818
+ validateSubscriptionName(name);
2812
2819
  const settings = readSettings();
2813
2820
  const subs = settings.subscriptions || [];
2814
2821
  const existingIndex = subs.findIndex((s) => s.name === name);
@@ -3006,7 +3013,7 @@ function parseYamlOrJson(content, errorMsg) {
3006
3013
  }
3007
3014
  try {
3008
3015
  const result = jsYaml.load(content);
3009
- if (result !== void 0) return result;
3016
+ if (result != null && typeof result === "object" && !Array.isArray(result)) return result;
3010
3017
  } catch {
3011
3018
  }
3012
3019
  try {
@@ -3160,7 +3167,7 @@ function formatBytes(bytes) {
3160
3167
  if (num === 0) return "0 B";
3161
3168
  const k = 1024;
3162
3169
  const sizes = ["B", "KB", "MB", "GB", "TB"];
3163
- const i = Math.floor(Math.log(num) / Math.log(k));
3170
+ const i = Math.min(Math.floor(Math.log(num) / Math.log(k)), sizes.length - 1);
3164
3171
  return `${parseFloat((num / k ** i).toFixed(2))} ${sizes[i]}`;
3165
3172
  }
3166
3173
  function formatTimestamp(ts) {
@@ -3405,14 +3412,14 @@ function killAllMihomo(forceSudo = false) {
3405
3412
  }
3406
3413
  }
3407
3414
  }
3408
- function cleanupAll(_forceSudo = false) {
3415
+ function cleanupAll(forceSudo = false) {
3409
3416
  const pids = getAllMihomoPids();
3410
3417
  if (pids.length === 0) {
3411
3418
  clearPid();
3412
3419
  return { killed: 0, failed: 0, remaining: [] };
3413
3420
  }
3414
3421
  const hasRootProcess = pids.some((p) => isProcessRoot(p));
3415
- const needsSudo = hasRootProcess;
3422
+ const needsSudo = forceSudo || hasRootProcess;
3416
3423
  let killedCount = 0;
3417
3424
  const failedPids = [];
3418
3425
  if (needsSudo) {
@@ -3560,12 +3567,12 @@ async function startMixedMode(staleState) {
3560
3567
  const configFile = PATHS.configFile;
3561
3568
  const logFile = PATHS.logFile;
3562
3569
  const args = ["-d", DIRS.data, "-f", configFile];
3563
- const out = fs5.openSync(logFile, "a");
3564
- const err = fs5.openSync(logFile, "a");
3570
+ const logFd = fs5.openSync(logFile, "a");
3565
3571
  const child = spawn(PATHS.mihomoBinary, args, {
3566
3572
  detached: true,
3567
- stdio: ["ignore", out, err]
3573
+ stdio: ["ignore", logFd, logFd]
3568
3574
  });
3575
+ fs5.closeSync(logFd);
3569
3576
  child.unref();
3570
3577
  const pid = child.pid;
3571
3578
  savePid(pid);
@@ -3735,7 +3742,10 @@ function getLogPathByName(name) {
3735
3742
  }
3736
3743
  function openUrl(url) {
3737
3744
  try {
3738
- spawn("open", [url], { stdio: "ignore", detached: true });
3745
+ const child = spawn("open", [url], { stdio: "ignore", detached: true });
3746
+ child.unref();
3747
+ child.on("error", () => {
3748
+ });
3739
3749
  return true;
3740
3750
  } catch {
3741
3751
  return false;
@@ -4293,6 +4303,7 @@ function loadSubscriptionConfig(subName) {
4293
4303
  };
4294
4304
  }
4295
4305
  function saveSubscriptionConfig(subName, parsed) {
4306
+ shortenProxyNames(parsed);
4296
4307
  parsed.raw.proxies = parsed.proxies;
4297
4308
  parsed.raw["proxy-groups"] = parsed.proxyGroups;
4298
4309
  saveSubscriptionRawConfig(subName, jsYaml.dump(parsed.raw, YAML_DUMP_OPTS));
@@ -4571,21 +4582,26 @@ function cleanDeadProxies(parsed, deadNames) {
4571
4582
  }
4572
4583
  async function autoCleanSubscription(subName, options = {}) {
4573
4584
  const parsed = loadSubscriptionConfig(subName);
4574
- shortenProxyNames(parsed);
4575
- saveSubscriptionConfig(subName, parsed);
4576
4585
  const summary = await testSubscriptionProxies(subName, { ...options, parsed });
4577
4586
  let removedProxies = 0;
4578
4587
  let updatedGroups = 0;
4579
4588
  let removedGroups = 0;
4589
+ let skipped = false;
4580
4590
  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;
4591
+ if (summary.alive === 0 || summary.alive / summary.total < 0.01) {
4592
+ skipped = true;
4593
+ } else {
4594
+ const deadNames = new Set(summary.results.filter((r) => r.delay === null).map((r) => r.name));
4595
+ const cleanResult = cleanDeadProxies(parsed, deadNames);
4596
+ removedProxies = cleanResult.removedProxies;
4597
+ updatedGroups = cleanResult.updatedGroups;
4598
+ removedGroups = cleanResult.removedGroups;
4599
+ }
4600
+ }
4601
+ if (!skipped) {
4586
4602
  saveSubscriptionConfig(subName, parsed);
4587
4603
  }
4588
- return { summary, removedProxies, updatedGroups, removedGroups };
4604
+ return { summary, removedProxies, updatedGroups, removedGroups, skipped };
4589
4605
  }
4590
4606
 
4591
4607
  // src/commands/status.ts
@@ -4921,11 +4937,14 @@ async function cmdSubscription(args) {
4921
4937
  });
4922
4938
  console.log("");
4923
4939
  console.log(formatTestSummary(result.summary));
4924
- if (result.removedProxies > 0) {
4940
+ if (result.skipped) {
4941
+ console.log("");
4942
+ 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"));
4943
+ } else if (result.removedProxies > 0) {
4925
4944
  console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
4945
+ console.log("");
4946
+ console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
4926
4947
  }
4927
- console.log("");
4928
- console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
4929
4948
  return;
4930
4949
  }
4931
4950
  if (action === "test") {
@@ -5003,7 +5022,9 @@ async function cmdStart(args) {
5003
5022
  const cleanResult = await autoCleanSubscription(sub.name, { onResult: printTestResult });
5004
5023
  console.log("");
5005
5024
  console.log(formatTestSummary(cleanResult.summary));
5006
- if (cleanResult.removedProxies > 0) {
5025
+ if (cleanResult.skipped) {
5026
+ 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"));
5027
+ } else if (cleanResult.removedProxies > 0) {
5007
5028
  console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(cleanResult)}`);
5008
5029
  console.log("");
5009
5030
  console.log("\u91CD\u65B0\u52A0\u8F7D\u914D\u7F6E...");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mihomo-cli",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "type": "module",
5
5
  "description": "A terminal-based mihomo (Clash.Meta) client for macOS",
6
6
  "bin": {