mihomo-cli 1.4.0 → 1.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 +48 -0
- package/README.md +24 -8
- package/index.js +48 -22
- package/package.json +1 -1
- package/src/config.js +75 -33
- package/src/overwrite.js +12 -14
- package/src/subscription.js +12 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.0] - 2026-04-10
|
|
4
|
+
|
|
5
|
+
### 新增功能
|
|
6
|
+
|
|
7
|
+
- **快捷命令**:新增顶层命令快捷方式,减少输入
|
|
8
|
+
- `mihomo up` = `mihomo start`
|
|
9
|
+
- `mihomo down` = `mihomo stop`
|
|
10
|
+
- `mihomo tun` = `mihomo start tun`
|
|
11
|
+
- `mihomo use <name>` = `mihomo sub use <name>`
|
|
12
|
+
- `mihomo on` / `mihomo off` = `mihomo ow on` / `mihomo ow off`
|
|
13
|
+
- `mihomo open <target>` = `mihomo dir open <target>`
|
|
14
|
+
- **订阅选择机制**:使用 `active_subscription` 字段标识当前订阅,不再依赖数组顺序
|
|
15
|
+
- **配置构建调试**:运行时目录生成 3 阶段中间文件,方便排查配置问题
|
|
16
|
+
- `1.subscription.yaml` — 订阅原始配置
|
|
17
|
+
- `2.overwrite.yaml` — 覆写合并内容
|
|
18
|
+
- `3.system.yaml` — 系统补充值(BASE_CONFIG + TUN)
|
|
19
|
+
|
|
20
|
+
### 重构
|
|
21
|
+
|
|
22
|
+
- **目录结构调整**:
|
|
23
|
+
- `core/` → `kernel/`(内核目录)
|
|
24
|
+
- `.runtime/` → `runtime/`(运行时目录)
|
|
25
|
+
- `overwrites/` 目录 → 根目录 `overwrite.yaml` + `overwrite.*.yaml`(覆写文件扁平化)
|
|
26
|
+
- **配置合并逻辑**:BASE_CONFIG / TUN_CONFIG 改为只补充订阅中缺失的字段,不再强制覆盖已有值
|
|
27
|
+
- **TUN 模式**:移除 `ipv6: false` 硬编码,交由订阅或覆写控制
|
|
28
|
+
- **`dir open` 目标精简**:移除 `overwrites` 和 `settings`,保留 `root|subs|logs|data|runtime|kernel`
|
|
29
|
+
|
|
30
|
+
### 优化
|
|
31
|
+
|
|
32
|
+
- **文案调整**:
|
|
33
|
+
- "默认订阅" → "当前订阅" / "使用中"
|
|
34
|
+
- 覆写文件名显示去除 `overwrite.` 前缀
|
|
35
|
+
- 覆写配置 "目录" → "位置"
|
|
36
|
+
- **dir 信息**:新增显示内核目录路径
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## [1.4.1] - 2026-04-08
|
|
41
|
+
|
|
42
|
+
### 优化
|
|
43
|
+
|
|
44
|
+
- **状态显示文案**:
|
|
45
|
+
- `○ 已停止` → `不在运行`(移除符号,更简洁明确)
|
|
46
|
+
- `未在运行` → `不在运行`(统一措辞)
|
|
47
|
+
- **代码风格统一**:标签输出格式统一为 `colors.gray('标签: ')`
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
3
51
|
## [1.4.0] - 2026-04-07
|
|
4
52
|
|
|
5
53
|
### 新增功能
|
package/README.md
CHANGED
|
@@ -96,7 +96,7 @@ mihomo ui yacd # YACD
|
|
|
96
96
|
| `mihomo sub add <url> [name]` | 添加订阅 |
|
|
97
97
|
| `mihomo sub update` | 更新所有订阅 |
|
|
98
98
|
| `mihomo sub update <name>` | 更新指定订阅(支持模糊匹配) |
|
|
99
|
-
| `mihomo sub use <name>` |
|
|
99
|
+
| `mihomo sub use <name>` | 切换当前订阅(支持模糊匹配,自动重启) |
|
|
100
100
|
| `mihomo sub web [name]` | 打开订阅页面(无参打开默认) |
|
|
101
101
|
|
|
102
102
|
### 覆写配置
|
|
@@ -115,7 +115,7 @@ mihomo ui yacd # YACD
|
|
|
115
115
|
| `mihomo update` | 更新 mihomo-cli (npm install -g) |
|
|
116
116
|
| `mihomo ui [zash\|dash\|yacd]` | 打开 Web UI |
|
|
117
117
|
| `mihomo dir` | 显示数据目录位置 |
|
|
118
|
-
| `mihomo dir open [target]` | 打开指定目录(`root`, `subs`, `logs`, `
|
|
118
|
+
| `mihomo dir open [target]` | 打开指定目录(`root`, `subs`, `logs`, `kernel` 等) |
|
|
119
119
|
| `mihomo reset [目标...] [--full]` | 重置用户数据(可用目标:`subs`, `logs`, `kernel`, `overwrites` 等) |
|
|
120
120
|
| `mihomo version` | 显示版本信息 |
|
|
121
121
|
| `mihomo help` | 显示帮助信息 |
|
|
@@ -129,6 +129,19 @@ mihomo ui yacd # YACD
|
|
|
129
129
|
- `mmc`
|
|
130
130
|
- `mh`
|
|
131
131
|
|
|
132
|
+
### 快捷命令
|
|
133
|
+
|
|
134
|
+
常用操作的快捷方式:
|
|
135
|
+
|
|
136
|
+
| 快捷命令 | 等效于 |
|
|
137
|
+
| ---------------------- | -------------------------- |
|
|
138
|
+
| `mihomo up` | `mihomo start` |
|
|
139
|
+
| `mihomo down` | `mihomo stop` |
|
|
140
|
+
| `mihomo tun` | `mihomo start tun` |
|
|
141
|
+
| `mihomo use <name>` | `mihomo sub use <name>` |
|
|
142
|
+
| `mihomo on` / `off` | `mihomo ow on` / `ow off` |
|
|
143
|
+
| `mihomo open <target>` | `mihomo dir open <target>` |
|
|
144
|
+
|
|
132
145
|
## 模式说明
|
|
133
146
|
|
|
134
147
|
### Mixed 模式(默认)
|
|
@@ -185,17 +198,18 @@ mihomo kernel --mirror-all hk.gh-proxy.org
|
|
|
185
198
|
```
|
|
186
199
|
~/.mihomo-cli/
|
|
187
200
|
├── settings.json # 用户设置(订阅列表等)
|
|
201
|
+
├── overwrite.yaml # 覆写配置(主文件,可选)
|
|
202
|
+
├── overwrite.*.yaml # 覆写配置(扩展文件,如 overwrite.dns.yaml)
|
|
188
203
|
├── subscriptions/
|
|
189
204
|
│ ├── cache.json # 订阅动态缓存(更新时间、流量、到期时间等)
|
|
190
205
|
│ └── <name>.yaml # 订阅原始配置
|
|
191
|
-
├──
|
|
192
|
-
├── core/
|
|
206
|
+
├── kernel/
|
|
193
207
|
│ └── mihomo # mihomo 内核二进制
|
|
194
208
|
├── logs/
|
|
195
209
|
│ ├── mihomo.log # 当前日志
|
|
196
210
|
│ └── mihomo.YYYY-MM-DD_HH-MM-SS.log # 归档日志
|
|
197
211
|
├── data/ # mihomo 运行数据(GeoIP 等,由内核自行管理)
|
|
198
|
-
└──
|
|
212
|
+
└── runtime/ # 运行时临时文件(stop 自动清除)
|
|
199
213
|
├── pid # 进程 PID
|
|
200
214
|
└── config.yaml # 运行时生成的配置
|
|
201
215
|
```
|
|
@@ -208,8 +222,10 @@ mihomo kernel --mirror-all hk.gh-proxy.org
|
|
|
208
222
|
|
|
209
223
|
### 使用方法
|
|
210
224
|
|
|
211
|
-
1. 在 `~/.mihomo-cli
|
|
212
|
-
|
|
225
|
+
1. 在 `~/.mihomo-cli/` 目录下创建覆写文件:
|
|
226
|
+
- `overwrite.yaml` — 主覆写文件
|
|
227
|
+
- `overwrite.dns.yaml` — 按功能拆分的扩展文件(`overwrite.*.yaml` 格式)
|
|
228
|
+
2. `overwrite.yaml` 始终最先加载,扩展文件按文件名排序加载
|
|
213
229
|
3. 使用 `mihomo ow on` 启用覆写配置(会自动重启)
|
|
214
230
|
|
|
215
231
|
### 特殊语法
|
|
@@ -226,7 +242,7 @@ mihomo kernel --mirror-all hk.gh-proxy.org
|
|
|
226
242
|
### 示例
|
|
227
243
|
|
|
228
244
|
```yaml
|
|
229
|
-
# ~/.mihomo-cli/
|
|
245
|
+
# ~/.mihomo-cli/overwrite.yaml
|
|
230
246
|
|
|
231
247
|
# 强制覆盖 dns 配置
|
|
232
248
|
dns!:
|
package/index.js
CHANGED
|
@@ -124,7 +124,7 @@ function printHelp() {
|
|
|
124
124
|
' update [name] 更新订阅(无参更新所有)\n' +
|
|
125
125
|
' ' +
|
|
126
126
|
colors.bold('subscription') +
|
|
127
|
-
' use <name>
|
|
127
|
+
' use <name> 切换当前订阅\n' +
|
|
128
128
|
' ' +
|
|
129
129
|
colors.bold('subscription') +
|
|
130
130
|
' web [name] 打开订阅页面\n' +
|
|
@@ -142,7 +142,7 @@ function printHelp() {
|
|
|
142
142
|
' 显示数据目录位置(别名 dir)\n' +
|
|
143
143
|
' ' +
|
|
144
144
|
colors.bold('directory') +
|
|
145
|
-
' open [target] 打开目录: root|subs|logs|
|
|
145
|
+
' open [target] 打开目录: root|subs|logs|runtime|...\n' +
|
|
146
146
|
'\n' +
|
|
147
147
|
colors.cyan('系统:') +
|
|
148
148
|
'\n' +
|
|
@@ -202,7 +202,7 @@ function printStatus() {
|
|
|
202
202
|
if (info && status.running) {
|
|
203
203
|
modeLabel = colors.cyan(info.tun ? ' (TUN)' : ' (Mixed)');
|
|
204
204
|
}
|
|
205
|
-
const statusText = status.running ? colors.green('● 运行中') : colors.yellow('
|
|
205
|
+
const statusText = status.running ? colors.green('● 运行中') : colors.yellow('不在运行');
|
|
206
206
|
console.log(colors.gray('状态: ') + statusText + modeLabel);
|
|
207
207
|
console.log(colors.gray('内核: ') + (status.kernelVersion || '未安装'));
|
|
208
208
|
|
|
@@ -235,7 +235,7 @@ function printStatus() {
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
if (overwriteEnabled && overwriteFiles.length > 0) {
|
|
238
|
-
const names = overwriteFiles.map(f => f.name).join(', ');
|
|
238
|
+
const names = overwriteFiles.map(f => f.name.replace(/^overwrite\.?/, '')).join(', ');
|
|
239
239
|
console.log(colors.gray('覆写: ') + colors.green('已启用') + ' (' + names + ')');
|
|
240
240
|
} else if (overwriteEnabled) {
|
|
241
241
|
console.log(colors.gray('覆写: ') + colors.green('已启用') + ' (无文件)');
|
|
@@ -307,7 +307,7 @@ async function cmdStart(args) {
|
|
|
307
307
|
async function cmdStop() {
|
|
308
308
|
const pids = processManager.getAllMihomoPids();
|
|
309
309
|
if (pids.length === 0) {
|
|
310
|
-
console.log(colors.yellow('
|
|
310
|
+
console.log(colors.yellow('不在运行'));
|
|
311
311
|
return;
|
|
312
312
|
}
|
|
313
313
|
|
|
@@ -507,16 +507,17 @@ async function printSubscriptionList() {
|
|
|
507
507
|
console.log('');
|
|
508
508
|
return;
|
|
509
509
|
}
|
|
510
|
+
const activeSub = subscription.getActiveSubscription();
|
|
510
511
|
console.log(colors.cyan('订阅列表:'));
|
|
511
512
|
subs.forEach((s, i) => {
|
|
512
513
|
const time = utils.formatDate(s.updated_at);
|
|
513
|
-
const defaultMark =
|
|
514
|
+
const defaultMark = activeSub && s.name === activeSub.name ? colors.green(' [使用中]') : '';
|
|
514
515
|
const interval = s.update_interval || subscription.DEFAULT_UPDATE_INTERVAL_HOURS;
|
|
515
516
|
console.log(' ' + (i + 1) + '. ' + s.name + defaultMark);
|
|
516
|
-
console.log(' ' + colors.gray('更新:') +
|
|
517
|
+
console.log(' ' + colors.gray('更新: ') + time + ' (间隔: ' + interval + 'h)');
|
|
517
518
|
|
|
518
519
|
if (s.username) {
|
|
519
|
-
console.log(' ' + colors.gray('用户:') +
|
|
520
|
+
console.log(' ' + colors.gray('用户: ') + s.username);
|
|
520
521
|
}
|
|
521
522
|
if (s.download !== undefined || s.total !== undefined) {
|
|
522
523
|
const used = (s.upload || 0) + (s.download || 0);
|
|
@@ -527,17 +528,17 @@ async function printSubscriptionList() {
|
|
|
527
528
|
const percent = Math.min((used / s.total) * 100, 100);
|
|
528
529
|
percentStr = ' (' + percent.toFixed(1) + '%)';
|
|
529
530
|
}
|
|
530
|
-
console.log(' ' + colors.gray('流量:') +
|
|
531
|
+
console.log(' ' + colors.gray('流量: ') + usedStr + ' / ' + totalStr + percentStr);
|
|
531
532
|
}
|
|
532
533
|
if (s.expire !== undefined) {
|
|
533
|
-
console.log(' ' + colors.gray('到期:') +
|
|
534
|
+
console.log(' ' + colors.gray('到期: ') + utils.formatTimestamp(s.expire));
|
|
534
535
|
}
|
|
535
536
|
if (s.web_page_url) {
|
|
536
|
-
console.log(' ' + colors.gray('页面:') +
|
|
537
|
+
console.log(' ' + colors.gray('页面: ') + s.web_page_url);
|
|
537
538
|
}
|
|
538
539
|
});
|
|
539
540
|
console.log('');
|
|
540
|
-
console.log('
|
|
541
|
+
console.log('切换订阅: mihomo sub use <name>');
|
|
541
542
|
console.log('更新订阅: mihomo sub update [name]');
|
|
542
543
|
console.log('打开页面: mihomo sub web [name]');
|
|
543
544
|
console.log('新增订阅: mihomo sub add <url> [name]');
|
|
@@ -638,7 +639,7 @@ async function cmdSubscription(args) {
|
|
|
638
639
|
const isAlreadyDefault = currentDefault && currentDefault.name === target.name;
|
|
639
640
|
|
|
640
641
|
if (isAlreadyDefault) {
|
|
641
|
-
console.log('"' + target.name + '"
|
|
642
|
+
console.log('"' + target.name + '" 已是当前使用的订阅');
|
|
642
643
|
console.log('');
|
|
643
644
|
await printSubscriptionList();
|
|
644
645
|
return;
|
|
@@ -650,7 +651,7 @@ async function cmdSubscription(args) {
|
|
|
650
651
|
|
|
651
652
|
const success = config.setDefaultSubscription(target.name);
|
|
652
653
|
if (success) {
|
|
653
|
-
console.log('
|
|
654
|
+
console.log('已切换到 "' + target.name + '"');
|
|
654
655
|
} else {
|
|
655
656
|
console.error('错误: 未找到订阅 "' + name + '"');
|
|
656
657
|
process.exit(1);
|
|
@@ -806,7 +807,7 @@ const RESET_TARGETS = [
|
|
|
806
807
|
id: 'kernel',
|
|
807
808
|
aliases: ['kernel', 'core'],
|
|
808
809
|
label: '内核',
|
|
809
|
-
paths: () => [config.DIRS.
|
|
810
|
+
paths: () => [config.DIRS.kernel],
|
|
810
811
|
needsStop: false,
|
|
811
812
|
onAfter: () => config.clearKernelVersionCache(),
|
|
812
813
|
checkEmpty: () => !config.hasKernel(),
|
|
@@ -817,7 +818,15 @@ const RESET_TARGETS = [
|
|
|
817
818
|
id: 'overwrites',
|
|
818
819
|
aliases: ['overwrite', 'overwrites', 'ow'],
|
|
819
820
|
label: '覆写',
|
|
820
|
-
paths: () =>
|
|
821
|
+
paths: () => {
|
|
822
|
+
const fs = require('fs');
|
|
823
|
+
const dir = config.USER_DATA_DIR;
|
|
824
|
+
if (!fs.existsSync(dir)) return [];
|
|
825
|
+
return fs
|
|
826
|
+
.readdirSync(dir)
|
|
827
|
+
.filter(f => f === 'overwrite.yaml' || /^overwrite\..+\.ya?ml$/.test(f))
|
|
828
|
+
.map(f => require('path').join(dir, f));
|
|
829
|
+
},
|
|
821
830
|
needsStop: false,
|
|
822
831
|
},
|
|
823
832
|
];
|
|
@@ -921,13 +930,14 @@ async function cmdReset(args) {
|
|
|
921
930
|
function printOverwriteList() {
|
|
922
931
|
const info = overwrite.listOverwriteFile();
|
|
923
932
|
const statusText = info.enabled ? colors.green('已启用') : colors.yellow('已禁用');
|
|
924
|
-
console.log(colors.gray('状态:') +
|
|
925
|
-
console.log(colors.gray('
|
|
933
|
+
console.log(colors.gray('状态: ') + statusText);
|
|
934
|
+
console.log(colors.gray('位置: ') + info.dir);
|
|
926
935
|
console.log('');
|
|
927
936
|
if (info.files.length === 0) {
|
|
928
937
|
console.log('暂无覆写文件');
|
|
929
938
|
console.log('');
|
|
930
|
-
console.log('用法示例: 创建文件 ' + path.join(info.dir, '
|
|
939
|
+
console.log('用法示例: 创建文件 ' + path.join(info.dir, 'overwrite.yaml'));
|
|
940
|
+
console.log(' 或 ' + path.join(info.dir, 'overwrite.dns.yaml'));
|
|
931
941
|
console.log('');
|
|
932
942
|
} else {
|
|
933
943
|
console.log(colors.cyan('覆写文件') + ' (' + info.files.length + ' 个,按顺序加载):');
|
|
@@ -936,7 +946,7 @@ function printOverwriteList() {
|
|
|
936
946
|
const num = i < 10 ? ' ' + i : '' + i;
|
|
937
947
|
console.log(' ' + num + '. ' + f.name);
|
|
938
948
|
if (f.keys.length > 0) {
|
|
939
|
-
console.log(' ' + colors.gray('字段:') +
|
|
949
|
+
console.log(' ' + colors.gray('字段: ') + f.keys.join(', '));
|
|
940
950
|
}
|
|
941
951
|
});
|
|
942
952
|
console.log('');
|
|
@@ -1045,6 +1055,7 @@ function cmdDirectory(args) {
|
|
|
1045
1055
|
console.log('数据目录位置:');
|
|
1046
1056
|
console.log(' 根目录: ' + config.USER_DATA_DIR);
|
|
1047
1057
|
console.log(' 全局设置: ' + config.PATHS.settingsFile);
|
|
1058
|
+
console.log(' 内核目录: ' + config.DIRS.kernel);
|
|
1048
1059
|
console.log(' 内核文件: ' + config.PATHS.mihomoBinary);
|
|
1049
1060
|
console.log(' 订阅目录: ' + config.DIRS.subscriptions);
|
|
1050
1061
|
console.log(' - cache.json (订阅缓存:更新时间、流量等)');
|
|
@@ -1061,8 +1072,6 @@ function cmdDirectory(args) {
|
|
|
1061
1072
|
console.log(' mihomo dir open subs 打开订阅目录');
|
|
1062
1073
|
console.log(' mihomo dir open logs 打开日志目录');
|
|
1063
1074
|
console.log(' mihomo dir open runtime 打开运行时目录');
|
|
1064
|
-
console.log(' mihomo dir open overwrites 打开覆写目录');
|
|
1065
|
-
console.log(' mihomo dir open settings 打开设置文件');
|
|
1066
1075
|
console.log(' mihomo dir open kernel 打开内核目录');
|
|
1067
1076
|
console.log('');
|
|
1068
1077
|
console.log('环境变量:');
|
|
@@ -1093,9 +1102,14 @@ async function main() {
|
|
|
1093
1102
|
}
|
|
1094
1103
|
|
|
1095
1104
|
switch (cmd) {
|
|
1105
|
+
case 'up':
|
|
1096
1106
|
case 'start':
|
|
1097
1107
|
await cmdStart(args);
|
|
1098
1108
|
break;
|
|
1109
|
+
case 'tun':
|
|
1110
|
+
await cmdStart(['start', 'tun']);
|
|
1111
|
+
break;
|
|
1112
|
+
case 'down':
|
|
1099
1113
|
case 'stop':
|
|
1100
1114
|
await cmdStop();
|
|
1101
1115
|
break;
|
|
@@ -1108,6 +1122,9 @@ async function main() {
|
|
|
1108
1122
|
case 'logs':
|
|
1109
1123
|
cmdLogs(args);
|
|
1110
1124
|
break;
|
|
1125
|
+
case 'open':
|
|
1126
|
+
cmdDirectory(['dir', 'open', ...args.slice(1)]);
|
|
1127
|
+
break;
|
|
1111
1128
|
case 'ui':
|
|
1112
1129
|
cmdUI(args);
|
|
1113
1130
|
break;
|
|
@@ -1119,6 +1136,9 @@ async function main() {
|
|
|
1119
1136
|
case 'upgrade':
|
|
1120
1137
|
await cmdUpdate();
|
|
1121
1138
|
break;
|
|
1139
|
+
case 'use':
|
|
1140
|
+
await cmdSubscription(['sub', 'use', ...args.slice(1)]);
|
|
1141
|
+
break;
|
|
1122
1142
|
case 'sub':
|
|
1123
1143
|
case 'subscription':
|
|
1124
1144
|
case 'subscriptions':
|
|
@@ -1133,6 +1153,12 @@ async function main() {
|
|
|
1133
1153
|
case 'reset':
|
|
1134
1154
|
await cmdReset(args);
|
|
1135
1155
|
break;
|
|
1156
|
+
case 'on':
|
|
1157
|
+
await cmdOverwrite(['ow', 'on']);
|
|
1158
|
+
break;
|
|
1159
|
+
case 'off':
|
|
1160
|
+
await cmdOverwrite(['ow', 'off']);
|
|
1161
|
+
break;
|
|
1136
1162
|
case 'ow':
|
|
1137
1163
|
case 'overwrite':
|
|
1138
1164
|
await cmdOverwrite(args);
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -30,22 +30,24 @@ const USER_DATA_DIR = getUserDataDir();
|
|
|
30
30
|
|
|
31
31
|
const DIRS = {
|
|
32
32
|
root: PROJECT_ROOT,
|
|
33
|
-
|
|
33
|
+
kernel: path.join(USER_DATA_DIR, 'kernel'),
|
|
34
34
|
subscriptions: path.join(USER_DATA_DIR, 'subscriptions'),
|
|
35
35
|
logs: path.join(USER_DATA_DIR, 'logs'),
|
|
36
36
|
data: path.join(USER_DATA_DIR, 'data'),
|
|
37
|
-
runtime: path.join(USER_DATA_DIR, '
|
|
38
|
-
overwrites: path.join(USER_DATA_DIR, 'overwrites'),
|
|
37
|
+
runtime: path.join(USER_DATA_DIR, 'runtime'),
|
|
39
38
|
};
|
|
40
39
|
|
|
41
40
|
const PATHS = {
|
|
42
41
|
root: DIRS.root,
|
|
43
|
-
mihomoBinary: path.join(DIRS.
|
|
42
|
+
mihomoBinary: path.join(DIRS.kernel, 'mihomo'),
|
|
44
43
|
settingsFile: path.join(USER_DATA_DIR, 'settings.json'),
|
|
45
44
|
subscriptionsCacheFile: path.join(DIRS.subscriptions, 'cache.json'),
|
|
46
45
|
configFile: path.join(DIRS.runtime, 'config.yaml'),
|
|
47
46
|
logFile: path.join(DIRS.logs, 'mihomo.log'),
|
|
48
47
|
pidFile: path.join(DIRS.runtime, 'pid'),
|
|
48
|
+
configStage1Subscription: path.join(DIRS.runtime, '1.subscription.yaml'),
|
|
49
|
+
configStage2Overwrite: path.join(DIRS.runtime, '2.overwrite.yaml'),
|
|
50
|
+
configStage3System: path.join(DIRS.runtime, '3.system.yaml'),
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
function ensureDirs() {
|
|
@@ -206,7 +208,11 @@ function addSubscription(url, name) {
|
|
|
206
208
|
} else {
|
|
207
209
|
subs.push({ name, url });
|
|
208
210
|
}
|
|
209
|
-
|
|
211
|
+
const updates = { subscriptions: subs };
|
|
212
|
+
if (!settings.active_subscription && subs.length === 1) {
|
|
213
|
+
updates.active_subscription = name;
|
|
214
|
+
}
|
|
215
|
+
writeSettings(updates);
|
|
210
216
|
}
|
|
211
217
|
|
|
212
218
|
function setDefaultSubscription(name) {
|
|
@@ -216,12 +222,7 @@ function setDefaultSubscription(name) {
|
|
|
216
222
|
if (idx < 0) {
|
|
217
223
|
return false;
|
|
218
224
|
}
|
|
219
|
-
|
|
220
|
-
return true; // 已经是第一个
|
|
221
|
-
}
|
|
222
|
-
const [sub] = subs.splice(idx, 1);
|
|
223
|
-
subs.unshift(sub);
|
|
224
|
-
writeSettings({ subscriptions: subs });
|
|
225
|
+
writeSettings({ active_subscription: name });
|
|
225
226
|
return true;
|
|
226
227
|
}
|
|
227
228
|
|
|
@@ -283,7 +284,6 @@ const TUN_CONFIG = {
|
|
|
283
284
|
'auto-detect-interface': true,
|
|
284
285
|
'strict-route': true,
|
|
285
286
|
},
|
|
286
|
-
ipv6: false,
|
|
287
287
|
};
|
|
288
288
|
|
|
289
289
|
const BASE_CONFIG = {
|
|
@@ -314,9 +314,9 @@ function parseYamlOrJson(content, errorMsg) {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
function buildConfig(subRawContent, mode) {
|
|
317
|
-
const
|
|
317
|
+
const subscriptionConfig = parseYamlOrJson(subRawContent, '订阅内容');
|
|
318
318
|
|
|
319
|
-
if (!
|
|
319
|
+
if (!subscriptionConfig) {
|
|
320
320
|
throw new Error('订阅内容为空');
|
|
321
321
|
}
|
|
322
322
|
|
|
@@ -324,24 +324,48 @@ function buildConfig(subRawContent, mode) {
|
|
|
324
324
|
const overwrite = require('./overwrite');
|
|
325
325
|
|
|
326
326
|
// 应用覆写配置
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
327
|
+
const overwriteEnabled = overwrite.isOverwriteEnabled();
|
|
328
|
+
const withOverwrites = overwrite.applyOverwrite(subscriptionConfig);
|
|
329
|
+
const overwriteFiles = overwriteEnabled ? overwrite.loadOverwriteFile() : [];
|
|
330
|
+
|
|
331
|
+
// 构建系统覆盖值(BASE_CONFIG + 可选 TUN)
|
|
332
|
+
// 只补充订阅中缺失的字段,不覆盖已有值
|
|
333
|
+
const systemConfig = {};
|
|
334
|
+
for (const [key, value] of Object.entries(BASE_CONFIG)) {
|
|
335
|
+
if (!(key in withOverwrites)) {
|
|
336
|
+
systemConfig[key] = value;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
331
339
|
|
|
332
340
|
if (mode === 'tun') {
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
341
|
+
// tun 块始终由系统控制
|
|
342
|
+
systemConfig.tun = TUN_CONFIG.tun;
|
|
343
|
+
// dns 只补充 TUN 必需的字段
|
|
344
|
+
const subDns = withOverwrites.dns || {};
|
|
345
|
+
systemConfig.dns = {};
|
|
346
|
+
if (!subDns.enable) systemConfig.dns.enable = true;
|
|
347
|
+
if (!subDns['enhanced-mode']) systemConfig.dns['enhanced-mode'] = 'fake-ip';
|
|
348
|
+
if (!subDns['fake-ip-range']) systemConfig.dns['fake-ip-range'] = '198.18.0.1/16';
|
|
349
|
+
// 如果没有需要补充的 dns 字段,则不设置
|
|
350
|
+
if (Object.keys(systemConfig.dns).length === 0) {
|
|
351
|
+
delete systemConfig.dns;
|
|
352
|
+
}
|
|
342
353
|
}
|
|
343
354
|
|
|
344
|
-
|
|
355
|
+
// 合并:订阅(+overwrite) → 系统补充
|
|
356
|
+
const merged = { ...withOverwrites, ...systemConfig };
|
|
357
|
+
|
|
358
|
+
// dns 需要深度合并:保留订阅的 DNS 服务器,叠加系统补充
|
|
359
|
+
if (systemConfig.dns) {
|
|
360
|
+
merged.dns = { ...(withOverwrites.dns || {}), ...systemConfig.dns };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
config: merged,
|
|
365
|
+
subscriptionConfig,
|
|
366
|
+
overwriteFiles,
|
|
367
|
+
systemConfig,
|
|
368
|
+
};
|
|
345
369
|
}
|
|
346
370
|
|
|
347
371
|
function writeMihomoConfig(configObj) {
|
|
@@ -354,6 +378,25 @@ function writeMihomoConfig(configObj) {
|
|
|
354
378
|
fs.writeFileSync(PATHS.configFile, content, { mode: 0o600 });
|
|
355
379
|
}
|
|
356
380
|
|
|
381
|
+
function writeDebugConfig(buildResult) {
|
|
382
|
+
ensureDirs();
|
|
383
|
+
const dumpOpts = { indent: 2, lineWidth: -1, noCompat: true };
|
|
384
|
+
|
|
385
|
+
// 1. 订阅原始配置
|
|
386
|
+
fs.writeFileSync(PATHS.configStage1Subscription, yaml.dump(buildResult.subscriptionConfig, dumpOpts), { mode: 0o600 });
|
|
387
|
+
|
|
388
|
+
// 2. overwrite 覆写内容(禁用时写空文件)
|
|
389
|
+
const overwriteMerged = {};
|
|
390
|
+
for (const f of buildResult.overwriteFiles) {
|
|
391
|
+
Object.assign(overwriteMerged, f.config);
|
|
392
|
+
}
|
|
393
|
+
const overwriteContent = buildResult.overwriteFiles.length > 0 ? yaml.dump(overwriteMerged, dumpOpts) : '# overwrite 已禁用或无覆写文件\n';
|
|
394
|
+
fs.writeFileSync(PATHS.configStage2Overwrite, overwriteContent, { mode: 0o600 });
|
|
395
|
+
|
|
396
|
+
// 3. 系统覆盖值(BASE_CONFIG + TUN_CONFIG)
|
|
397
|
+
fs.writeFileSync(PATHS.configStage3System, yaml.dump(buildResult.systemConfig, dumpOpts), { mode: 0o600 });
|
|
398
|
+
}
|
|
399
|
+
|
|
357
400
|
function hasConfig() {
|
|
358
401
|
return fs.existsSync(PATHS.configFile);
|
|
359
402
|
}
|
|
@@ -394,11 +437,11 @@ function resetUserData(options) {
|
|
|
394
437
|
|
|
395
438
|
let itemsToRemove;
|
|
396
439
|
if (kernelOnly) {
|
|
397
|
-
itemsToRemove = [DIRS.
|
|
440
|
+
itemsToRemove = [DIRS.kernel];
|
|
398
441
|
} else {
|
|
399
442
|
itemsToRemove = [PATHS.settingsFile, DIRS.subscriptions, DIRS.logs, DIRS.data, DIRS.runtime];
|
|
400
443
|
if (!keepKernel) {
|
|
401
|
-
itemsToRemove.push(DIRS.
|
|
444
|
+
itemsToRemove.push(DIRS.kernel);
|
|
402
445
|
}
|
|
403
446
|
}
|
|
404
447
|
|
|
@@ -428,9 +471,7 @@ const DIRECTORY_TARGETS = {
|
|
|
428
471
|
logs: { path: DIRS.logs, label: '日志目录' },
|
|
429
472
|
data: { path: DIRS.data, label: 'mihomo 数据目录' },
|
|
430
473
|
runtime: { path: DIRS.runtime, label: '运行时目录' },
|
|
431
|
-
|
|
432
|
-
settings: { path: PATHS.settingsFile, label: '设置文件' },
|
|
433
|
-
kernel: { path: DIRS.core, label: '内核目录' },
|
|
474
|
+
kernel: { path: DIRS.kernel, label: '内核目录' },
|
|
434
475
|
};
|
|
435
476
|
|
|
436
477
|
module.exports = {
|
|
@@ -463,6 +504,7 @@ module.exports = {
|
|
|
463
504
|
parseYamlOrJson,
|
|
464
505
|
buildConfig,
|
|
465
506
|
writeMihomoConfig,
|
|
507
|
+
writeDebugConfig,
|
|
466
508
|
hasConfig,
|
|
467
509
|
getConfigInfo,
|
|
468
510
|
resetUserData,
|
package/src/overwrite.js
CHANGED
|
@@ -150,13 +150,6 @@ function deepMergeWithOverrides(target, override) {
|
|
|
150
150
|
return result;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
/**
|
|
154
|
-
* 获取覆写目录路径
|
|
155
|
-
*/
|
|
156
|
-
function getOverwritesDir() {
|
|
157
|
-
return config.DIRS.overwrites;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
153
|
/**
|
|
161
154
|
* 检查覆写功能是否启用
|
|
162
155
|
*/
|
|
@@ -173,11 +166,12 @@ function setOverwriteEnabled(enabled) {
|
|
|
173
166
|
}
|
|
174
167
|
|
|
175
168
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
169
|
+
* 读取根目录下的覆写文件
|
|
170
|
+
* 匹配 overwrite.yaml 和 overwrite.*.yaml,按文件名排序
|
|
171
|
+
* overwrite.yaml 始终第一
|
|
178
172
|
*/
|
|
179
173
|
function loadOverwriteFile() {
|
|
180
|
-
const dir =
|
|
174
|
+
const dir = config.USER_DATA_DIR;
|
|
181
175
|
|
|
182
176
|
if (!fs.existsSync(dir)) {
|
|
183
177
|
return [];
|
|
@@ -185,8 +179,12 @@ function loadOverwriteFile() {
|
|
|
185
179
|
|
|
186
180
|
const files = fs
|
|
187
181
|
.readdirSync(dir)
|
|
188
|
-
.filter(f => f
|
|
189
|
-
.sort()
|
|
182
|
+
.filter(f => f === 'overwrite.yaml' || /^overwrite\..+\.ya?ml$/.test(f))
|
|
183
|
+
.sort((a, b) => {
|
|
184
|
+
if (a === 'overwrite.yaml') return -1;
|
|
185
|
+
if (b === 'overwrite.yaml') return 1;
|
|
186
|
+
return a.localeCompare(b);
|
|
187
|
+
});
|
|
190
188
|
|
|
191
189
|
const results = [];
|
|
192
190
|
|
|
@@ -239,11 +237,10 @@ function applyOverwrite(baseConfig) {
|
|
|
239
237
|
function listOverwriteFile() {
|
|
240
238
|
const files = loadOverwriteFile();
|
|
241
239
|
const enabled = isOverwriteEnabled();
|
|
242
|
-
const dir = getOverwritesDir();
|
|
243
240
|
|
|
244
241
|
return {
|
|
245
242
|
enabled,
|
|
246
|
-
dir,
|
|
243
|
+
dir: config.USER_DATA_DIR,
|
|
247
244
|
files: files.map(f => ({
|
|
248
245
|
name: f.name,
|
|
249
246
|
path: f.path,
|
|
@@ -257,4 +254,5 @@ module.exports = {
|
|
|
257
254
|
setOverwriteEnabled,
|
|
258
255
|
applyOverwrite,
|
|
259
256
|
listOverwriteFile,
|
|
257
|
+
loadOverwriteFile,
|
|
260
258
|
};
|
package/src/subscription.js
CHANGED
|
@@ -48,13 +48,19 @@ function formatProxySummary(info) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
51
|
+
* 获取当前使用的订阅
|
|
52
52
|
*/
|
|
53
53
|
function getActiveSubscription() {
|
|
54
54
|
const subs = config.getSubscriptions();
|
|
55
55
|
if (subs.length === 0) {
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
|
+
const settings = config.readSettings();
|
|
59
|
+
const activeName = settings.active_subscription;
|
|
60
|
+
if (activeName) {
|
|
61
|
+
const found = subs.find(s => s.name === activeName);
|
|
62
|
+
if (found) return found;
|
|
63
|
+
}
|
|
58
64
|
return subs[0];
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -175,12 +181,13 @@ function prepareConfigForStart(mode, subName) {
|
|
|
175
181
|
throw new Error('未找到订阅配置 "' + subName + '",请先添加订阅');
|
|
176
182
|
}
|
|
177
183
|
|
|
178
|
-
const
|
|
179
|
-
config.writeMihomoConfig(
|
|
184
|
+
const buildResult = config.buildConfig(rawContent, mode);
|
|
185
|
+
config.writeMihomoConfig(buildResult.config);
|
|
186
|
+
config.writeDebugConfig(buildResult);
|
|
180
187
|
|
|
181
188
|
return {
|
|
182
|
-
proxies:
|
|
183
|
-
proxyGroups:
|
|
189
|
+
proxies: buildResult.config.proxies ? buildResult.config.proxies.length : 0,
|
|
190
|
+
proxyGroups: buildResult.config['proxy-groups'] ? buildResult.config['proxy-groups'].length : 0,
|
|
184
191
|
};
|
|
185
192
|
}
|
|
186
193
|
|