mihomo-cli 2.6.2 → 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 +12 -0
  2. package/dist/index.js +126 -28
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
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
+
3
15
  ## [2.6.2] - 2026-05-03
4
16
 
5
17
  ### 改进
package/dist/index.js CHANGED
@@ -4606,17 +4606,20 @@ async function testSubscriptionProxies(subName, options = {}) {
4606
4606
  return { total: 0, alive: 0, dead: 0, results: [] };
4607
4607
  }
4608
4608
  const client = createHttpClient({ timeout: timeout + 3e3 });
4609
- const results = [];
4609
+ const results = new Array(proxies.length);
4610
4610
  let completedCount = 0;
4611
- for (let i = 0; i < proxies.length; i += concurrency) {
4612
- const batch = proxies.slice(i, i + concurrency);
4613
- const batchResults = await Promise.all(batch.map((proxy) => testProxyDelay(proxy.name, timeout, testUrl, client, apiBase)));
4614
- for (const result of batchResults) {
4615
- 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;
4616
4617
  onResult?.(result, completedCount, proxies.length);
4617
4618
  completedCount++;
4618
4619
  }
4619
4620
  }
4621
+ const workers = Array.from({ length: Math.min(concurrency, proxies.length) }, () => runNext());
4622
+ await Promise.all(workers);
4620
4623
  const alive = results.filter((r) => r.delay !== null).length;
4621
4624
  return { total: results.length, alive, dead: results.length - alive, results };
4622
4625
  }
@@ -4676,7 +4679,13 @@ function cleanDeadProxies(parsed, deadNames) {
4676
4679
  }
4677
4680
  async function autoCleanSubscription(subName, options = {}) {
4678
4681
  const parsed = loadSubscriptionConfig(subName);
4679
- 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
+ });
4680
4689
  let removedProxies = 0;
4681
4690
  let updatedGroups = 0;
4682
4691
  let removedGroups = 0;
@@ -4686,10 +4695,32 @@ async function autoCleanSubscription(subName, options = {}) {
4686
4695
  skipped = true;
4687
4696
  } else {
4688
4697
  const deadNames = new Set(summary.results.filter((r) => r.delay === null).map((r) => r.name));
4689
- const cleanResult = cleanDeadProxies(parsed, deadNames);
4690
- removedProxies = cleanResult.removedProxies;
4691
- updatedGroups = cleanResult.updatedGroups;
4692
- 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
+ }
4693
4724
  }
4694
4725
  }
4695
4726
  if (!skipped && removedProxies > 0) {
@@ -4933,8 +4964,12 @@ async function cmdStart(args) {
4933
4964
  console.log(`\u8282\u70B9\u6570 ${configInfo.proxies} \u8D85\u8FC7 ${AUTO_CLEAN_THRESHOLD}\uFF0C\u81EA\u52A8\u6E05\u7406...`);
4934
4965
  console.log("");
4935
4966
  await sleep(1e3);
4936
- const cleanResult = await autoCleanSubscription(sub.name, { onResult: printTestResult });
4937
- 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();
4938
4973
  console.log(formatTestSummary(cleanResult.summary));
4939
4974
  if (cleanResult.skipped) {
4940
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"));
@@ -4957,14 +4992,71 @@ async function cmdStart(args) {
4957
4992
  }
4958
4993
 
4959
4994
  // src/commands/subscription.ts
4960
- function printTestResult(result, index, total) {
4961
- const prefix = `[${index + 1}/${total}]`;
4962
- if (result.delay !== null) {
4963
- const delayColor = result.delay < 300 ? colors.green : result.delay < 800 ? colors.yellow : colors.red;
4964
- console.log(`${prefix} ${colors.green("\u2713")} ${result.name} ${delayColor(`${result.delay}ms`)}`);
4965
- } else {
4966
- 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}`)}`);
4967
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
+ };
4968
5060
  }
4969
5061
  function formatCleanSummary(result) {
4970
5062
  const parts = [`\u79FB\u9664 ${result.removedProxies} \u4E2A\u8282\u70B9`];
@@ -5328,15 +5420,17 @@ async function cmdSubscription(args) {
5328
5420
  console.log(`\u6E05\u7406\u8BA2\u9605 "${target.name}"...`);
5329
5421
  console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
5330
5422
  console.log("");
5423
+ const progress = createProgressPrinter();
5331
5424
  const result = await withTestInstance(target.name, async (apiBase) => {
5332
5425
  return autoCleanSubscription(target.name, {
5333
5426
  timeout,
5334
5427
  concurrency,
5335
5428
  apiBase,
5336
- onResult: printTestResult
5429
+ onResult: progress.onResult,
5430
+ onRetryRound: progress.onRetryRound
5337
5431
  });
5338
5432
  });
5339
- console.log("");
5433
+ progress.finish();
5340
5434
  console.log(formatTestSummary(result.summary));
5341
5435
  if (result.skipped) {
5342
5436
  console.log("");
@@ -5356,15 +5450,16 @@ async function cmdSubscription(args) {
5356
5450
  console.log(`\u6D4B\u8BD5\u8BA2\u9605 "${target.name}" \u7684\u8282\u70B9\u8FDE\u901A\u6027...`);
5357
5451
  console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
5358
5452
  console.log("");
5453
+ const progress = createProgressPrinter();
5359
5454
  const summary = await withTestInstance(target.name, async (apiBase) => {
5360
5455
  return testSubscriptionProxies(target.name, {
5361
5456
  timeout,
5362
5457
  concurrency,
5363
5458
  apiBase,
5364
- onResult: printTestResult
5459
+ onResult: progress.onResult
5365
5460
  });
5366
5461
  });
5367
- console.log("");
5462
+ progress.finish();
5368
5463
  console.log(formatTestSummary(summary));
5369
5464
  return;
5370
5465
  }
@@ -6281,12 +6376,13 @@ async function cmdTest(args) {
6281
6376
  console.log(`\u6D4B\u8BD5 "${activeSub.name}" \u8282\u70B9\u8FDE\u901A\u6027...`);
6282
6377
  console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6283
6378
  console.log("");
6379
+ const progress = createProgressPrinter();
6284
6380
  const summary = await testSubscriptionProxies(activeSub.name, {
6285
6381
  timeout,
6286
6382
  concurrency,
6287
- onResult: printTestResult
6383
+ onResult: progress.onResult
6288
6384
  });
6289
- console.log("");
6385
+ progress.finish();
6290
6386
  console.log(formatTestSummary(summary));
6291
6387
  }
6292
6388
  async function cmdClean(args) {
@@ -6297,12 +6393,14 @@ async function cmdClean(args) {
6297
6393
  console.log(`\u6E05\u7406 "${activeSub.name}" \u5931\u8D25\u8282\u70B9...`);
6298
6394
  console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6299
6395
  console.log("");
6396
+ const progress = createProgressPrinter();
6300
6397
  const result = await autoCleanSubscription(activeSub.name, {
6301
6398
  timeout,
6302
6399
  concurrency,
6303
- onResult: printTestResult
6400
+ onResult: progress.onResult,
6401
+ onRetryRound: progress.onRetryRound
6304
6402
  });
6305
- console.log("");
6403
+ progress.finish();
6306
6404
  console.log(formatTestSummary(result.summary));
6307
6405
  if (result.skipped) {
6308
6406
  console.log("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mihomo-cli",
3
- "version": "2.6.2",
3
+ "version": "2.6.3",
4
4
  "type": "module",
5
5
  "description": "A terminal-based mihomo (Clash.Meta) client for macOS",
6
6
  "bin": {