mihomo-cli 2.4.2 → 2.5.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 +18 -0
- package/README.md +3 -3
- package/dist/index.js +271 -116
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.5.0] - 2026-05-03
|
|
4
|
+
|
|
5
|
+
### 新功能
|
|
6
|
+
|
|
7
|
+
- **test 命令** - `mihomo test` 快速测试当前运行实例的节点连通性
|
|
8
|
+
- **clean 命令** - `mihomo clean` 清理失败节点并自动重启
|
|
9
|
+
|
|
10
|
+
### 改进
|
|
11
|
+
|
|
12
|
+
- `sub test` / `sub clean` 改用独立临时进程测试,不影响当前代理,支持测试任意订阅(不限于活跃订阅)
|
|
13
|
+
- 启动时 auto-clean 使用当前运行实例直接测速,提升启动速度
|
|
14
|
+
|
|
15
|
+
### 移除
|
|
16
|
+
|
|
17
|
+
- 移除 `sub best` 命令
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
3
21
|
## [2.4.2] - 2026-05-02
|
|
4
22
|
|
|
5
23
|
### 改进
|
package/README.md
CHANGED
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
- 🌐 **订阅管理** - 添加/更新订阅,支持流量统计和到期时间显示
|
|
8
8
|
- 🔄 **自动更新** - 启动时自动检查并更新过期订阅
|
|
9
9
|
- 🔍 **模糊匹配** - `sub use` / `sub web` 支持订阅名称模糊匹配
|
|
10
|
-
- 🧹 **节点测速清理** - `
|
|
10
|
+
- 🧹 **节点测速清理** - `test` 快速测试、`clean` 清理并重启;`sub test/clean` 独立进程测试任意订阅
|
|
11
11
|
- 📊 **免费订阅基准测试** - `bench` 命令一键测试 20 个内置免费订阅源质量排名
|
|
12
12
|
- 🆓 **快速添加免费订阅** - `sub free <id>` 一键添加内置免费订阅源
|
|
13
|
-
- 🏆 **聚合订阅** - `sub best <id>` 一键添加自动更新的聚合订阅(每小时更新、去重、测活)
|
|
14
13
|
- 📝 **覆写配置** - 在订阅基础上进行自定义覆写,支持强制覆盖、数组合并
|
|
15
14
|
- 🔄 **智能重启** - `sub use` 切换订阅、`ow on/off` 切换覆写后自动重启
|
|
16
15
|
- 🚀 **进程管理** - 启动/停止/切换模式,自动清理残留进程
|
|
@@ -101,13 +100,14 @@ mihomo ui yacd # YACD
|
|
|
101
100
|
| `mihomo sub use <name>` | 切换当前订阅(支持模糊匹配,自动重启) |
|
|
102
101
|
| `mihomo sub add <url> [name]` | 添加订阅并自动切换(支持逗号分隔多 URL 合并) |
|
|
103
102
|
| `mihomo sub free <id>` | 添加内置免费订阅(`0`=合并 #1+#2,`sub free` 列出可用源)|
|
|
104
|
-
| `mihomo sub best <id>` | 添加聚合订阅(`sub best` 列出可用源)|
|
|
105
103
|
| `mihomo sub update` | 更新所有订阅 |
|
|
106
104
|
| `mihomo sub update <name>` | 更新指定订阅(支持模糊匹配) |
|
|
107
105
|
| `mihomo sub remove <name>` | 删除订阅(支持模糊匹配) |
|
|
108
106
|
| `mihomo sub web [name]` | 打开订阅页面(无参打开默认) |
|
|
109
107
|
| `mihomo sub test [name]` | 测试节点连通性(`-t` 超时,`-j` 并发) |
|
|
110
108
|
| `mihomo sub clean [name]` | 测速并清理失败节点 |
|
|
109
|
+
| `mihomo test` | 快速测试当前节点连通性(`-t` 超时,`-j` 并发) |
|
|
110
|
+
| `mihomo clean` | 清理失败节点并自动重启(`-t` 超时,`-j` 并发) |
|
|
111
111
|
|
|
112
112
|
### 覆写配置
|
|
113
113
|
|
package/dist/index.js
CHANGED
|
@@ -2687,14 +2687,6 @@ function getFreeSubscriptionSources() {
|
|
|
2687
2687
|
{ name: "NoMoreWalls", url: "https://gh-proxy.org/raw.githubusercontent.com/peasoft/NoMoreWalls/master/list.meta.yml" }
|
|
2688
2688
|
];
|
|
2689
2689
|
}
|
|
2690
|
-
var GH_SUB = "https://gh-proxy.org/raw.githubusercontent.com/imaex/mihomo-free-sub/sub";
|
|
2691
|
-
function getBestSubscriptionSources() {
|
|
2692
|
-
return [
|
|
2693
|
-
{ name: "curated", url: `${GH_SUB}/curated.yaml`, description: "\u7CBE\u9009 29 \u7EC4\uFF08\u4EC5\u6D4B\u901F\u6E90\uFF09" },
|
|
2694
|
-
{ name: "acl4ssr", url: `${GH_SUB}/acl4ssr.yaml`, description: "ACL4SSR 29 \u7EC4\uFF08\u5168\u90E8\u6E90\uFF09" },
|
|
2695
|
-
{ name: "freesub", url: `${GH_SUB}/freesub.yaml`, description: "freeSub 24 \u7EC4" }
|
|
2696
|
-
];
|
|
2697
|
-
}
|
|
2698
2690
|
var BENCH_CONFIG = {
|
|
2699
2691
|
"allow-lan": false,
|
|
2700
2692
|
"external-controller": "127.0.0.1:19090",
|
|
@@ -2703,6 +2695,14 @@ var BENCH_CONFIG = {
|
|
|
2703
2695
|
"log-level": "error",
|
|
2704
2696
|
"geodata-mode": true
|
|
2705
2697
|
};
|
|
2698
|
+
var TEST_CONFIG = {
|
|
2699
|
+
"allow-lan": false,
|
|
2700
|
+
"external-controller": "127.0.0.1:29090",
|
|
2701
|
+
port: 27890,
|
|
2702
|
+
"socks-port": 27891,
|
|
2703
|
+
"log-level": "error",
|
|
2704
|
+
"geodata-mode": true
|
|
2705
|
+
};
|
|
2706
2706
|
var BASE_CONFIG = {
|
|
2707
2707
|
"allow-lan": false,
|
|
2708
2708
|
"external-controller": "127.0.0.1:9090",
|
|
@@ -3397,6 +3397,15 @@ function parseMirrorArg(args) {
|
|
|
3397
3397
|
}
|
|
3398
3398
|
return { mirror: null, isOverride: false, type: "download" };
|
|
3399
3399
|
}
|
|
3400
|
+
function isProxyValid(proxy) {
|
|
3401
|
+
if (!proxy.name || !proxy.server || !proxy.port) return false;
|
|
3402
|
+
if (!proxy.type) return false;
|
|
3403
|
+
if (proxy.type === "ss" && typeof proxy.cipher === "string" && proxy.cipher.startsWith("2022-blake3")) {
|
|
3404
|
+
const pw = String(proxy.password || "");
|
|
3405
|
+
if (!/^[A-Za-z0-9+/]+=*$/.test(pw) || pw.length < 20) return false;
|
|
3406
|
+
}
|
|
3407
|
+
return true;
|
|
3408
|
+
}
|
|
3400
3409
|
|
|
3401
3410
|
// src/bench.ts
|
|
3402
3411
|
var BENCH_DIR = path3.join(USER_DATA_DIR, "bench");
|
|
@@ -3541,15 +3550,6 @@ async function downloadAllSources(sources, onProgress) {
|
|
|
3541
3550
|
});
|
|
3542
3551
|
return await Promise.all(tasks);
|
|
3543
3552
|
}
|
|
3544
|
-
function isProxyValid(proxy) {
|
|
3545
|
-
if (!proxy.name || !proxy.server || !proxy.port) return false;
|
|
3546
|
-
if (!proxy.type) return false;
|
|
3547
|
-
if (proxy.type === "ss" && typeof proxy.cipher === "string" && proxy.cipher.startsWith("2022-blake3")) {
|
|
3548
|
-
const pw = String(proxy.password || "");
|
|
3549
|
-
if (!/^[A-Za-z0-9+/]+=*$/.test(pw) || pw.length < 20) return false;
|
|
3550
|
-
}
|
|
3551
|
-
return true;
|
|
3552
|
-
}
|
|
3553
3553
|
function buildMergedBenchConfig(allProxies) {
|
|
3554
3554
|
ensureBenchDirs();
|
|
3555
3555
|
const validProxies = allProxies.filter(isProxyValid);
|
|
@@ -4484,9 +4484,9 @@ async function autoUpdateStaleSubscription() {
|
|
|
4484
4484
|
}
|
|
4485
4485
|
var API_BASE = `http://${BASE_CONFIG["external-controller"]}`;
|
|
4486
4486
|
var DEFAULT_TEST_URL = "http://www.gstatic.com/generate_204";
|
|
4487
|
-
async function testProxyDelay(proxyName, timeout, testUrl, client) {
|
|
4487
|
+
async function testProxyDelay(proxyName, timeout, testUrl, client, apiBase = API_BASE) {
|
|
4488
4488
|
const encodedName = encodeURIComponent(proxyName);
|
|
4489
|
-
const url = `${
|
|
4489
|
+
const url = `${apiBase}/proxies/${encodedName}/delay?timeout=${timeout}&url=${encodeURIComponent(testUrl)}`;
|
|
4490
4490
|
try {
|
|
4491
4491
|
const response = await client.get(url);
|
|
4492
4492
|
const data = JSON.parse(response.data);
|
|
@@ -4506,7 +4506,7 @@ async function testProxyDelay(proxyName, timeout, testUrl, client) {
|
|
|
4506
4506
|
}
|
|
4507
4507
|
}
|
|
4508
4508
|
async function testSubscriptionProxies(subName, options = {}) {
|
|
4509
|
-
const { timeout = 2e3, concurrency = 100, testUrl = DEFAULT_TEST_URL, onResult } = options;
|
|
4509
|
+
const { timeout = 2e3, concurrency = 100, testUrl = DEFAULT_TEST_URL, apiBase = API_BASE, onResult } = options;
|
|
4510
4510
|
const { proxies } = options.parsed || loadSubscriptionConfig(subName);
|
|
4511
4511
|
if (proxies.length === 0) {
|
|
4512
4512
|
return { total: 0, alive: 0, dead: 0, results: [] };
|
|
@@ -4516,7 +4516,7 @@ async function testSubscriptionProxies(subName, options = {}) {
|
|
|
4516
4516
|
let completedCount = 0;
|
|
4517
4517
|
for (let i = 0; i < proxies.length; i += concurrency) {
|
|
4518
4518
|
const batch = proxies.slice(i, i + concurrency);
|
|
4519
|
-
const batchResults = await Promise.all(batch.map((proxy) => testProxyDelay(proxy.name, timeout, testUrl, client)));
|
|
4519
|
+
const batchResults = await Promise.all(batch.map((proxy) => testProxyDelay(proxy.name, timeout, testUrl, client, apiBase)));
|
|
4520
4520
|
for (const result of batchResults) {
|
|
4521
4521
|
results.push(result);
|
|
4522
4522
|
onResult?.(result, completedCount, proxies.length);
|
|
@@ -4598,12 +4598,138 @@ async function autoCleanSubscription(subName, options = {}) {
|
|
|
4598
4598
|
removedGroups = cleanResult.removedGroups;
|
|
4599
4599
|
}
|
|
4600
4600
|
}
|
|
4601
|
-
if (!skipped) {
|
|
4601
|
+
if (!skipped && removedProxies > 0) {
|
|
4602
4602
|
saveSubscriptionConfig(subName, parsed);
|
|
4603
4603
|
}
|
|
4604
4604
|
return { summary, removedProxies, updatedGroups, removedGroups, skipped };
|
|
4605
4605
|
}
|
|
4606
4606
|
|
|
4607
|
+
// src/test-instance.ts
|
|
4608
|
+
import { spawn as spawn3 } from "child_process";
|
|
4609
|
+
import fs7 from "fs";
|
|
4610
|
+
import path5 from "path";
|
|
4611
|
+
var TEST_DIR = path5.join(USER_DATA_DIR, "test");
|
|
4612
|
+
var TEST_DIRS = {
|
|
4613
|
+
data: path5.join(TEST_DIR, "data"),
|
|
4614
|
+
runtime: path5.join(TEST_DIR, "runtime")
|
|
4615
|
+
};
|
|
4616
|
+
var TEST_PATHS = {
|
|
4617
|
+
configFile: path5.join(TEST_DIRS.runtime, "config.yaml"),
|
|
4618
|
+
pidFile: path5.join(TEST_DIRS.runtime, "pid"),
|
|
4619
|
+
logFile: path5.join(TEST_DIR, "test.log")
|
|
4620
|
+
};
|
|
4621
|
+
var TEST_API = `http://${TEST_CONFIG["external-controller"]}`;
|
|
4622
|
+
function ensureTestDirs() {
|
|
4623
|
+
for (const dir of Object.values(TEST_DIRS)) {
|
|
4624
|
+
fs7.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4627
|
+
function cleanupTestDir() {
|
|
4628
|
+
rmrf(TEST_DIR);
|
|
4629
|
+
}
|
|
4630
|
+
function buildTestConfig(subName) {
|
|
4631
|
+
ensureTestDirs();
|
|
4632
|
+
const rawContent = readSubscriptionRawConfig(subName);
|
|
4633
|
+
if (!rawContent) {
|
|
4634
|
+
throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
|
|
4635
|
+
}
|
|
4636
|
+
const parsed = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
|
|
4637
|
+
const proxies = (parsed.proxies || []).filter(isProxyValid);
|
|
4638
|
+
if (proxies.length === 0) {
|
|
4639
|
+
throw new Error(`\u8BA2\u9605 "${subName}" \u6CA1\u6709\u6709\u6548\u8282\u70B9`);
|
|
4640
|
+
}
|
|
4641
|
+
const nameCount = /* @__PURE__ */ new Map();
|
|
4642
|
+
for (const proxy of proxies) {
|
|
4643
|
+
const count = (nameCount.get(proxy.name) || 0) + 1;
|
|
4644
|
+
nameCount.set(proxy.name, count);
|
|
4645
|
+
if (count > 1) {
|
|
4646
|
+
proxy.name = `${proxy.name} #${count}`;
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
const config = {
|
|
4650
|
+
...TEST_CONFIG,
|
|
4651
|
+
proxies,
|
|
4652
|
+
"proxy-groups": [
|
|
4653
|
+
{
|
|
4654
|
+
name: "PROXY",
|
|
4655
|
+
type: "select",
|
|
4656
|
+
proxies: proxies.map((p) => p.name)
|
|
4657
|
+
}
|
|
4658
|
+
],
|
|
4659
|
+
rules: ["MATCH,PROXY"]
|
|
4660
|
+
};
|
|
4661
|
+
const content = jsYaml.dump(config, { indent: 2, lineWidth: -1, noCompatMode: true });
|
|
4662
|
+
fs7.writeFileSync(TEST_PATHS.configFile, content, { mode: 384 });
|
|
4663
|
+
}
|
|
4664
|
+
async function startTestInstance() {
|
|
4665
|
+
const binary2 = PATHS.mihomoBinary;
|
|
4666
|
+
if (!fs7.existsSync(binary2)) throw new Error("\u672A\u627E\u5230 mihomo \u5185\u6838");
|
|
4667
|
+
stopTestInstance();
|
|
4668
|
+
const logFd = fs7.openSync(TEST_PATHS.logFile, "a");
|
|
4669
|
+
const child = spawn3(binary2, ["-d", TEST_DIRS.data, "-f", TEST_PATHS.configFile], {
|
|
4670
|
+
detached: true,
|
|
4671
|
+
stdio: ["ignore", logFd, logFd]
|
|
4672
|
+
});
|
|
4673
|
+
fs7.closeSync(logFd);
|
|
4674
|
+
child.unref();
|
|
4675
|
+
const pid = child.pid;
|
|
4676
|
+
fs7.writeFileSync(TEST_PATHS.pidFile, pid.toString(), { mode: 384 });
|
|
4677
|
+
const client = createHttpClient({ timeout: 2e3 });
|
|
4678
|
+
let ready = false;
|
|
4679
|
+
for (let i = 0; i < 60; i++) {
|
|
4680
|
+
if (!isProcessRunning(pid)) break;
|
|
4681
|
+
try {
|
|
4682
|
+
await client.get(`${TEST_API}/version`);
|
|
4683
|
+
ready = true;
|
|
4684
|
+
break;
|
|
4685
|
+
} catch {
|
|
4686
|
+
await sleep(500);
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
if (!isProcessRunning(pid)) {
|
|
4690
|
+
let errorDetail = "";
|
|
4691
|
+
try {
|
|
4692
|
+
errorDetail = fs7.readFileSync(TEST_PATHS.logFile, "utf8").slice(-1e3);
|
|
4693
|
+
} catch {
|
|
4694
|
+
}
|
|
4695
|
+
throw new Error(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
|
|
4696
|
+
${errorDetail}` : ""}`);
|
|
4697
|
+
}
|
|
4698
|
+
if (!ready) {
|
|
4699
|
+
throw new Error("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u8D85\u65F6\uFF0CAPI \u672A\u54CD\u5E94");
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
function stopTestInstance() {
|
|
4703
|
+
let pid;
|
|
4704
|
+
try {
|
|
4705
|
+
pid = parseInt(fs7.readFileSync(TEST_PATHS.pidFile, "utf8").trim(), 10);
|
|
4706
|
+
} catch {
|
|
4707
|
+
return;
|
|
4708
|
+
}
|
|
4709
|
+
if (pid > 0 && isProcessRunning(pid)) {
|
|
4710
|
+
process.kill(pid, "SIGKILL");
|
|
4711
|
+
for (let i = 0; i < 20; i++) {
|
|
4712
|
+
if (!isProcessRunning(pid)) break;
|
|
4713
|
+
sleepSync(100);
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
try {
|
|
4717
|
+
fs7.unlinkSync(TEST_PATHS.pidFile);
|
|
4718
|
+
} catch {
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
async function withTestInstance(subName, fn) {
|
|
4722
|
+
cleanupTestDir();
|
|
4723
|
+
buildTestConfig(subName);
|
|
4724
|
+
try {
|
|
4725
|
+
await startTestInstance();
|
|
4726
|
+
return await fn(TEST_API);
|
|
4727
|
+
} finally {
|
|
4728
|
+
stopTestInstance();
|
|
4729
|
+
cleanupTestDir();
|
|
4730
|
+
}
|
|
4731
|
+
}
|
|
4732
|
+
|
|
4607
4733
|
// src/commands/status.ts
|
|
4608
4734
|
function printStatus() {
|
|
4609
4735
|
const status = getStatus();
|
|
@@ -4741,9 +4867,9 @@ function printTestResult(result, index, total) {
|
|
|
4741
4867
|
const prefix = `[${index + 1}/${total}]`;
|
|
4742
4868
|
if (result.delay !== null) {
|
|
4743
4869
|
const delayColor = result.delay < 300 ? colors.green : result.delay < 800 ? colors.yellow : colors.red;
|
|
4744
|
-
console.log(
|
|
4870
|
+
console.log(`${prefix} ${colors.green("\u2713")} ${result.name} ${delayColor(`${result.delay}ms`)}`);
|
|
4745
4871
|
} else {
|
|
4746
|
-
console.log(
|
|
4872
|
+
console.log(`${prefix} ${colors.red("\u2717")} ${result.name} ${colors.gray(result.error || "timeout")}`);
|
|
4747
4873
|
}
|
|
4748
4874
|
}
|
|
4749
4875
|
function formatCleanSummary(result) {
|
|
@@ -4760,7 +4886,7 @@ function githubRepoUrl(rawUrl) {
|
|
|
4760
4886
|
if (match) return `https://github.com/${match[1]}`;
|
|
4761
4887
|
return null;
|
|
4762
4888
|
}
|
|
4763
|
-
function
|
|
4889
|
+
function resolveTestTarget(args) {
|
|
4764
4890
|
const subs = getSubscriptions();
|
|
4765
4891
|
if (subs.length === 0) {
|
|
4766
4892
|
console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
|
|
@@ -4769,28 +4895,18 @@ function resolveActiveTestTarget(args) {
|
|
|
4769
4895
|
const nameArg = getNonFlagArg(args, 2);
|
|
4770
4896
|
const timeout = parseIntArg(args, "-t", "--timeout", 2e3);
|
|
4771
4897
|
const concurrency = parseIntArg(args, "-j", "--concurrency", 100);
|
|
4772
|
-
const activeSub = getActiveSubscription();
|
|
4773
4898
|
let target;
|
|
4774
4899
|
if (nameArg) {
|
|
4775
4900
|
const matches = findSubscriptionFuzzy(subs, nameArg);
|
|
4776
4901
|
target = pickSingleSubscription(matches, nameArg);
|
|
4777
4902
|
} else {
|
|
4903
|
+
const activeSub = getActiveSubscription();
|
|
4778
4904
|
if (!activeSub) {
|
|
4779
|
-
console.error("\u9519\u8BEF: \u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
|
|
4905
|
+
console.error("\u9519\u8BEF: \u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605\uFF0C\u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
|
|
4780
4906
|
process.exit(1);
|
|
4781
4907
|
}
|
|
4782
4908
|
target = activeSub;
|
|
4783
4909
|
}
|
|
4784
|
-
const status = getStatus();
|
|
4785
|
-
if (!status.running) {
|
|
4786
|
-
console.error("\u9519\u8BEF: mihomo \u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 (mihomo start)");
|
|
4787
|
-
process.exit(1);
|
|
4788
|
-
}
|
|
4789
|
-
if (!activeSub || activeSub.name !== target.name) {
|
|
4790
|
-
console.error(`\u9519\u8BEF: \u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605\u662F "${activeSub?.name}"\uFF0C\u4E0D\u662F "${target.name}"`);
|
|
4791
|
-
console.log(`\u8BF7\u5148\u5207\u6362: mihomo sub use ${target.name}`);
|
|
4792
|
-
process.exit(1);
|
|
4793
|
-
}
|
|
4794
4910
|
return { target, timeout, concurrency };
|
|
4795
4911
|
}
|
|
4796
4912
|
async function printSubscriptionList(options) {
|
|
@@ -4899,36 +5015,6 @@ async function addFreeSubscription(freeId) {
|
|
|
4899
5015
|
console.log("");
|
|
4900
5016
|
await printSubscriptionList();
|
|
4901
5017
|
}
|
|
4902
|
-
function printBestSourceList() {
|
|
4903
|
-
const bestSources = getBestSubscriptionSources();
|
|
4904
|
-
for (let i = 0; i < bestSources.length; i++) {
|
|
4905
|
-
console.log(` ${i + 1} ${bestSources[i].name} \u2014 ${bestSources[i].description}`);
|
|
4906
|
-
}
|
|
4907
|
-
}
|
|
4908
|
-
async function addBestSubscription(bestId) {
|
|
4909
|
-
const bestSources = getBestSubscriptionSources();
|
|
4910
|
-
if (bestId < 1 || bestId > bestSources.length) {
|
|
4911
|
-
console.error(`\u9519\u8BEF: best \u8BA2\u9605 ID \u8303\u56F4 1-${bestSources.length}`);
|
|
4912
|
-
console.log("\n\u53EF\u7528\u6E90:");
|
|
4913
|
-
printBestSourceList();
|
|
4914
|
-
process.exit(1);
|
|
4915
|
-
}
|
|
4916
|
-
const source = bestSources[bestId - 1];
|
|
4917
|
-
const name = `best${bestId}`;
|
|
4918
|
-
console.log(`\u6DFB\u52A0 best \u8BA2\u9605: ${name} (${source.description})`);
|
|
4919
|
-
try {
|
|
4920
|
-
addSubscription(source.url, name);
|
|
4921
|
-
const info = await downloadSubscription(source.url, name);
|
|
4922
|
-
setDefaultSubscription(name);
|
|
4923
|
-
saveSubscriptionCache(name, { web_page_url: "https://github.com/imaex/mihomo-free-sub" });
|
|
4924
|
-
console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
|
|
4925
|
-
} catch (e) {
|
|
4926
|
-
console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
|
|
4927
|
-
process.exit(1);
|
|
4928
|
-
}
|
|
4929
|
-
console.log("");
|
|
4930
|
-
await printSubscriptionList();
|
|
4931
|
-
}
|
|
4932
5018
|
async function cmdSubscription(args) {
|
|
4933
5019
|
const action = args[1];
|
|
4934
5020
|
if (!action || action === "list") {
|
|
@@ -4946,17 +5032,6 @@ async function cmdSubscription(args) {
|
|
|
4946
5032
|
await addFreeSubscription(id);
|
|
4947
5033
|
return;
|
|
4948
5034
|
}
|
|
4949
|
-
if (action === "best") {
|
|
4950
|
-
const id = parseInt(args[2], 10);
|
|
4951
|
-
if (Number.isNaN(id)) {
|
|
4952
|
-
console.log("\u7528\u6CD5: mihomo sub best <id>\n");
|
|
4953
|
-
console.log("\u53EF\u7528\u6E90:");
|
|
4954
|
-
printBestSourceList();
|
|
4955
|
-
process.exit(1);
|
|
4956
|
-
}
|
|
4957
|
-
await addBestSubscription(id);
|
|
4958
|
-
return;
|
|
4959
|
-
}
|
|
4960
5035
|
if (action === "add") {
|
|
4961
5036
|
const freeId = parseIntArg(args, "--free", "--free", -1);
|
|
4962
5037
|
if (freeId > 0) {
|
|
@@ -5155,14 +5230,17 @@ async function cmdSubscription(args) {
|
|
|
5155
5230
|
return;
|
|
5156
5231
|
}
|
|
5157
5232
|
if (action === "clean") {
|
|
5158
|
-
const { target, timeout, concurrency } =
|
|
5233
|
+
const { target, timeout, concurrency } = resolveTestTarget(args);
|
|
5159
5234
|
console.log(`\u6E05\u7406\u8BA2\u9605 "${target.name}"...`);
|
|
5160
5235
|
console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
|
|
5161
5236
|
console.log("");
|
|
5162
|
-
const result = await
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5237
|
+
const result = await withTestInstance(target.name, async (apiBase) => {
|
|
5238
|
+
return autoCleanSubscription(target.name, {
|
|
5239
|
+
timeout,
|
|
5240
|
+
concurrency,
|
|
5241
|
+
apiBase,
|
|
5242
|
+
onResult: printTestResult
|
|
5243
|
+
});
|
|
5166
5244
|
});
|
|
5167
5245
|
console.log("");
|
|
5168
5246
|
console.log(formatTestSummary(result.summary));
|
|
@@ -5171,20 +5249,26 @@ async function cmdSubscription(args) {
|
|
|
5171
5249
|
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"));
|
|
5172
5250
|
} else if (result.removedProxies > 0) {
|
|
5173
5251
|
console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
|
|
5174
|
-
|
|
5175
|
-
|
|
5252
|
+
const status = getStatus();
|
|
5253
|
+
if (status.running) {
|
|
5254
|
+
console.log("");
|
|
5255
|
+
console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
|
|
5256
|
+
}
|
|
5176
5257
|
}
|
|
5177
5258
|
return;
|
|
5178
5259
|
}
|
|
5179
5260
|
if (action === "test") {
|
|
5180
|
-
const { target, timeout, concurrency } =
|
|
5261
|
+
const { target, timeout, concurrency } = resolveTestTarget(args);
|
|
5181
5262
|
console.log(`\u6D4B\u8BD5\u8BA2\u9605 "${target.name}" \u7684\u8282\u70B9\u8FDE\u901A\u6027...`);
|
|
5182
5263
|
console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
|
|
5183
5264
|
console.log("");
|
|
5184
|
-
const summary = await
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5265
|
+
const summary = await withTestInstance(target.name, async (apiBase) => {
|
|
5266
|
+
return testSubscriptionProxies(target.name, {
|
|
5267
|
+
timeout,
|
|
5268
|
+
concurrency,
|
|
5269
|
+
apiBase,
|
|
5270
|
+
onResult: printTestResult
|
|
5271
|
+
});
|
|
5188
5272
|
});
|
|
5189
5273
|
console.log("");
|
|
5190
5274
|
console.log(formatTestSummary(summary));
|
|
@@ -5431,6 +5515,8 @@ ${colors.cyan("\u8BA2\u9605:")}
|
|
|
5431
5515
|
${colors.bold("subscription")} web [name] \u6253\u5F00\u8BA2\u9605\u9875\u9762
|
|
5432
5516
|
${colors.bold("subscription")} test [name] \u6D4B\u8BD5\u8282\u70B9\u8FDE\u901A\u6027
|
|
5433
5517
|
${colors.bold("subscription")} clean [name] \u6D4B\u901F\u5E76\u6E05\u7406\u5931\u8D25\u8282\u70B9
|
|
5518
|
+
${colors.bold("test")} [-t ms] [-j N] \u5FEB\u901F\u6D4B\u8BD5\u5F53\u524D\u8282\u70B9\u8FDE\u901A\u6027
|
|
5519
|
+
${colors.bold("clean")} [-t ms] [-j N] \u6E05\u7406\u5931\u8D25\u8282\u70B9\u5E76\u81EA\u52A8\u91CD\u542F
|
|
5434
5520
|
${colors.bold("bench")} [name] [-t ms] [-j N] \u6D4B\u8BD5\u514D\u8D39\u8BA2\u9605\u6E90\u8D28\u91CF\u6392\u540D
|
|
5435
5521
|
|
|
5436
5522
|
${colors.cyan("\u914D\u7F6E:")}
|
|
@@ -5472,8 +5558,8 @@ function printVersion() {
|
|
|
5472
5558
|
|
|
5473
5559
|
// src/kernel.ts
|
|
5474
5560
|
import { execSync as execSync4, spawnSync } from "child_process";
|
|
5475
|
-
import
|
|
5476
|
-
import
|
|
5561
|
+
import fs8 from "fs";
|
|
5562
|
+
import path6 from "path";
|
|
5477
5563
|
|
|
5478
5564
|
// node_modules/compare-versions/lib/esm/utils.js
|
|
5479
5565
|
var semver = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
|
|
@@ -5598,10 +5684,10 @@ async function checkUpdate(mirror) {
|
|
|
5598
5684
|
};
|
|
5599
5685
|
}
|
|
5600
5686
|
function findBinaryInDir(dir) {
|
|
5601
|
-
const files =
|
|
5687
|
+
const files = fs8.readdirSync(dir);
|
|
5602
5688
|
for (const f of files) {
|
|
5603
|
-
const fullPath =
|
|
5604
|
-
const stat =
|
|
5689
|
+
const fullPath = path6.join(dir, f);
|
|
5690
|
+
const stat = fs8.statSync(fullPath);
|
|
5605
5691
|
if (stat.isDirectory()) {
|
|
5606
5692
|
const found = findBinaryInDir(fullPath);
|
|
5607
5693
|
if (found) return found;
|
|
@@ -5627,7 +5713,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
5627
5713
|
\u5E73\u53F0: ${platform}, \u67B6\u6784: ${arch}${hint}`);
|
|
5628
5714
|
}
|
|
5629
5715
|
const downloadUrl = withMirror(asset.browser_download_url, mirror);
|
|
5630
|
-
const tempPath =
|
|
5716
|
+
const tempPath = path6.join(DIRS.kernel, asset.name);
|
|
5631
5717
|
const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
|
|
5632
5718
|
if (progressCallback) {
|
|
5633
5719
|
progressCallback(`\u4E0B\u8F7D\u5185\u6838: ${asset.name} (${sizeMB} MB)`);
|
|
@@ -5639,12 +5725,12 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
5639
5725
|
);
|
|
5640
5726
|
if (curlResult.status !== 0) {
|
|
5641
5727
|
try {
|
|
5642
|
-
|
|
5728
|
+
fs8.unlinkSync(tempPath);
|
|
5643
5729
|
} catch {
|
|
5644
5730
|
}
|
|
5645
5731
|
throw new Error(`\u4E0B\u8F7D\u5931\u8D25 (curl \u9000\u51FA\u7801 ${curlResult.status})`);
|
|
5646
5732
|
}
|
|
5647
|
-
if (!
|
|
5733
|
+
if (!fs8.existsSync(tempPath)) {
|
|
5648
5734
|
throw new Error("\u4E0B\u8F7D\u5931\u8D25: \u6587\u4EF6\u672A\u751F\u6210");
|
|
5649
5735
|
}
|
|
5650
5736
|
if (progressCallback) {
|
|
@@ -5656,14 +5742,14 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
5656
5742
|
if (tempPath.endsWith(".tar.gz") || tempPath.endsWith(".tgz")) {
|
|
5657
5743
|
execSync4(`tar -xzf "${tempPath}" -C "${extractPath}"`, { stdio: ["pipe", "pipe", "inherit"] });
|
|
5658
5744
|
} else if (tempPath.endsWith(".gz")) {
|
|
5659
|
-
const baseName =
|
|
5660
|
-
const outputPath =
|
|
5745
|
+
const baseName = path6.basename(tempPath, ".gz");
|
|
5746
|
+
const outputPath = path6.join(extractPath, baseName);
|
|
5661
5747
|
execSync4(`gzip -dc "${tempPath}" > "${outputPath}"`, { stdio: ["pipe", "pipe", "inherit"] });
|
|
5662
5748
|
extractedBinary = outputPath;
|
|
5663
5749
|
}
|
|
5664
5750
|
} catch (e) {
|
|
5665
5751
|
try {
|
|
5666
|
-
|
|
5752
|
+
fs8.unlinkSync(tempPath);
|
|
5667
5753
|
} catch {
|
|
5668
5754
|
}
|
|
5669
5755
|
throw new Error(`\u89E3\u538B\u5931\u8D25: ${e.message}`);
|
|
@@ -5671,25 +5757,25 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
5671
5757
|
const foundBinary = extractedBinary || findBinaryInDir(extractPath);
|
|
5672
5758
|
if (!foundBinary) {
|
|
5673
5759
|
try {
|
|
5674
|
-
|
|
5760
|
+
fs8.unlinkSync(tempPath);
|
|
5675
5761
|
} catch {
|
|
5676
5762
|
}
|
|
5677
5763
|
throw new Error("\u89E3\u538B\u540E\u672A\u627E\u5230\u53EF\u6267\u884C\u6587\u4EF6");
|
|
5678
5764
|
}
|
|
5679
5765
|
const targetPath = PATHS.mihomoBinary;
|
|
5680
5766
|
if (foundBinary !== targetPath) {
|
|
5681
|
-
if (
|
|
5682
|
-
|
|
5767
|
+
if (fs8.existsSync(targetPath)) {
|
|
5768
|
+
fs8.chmodSync(targetPath, 493);
|
|
5683
5769
|
try {
|
|
5684
|
-
|
|
5770
|
+
fs8.unlinkSync(targetPath);
|
|
5685
5771
|
} catch {
|
|
5686
5772
|
}
|
|
5687
5773
|
}
|
|
5688
|
-
|
|
5774
|
+
fs8.renameSync(foundBinary, targetPath);
|
|
5689
5775
|
}
|
|
5690
|
-
|
|
5776
|
+
fs8.chmodSync(targetPath, 493);
|
|
5691
5777
|
try {
|
|
5692
|
-
|
|
5778
|
+
fs8.unlinkSync(tempPath);
|
|
5693
5779
|
} catch {
|
|
5694
5780
|
}
|
|
5695
5781
|
clearKernelVersionCache();
|
|
@@ -5830,7 +5916,7 @@ function cmdLogs(args) {
|
|
|
5830
5916
|
}
|
|
5831
5917
|
|
|
5832
5918
|
// src/commands/overwrite.ts
|
|
5833
|
-
import
|
|
5919
|
+
import path7 from "path";
|
|
5834
5920
|
function printOverwriteList() {
|
|
5835
5921
|
const info = listOverwriteFile();
|
|
5836
5922
|
const statusText = info.enabled ? colors.green("\u5DF2\u542F\u7528") : colors.yellow("\u5DF2\u7981\u7528");
|
|
@@ -5840,8 +5926,8 @@ function printOverwriteList() {
|
|
|
5840
5926
|
if (info.files.length === 0) {
|
|
5841
5927
|
console.log("\u6682\u65E0\u8986\u5199\u6587\u4EF6");
|
|
5842
5928
|
console.log("");
|
|
5843
|
-
console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${
|
|
5844
|
-
console.log(` \u6216 ${
|
|
5929
|
+
console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path7.join(info.dir, "overwrite.yaml")}`);
|
|
5930
|
+
console.log(` \u6216 ${path7.join(info.dir, "overwrite.dns.yaml")}`);
|
|
5845
5931
|
console.log("");
|
|
5846
5932
|
} else {
|
|
5847
5933
|
console.log(`${colors.cyan("\u8986\u5199\u6587\u4EF6")} (${info.files.length} \u4E2A\uFF0C\u6309\u987A\u5E8F\u52A0\u8F7D):`);
|
|
@@ -5905,7 +5991,7 @@ async function cmdOverwrite(args) {
|
|
|
5905
5991
|
}
|
|
5906
5992
|
|
|
5907
5993
|
// src/commands/reset.ts
|
|
5908
|
-
import
|
|
5994
|
+
import fs9 from "fs";
|
|
5909
5995
|
import readline from "readline";
|
|
5910
5996
|
var RESET_TARGETS = [
|
|
5911
5997
|
{
|
|
@@ -5960,8 +6046,8 @@ var RESET_TARGETS = [
|
|
|
5960
6046
|
label: "\u8986\u5199",
|
|
5961
6047
|
paths: () => {
|
|
5962
6048
|
const dir = USER_DATA_DIR;
|
|
5963
|
-
if (!
|
|
5964
|
-
return
|
|
6049
|
+
if (!fs9.existsSync(dir)) return [];
|
|
6050
|
+
return fs9.readdirSync(dir).filter((f) => f === "overwrite.yaml" || /^overwrite\..+\.ya?ml$/.test(f)).map((f) => `${dir}/${f}`);
|
|
5965
6051
|
},
|
|
5966
6052
|
needsStop: false
|
|
5967
6053
|
}
|
|
@@ -6077,6 +6163,69 @@ async function cmdStop() {
|
|
|
6077
6163
|
console.log(colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B"));
|
|
6078
6164
|
}
|
|
6079
6165
|
|
|
6166
|
+
// src/commands/test.ts
|
|
6167
|
+
function requireRunning() {
|
|
6168
|
+
const status = getStatus();
|
|
6169
|
+
if (!status.running) {
|
|
6170
|
+
console.error("\u9519\u8BEF: mihomo \u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 (mihomo start)");
|
|
6171
|
+
process.exit(1);
|
|
6172
|
+
}
|
|
6173
|
+
}
|
|
6174
|
+
function requireActiveSub() {
|
|
6175
|
+
const activeSub = getActiveSubscription();
|
|
6176
|
+
if (!activeSub) {
|
|
6177
|
+
console.error("\u9519\u8BEF: \u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
|
|
6178
|
+
process.exit(1);
|
|
6179
|
+
}
|
|
6180
|
+
return activeSub;
|
|
6181
|
+
}
|
|
6182
|
+
async function cmdTest(args) {
|
|
6183
|
+
requireRunning();
|
|
6184
|
+
const activeSub = requireActiveSub();
|
|
6185
|
+
const timeout = parseIntArg(args, "-t", "--timeout", 1500);
|
|
6186
|
+
const concurrency = parseIntArg(args, "-j", "--concurrency", 100);
|
|
6187
|
+
console.log(`\u6D4B\u8BD5 "${activeSub.name}" \u8282\u70B9\u8FDE\u901A\u6027...`);
|
|
6188
|
+
console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
|
|
6189
|
+
console.log("");
|
|
6190
|
+
const summary = await testSubscriptionProxies(activeSub.name, {
|
|
6191
|
+
timeout,
|
|
6192
|
+
concurrency,
|
|
6193
|
+
onResult: printTestResult
|
|
6194
|
+
});
|
|
6195
|
+
console.log("");
|
|
6196
|
+
console.log(formatTestSummary(summary));
|
|
6197
|
+
}
|
|
6198
|
+
async function cmdClean(args) {
|
|
6199
|
+
requireRunning();
|
|
6200
|
+
const activeSub = requireActiveSub();
|
|
6201
|
+
const timeout = parseIntArg(args, "-t", "--timeout", 1500);
|
|
6202
|
+
const concurrency = parseIntArg(args, "-j", "--concurrency", 100);
|
|
6203
|
+
console.log(`\u6E05\u7406 "${activeSub.name}" \u5931\u8D25\u8282\u70B9...`);
|
|
6204
|
+
console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
|
|
6205
|
+
console.log("");
|
|
6206
|
+
const result = await autoCleanSubscription(activeSub.name, {
|
|
6207
|
+
timeout,
|
|
6208
|
+
concurrency,
|
|
6209
|
+
onResult: printTestResult
|
|
6210
|
+
});
|
|
6211
|
+
console.log("");
|
|
6212
|
+
console.log(formatTestSummary(result.summary));
|
|
6213
|
+
if (result.skipped) {
|
|
6214
|
+
console.log("");
|
|
6215
|
+
console.log("\u5B58\u6D3B\u8282\u70B9\u4E0D\u8DB3 1%\uFF0C\u8DF3\u8FC7\u6E05\u7406\u3002\u8BF7\u68C0\u67E5\u539F\u59CB\u8BA2\u9605\u662F\u5426\u6709\u6548");
|
|
6216
|
+
} else if (result.removedProxies === 0) {
|
|
6217
|
+
console.log("\u6240\u6709\u8282\u70B9\u6B63\u5E38\uFF0C\u65E0\u9700\u6E05\u7406");
|
|
6218
|
+
} else {
|
|
6219
|
+
console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
|
|
6220
|
+
console.log("");
|
|
6221
|
+
console.log("\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548...");
|
|
6222
|
+
stop();
|
|
6223
|
+
const configInfo = prepareConfigForStart("mixed", activeSub.name);
|
|
6224
|
+
const startResult = await start("mixed");
|
|
6225
|
+
console.log(`${colors.green("\u5DF2\u91CD\u542F")} (PID ${startResult.pid}) \xB7 ${formatProxySummary(configInfo)}`);
|
|
6226
|
+
}
|
|
6227
|
+
}
|
|
6228
|
+
|
|
6080
6229
|
// src/commands/ui.ts
|
|
6081
6230
|
function cmdUI(args) {
|
|
6082
6231
|
const uiName = args[1] || "zash";
|
|
@@ -6095,7 +6244,7 @@ function cmdUI(args) {
|
|
|
6095
6244
|
}
|
|
6096
6245
|
|
|
6097
6246
|
// src/commands/update.ts
|
|
6098
|
-
import { exec, spawn as
|
|
6247
|
+
import { exec, spawn as spawn4 } from "child_process";
|
|
6099
6248
|
import { promisify } from "util";
|
|
6100
6249
|
var execAsync = promisify(exec);
|
|
6101
6250
|
async function cmdUpdate() {
|
|
@@ -6104,7 +6253,7 @@ async function cmdUpdate() {
|
|
|
6104
6253
|
console.log("\u6B63\u5728\u66F4\u65B0 mihomo-cli...");
|
|
6105
6254
|
console.log("");
|
|
6106
6255
|
await new Promise((resolve) => {
|
|
6107
|
-
const npm =
|
|
6256
|
+
const npm = spawn4("npm", ["install", "-g", "mihomo-cli"], { stdio: "inherit" });
|
|
6108
6257
|
npm.on("close", (code) => {
|
|
6109
6258
|
if (code === 0) {
|
|
6110
6259
|
resolve();
|
|
@@ -6252,6 +6401,12 @@ async function main() {
|
|
|
6252
6401
|
case "bench":
|
|
6253
6402
|
await cmdBench(args);
|
|
6254
6403
|
break;
|
|
6404
|
+
case "test":
|
|
6405
|
+
await cmdTest(args);
|
|
6406
|
+
break;
|
|
6407
|
+
case "clean":
|
|
6408
|
+
await cmdClean(args);
|
|
6409
|
+
break;
|
|
6255
6410
|
default:
|
|
6256
6411
|
console.error(`\u672A\u77E5\u547D\u4EE4: ${cmd}`);
|
|
6257
6412
|
console.error('\u4F7F\u7528 "mihomo help" \u67E5\u770B\u5E2E\u52A9');
|