mihomo-cli 2.6.1 → 2.6.3

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/dist/index.js +171 -56
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,11 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.6.3] - 2026-05-03
4
+
5
+ ### 改进
6
+
7
+ - **自动清理三轮重试** - 节点测试失败后自动重试两轮,三轮都失败才删除,减少网络抖动导致的误删
8
+ - **实时进度条** - 测试/清理过程中显示单行刷新进度条,测完输出按名称排序的最终结果
9
+ - **并发模型优化** - 节点测试从分批等待改为 worker pool,逐个完成即时反馈
10
+ - 重试通过的节点标注轮次(第N轮通过)
11
+ - start/test/clean 命令统一使用进度条
12
+
13
+ ---
14
+
15
+ ## [2.6.2] - 2026-05-03
16
+
17
+ ### 改进
18
+
19
+ - **配置校验增强** - 新增重名节点/分组去重、无效规则清理,覆盖更多启动失败场景
20
+ - 移除多余的 `mihomo -t` 预校验(启动本身即校验)
21
+
22
+ ---
23
+
3
24
  ## [2.6.1] - 2026-05-03
4
25
 
5
26
  ### 修复
6
27
 
7
28
  - **启动前配置校验** - 自动检测并修复 proxy-group 中引用不存在的节点/分组,避免内核启动失败
8
- - 写入配置后使用 `mihomo -t` 做内核级校验,提前发现配置错误
9
29
 
10
30
  ### 改进
11
31
 
package/dist/index.js CHANGED
@@ -3113,19 +3113,42 @@ function excludeOverwriteProxiesFromIncludeAll(config, overwriteFiles) {
3113
3113
  }
3114
3114
  }
3115
3115
  var BUILTIN_PROXY_NAMES = /* @__PURE__ */ new Set(["DIRECT", "REJECT", "REJECT-DROP", "PASS", "COMPATIBLE"]);
