mihomo-cli 2.3.0 → 2.3.1
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 +15 -0
- package/README.md +2 -2
- package/dist/index.js +157 -24
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.3.1] - 2026-05-02
|
|
4
|
+
|
|
5
|
+
### 新功能
|
|
6
|
+
|
|
7
|
+
- **合并订阅** - `sub add url1,url2 name` 支持逗号分隔多 URL,合并节点(按名去重),分组/规则取第一个源
|
|
8
|
+
- **sub free 0** - 特殊 ID `0` 自动合并免费源 #1 + #2(节点更多,配置相同)
|
|
9
|
+
|
|
10
|
+
### 改进
|
|
11
|
+
|
|
12
|
+
- 合并订阅在列表中显示 `[合并 N 源]` 标记
|
|
13
|
+
- `sub update` 自动识别合并订阅并重新下载合并
|
|
14
|
+
- URL 脱敏支持逗号分隔多 URL
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
3
18
|
## [2.3.0] - 2026-05-02
|
|
4
19
|
|
|
5
20
|
### 新功能
|
package/README.md
CHANGED
|
@@ -98,8 +98,8 @@ mihomo ui yacd # YACD
|
|
|
98
98
|
| ----------------------------- | -------------------------------------- |
|
|
99
99
|
| `mihomo sub` | 列出所有订阅(含流量、到期时间) |
|
|
100
100
|
| `mihomo sub use <name>` | 切换当前订阅(支持模糊匹配,自动重启) |
|
|
101
|
-
| `mihomo sub add <url> [name]` |
|
|
102
|
-
| `mihomo sub free <id>` | 添加内置免费订阅(`sub free` 列出可用源)|
|
|
101
|
+
| `mihomo sub add <url> [name]` | 添加订阅并自动切换(支持逗号分隔多 URL 合并) |
|
|
102
|
+
| `mihomo sub free <id>` | 添加内置免费订阅(`0`=合并 #1+#2,`sub free` 列出可用源)|
|
|
103
103
|
| `mihomo sub update` | 更新所有订阅 |
|
|
104
104
|
| `mihomo sub update <name>` | 更新指定订阅(支持模糊匹配) |
|
|
105
105
|
| `mihomo sub remove <name>` | 删除订阅(支持模糊匹配) |
|
package/dist/index.js
CHANGED
|
@@ -2795,6 +2795,9 @@ function invalidateSettingsCache() {
|
|
|
2795
2795
|
}
|
|
2796
2796
|
function maskUrl(url) {
|
|
2797
2797
|
if (!url) return url;
|
|
2798
|
+
if (url.includes(",")) {
|
|
2799
|
+
return url.split(",").map((u) => maskUrl(u.trim())).join(", ");
|
|
2800
|
+
}
|
|
2798
2801
|
try {
|
|
2799
2802
|
const parsed = new URL(url);
|
|
2800
2803
|
const tokenKeys = ["token", "key", "secret", "pass", "password", "auth", "access_token", "api_key"];
|
|
@@ -4207,6 +4210,12 @@ function viewLogWithTail(logPath, options) {
|
|
|
4207
4210
|
var DEFAULT_UPDATE_INTERVAL_HOURS = 12;
|
|
4208
4211
|
var YAML_DUMP_OPTS = { indent: 2, lineWidth: -1, noCompatMode: true };
|
|
4209
4212
|
var HTTP_CLIENT = createHttpClient({ timeout: 6e4 });
|
|
4213
|
+
function isMultiUrl(url) {
|
|
4214
|
+
return url.includes(",");
|
|
4215
|
+
}
|
|
4216
|
+
function splitUrls(url) {
|
|
4217
|
+
return url.split(",").map((u) => u.trim()).filter(Boolean);
|
|
4218
|
+
}
|
|
4210
4219
|
function loadSubscriptionConfig(subName) {
|
|
4211
4220
|
const rawContent = readSubscriptionRawConfig(subName);
|
|
4212
4221
|
if (!rawContent) {
|
|
@@ -4343,6 +4352,71 @@ async function downloadSubscription(url, subName = "default") {
|
|
|
4343
4352
|
username
|
|
4344
4353
|
};
|
|
4345
4354
|
}
|
|
4355
|
+
async function downloadMergedSubscription(urls, subName) {
|
|
4356
|
+
const responses = await Promise.all(
|
|
4357
|
+
urls.map(async (url, index) => {
|
|
4358
|
+
try {
|
|
4359
|
+
const response = await HTTP_CLIENT.get(url, { responseType: "text" });
|
|
4360
|
+
return { url, index, response, error: null };
|
|
4361
|
+
} catch (e) {
|
|
4362
|
+
return { url, index, response: null, error: e };
|
|
4363
|
+
}
|
|
4364
|
+
})
|
|
4365
|
+
);
|
|
4366
|
+
for (const r of responses) {
|
|
4367
|
+
if (r.error) {
|
|
4368
|
+
const maskedUrl = maskUrl(r.url);
|
|
4369
|
+
throw new Error(`\u5408\u5E76\u8BA2\u9605\u7B2C ${r.index + 1} \u4E2A URL \u83B7\u53D6\u5931\u8D25: ${r.error.message}
|
|
4370
|
+
URL: ${maskedUrl}`);
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
const parsed = responses.map((r, i) => {
|
|
4374
|
+
const content = r.response?.data;
|
|
4375
|
+
if (!content?.trim()) throw new Error(`\u5408\u5E76\u8BA2\u9605\u7B2C ${i + 1} \u4E2A URL \u5185\u5BB9\u4E3A\u7A7A`);
|
|
4376
|
+
return parseYamlOrJson(content, `\u5408\u5E76\u8BA2\u9605\u7B2C ${i + 1} \u4E2A`);
|
|
4377
|
+
});
|
|
4378
|
+
const base = parsed[0];
|
|
4379
|
+
const baseProxies = base.proxies || [];
|
|
4380
|
+
const seenNames = new Set(baseProxies.map((p) => p.name));
|
|
4381
|
+
for (let i = 1; i < parsed.length; i++) {
|
|
4382
|
+
const extraProxies = parsed[i].proxies || [];
|
|
4383
|
+
for (const proxy of extraProxies) {
|
|
4384
|
+
if (!seenNames.has(proxy.name)) {
|
|
4385
|
+
baseProxies.push(proxy);
|
|
4386
|
+
seenNames.add(proxy.name);
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
base.proxies = baseProxies;
|
|
4391
|
+
const mergedContent = jsYaml.dump(base, YAML_DUMP_OPTS);
|
|
4392
|
+
saveSubscriptionRawConfig(subName, mergedContent);
|
|
4393
|
+
const firstHeaders = responses[0].response?.headers;
|
|
4394
|
+
const userInfo = parseUserInfo(firstHeaders?.get("subscription-userinfo") ?? null);
|
|
4395
|
+
const updateIntervalHeader = firstHeaders?.get("profile-update-interval");
|
|
4396
|
+
const updateInterval = updateIntervalHeader ? parseInt(updateIntervalHeader, 10) : null;
|
|
4397
|
+
const webPageUrl = firstHeaders?.get("profile-web-page-url") || null;
|
|
4398
|
+
const username = parseUsernameFromContentDisposition(firstHeaders?.get("content-disposition") ?? null);
|
|
4399
|
+
const cacheData = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
4400
|
+
if (userInfo) {
|
|
4401
|
+
cacheData.upload = userInfo.upload;
|
|
4402
|
+
cacheData.download = userInfo.download;
|
|
4403
|
+
cacheData.total = userInfo.total;
|
|
4404
|
+
cacheData.expire = userInfo.expire;
|
|
4405
|
+
}
|
|
4406
|
+
if (updateInterval) cacheData.update_interval = updateInterval;
|
|
4407
|
+
if (webPageUrl) cacheData.web_page_url = webPageUrl;
|
|
4408
|
+
if (username) cacheData.username = username;
|
|
4409
|
+
saveSubscriptionCache(subName, cacheData);
|
|
4410
|
+
const proxyGroups = base["proxy-groups"];
|
|
4411
|
+
return {
|
|
4412
|
+
proxies: baseProxies.length,
|
|
4413
|
+
proxyGroups: proxyGroups ? proxyGroups.length : 0,
|
|
4414
|
+
userInfo,
|
|
4415
|
+
updateInterval,
|
|
4416
|
+
webPageUrl,
|
|
4417
|
+
username
|
|
4418
|
+
};
|
|
4419
|
+
}
|
|
4346
4420
|
function prepareConfigForStart(mode, subName = "default") {
|
|
4347
4421
|
const rawContent = readSubscriptionRawConfig(subName);
|
|
4348
4422
|
if (!rawContent) {
|
|
@@ -4368,7 +4442,12 @@ function needsAutoUpdate(sub) {
|
|
|
4368
4442
|
}
|
|
4369
4443
|
async function tryUpdateOne(sub) {
|
|
4370
4444
|
try {
|
|
4371
|
-
|
|
4445
|
+
let info;
|
|
4446
|
+
if (isMultiUrl(sub.url)) {
|
|
4447
|
+
info = await downloadMergedSubscription(splitUrls(sub.url), sub.name);
|
|
4448
|
+
} else {
|
|
4449
|
+
info = await downloadSubscription(sub.url, sub.name);
|
|
4450
|
+
}
|
|
4372
4451
|
return { name: sub.name, success: true, proxies: info.proxies, proxyGroups: info.proxyGroups };
|
|
4373
4452
|
} catch (e) {
|
|
4374
4453
|
return { name: sub.name, success: false, error: e.message };
|
|
@@ -4730,8 +4809,9 @@ async function printSubscriptionList(options) {
|
|
|
4730
4809
|
subs.forEach((s, i) => {
|
|
4731
4810
|
const time = formatDate(s.updated_at);
|
|
4732
4811
|
const defaultMark = activeSub && s.name === activeSub.name ? colors.green(" [\u4F7F\u7528\u4E2D]") : "";
|
|
4812
|
+
const mergeBadge = isMultiUrl(s.url) ? colors.cyan(` [\u5408\u5E76 ${splitUrls(s.url).length} \u6E90]`) : "";
|
|
4733
4813
|
const interval = s.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
|
|
4734
|
-
console.log(` ${i + 1}. ${s.name}${defaultMark}`);
|
|
4814
|
+
console.log(` ${i + 1}. ${s.name}${defaultMark}${mergeBadge}`);
|
|
4735
4815
|
console.log(` ${colors.gray("\u66F4\u65B0: ")}${time} (\u95F4\u9694: ${interval}h)`);
|
|
4736
4816
|
if (s.username) {
|
|
4737
4817
|
console.log(` ${colors.gray("\u7528\u6237: ")}${s.username}`);
|
|
@@ -4764,14 +4844,40 @@ async function printSubscriptionList(options) {
|
|
|
4764
4844
|
console.log("\u6253\u5F00\u9875\u9762: mihomo sub web [name]");
|
|
4765
4845
|
console.log("");
|
|
4766
4846
|
}
|
|
4847
|
+
function printFreeSourceList() {
|
|
4848
|
+
const freeSources = getFreeSubscriptionSources();
|
|
4849
|
+
console.log(` 00 \u5408\u5E76 #1 + #2 (\u8282\u70B9\u66F4\u591A)`);
|
|
4850
|
+
for (let i = 0; i < freeSources.length; i++) {
|
|
4851
|
+
console.log(` ${String(i + 1).padStart(2, "0")} ${freeSources[i].name}`);
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4767
4854
|
async function addFreeSubscription(freeId) {
|
|
4768
4855
|
const freeSources = getFreeSubscriptionSources();
|
|
4856
|
+
if (freeId === 0) {
|
|
4857
|
+
const sources = [freeSources[0], freeSources[1]];
|
|
4858
|
+
const urls = sources.map((s) => s.url);
|
|
4859
|
+
const mergedUrl = urls.join(",");
|
|
4860
|
+
const name2 = "free0";
|
|
4861
|
+
console.log(`\u6DFB\u52A0\u5408\u5E76\u514D\u8D39\u8BA2\u9605: ${name2} (${sources.map((s) => s.name).join(" + ")})`);
|
|
4862
|
+
try {
|
|
4863
|
+
addSubscription(mergedUrl, name2);
|
|
4864
|
+
setDefaultSubscription(name2);
|
|
4865
|
+
const info = await downloadMergedSubscription(urls, name2);
|
|
4866
|
+
const repoUrls = sources.map((s) => githubRepoUrl(s.url)).filter(Boolean);
|
|
4867
|
+
if (repoUrls.length > 0) saveSubscriptionCache(name2, { web_page_url: repoUrls.join(", ") });
|
|
4868
|
+
console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name2}" (${formatProxySummary(info)}, \u5408\u5E76 ${sources.length} \u6E90)`);
|
|
4869
|
+
} catch (e) {
|
|
4870
|
+
console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
|
|
4871
|
+
process.exit(1);
|
|
4872
|
+
}
|
|
4873
|
+
console.log("");
|
|
4874
|
+
await printSubscriptionList();
|
|
4875
|
+
return;
|
|
4876
|
+
}
|
|
4769
4877
|
if (freeId < 1 || freeId > freeSources.length) {
|
|
4770
|
-
console.error(`\u9519\u8BEF: \u514D\u8D39\u8BA2\u9605 ID \u8303\u56F4
|
|
4878
|
+
console.error(`\u9519\u8BEF: \u514D\u8D39\u8BA2\u9605 ID \u8303\u56F4 0-${freeSources.length}`);
|
|
4771
4879
|
console.log("\n\u53EF\u7528\u6E90:");
|
|
4772
|
-
|
|
4773
|
-
console.log(` ${String(i + 1).padStart(2, "0")} ${freeSources[i].name}`);
|
|
4774
|
-
}
|
|
4880
|
+
printFreeSourceList();
|
|
4775
4881
|
process.exit(1);
|
|
4776
4882
|
}
|
|
4777
4883
|
const source = freeSources[freeId - 1];
|
|
@@ -4799,13 +4905,10 @@ async function cmdSubscription(args) {
|
|
|
4799
4905
|
}
|
|
4800
4906
|
if (action === "free") {
|
|
4801
4907
|
const id = parseInt(args[2], 10);
|
|
4802
|
-
if (
|
|
4803
|
-
const freeSources = getFreeSubscriptionSources();
|
|
4908
|
+
if (Number.isNaN(id)) {
|
|
4804
4909
|
console.log("\u7528\u6CD5: mihomo sub free <id>\n");
|
|
4805
4910
|
console.log("\u53EF\u7528\u6E90:");
|
|
4806
|
-
|
|
4807
|
-
console.log(` ${String(i + 1).padStart(2, "0")} ${freeSources[i].name}`);
|
|
4808
|
-
}
|
|
4911
|
+
printFreeSourceList();
|
|
4809
4912
|
process.exit(1);
|
|
4810
4913
|
}
|
|
4811
4914
|
await addFreeSubscription(id);
|
|
@@ -4819,21 +4922,45 @@ async function cmdSubscription(args) {
|
|
|
4819
4922
|
}
|
|
4820
4923
|
const url = args[2];
|
|
4821
4924
|
const name = args[3] || "default";
|
|
4822
|
-
if (!url
|
|
4925
|
+
if (!url) {
|
|
4823
4926
|
console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
|
|
4824
4927
|
process.exit(1);
|
|
4825
4928
|
}
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4929
|
+
if (isMultiUrl(url)) {
|
|
4930
|
+
const urls = splitUrls(url);
|
|
4931
|
+
for (const u of urls) {
|
|
4932
|
+
if (!u.startsWith("http")) {
|
|
4933
|
+
console.error(`\u9519\u8BEF: \u65E0\u6548\u7684 URL: ${u}`);
|
|
4934
|
+
process.exit(1);
|
|
4935
|
+
}
|
|
4936
|
+
}
|
|
4937
|
+
console.log(`\u6DFB\u52A0\u5408\u5E76\u8BA2\u9605: ${name} (${urls.length} \u4E2A\u6E90)`);
|
|
4938
|
+
try {
|
|
4939
|
+
addSubscription(url, name);
|
|
4940
|
+
setDefaultSubscription(name);
|
|
4941
|
+
const info = await downloadMergedSubscription(urls, name);
|
|
4942
|
+
console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)}, \u5408\u5E76 ${urls.length} \u6E90)`);
|
|
4943
|
+
} catch (e) {
|
|
4944
|
+
console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
|
|
4945
|
+
process.exit(1);
|
|
4946
|
+
}
|
|
4947
|
+
} else {
|
|
4948
|
+
if (!url.startsWith("http")) {
|
|
4949
|
+
console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
|
|
4950
|
+
process.exit(1);
|
|
4951
|
+
}
|
|
4952
|
+
console.log(`\u6DFB\u52A0\u8BA2\u9605: ${name}`);
|
|
4953
|
+
try {
|
|
4954
|
+
addSubscription(url, name);
|
|
4955
|
+
setDefaultSubscription(name);
|
|
4956
|
+
const info = await downloadSubscription(url, name);
|
|
4957
|
+
const repoUrl = githubRepoUrl(url);
|
|
4958
|
+
if (repoUrl) saveSubscriptionCache(name, { web_page_url: repoUrl });
|
|
4959
|
+
console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
|
|
4960
|
+
} catch (e) {
|
|
4961
|
+
console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
|
|
4962
|
+
process.exit(1);
|
|
4963
|
+
}
|
|
4837
4964
|
}
|
|
4838
4965
|
console.log("");
|
|
4839
4966
|
await printSubscriptionList();
|
|
@@ -4867,7 +4994,13 @@ async function cmdSubscription(args) {
|
|
|
4867
4994
|
const target = pickSingleSubscription(matches, name);
|
|
4868
4995
|
console.log(`\u66F4\u65B0\u8BA2\u9605: ${target.name}`);
|
|
4869
4996
|
try {
|
|
4870
|
-
|
|
4997
|
+
let info;
|
|
4998
|
+
if (isMultiUrl(target.url)) {
|
|
4999
|
+
const urls = splitUrls(target.url);
|
|
5000
|
+
info = await downloadMergedSubscription(urls, target.name);
|
|
5001
|
+
} else {
|
|
5002
|
+
info = await downloadSubscription(target.url, target.name);
|
|
5003
|
+
}
|
|
4871
5004
|
console.log(`\u5DF2\u66F4\u65B0 (${formatProxySummary(info)})`);
|
|
4872
5005
|
} catch (e) {
|
|
4873
5006
|
console.error(`\u66F4\u65B0\u5931\u8D25: ${e.message}`);
|