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.
- package/CHANGELOG.md +12 -0
- package/dist/index.js +126 -28
- 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
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
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
|
|
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
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
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
|
|
4937
|
-
|
|
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
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
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:
|
|
5429
|
+
onResult: progress.onResult,
|
|
5430
|
+
onRetryRound: progress.onRetryRound
|
|
5337
5431
|
});
|
|
5338
5432
|
});
|
|
5339
|
-
|
|
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:
|
|
5459
|
+
onResult: progress.onResult
|
|
5365
5460
|
});
|
|
5366
5461
|
});
|
|
5367
|
-
|
|
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:
|
|
6383
|
+
onResult: progress.onResult
|
|
6288
6384
|
});
|
|
6289
|
-
|
|
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:
|
|
6400
|
+
onResult: progress.onResult,
|
|
6401
|
+
onRetryRound: progress.onRetryRound
|
|
6304
6402
|
});
|
|
6305
|
-
|
|
6403
|
+
progress.finish();
|
|
6306
6404
|
console.log(formatTestSummary(result.summary));
|
|
6307
6405
|
if (result.skipped) {
|
|
6308
6406
|
console.log("");
|