3116
- function validateProxyGroupDependencies(config) {
3116
+ function deduplicateByName(items) {
3117
+ const names = /* @__PURE__ */ new Set();
3118
+ const duplicates = [];
3119
+ const result = items.filter((item) => {
3120
+ if (names.has(item.name)) {
3121
+ duplicates.push(item.name);
3122
+ return false;
3123
+ }
3124
+ names.add(item.name);
3125
+ return true;
3126
+ });
3127
+ return { result, names, duplicates };
3128
+ }
3129
+ function validateConfig(config) {
3117
3130
  const warnings = [];
3118
3131
  const proxies = config.proxies || [];
3119
3132
  const groups = config["proxy-groups"] || [];
3120
- if (groups.length === 0) return warnings;
3121
- const validNames = new Set(BUILTIN_PROXY_NAMES);
3122
- for (const p of proxies) validNames.add(p.name);
3123
- for (const g of groups) validNames.add(g.name);
3133
+ const rules = config.rules || [];
3134
+ const proxyDedup = deduplicateByName(proxies);
3135
+ config.proxies = proxyDedup.result;
3136
+ if (proxyDedup.duplicates.length > 0) {
3137
+ const preview = proxyDedup.duplicates.slice(0, 3).map((n) => `"${n}"`).join(", ");
3138
+ warnings.push(`\u79FB\u9664\u4E86 ${proxyDedup.duplicates.length} \u4E2A\u91CD\u540D\u8282\u70B9: ${preview}${proxyDedup.duplicates.length > 3 ? " ..." : ""}`);
3139
+ }
3140
+ const groupDedup = deduplicateByName(groups);
3141
+ config["proxy-groups"] = groupDedup.result;
3142
+ if (groupDedup.duplicates.length > 0) {
3143
+ warnings.push(`\u79FB\u9664\u4E86 ${groupDedup.duplicates.length} \u4E2A\u91CD\u540D\u5206\u7EC4: ${groupDedup.duplicates.map((n) => `"${n}"`).join(", ")}`);
3144
+ }
3145
+ const validNames = /* @__PURE__ */ new Set([...BUILTIN_PROXY_NAMES, ...proxyDedup.names, ...groupDedup.names]);
3146
+ const activeGroups = groupDedup.result;
3124
3147
  const removedGroups = /* @__PURE__ */ new Set();
3125
3148
  let changed = true;
3126
3149
  while (changed) {
3127
3150
  changed = false;
3128
- for (const group of groups) {
3151
+ for (const group of activeGroups) {
3129
3152
  if (removedGroups.has(group.name)) continue;
3130
3153
  if (!Array.isArray(group.proxies)) continue;
3131
3154
  const invalid = group.proxies.filter((name) => !validNames.has(name));
@@ -3142,7 +3165,21 @@ function validateProxyGroupDependencies(config) {
3142
3165
  }
3143
3166
  }
3144
3167
  if (removedGroups.size > 0) {
3145
- config["proxy-groups"] = groups.filter((g) => !removedGroups.has(g.name));
3168
+ config["proxy-groups"] = activeGroups.filter((g) => !removedGroups.has(g.name));
3169
+ }
3170
+ if (rules.length > 0) {
3171
+ const removedRules = [];
3172
+ config.rules = rules.filter((rule) => {
3173
+ const parts = rule.split(",");
3174
+ if (parts.length < 2) return true;
3175
+ const target = parts[parts.length - 1].trim();
3176
+ if (!target || validNames.has(target)) return true;
3177
+ removedRules.push(rule);
3178
+ return false;
3179
+ });
3180
+ if (removedRules.length > 0) {
3181
+ warnings.push(`\u79FB\u9664\u4E86 ${removedRules.length} \u6761\u5F15\u7528\u4E0D\u5B58\u5728\u76EE\u6807\u7684\u89C4\u5219`);
3182
+ }
3146
3183
  }
3147
3184
  return warnings;
3148
3185
  }
@@ -3199,23 +3236,9 @@ function buildConfig(subRawContent, mode) {
3199
3236
  "skip-domain": ["+.push.apple.com"]
3200
3237
  };
3201
3238
  }
3202
- const warnings = validateProxyGroupDependencies(merged);
3239
+ const warnings = validateConfig(merged);
3203
3240
  return { config: merged, subscriptionConfig, overwriteFiles, systemConfig, warnings };
3204
3241
  }
3205
- function checkConfig() {
3206
- if (!hasKernel() || !hasConfig()) return { ok: true, errors: [] };
3207
- try {
3208
- execSync(`"${PATHS.mihomoBinary}" -t -f "${PATHS.configFile}" 2>&1`, { encoding: "utf8" });
3209
- return { ok: true, errors: [] };
3210
- } catch (e) {
3211
- const output = e.stdout || e.message || "";
3212
- const errors = output.split("\n").filter((line) => line.includes("level=error") || line.includes("level=fatal")).map((line) => {
3213
- const match = line.match(/msg="(.+)"/);
3214
- return match ? match[1] : line;
3215
- });
3216
- return { ok: false, errors };
3217
- }
3218
- }
3219
3242
  function writeMihomoConfig(configObj) {
3220
3243
  ensureDirs();
3221
3244
  const content = jsYaml.dump(configObj, { indent: 2, lineWidth: -1, noCompatMode: true });
@@ -4500,12 +4523,6 @@ function prepareConfigForStart(mode, subName = "default") {
4500
4523
  }
4501
4524
  writeMihomoConfig(buildResult.config);
4502
4525
  writeDebugConfig(buildResult);
4503
- const configCheck = checkConfig();
4504
- if (!configCheck.ok) {
4505
- const errorDetail = configCheck.errors.length > 0 ? configCheck.errors.join("\n ") : "\u672A\u77E5\u9519\u8BEF";
4506
- throw new Error(`\u914D\u7F6E\u6821\u9A8C\u5931\u8D25:
4507
- ${errorDetail}`);
4508
- }
4509
4526
  const proxies = buildResult.config.proxies;
4510
4527
  const proxyGroups = buildResult.config["proxy-groups"];
4511
4528
  return {
@@ -4589,17 +4606,20 @@ async function testSubscriptionProxies(subName, options = {}) {
4589
4606
  return { total: 0, alive: 0, dead: 0, results: [] };
4590
4607
  }
4591
4608
  const client = createHttpClient({ timeout: timeout + 3e3 });
4592
- const results = [];
4609
+ const results = new Array(proxies.length);
4593
4610
  let completedCount = 0;
4594
- for (let i = 0; i < proxies.length; i += concurrency) {
4595
- const batch = proxies.slice(i, i + concurrency);
4596
- const batchResults = await Promise.all(batch.map((proxy) => testProxyDelay(proxy.name, timeout, testUrl, client, apiBase)));
4597
- for (const result of batchResults) {
4598
- results.push(result);
4611
+ let nextIndex = 0;
4612
+ async function runNext() {
4613
+ while (nextIndex < proxies.length) {
4614
+ const idx = nextIndex++;
4615
+ const result = await testProxyDelay(proxies[idx].name, timeout, testUrl, client, apiBase);
4616
+ results[idx] = result;
4599
4617
  onResult?.(result, completedCount, proxies.length);
4600
4618
  completedCount++;
4601
4619
  }
4602
4620
  }
4621
+ const workers = Array.from({ length: Math.min(concurrency, proxies.length) }, () => runNext());
4622
+ await Promise.all(workers);
4603
4623
  const alive = results.filter((r) => r.delay !== null).length;
4604
4624
  return { total: results.length, alive, dead: results.length - alive, results };
4605
4625
  }
@@ -4659,7 +4679,13 @@ function cleanDeadProxies(parsed, deadNames) {
4659
4679
  }
4660
4680
  async function autoCleanSubscription(subName, options = {}) {
4661
4681
  const parsed = loadSubscriptionConfig(subName);
4662
- const summary = await testSubscriptionProxies(subName, { ...options, parsed });
4682
+ const { onResult, onRetryRound, ...testOptions } = options;
4683
+ const wrapOnResult = (round) => onResult ? (r, i, t) => onResult(r, i, t, round) : void 0;
4684
+ const summary = await testSubscriptionProxies(subName, {
4685
+ ...testOptions,
4686
+ parsed,
4687
+ onResult: wrapOnResult(1)
4688
+ });
4663
4689
  let removedProxies = 0;
4664
4690
  let updatedGroups = 0;
4665
4691
  let removedGroups = 0;
@@ -4669,10 +4695,32 @@ async function autoCleanSubscription(subName, options = {}) {
4669
4695
  skipped = true;
4670
4696
  } else {
4671
4697
  const deadNames = new Set(summary.results.filter((r) => r.delay === null).map((r) => r.name));
4672
- const cleanResult = cleanDeadProxies(parsed, deadNames);
4673
- removedProxies = cleanResult.removedProxies;
4674
- updatedGroups = cleanResult.updatedGroups;
4675
- removedGroups = cleanResult.removedGroups;
4698
+ const deadProxies = parsed.proxies.filter((p) => deadNames.has(p.name));
4699
+ for (let retry = 0; retry < 2; retry++) {
4700
+ const round = retry + 2;
4701
+ const retryTargets = deadProxies.filter((p) => deadNames.has(p.name));
4702
+ if (retryTargets.length === 0) break;
4703
+ onRetryRound?.(round, retryTargets.length);
4704
+ const retryParsed = { raw: {}, proxies: retryTargets, proxyGroups: [] };
4705
+ const retrySummary = await testSubscriptionProxies(subName, {
4706
+ ...testOptions,
4707
+ parsed: retryParsed,
4708
+ onResult: wrapOnResult(round)
4709
+ });
4710
+ for (const r of retrySummary.results) {
4711
+ if (r.delay !== null) {
4712
+ deadNames.delete(r.name);
4713
+ }
4714
+ }
4715
+ }
4716
+ summary.dead = deadNames.size;
4717
+ summary.alive = summary.total - summary.dead;
4718
+ if (deadNames.size > 0) {
4719
+ const cleanResult = cleanDeadProxies(parsed, deadNames);
4720
+ removedProxies = cleanResult.removedProxies;
4721
+ updatedGroups = cleanResult.updatedGroups;
4722
+ removedGroups = cleanResult.removedGroups;
4723
+ }
4676
4724
  }
4677
4725
  }
4678
4726
  if (!skipped && removedProxies > 0) {
@@ -4916,8 +4964,12 @@ async function cmdStart(args) {
4916
4964
  console.log(`\u8282\u70B9\u6570 ${configInfo.proxies} \u8D85\u8FC7 ${AUTO_CLEAN_THRESHOLD}\uFF0C\u81EA\u52A8\u6E05\u7406...`);
4917
4965
  console.log("");
4918
4966
  await sleep(1e3);
4919
- const cleanResult = await autoCleanSubscription(sub.name, { onResult: printTestResult });
4920
- console.log("");
4967
+ const progress = createProgressPrinter();
4968
+ const cleanResult = await autoCleanSubscription(sub.name, {
4969
+ onResult: progress.onResult,
4970
+ onRetryRound: progress.onRetryRound
4971
+ });
4972
+ progress.finish();
4921
4973
  console.log(formatTestSummary(cleanResult.summary));
4922
4974
  if (cleanResult.skipped) {
4923
4975
  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"));
@@ -4940,14 +4992,71 @@ async function cmdStart(args) {
4940
4992
  }
4941
4993
 
4942
4994
  // src/commands/subscription.ts
4943
- function printTestResult(result, index, total) {
4944
- const prefix = `[${index + 1}/${total}]`;
4945
- if (result.delay !== null) {
4946
- const delayColor = result.delay < 300 ? colors.green : result.delay < 800 ? colors.yellow : colors.red;
4947
- console.log(`${prefix} ${colors.green("\u2713")} ${result.name} ${delayColor(`${result.delay}ms`)}`);
4948
- } else {
4949
- console.log(`${prefix} ${colors.red("\u2717")} ${result.name} ${colors.gray(result.error || "timeout")}`);
4995
+ var IS_TTY = process.stdout.isTTY === true;
4996
+ var BAR_WIDTH = 20;
4997
+ function createProgressPrinter() {
4998
+ let alive = 0;
4999
+ let dead = 0;
5000
+ let hasRetry = false;
5001
+ const resultMap = /* @__PURE__ */ new Map();
5002
+ function render(done, total) {
5003
+ if (!IS_TTY) return;
5004
+ const pct = Math.round(done / total * 100);
5005
+ const filled = Math.round(done / total * BAR_WIDTH);
5006
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(BAR_WIDTH - filled);
5007
+ process.stdout.write(`\r${bar} ${done}/${total} (${pct}%) | ${colors.green(`\u2713${alive}`)} ${colors.red(`\u2717${dead}`)}`);
4950
5008
  }
5009
+ return {
5010
+ onResult(result, index, total, round = 1) {
5011
+ const prev = resultMap.get(result.name);
5012
+ if (prev) {
5013
+ if (prev.result.delay === null && result.delay !== null) {
5014
+ alive++;
5015
+ dead--;
5016
+ }
5017
+ } else {
5018
+ if (result.delay !== null) alive++;
5019
+ else dead++;
5020
+ }
5021
+ resultMap.set(result.name, { result, round });
5022
+ render(index + 1, total);
5023
+ },
5024
+ onRetryRound(round, count) {
5025
+ if (!hasRetry) {
5026
+ hasRetry = true;
5027
+ if (IS_TTY) {
5028
+ process.stdout.write("\n");
5029
+ }
5030
+ console.log(`--- \u7B2C 1 \u8F6E\u6D4B\u8BD5 (${resultMap.size} \u4E2A\u8282\u70B9) ---`);
5031
+ }
5032
+ if (IS_TTY) {
5033
+ process.stdout.write("\n");
5034
+ }
5035
+ console.log(`--- \u7B2C ${round} \u8F6E\u91CD\u8BD5 (${count} \u4E2A\u8282\u70B9) ---`);
5036
+ },
5037
+ finish() {
5038
+ if (IS_TTY) {
5039
+ process.stdout.write("\n");
5040
+ }
5041
+ console.log("");
5042
+ const entries = [...resultMap.values()];
5043
+ entries.sort((a, b) => a.result.name.localeCompare(b.result.name));
5044
+ const total = entries.length;
5045
+ console.log("\u8282\u70B9\u6700\u7EC8\u72B6\u6001:");
5046
+ for (let i = 0; i < entries.length; i++) {
5047
+ const { result, round } = entries[i];
5048
+ const prefix = `[${i + 1}/${total}]`;
5049
+ if (result.delay !== null) {
5050
+ const delayColor = result.delay < 300 ? colors.green : result.delay < 800 ? colors.yellow : colors.red;
5051
+ const retryNote = round > 1 ? colors.gray(` (\u7B2C${round}\u8F6E\u901A\u8FC7)`) : "";
5052
+ console.log(`${prefix} ${colors.green("\u2713")} ${result.name} ${delayColor(`${result.delay}ms`)}${retryNote}`);
5053
+ } else {
5054
+ console.log(`${prefix} ${colors.red("\u2717")} ${result.name} ${colors.gray(result.error || "timeout")}`);
5055
+ }
5056
+ }
5057
+ console.log("");
5058
+ }
5059
+ };
4951
5060
  }
4952
5061
  function formatCleanSummary(result) {
4953
5062
  const parts = [`\u79FB\u9664 ${result.removedProxies} \u4E2A\u8282\u70B9`];
@@ -5311,15 +5420,17 @@ async function cmdSubscription(args) {
5311
5420
  console.log(`\u6E05\u7406\u8BA2\u9605 "${target.name}"...`);
5312
5421
  console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
5313
5422
  console.log("");
5423
+ const progress = createProgressPrinter();
5314
5424
  const result = await withTestInstance(target.name, async (apiBase) => {
5315
5425
  return autoCleanSubscription(target.name, {
5316
5426
  timeout,
5317
5427
  concurrency,
5318
5428
  apiBase,
5319
- onResult: printTestResult
5429
+ onResult: progress.onResult,
5430
+ onRetryRound: progress.onRetryRound
5320
5431
  });
5321
5432
  });
5322
- console.log("");
5433
+ progress.finish();
5323
5434
  console.log(formatTestSummary(result.summary));
5324
5435
  if (result.skipped) {
5325
5436
  console.log("");
@@ -5339,15 +5450,16 @@ async function cmdSubscription(args) {
5339
5450
  console.log(`\u6D4B\u8BD5\u8BA2\u9605 "${target.name}" \u7684\u8282\u70B9\u8FDE\u901A\u6027...`);
5340
5451
  console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
5341
5452
  console.log("");
5453
+ const progress = createProgressPrinter();
5342
5454
  const summary = await withTestInstance(target.name, async (apiBase) => {
5343
5455
  return testSubscriptionProxies(target.name, {
5344
5456
  timeout,
5345
5457
  concurrency,
5346
5458
  apiBase,
5347
- onResult: printTestResult
5459
+ onResult: progress.onResult
5348
5460
  });
5349
5461
  });
5350
- console.log("");
5462
+ progress.finish();
5351
5463
  console.log(formatTestSummary(summary));
5352
5464
  return;
5353
5465
  }
@@ -6264,12 +6376,13 @@ async function cmdTest(args) {
6264
6376
  console.log(`\u6D4B\u8BD5 "${activeSub.name}" \u8282\u70B9\u8FDE\u901A\u6027...`);
6265
6377
  console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6266
6378
  console.log("");
6379
+ const progress = createProgressPrinter();
6267
6380
  const summary = await testSubscriptionProxies(activeSub.name, {
6268
6381
  timeout,
6269
6382
  concurrency,
6270
- onResult: printTestResult
6383
+ onResult: progress.onResult
6271
6384
  });
6272
- console.log("");
6385
+ progress.finish();
6273
6386
  console.log(formatTestSummary(summary));
6274
6387
  }
6275
6388
  async function cmdClean(args) {
@@ -6280,12 +6393,14 @@ async function cmdClean(args) {
6280
6393
  console.log(`\u6E05\u7406 "${activeSub.name}" \u5931\u8D25\u8282\u70B9...`);
6281
6394
  console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6282
6395
  console.log("");
6396
+ const progress = createProgressPrinter();
6283
6397
  const result = await autoCleanSubscription(activeSub.name, {
6284
6398
  timeout,
6285
6399
  concurrency,
6286
- onResult: printTestResult
6400
+ onResult: progress.onResult,
6401
+ onRetryRound: progress.onRetryRound
6287
6402
  });
6288
- console.log("");
6403
+ progress.finish();
6289
6404
  console.log(formatTestSummary(result.summary));
6290
6405
  if (result.skipped) {
6291
6406
  console.log("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mihomo-cli",
3
- "version": "2.6.1",
3
+ "version": "2.6.3",
4
4
  "type": "module",
5
5
  "description": "A terminal-based mihomo (Clash.Meta) client for macOS",
6
6
  "bin": {