mihomo-cli 1.3.1 → 1.4.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 +33 -0
- package/README.md +37 -27
- package/index.js +201 -76
- package/package.json +2 -2
- package/src/config.js +30 -15
- package/src/kernel.js +26 -30
- package/src/process.js +16 -28
- package/src/utils.js +19 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.0] - 2026-04-07
|
|
4
|
+
|
|
5
|
+
### 新增功能
|
|
6
|
+
|
|
7
|
+
- **reset 命令增强**:
|
|
8
|
+
- 支持按目标名称模糊删除:`mihomo reset subs logs` 删除订阅和日志
|
|
9
|
+
- 可用目标:`subs`, `logs`, `kernel`, `overwrites`, `settings`, `data`, `runtime`
|
|
10
|
+
- `--full` 删除全部
|
|
11
|
+
- 留空默认保留:设置、内核、覆写配置
|
|
12
|
+
- **kernel 镜像改进**:
|
|
13
|
+
- 默认改为**直连**下载(不再强制使用镜像)
|
|
14
|
+
- `--mirror` 不带参数时使用默认镜像 `v6.gh-proxy.org`
|
|
15
|
+
- `--mirror hk.gh-proxy.org` 指定镜像
|
|
16
|
+
- `--mirror-all` API 请求和下载都使用镜像(解决 API 访问受限问题)
|
|
17
|
+
- 命令中列出所有可用镜像
|
|
18
|
+
|
|
19
|
+
### 修复
|
|
20
|
+
|
|
21
|
+
- **reset 覆写目录**:补充删除 `overwrites` 目录
|
|
22
|
+
- **reset 保留逻辑**:修复覆写配置的保留/删除逻辑
|
|
23
|
+
|
|
24
|
+
### 优化
|
|
25
|
+
|
|
26
|
+
- **状态显示**:运行中/已停止添加图标区分
|
|
27
|
+
- `● 运行中` (绿色)
|
|
28
|
+
- `○ 已停止` (黄色)
|
|
29
|
+
- **措辞明确**:动作成功的"已停止"改为"已停止进程",避免与状态显示混淆
|
|
30
|
+
- **代码重构**:
|
|
31
|
+
- 常量提取:`BATCH_KILL_THRESHOLD` 等
|
|
32
|
+
- 镜像处理逻辑优化,新增 `normalizeMirrorUrl()` 统一处理
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
3
36
|
## [1.3.1] - 2026-04-07
|
|
4
37
|
|
|
5
38
|
### 优化
|
package/README.md
CHANGED
|
@@ -39,14 +39,14 @@ npm link
|
|
|
39
39
|
### 1. 下载内核
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
#
|
|
42
|
+
# 默认直连下载
|
|
43
43
|
mihomo kernel
|
|
44
44
|
|
|
45
|
-
#
|
|
46
|
-
mihomo kernel
|
|
45
|
+
# 国内网络使用镜像加速
|
|
46
|
+
mihomo kernel --mirror
|
|
47
47
|
|
|
48
|
-
#
|
|
49
|
-
mihomo kernel --
|
|
48
|
+
# 或指定镜像
|
|
49
|
+
mihomo kernel --mirror hk.gh-proxy.org
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
### 2. 添加订阅
|
|
@@ -109,16 +109,16 @@ mihomo ui yacd # YACD
|
|
|
109
109
|
|
|
110
110
|
### 其他命令
|
|
111
111
|
|
|
112
|
-
| 命令
|
|
113
|
-
|
|
|
114
|
-
| `mihomo kernel [
|
|
115
|
-
| `mihomo update`
|
|
116
|
-
| `mihomo ui [zash\|dash\|yacd]`
|
|
117
|
-
| `mihomo dir`
|
|
118
|
-
| `mihomo dir open [target]`
|
|
119
|
-
| `mihomo reset [--full]`
|
|
120
|
-
| `mihomo version`
|
|
121
|
-
| `mihomo help`
|
|
112
|
+
| 命令 | 说明 |
|
|
113
|
+
| --------------------------------- | ------------------------------------------------------------------- |
|
|
114
|
+
| `mihomo kernel [--mirror [镜像]]` | 更新内核(默认直连,`--mirror` 使用镜像) |
|
|
115
|
+
| `mihomo update` | 更新 mihomo-cli (npm install -g) |
|
|
116
|
+
| `mihomo ui [zash\|dash\|yacd]` | 打开 Web UI |
|
|
117
|
+
| `mihomo dir` | 显示数据目录位置 |
|
|
118
|
+
| `mihomo dir open [target]` | 打开指定目录(`root`, `subs`, `logs`, `overwrites` 等) |
|
|
119
|
+
| `mihomo reset [目标...] [--full]` | 重置用户数据(可用目标:`subs`, `logs`, `kernel`, `overwrites` 等) |
|
|
120
|
+
| `mihomo version` | 显示版本信息 |
|
|
121
|
+
| `mihomo help` | 显示帮助信息 |
|
|
122
122
|
|
|
123
123
|
### 命令别名
|
|
124
124
|
|
|
@@ -148,20 +148,30 @@ mihomo ui yacd # YACD
|
|
|
148
148
|
国内网络可使用镜像加速 GitHub 下载:
|
|
149
149
|
|
|
150
150
|
```bash
|
|
151
|
-
#
|
|
152
|
-
mihomo kernel
|
|
153
|
-
|
|
154
|
-
#
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
mihomo kernel --
|
|
151
|
+
# 默认直连(不使用镜像)
|
|
152
|
+
mihomo kernel
|
|
153
|
+
|
|
154
|
+
# 使用默认镜像 (v6.gh-proxy.org)
|
|
155
|
+
mihomo kernel --mirror
|
|
156
|
+
|
|
157
|
+
# 指定镜像
|
|
158
|
+
mihomo kernel --mirror hk.gh-proxy.org
|
|
159
|
+
|
|
160
|
+
# API 请求和下载都使用镜像(解决 API 访问受限问题)
|
|
161
|
+
mihomo kernel --mirror-all
|
|
162
|
+
mihomo kernel --mirror-all hk.gh-proxy.org
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
+
**可用镜像:**
|
|
166
|
+
|
|
167
|
+
| 镜像 | 说明 |
|
|
168
|
+
| ---------------------- | -------- |
|
|
169
|
+
| `v6.gh-proxy.org` | 默认镜像 |
|
|
170
|
+
| `gh-proxy.org` | 官方 |
|
|
171
|
+
| `hk.gh-proxy.org` | 香港 |
|
|
172
|
+
| `cdn.gh-proxy.org` | CDN |
|
|
173
|
+
| `edgeone.gh-proxy.org` | EdgeOne |
|
|
174
|
+
|
|
165
175
|
## 订阅自动更新
|
|
166
176
|
|
|
167
177
|
- 默认更新间隔:12 小时(或订阅服务端指定的 `profile-update-interval`)
|
package/index.js
CHANGED
|
@@ -148,13 +148,13 @@ function printHelp() {
|
|
|
148
148
|
'\n' +
|
|
149
149
|
' ' +
|
|
150
150
|
colors.bold('kernel') +
|
|
151
|
-
' [
|
|
151
|
+
' [--mirror [镜像]] 更新内核(默认直连,--mirror 使用 v6)\n' +
|
|
152
152
|
' ' +
|
|
153
153
|
colors.bold('update') +
|
|
154
154
|
' 更新 mihomo-cli (npm install -g)\n' +
|
|
155
155
|
' ' +
|
|
156
156
|
colors.bold('reset') +
|
|
157
|
-
' [--full]
|
|
157
|
+
' [目标...] [--full] 重置: 留空保留设置/内核/覆写, 指定目标删对应项, --full 删全部\n' +
|
|
158
158
|
' ' +
|
|
159
159
|
colors.bold('help') +
|
|
160
160
|
', -h 显示帮助\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
|
|
|
@@ -245,6 +245,14 @@ function printStatus() {
|
|
|
245
245
|
console.log('');
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
function handleStopResult(result) {
|
|
249
|
+
if (result.remaining && result.remaining.length > 0) {
|
|
250
|
+
console.error(colors.red('部分进程未终止:') + ' ' + result.remaining.join(', '));
|
|
251
|
+
console.error('请手动运行: sudo pkill -9 mihomo');
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
248
256
|
async function cmdStart(args) {
|
|
249
257
|
if (!config.hasKernel()) {
|
|
250
258
|
console.error('错误: 未找到内核,请运行 "mihomo kernel"');
|
|
@@ -269,16 +277,10 @@ async function cmdStart(args) {
|
|
|
269
277
|
console.log('停止 ' + count + ' 个进程...');
|
|
270
278
|
}
|
|
271
279
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (stopResult.remaining && stopResult.remaining.length > 0) {
|
|
275
|
-
console.error(colors.red('部分进程未终止:') + ' ' + stopResult.remaining.join(', '));
|
|
276
|
-
console.error('请手动运行: sudo pkill -9 mihomo');
|
|
277
|
-
process.exit(1);
|
|
278
|
-
}
|
|
280
|
+
handleStopResult(processManager.stop(true));
|
|
279
281
|
|
|
280
282
|
if (hasProcess) {
|
|
281
|
-
console.log(colors.green('
|
|
283
|
+
console.log(colors.green('已停止进程') + '\n');
|
|
282
284
|
}
|
|
283
285
|
|
|
284
286
|
let configInfo;
|
|
@@ -310,14 +312,8 @@ async function cmdStop() {
|
|
|
310
312
|
}
|
|
311
313
|
|
|
312
314
|
console.log('停止 ' + pids.length + ' 个进程...');
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (result.remaining && result.remaining.length > 0) {
|
|
316
|
-
console.error(colors.red('部分进程未终止:') + ' ' + result.remaining.join(', '));
|
|
317
|
-
console.error('请手动运行: sudo pkill -9 mihomo');
|
|
318
|
-
process.exit(1);
|
|
319
|
-
}
|
|
320
|
-
console.log(colors.green('已停止'));
|
|
315
|
+
handleStopResult(processManager.stop(true));
|
|
316
|
+
console.log(colors.green('已停止进程'));
|
|
321
317
|
}
|
|
322
318
|
|
|
323
319
|
function cmdUI(args) {
|
|
@@ -439,52 +435,60 @@ function cmdLogs(args) {
|
|
|
439
435
|
|
|
440
436
|
async function cmdKernel(args) {
|
|
441
437
|
const mirrorInfo = utils.parseMirrorArg(args);
|
|
442
|
-
const effectiveMirror = mirrorInfo.
|
|
443
|
-
const isDefault = !mirrorInfo.isOverride && effectiveMirror === config.DEFAULT_GITHUB_MIRROR;
|
|
438
|
+
const effectiveMirror = mirrorInfo.mirror;
|
|
444
439
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (effectiveMirror === null) {
|
|
449
|
-
console.log('镜像: 直连(命令行指定 --no-mirror)');
|
|
450
|
-
} else {
|
|
451
|
-
console.log('镜像: ' + effectiveMirror + ' (命令行指定)');
|
|
452
|
-
}
|
|
453
|
-
} else {
|
|
454
|
-
console.log('镜像: ' + (effectiveMirror || '直连(无镜像)') + (isDefault && effectiveMirror ? ' (默认)' : ''));
|
|
440
|
+
if (effectiveMirror) {
|
|
441
|
+
let mirrorDesc = mirrorInfo.type === 'all' ? ' (API和下载均使用镜像)' : ' (下载时使用镜像)';
|
|
442
|
+
console.log('镜像: ' + effectiveMirror + mirrorDesc);
|
|
455
443
|
}
|
|
456
444
|
|
|
445
|
+
console.log('\n提示: 如果下载速度过慢或直连失败,可使用 --mirror 参数通过镜像下载');
|
|
446
|
+
console.log('\n用法:');
|
|
447
|
+
console.log(' mihomo kernel # 直连');
|
|
448
|
+
console.log(' mihomo kernel --mirror # 下载使用默认镜像 (v6.gh-proxy.org)');
|
|
449
|
+
console.log(' mihomo kernel --mirror hk.gh-proxy.org # 下载使用指定镜像');
|
|
450
|
+
console.log(' mihomo kernel --mirror-all # API请求和下载都使用默认镜像');
|
|
451
|
+
console.log(' mihomo kernel --mirror-all hk.gh-proxy.org # API和下载都使用指定镜像');
|
|
452
|
+
|
|
457
453
|
console.log('\n可用镜像:');
|
|
458
454
|
config.AVAILABLE_MIRRORS.forEach(m => {
|
|
459
455
|
const isCurrent =
|
|
460
456
|
effectiveMirror && (effectiveMirror.includes('//' + m + '/') || effectiveMirror.includes('//' + m + ':') || effectiveMirror.endsWith('//' + m));
|
|
461
457
|
console.log(' ' + m + (isCurrent ? ' (当前)' : ''));
|
|
462
458
|
});
|
|
463
|
-
|
|
464
|
-
console.log('\n用法:');
|
|
465
|
-
console.log(' mihomo kernel # 使用默认镜像');
|
|
466
|
-
console.log(' mihomo kernel hk.gh-proxy.org # 使用指定镜像');
|
|
467
|
-
console.log(' mihomo kernel --mirror hk.gh-proxy.org');
|
|
468
|
-
console.log(' mihomo kernel --no-mirror # 直连,不使用镜像');
|
|
469
459
|
console.log('');
|
|
470
460
|
|
|
461
|
+
console.log('检查内核更新...');
|
|
462
|
+
|
|
471
463
|
try {
|
|
472
|
-
const
|
|
464
|
+
const apiMirror = mirrorInfo.type === 'all' ? effectiveMirror : null;
|
|
465
|
+
const info = await kernel.checkUpdate(apiMirror);
|
|
473
466
|
console.log('当前: ' + info.current);
|
|
474
467
|
console.log('最新: ' + info.latest);
|
|
475
468
|
|
|
476
469
|
if (!info.needsUpdate) {
|
|
477
470
|
console.log('已是最新版本');
|
|
478
|
-
|
|
471
|
+
} else {
|
|
472
|
+
console.log('\n正在下载...');
|
|
473
|
+
const result = await kernel.downloadKernel(
|
|
474
|
+
msg => {
|
|
475
|
+
console.log(msg);
|
|
476
|
+
},
|
|
477
|
+
mirrorInfo.mirror,
|
|
478
|
+
info.release,
|
|
479
|
+
);
|
|
480
|
+
console.log('\n已更新到 ' + result.version);
|
|
479
481
|
}
|
|
480
|
-
|
|
481
|
-
console.log('\n正在下载...');
|
|
482
|
-
const result = await kernel.downloadKernel(msg => {
|
|
483
|
-
console.log(msg);
|
|
484
|
-
}, mirrorInfo.mirror);
|
|
485
|
-
console.log('已更新到 ' + result.version);
|
|
486
482
|
} catch (e) {
|
|
487
|
-
console.error('更新失败: ' + e.message);
|
|
483
|
+
console.error('\n更新失败: ' + e.message);
|
|
484
|
+
if (e.response && e.response.data) {
|
|
485
|
+
if (e.response.data.message) {
|
|
486
|
+
console.error('原因: ' + e.response.data.message);
|
|
487
|
+
}
|
|
488
|
+
if (e.response.data.documentation_url) {
|
|
489
|
+
console.error('文档: ' + e.response.data.documentation_url);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
488
492
|
process.exit(1);
|
|
489
493
|
}
|
|
490
494
|
}
|
|
@@ -751,44 +755,167 @@ async function cmdUpdate() {
|
|
|
751
755
|
}
|
|
752
756
|
}
|
|
753
757
|
|
|
758
|
+
async function confirmPrompt(question) {
|
|
759
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
760
|
+
const answer = await new Promise(resolve => {
|
|
761
|
+
rl.question(question + ' (y/N) ', a => {
|
|
762
|
+
rl.close();
|
|
763
|
+
resolve(a);
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const RESET_TARGETS = [
|
|
770
|
+
{
|
|
771
|
+
id: 'subs',
|
|
772
|
+
aliases: ['sub', 'subs', 'subscription', 'subscriptions'],
|
|
773
|
+
label: '订阅',
|
|
774
|
+
paths: () => [config.DIRS.subscriptions],
|
|
775
|
+
needsStop: true,
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
id: 'logs',
|
|
779
|
+
aliases: ['log', 'logs'],
|
|
780
|
+
label: '日志',
|
|
781
|
+
paths: () => [config.DIRS.logs],
|
|
782
|
+
needsStop: false,
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
id: 'data',
|
|
786
|
+
aliases: ['data'],
|
|
787
|
+
label: '运行数据',
|
|
788
|
+
paths: () => [config.DIRS.data],
|
|
789
|
+
needsStop: true,
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
id: 'runtime',
|
|
793
|
+
aliases: ['runtime'],
|
|
794
|
+
label: '运行时',
|
|
795
|
+
paths: () => [config.DIRS.runtime],
|
|
796
|
+
needsStop: true,
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
id: 'settings',
|
|
800
|
+
aliases: ['setting', 'settings', 'config'],
|
|
801
|
+
label: '设置',
|
|
802
|
+
paths: () => [config.PATHS.settingsFile],
|
|
803
|
+
needsStop: false,
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
id: 'kernel',
|
|
807
|
+
aliases: ['kernel', 'core'],
|
|
808
|
+
label: '内核',
|
|
809
|
+
paths: () => [config.DIRS.core],
|
|
810
|
+
needsStop: false,
|
|
811
|
+
onAfter: () => config.clearKernelVersionCache(),
|
|
812
|
+
checkEmpty: () => !config.hasKernel(),
|
|
813
|
+
emptyMsg: '内核未安装,无需删除',
|
|
814
|
+
warnIfRunning: true,
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
id: 'overwrites',
|
|
818
|
+
aliases: ['overwrite', 'overwrites', 'ow'],
|
|
819
|
+
label: '覆写',
|
|
820
|
+
paths: () => [config.DIRS.overwrites],
|
|
821
|
+
needsStop: false,
|
|
822
|
+
},
|
|
823
|
+
];
|
|
824
|
+
|
|
825
|
+
function resolveResetTargets(names) {
|
|
826
|
+
const matched = [];
|
|
827
|
+
const unmatched = [];
|
|
828
|
+
for (const name of names) {
|
|
829
|
+
const t = RESET_TARGETS.find(t => t.aliases.includes(name.toLowerCase()));
|
|
830
|
+
if (t) {
|
|
831
|
+
if (!matched.find(m => m.id === t.id)) matched.push(t);
|
|
832
|
+
} else {
|
|
833
|
+
unmatched.push(name);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return { matched, unmatched };
|
|
837
|
+
}
|
|
838
|
+
|
|
754
839
|
async function cmdReset(args) {
|
|
755
|
-
const
|
|
756
|
-
const
|
|
840
|
+
const flags = (args || []).filter(a => a.startsWith('-'));
|
|
841
|
+
const names = (args || []).slice(1).filter(a => !a.startsWith('-'));
|
|
842
|
+
const fullReset = flags.includes('--full') || flags.includes('-f');
|
|
843
|
+
const skipConfirm = flags.includes('--yes') || flags.includes('-y');
|
|
844
|
+
|
|
845
|
+
let targets;
|
|
846
|
+
|
|
847
|
+
if (fullReset) {
|
|
848
|
+
targets = RESET_TARGETS;
|
|
849
|
+
} else if (names.length > 0) {
|
|
850
|
+
const { matched, unmatched } = resolveResetTargets(names);
|
|
851
|
+
if (unmatched.length > 0) {
|
|
852
|
+
console.error('错误: 未知的重置目标: ' + unmatched.join(', '));
|
|
853
|
+
console.log('');
|
|
854
|
+
console.log('可用目标: ' + RESET_TARGETS.map(t => t.aliases[0]).join(', '));
|
|
855
|
+
console.log('');
|
|
856
|
+
console.log('示例:');
|
|
857
|
+
console.log(' mihomo reset sub log # 删除订阅和日志');
|
|
858
|
+
console.log(' mihomo reset kernel # 只删内核');
|
|
859
|
+
console.log(' mihomo reset --full # 删除全部');
|
|
860
|
+
console.log(' mihomo reset # 删除全部(保留设置、内核、覆写)');
|
|
861
|
+
process.exit(1);
|
|
862
|
+
}
|
|
863
|
+
targets = matched;
|
|
864
|
+
} else {
|
|
865
|
+
targets = RESET_TARGETS.filter(t => !['settings', 'kernel', 'overwrites'].includes(t.id));
|
|
866
|
+
}
|
|
757
867
|
|
|
758
|
-
const
|
|
759
|
-
|
|
868
|
+
for (const t of targets) {
|
|
869
|
+
if (t.checkEmpty && t.checkEmpty()) {
|
|
870
|
+
if (targets.length === 1) {
|
|
871
|
+
console.log(t.emptyMsg);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const needsStop = targets.some(t => t.needsStop);
|
|
878
|
+
const warnRunning = targets.some(t => t.warnIfRunning);
|
|
879
|
+
|
|
880
|
+
const pids = needsStop || warnRunning ? processManager.getAllMihomoPids() : [];
|
|
881
|
+
|
|
882
|
+
if (needsStop && pids.length > 0) {
|
|
760
883
|
console.log('停止 ' + pids.length + ' 个进程...');
|
|
761
884
|
processManager.cleanupAll(true);
|
|
762
885
|
for (let i = 0; i < processManager.PROCESS_WAIT_ATTEMPTS; i++) {
|
|
763
886
|
if (processManager.getAllMihomoPids().length === 0) break;
|
|
764
887
|
await new Promise(r => setTimeout(r, processManager.PROCESS_WAIT_INTERVAL));
|
|
765
888
|
}
|
|
889
|
+
} else if (warnRunning && pids.length > 0) {
|
|
890
|
+
console.log(colors.yellow('警告: mihomo 正在运行 (PID ' + pids.join(', ') + '),删除内核后将无法重新启动'));
|
|
766
891
|
}
|
|
767
892
|
|
|
768
|
-
|
|
769
|
-
console.log(mode);
|
|
770
|
-
|
|
771
|
-
if (!skipConfirm) {
|
|
772
|
-
const rl = readline.createInterface({
|
|
773
|
-
input: process.stdin,
|
|
774
|
-
output: process.stdout,
|
|
775
|
-
});
|
|
893
|
+
console.log('将删除: ' + targets.map(t => t.label).join('、'));
|
|
776
894
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
});
|
|
782
|
-
});
|
|
895
|
+
if (!skipConfirm && !(await confirmPrompt('确认?'))) {
|
|
896
|
+
console.log('已取消');
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
783
899
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
900
|
+
for (const t of targets) {
|
|
901
|
+
for (const p of t.paths()) {
|
|
902
|
+
if (config.fsExistsSync(p)) {
|
|
903
|
+
try {
|
|
904
|
+
config.rmrf(p);
|
|
905
|
+
} catch (e) {
|
|
906
|
+
console.warn(' 警告: 无法删除 ' + p + ': ' + e.message);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
787
909
|
}
|
|
910
|
+
if (t.onAfter) t.onAfter();
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
config.ensureDirs();
|
|
914
|
+
if (targets.some(t => t.id === 'settings')) {
|
|
915
|
+
config.invalidateSettingsCache();
|
|
788
916
|
}
|
|
789
917
|
|
|
790
|
-
|
|
791
|
-
console.log('已重置 ' + count + ' 项');
|
|
918
|
+
console.log(colors.green('已重置: ' + targets.map(t => t.label).join('、')));
|
|
792
919
|
}
|
|
793
920
|
|
|
794
921
|
function printOverwriteList() {
|
|
@@ -905,13 +1032,11 @@ function cmdDirectory(args) {
|
|
|
905
1032
|
console.log('');
|
|
906
1033
|
console.log('可用目标:');
|
|
907
1034
|
console.log(' root (默认) 根目录');
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
console.log(' settings 设置文件 (settings.json)');
|
|
914
|
-
console.log(' kernel 内核目录');
|
|
1035
|
+
Object.entries(config.DIRECTORY_TARGETS).forEach(([key, val]) => {
|
|
1036
|
+
if (key !== 'root') {
|
|
1037
|
+
console.log(' ' + key.padEnd(14) + val.label);
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
915
1040
|
console.log('');
|
|
916
1041
|
process.exit(1);
|
|
917
1042
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mihomo-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "A terminal-based mihomo (Clash.Meta) client for macOS",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mihomo-cli": "index.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
35
|
-
"url": "https://github.com/adaex/mihomo-cli.git"
|
|
35
|
+
"url": "git+https://github.com/adaex/mihomo-cli.git"
|
|
36
36
|
},
|
|
37
37
|
"engines": {
|
|
38
38
|
"node": ">=18.0.0"
|
package/src/config.js
CHANGED
|
@@ -92,6 +92,7 @@ function readSettings() {
|
|
|
92
92
|
settingsCache = JSON.parse(content);
|
|
93
93
|
return settingsCache;
|
|
94
94
|
} catch (_e) {
|
|
95
|
+
console.warn('警告: settings.json 格式损坏,使用默认设置(原文件已保留)');
|
|
95
96
|
settingsCache = {};
|
|
96
97
|
return settingsCache;
|
|
97
98
|
}
|
|
@@ -115,7 +116,7 @@ function writeSettings(settings) {
|
|
|
115
116
|
|
|
116
117
|
// GitHub 镜像配置
|
|
117
118
|
const DEFAULT_GITHUB_MIRROR = 'https://v6.gh-proxy.org/';
|
|
118
|
-
const AVAILABLE_MIRRORS = ['
|
|
119
|
+
const AVAILABLE_MIRRORS = ['gh-proxy.org', 'v6.gh-proxy.org', 'hk.gh-proxy.org', 'cdn.gh-proxy.org'];
|
|
119
120
|
|
|
120
121
|
function getGitHubMirror() {
|
|
121
122
|
const settings = readSettings();
|
|
@@ -246,14 +247,16 @@ function hasKernel() {
|
|
|
246
247
|
return fs.existsSync(PATHS.mihomoBinary);
|
|
247
248
|
}
|
|
248
249
|
|
|
249
|
-
let kernelVersionCache =
|
|
250
|
+
let kernelVersionCache = null;
|
|
251
|
+
let kernelVersionCached = false;
|
|
250
252
|
|
|
251
253
|
function getKernelVersion() {
|
|
252
254
|
if (!hasKernel()) {
|
|
253
|
-
kernelVersionCache =
|
|
255
|
+
kernelVersionCache = null;
|
|
256
|
+
kernelVersionCached = false;
|
|
254
257
|
return null;
|
|
255
258
|
}
|
|
256
|
-
if (
|
|
259
|
+
if (kernelVersionCached) return kernelVersionCache;
|
|
257
260
|
try {
|
|
258
261
|
const output = execSync('"' + PATHS.mihomoBinary + '" -v 2>&1 || true', {
|
|
259
262
|
encoding: 'utf8',
|
|
@@ -261,14 +264,14 @@ function getKernelVersion() {
|
|
|
261
264
|
if (output) {
|
|
262
265
|
const match = output.match(/v?[\d]+\.[\d]+\.[\d]+/);
|
|
263
266
|
kernelVersionCache = match ? match[0] : output;
|
|
264
|
-
|
|
267
|
+
} else {
|
|
268
|
+
kernelVersionCache = 'unknown';
|
|
265
269
|
}
|
|
266
|
-
kernelVersionCache = 'unknown';
|
|
267
|
-
return kernelVersionCache;
|
|
268
270
|
} catch (_e) {
|
|
269
271
|
kernelVersionCache = 'unknown';
|
|
270
|
-
return kernelVersionCache;
|
|
271
272
|
}
|
|
273
|
+
kernelVersionCached = true;
|
|
274
|
+
return kernelVersionCache;
|
|
272
275
|
}
|
|
273
276
|
|
|
274
277
|
const TUN_CONFIG = {
|
|
@@ -387,11 +390,16 @@ function rmrf(dir) {
|
|
|
387
390
|
function resetUserData(options) {
|
|
388
391
|
if (options === undefined) options = {};
|
|
389
392
|
const keepKernel = options.keepKernel !== false;
|
|
393
|
+
const kernelOnly = options.kernelOnly === true;
|
|
390
394
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
+
let itemsToRemove;
|
|
396
|
+
if (kernelOnly) {
|
|
397
|
+
itemsToRemove = [DIRS.core];
|
|
398
|
+
} else {
|
|
399
|
+
itemsToRemove = [PATHS.settingsFile, DIRS.subscriptions, DIRS.logs, DIRS.data, DIRS.runtime];
|
|
400
|
+
if (!keepKernel) {
|
|
401
|
+
itemsToRemove.push(DIRS.core);
|
|
402
|
+
}
|
|
395
403
|
}
|
|
396
404
|
|
|
397
405
|
let removedCount = 0;
|
|
@@ -406,8 +414,10 @@ function resetUserData(options) {
|
|
|
406
414
|
}
|
|
407
415
|
}
|
|
408
416
|
|
|
409
|
-
|
|
410
|
-
|
|
417
|
+
if (!kernelOnly) {
|
|
418
|
+
ensureDirs();
|
|
419
|
+
settingsCache = null;
|
|
420
|
+
}
|
|
411
421
|
return removedCount;
|
|
412
422
|
}
|
|
413
423
|
|
|
@@ -443,7 +453,8 @@ module.exports = {
|
|
|
443
453
|
hasKernel,
|
|
444
454
|
getKernelVersion,
|
|
445
455
|
clearKernelVersionCache: () => {
|
|
446
|
-
kernelVersionCache =
|
|
456
|
+
kernelVersionCache = null;
|
|
457
|
+
kernelVersionCached = false;
|
|
447
458
|
},
|
|
448
459
|
getGitHubMirror,
|
|
449
460
|
setGitHubMirror,
|
|
@@ -455,5 +466,9 @@ module.exports = {
|
|
|
455
466
|
hasConfig,
|
|
456
467
|
getConfigInfo,
|
|
457
468
|
resetUserData,
|
|
469
|
+
invalidateSettingsCache: () => {
|
|
470
|
+
settingsCache = null;
|
|
471
|
+
},
|
|
472
|
+
fsExistsSync: p => fs.existsSync(p),
|
|
458
473
|
rmrf,
|
|
459
474
|
};
|
package/src/kernel.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// 内置模块
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const { execSync } = require('child_process');
|
|
4
|
+
const { execSync, spawnSync } = require('child_process');
|
|
5
5
|
|
|
6
6
|
// 第三方模块
|
|
7
7
|
const { compareVersions } = require('compare-versions');
|
|
@@ -10,21 +10,18 @@ const { compareVersions } = require('compare-versions');
|
|
|
10
10
|
const config = require('./config');
|
|
11
11
|
const utils = require('./utils');
|
|
12
12
|
|
|
13
|
-
// 常量定义
|
|
14
13
|
const GITHUB_REPO = 'MetaCubeX/mihomo';
|
|
15
14
|
const KERNEL_HTTP_TIMEOUT = 120000;
|
|
16
15
|
const KERNEL_MAX_CONTENT_LENGTH = 200 * 1024 * 1024;
|
|
17
16
|
const KERNEL_DOWNLOAD_TIMEOUT = 180000;
|
|
18
17
|
|
|
19
|
-
// 内核专用 HTTP 客户端(超时和容量较大,适合下载大文件)
|
|
20
18
|
const HTTP_CLIENT = utils.createHttpClient({
|
|
21
19
|
timeout: KERNEL_HTTP_TIMEOUT,
|
|
22
20
|
maxContentLength: KERNEL_MAX_CONTENT_LENGTH,
|
|
23
21
|
});
|
|
24
22
|
|
|
25
|
-
function withMirror(url,
|
|
26
|
-
|
|
27
|
-
if (mirror && url.startsWith('https://github.com/')) {
|
|
23
|
+
function withMirror(url, mirror) {
|
|
24
|
+
if (mirror && (url.startsWith('https://github.com/') || url.startsWith('https://api.github.com/'))) {
|
|
28
25
|
return mirror + url;
|
|
29
26
|
}
|
|
30
27
|
return url;
|
|
@@ -65,8 +62,8 @@ function findMatchingAsset(assets, platform, arch) {
|
|
|
65
62
|
return matchingAssets[0];
|
|
66
63
|
}
|
|
67
64
|
|
|
68
|
-
async function getLatestRelease(repo) {
|
|
69
|
-
const url = 'https://api.github.com/repos/' + repo + '/releases';
|
|
65
|
+
async function getLatestRelease(repo, mirror) {
|
|
66
|
+
const url = withMirror('https://api.github.com/repos/' + repo + '/releases', mirror);
|
|
70
67
|
const response = await HTTP_CLIENT.get(url);
|
|
71
68
|
|
|
72
69
|
const releases = response.data;
|
|
@@ -90,9 +87,9 @@ async function getLatestRelease(repo) {
|
|
|
90
87
|
return releases[0];
|
|
91
88
|
}
|
|
92
89
|
|
|
93
|
-
async function checkUpdate() {
|
|
90
|
+
async function checkUpdate(mirror) {
|
|
94
91
|
const currentVersion = config.getKernelVersion();
|
|
95
|
-
const latest = await getLatestRelease(GITHUB_REPO);
|
|
92
|
+
const latest = await getLatestRelease(GITHUB_REPO, mirror);
|
|
96
93
|
const latestVersion = latest.tag_name;
|
|
97
94
|
|
|
98
95
|
let needsUpdate = false;
|
|
@@ -114,6 +111,7 @@ async function checkUpdate() {
|
|
|
114
111
|
needsUpdate,
|
|
115
112
|
assets: latest.assets,
|
|
116
113
|
htmlUrl: latest.html_url,
|
|
114
|
+
release: latest,
|
|
117
115
|
};
|
|
118
116
|
}
|
|
119
117
|
|
|
@@ -141,12 +139,12 @@ function findBinaryInDir(dir) {
|
|
|
141
139
|
return null;
|
|
142
140
|
}
|
|
143
141
|
|
|
144
|
-
async function downloadKernel(progressCallback, mirror) {
|
|
142
|
+
async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
145
143
|
config.ensureDirs();
|
|
146
144
|
|
|
147
|
-
const latest = await getLatestRelease(GITHUB_REPO);
|
|
145
|
+
const latest = releaseInfo || (await getLatestRelease(GITHUB_REPO, mirror));
|
|
148
146
|
const arch = getArch();
|
|
149
|
-
const platform =
|
|
147
|
+
const platform = process.platform;
|
|
150
148
|
|
|
151
149
|
const asset = findMatchingAsset(latest.assets, platform, arch);
|
|
152
150
|
|
|
@@ -161,26 +159,28 @@ async function downloadKernel(progressCallback, mirror) {
|
|
|
161
159
|
|
|
162
160
|
const downloadUrl = withMirror(asset.browser_download_url, mirror);
|
|
163
161
|
const tempPath = path.join(config.DIRS.core, asset.name);
|
|
162
|
+
const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
|
|
164
163
|
|
|
165
164
|
if (progressCallback) {
|
|
166
|
-
const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
|
|
167
165
|
progressCallback('下载内核: ' + asset.name + ' (' + sizeMB + ' MB)');
|
|
168
166
|
}
|
|
169
167
|
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
});
|
|
168
|
+
const curlResult = spawnSync(
|
|
169
|
+
'curl',
|
|
170
|
+
['-L', '--progress-bar', '--connect-timeout', '30', '--max-time', String(Math.floor(KERNEL_DOWNLOAD_TIMEOUT / 1000)), '-o', tempPath, downloadUrl],
|
|
171
|
+
{ stdio: 'inherit' },
|
|
172
|
+
);
|
|
176
173
|
|
|
177
|
-
|
|
178
|
-
|
|
174
|
+
if (curlResult.status !== 0) {
|
|
175
|
+
try {
|
|
176
|
+
fs.unlinkSync(tempPath);
|
|
177
|
+
} catch {}
|
|
178
|
+
throw new Error('下载失败 (curl 退出码 ' + curlResult.status + ')');
|
|
179
|
+
}
|
|
179
180
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
});
|
|
181
|
+
if (!fs.existsSync(tempPath)) {
|
|
182
|
+
throw new Error('下载失败: 文件未生成');
|
|
183
|
+
}
|
|
184
184
|
|
|
185
185
|
if (progressCallback) {
|
|
186
186
|
progressCallback('解压内核...');
|
|
@@ -245,10 +245,6 @@ async function downloadKernel(progressCallback, mirror) {
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
module.exports = {
|
|
248
|
-
GITHUB_REPO,
|
|
249
|
-
KERNEL_HTTP_TIMEOUT,
|
|
250
|
-
KERNEL_MAX_CONTENT_LENGTH,
|
|
251
|
-
KERNEL_DOWNLOAD_TIMEOUT,
|
|
252
248
|
checkUpdate,
|
|
253
249
|
downloadKernel,
|
|
254
250
|
};
|
package/src/process.js
CHANGED
|
@@ -16,6 +16,7 @@ const PROCESS_WAIT_INTERVAL = 100; // ms
|
|
|
16
16
|
const STARTUP_WAIT_MS = 800; // ms
|
|
17
17
|
const SUDO_TIMEOUT_MS = 60000; // ms
|
|
18
18
|
const TUN_MODE_POST_WAIT_MS = 500; // ms
|
|
19
|
+
const BATCH_KILL_THRESHOLD = 3;
|
|
19
20
|
|
|
20
21
|
// 日志清理常量
|
|
21
22
|
const DEFAULT_LOG_RETENTION_DAYS = 7;
|
|
@@ -187,7 +188,7 @@ function cleanupAll(forceSudo) {
|
|
|
187
188
|
failedPids = pids;
|
|
188
189
|
}
|
|
189
190
|
} else {
|
|
190
|
-
if (pids.length >
|
|
191
|
+
if (pids.length > BATCH_KILL_THRESHOLD) {
|
|
191
192
|
killAllMihomo(false);
|
|
192
193
|
killedCount = pids.length;
|
|
193
194
|
} else {
|
|
@@ -322,6 +323,19 @@ async function start(mode) {
|
|
|
322
323
|
if (mode === undefined) mode = 'mixed';
|
|
323
324
|
const isTunMode = mode === 'tun';
|
|
324
325
|
|
|
326
|
+
config.ensureDirs();
|
|
327
|
+
rotateAndCleanupLogs();
|
|
328
|
+
|
|
329
|
+
const binary = config.PATHS.mihomoBinary;
|
|
330
|
+
if (!fs.existsSync(binary)) {
|
|
331
|
+
throw new Error('未找到 mihomo 内核,请先下载内核');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const configFile = config.PATHS.configFile;
|
|
335
|
+
if (!fs.existsSync(configFile)) {
|
|
336
|
+
throw new Error('未找到配置文件,请先添加订阅并启动');
|
|
337
|
+
}
|
|
338
|
+
|
|
325
339
|
const staleState = checkStaleState();
|
|
326
340
|
|
|
327
341
|
if (isTunMode) {
|
|
@@ -351,27 +365,15 @@ async function startMixedMode(staleState) {
|
|
|
351
365
|
return { success: true, pid, alreadyRunning: true };
|
|
352
366
|
}
|
|
353
367
|
|
|
354
|
-
config.ensureDirs();
|
|
355
|
-
rotateAndCleanupLogs();
|
|
356
|
-
|
|
357
|
-
const binary = config.PATHS.mihomoBinary;
|
|
358
|
-
if (!fs.existsSync(binary)) {
|
|
359
|
-
throw new Error('未找到 mihomo 内核,请先下载内核');
|
|
360
|
-
}
|
|
361
|
-
|
|
362
368
|
const configFile = config.PATHS.configFile;
|
|
363
369
|
const logFile = config.PATHS.logFile;
|
|
364
370
|
|
|
365
|
-
if (!fs.existsSync(configFile)) {
|
|
366
|
-
throw new Error('未找到配置文件,请先添加订阅并启动');
|
|
367
|
-
}
|
|
368
|
-
|
|
369
371
|
const args = ['-d', config.DIRS.data, '-f', configFile];
|
|
370
372
|
|
|
371
373
|
const out = fs.openSync(logFile, 'a');
|
|
372
374
|
const err = fs.openSync(logFile, 'a');
|
|
373
375
|
|
|
374
|
-
const child = spawn(
|
|
376
|
+
const child = spawn(config.PATHS.mihomoBinary, args, {
|
|
375
377
|
detached: true,
|
|
376
378
|
stdio: ['ignore', out, err],
|
|
377
379
|
cwd: config.PATHS.root,
|
|
@@ -407,20 +409,6 @@ async function startMixedMode(staleState) {
|
|
|
407
409
|
}
|
|
408
410
|
|
|
409
411
|
async function startTunMode(staleState) {
|
|
410
|
-
config.ensureDirs();
|
|
411
|
-
rotateAndCleanupLogs();
|
|
412
|
-
|
|
413
|
-
const binary = config.PATHS.mihomoBinary;
|
|
414
|
-
if (!fs.existsSync(binary)) {
|
|
415
|
-
throw new Error('未找到 mihomo 内核,请先下载内核');
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const configFile = config.PATHS.configFile;
|
|
419
|
-
|
|
420
|
-
if (!fs.existsSync(configFile)) {
|
|
421
|
-
throw new Error('未找到配置文件,请先添加订阅并启动');
|
|
422
|
-
}
|
|
423
|
-
|
|
424
412
|
const launchScript = createTunLaunchScript();
|
|
425
413
|
|
|
426
414
|
if (staleState.needsCleanup) {
|
package/src/utils.js
CHANGED
|
@@ -149,33 +149,39 @@ function normalizeMirrorUrl(val) {
|
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
151
|
* 解析镜像参数(从 index.js 移入)
|
|
152
|
-
* 返回: { mirror: 镜像URL|null, isOverride: boolean }
|
|
153
|
-
* mirror = null
|
|
152
|
+
* 返回: { mirror: 镜像URL|null, isOverride: boolean, type: 'download'|'all' }
|
|
153
|
+
* mirror = null 表示禁用镜像(直连)
|
|
154
154
|
* mirror = undefined 表示使用默认/配置
|
|
155
|
+
* type: download=仅下载用镜像, all=API和下载都用镜像
|
|
155
156
|
*/
|
|
156
157
|
function parseMirrorArg(args) {
|
|
157
158
|
if (!args || args.length < 2) {
|
|
158
|
-
return { mirror:
|
|
159
|
+
return { mirror: null, isOverride: false, type: 'download' };
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
if (args.includes('--no-mirror') || args.includes('--direct')) {
|
|
162
|
-
return { mirror: null, isOverride: true };
|
|
163
|
+
return { mirror: null, isOverride: true, type: 'download' };
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
const
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
const mirrorAllIdx = args.indexOf('--mirror-all');
|
|
167
|
+
if (mirrorAllIdx >= 0) {
|
|
168
|
+
const nextArg = args[mirrorAllIdx + 1];
|
|
169
|
+
if (!nextArg || nextArg.startsWith('-')) {
|
|
170
|
+
return { mirror: 'https://v6.gh-proxy.org/', isOverride: true, type: 'all' };
|
|
171
|
+
}
|
|
172
|
+
return { mirror: normalizeMirrorUrl(nextArg), isOverride: true, type: 'all' };
|
|
169
173
|
}
|
|
170
174
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
const mirrorIdx = args.indexOf('--mirror');
|
|
176
|
+
if (mirrorIdx >= 0) {
|
|
177
|
+
const nextArg = args[mirrorIdx + 1];
|
|
178
|
+
if (!nextArg || nextArg.startsWith('-')) {
|
|
179
|
+
return { mirror: 'https://v6.gh-proxy.org/', isOverride: true, type: 'download' };
|
|
175
180
|
}
|
|
181
|
+
return { mirror: normalizeMirrorUrl(nextArg), isOverride: true, type: 'download' };
|
|
176
182
|
}
|
|
177
183
|
|
|
178
|
-
return { mirror:
|
|
184
|
+
return { mirror: null, isOverride: false, type: 'download' };
|
|
179
185
|
}
|
|
180
186
|
|
|
181
187
|
module.exports = {
|