mihomo-cli 1.2.2 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +44 -42
- package/index.js +119 -120
- package/package.json +1 -1
- package/src/config.js +31 -34
- package/src/kernel.js +16 -12
- package/src/process.js +45 -61
- package/src/subscription.js +10 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.3] - 2026-04-07
|
|
4
|
+
|
|
5
|
+
### 优化
|
|
6
|
+
|
|
7
|
+
- **简短帮助**:大幅精简,只列出最常用命令,增加 `mihomo help` 提示
|
|
8
|
+
- **性能**:
|
|
9
|
+
- settings 读取增加内存缓存,减少重复 JSON 解析
|
|
10
|
+
- 内核版本增加缓存,避免重复执行 `mihomo -v`
|
|
11
|
+
- `sleepSync()` 改用 `Atomics.wait` 而非 `execSync('sleep')`,减少子进程开销
|
|
12
|
+
|
|
13
|
+
### 重构
|
|
14
|
+
|
|
15
|
+
- 精简模块导出接口,移除不必要的内部函数导出
|
|
16
|
+
- 新增 `formatProxySummary()` 复用函数(消除 3 处重复代码)
|
|
17
|
+
- `pickSingleSubscription()` 移除多余参数
|
|
18
|
+
- 统一代码风格:数组/条件表达式换行风格、`cmdUi` → `cmdUI` 命名一致性
|
|
19
|
+
|
|
20
|
+
### 文档
|
|
21
|
+
|
|
22
|
+
- README 修复:GitHub 链接、重复段落、`profile-update-interval` 格式
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
3
26
|
## [1.2.2] - 2026-04-05
|
|
4
27
|
|
|
5
28
|
### 优化
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ npm install -g mihomo-cli
|
|
|
28
28
|
### 方式二:源码安装
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
git clone https://github.com/
|
|
31
|
+
git clone https://github.com/adaex/mihomo-cli.git
|
|
32
32
|
cd mihomo-cli
|
|
33
33
|
npm install
|
|
34
34
|
npm link
|
|
@@ -79,49 +79,50 @@ mihomo ui yacd # YACD
|
|
|
79
79
|
|
|
80
80
|
### 核心命令
|
|
81
81
|
|
|
82
|
-
| 命令
|
|
83
|
-
|
|
84
|
-
| `mihomo start [tun\|mixed]` | 启动/重启/切换代理模式
|
|
85
|
-
| `mihomo stop`
|
|
86
|
-
| `mihomo status`
|
|
87
|
-
| `mihomo log`
|
|
88
|
-
| `mihomo logs`
|
|
89
|
-
| `mihomo logs <编号>`
|
|
82
|
+
| 命令 | 说明 |
|
|
83
|
+
| --------------------------- | ---------------------------------------------------------------------------- |
|
|
84
|
+
| `mihomo start [tun\|mixed]` | 启动/重启/切换代理模式 |
|
|
85
|
+
| `mihomo stop` | 停止代理 |
|
|
86
|
+
| `mihomo status` | 查看运行状态 |
|
|
87
|
+
| `mihomo log` | 实时查看日志 (`-o` 用系统编辑器打开) |
|
|
88
|
+
| `mihomo logs` | 列出所有日志(当前 + 历史归档) |
|
|
89
|
+
| `mihomo logs <编号>` | 查看指定日志(`0`=当前日志,`1+`=归档日志,支持 `-n N` 指定行数、`-o` 打开) |
|
|
90
90
|
|
|
91
91
|
### 订阅管理
|
|
92
92
|
|
|
93
|
-
| 命令
|
|
94
|
-
|
|
95
|
-
| `mihomo sub
|
|
96
|
-
| `mihomo sub add <url> [name]` | 添加订阅
|
|
97
|
-
| `mihomo sub update`
|
|
98
|
-
| `mihomo sub update <name>`
|
|
99
|
-
| `mihomo sub use <name>`
|
|
100
|
-
| `mihomo sub web [name]`
|
|
93
|
+
| 命令 | 说明 |
|
|
94
|
+
| ----------------------------- | -------------------------------------- |
|
|
95
|
+
| `mihomo sub` | 列出所有订阅(含流量、到期时间) |
|
|
96
|
+
| `mihomo sub add <url> [name]` | 添加订阅 |
|
|
97
|
+
| `mihomo sub update` | 更新所有订阅 |
|
|
98
|
+
| `mihomo sub update <name>` | 更新指定订阅(支持模糊匹配) |
|
|
99
|
+
| `mihomo sub use <name>` | 设置默认订阅(支持模糊匹配,自动重启) |
|
|
100
|
+
| `mihomo sub web [name]` | 打开订阅页面(无参打开默认) |
|
|
101
101
|
|
|
102
102
|
### 覆写配置
|
|
103
103
|
|
|
104
|
-
| 命令
|
|
105
|
-
|
|
104
|
+
| 命令 | 说明 |
|
|
105
|
+
| ------------------------------ | -------------------------- |
|
|
106
106
|
| `mihomo ow` / `mihomo ow list` | 查看覆写配置状态和文件列表 |
|
|
107
|
-
| `mihomo ow on`
|
|
108
|
-
| `mihomo ow off`
|
|
107
|
+
| `mihomo ow on` | 启用覆写配置(自动重启) |
|
|
108
|
+
| `mihomo ow off` | 禁用覆写配置(自动重启) |
|
|
109
109
|
|
|
110
110
|
### 其他命令
|
|
111
111
|
|
|
112
|
-
| 命令
|
|
113
|
-
|
|
114
|
-
| `mihomo kernel [镜像\|--no-mirror]` | 更新内核
|
|
115
|
-
| `mihomo ui [zash\|dash\|yacd]`
|
|
116
|
-
| `mihomo dir`
|
|
117
|
-
| `mihomo dir open [target]`
|
|
118
|
-
| `mihomo reset [--full]`
|
|
119
|
-
| `mihomo version`
|
|
120
|
-
| `mihomo help`
|
|
112
|
+
| 命令 | 说明 |
|
|
113
|
+
| ----------------------------------- | ------------------------------------------------------- |
|
|
114
|
+
| `mihomo kernel [镜像\|--no-mirror]` | 更新内核 |
|
|
115
|
+
| `mihomo ui [zash\|dash\|yacd]` | 打开 Web UI |
|
|
116
|
+
| `mihomo dir` | 显示数据目录位置 |
|
|
117
|
+
| `mihomo dir open [target]` | 打开指定目录(`root`, `subs`, `logs`, `overwrites` 等) |
|
|
118
|
+
| `mihomo reset [--full]` | 重置用户数据 (--full 同时删除内核) |
|
|
119
|
+
| `mihomo version` | 显示版本信息 |
|
|
120
|
+
| `mihomo help` | 显示帮助信息 |
|
|
121
121
|
|
|
122
122
|
### 命令别名
|
|
123
123
|
|
|
124
124
|
以下任意命令等效:
|
|
125
|
+
|
|
125
126
|
- `mihomo-cli` (原名)
|
|
126
127
|
- `mihomo`
|
|
127
128
|
- `mmc`
|
|
@@ -204,12 +205,12 @@ mihomo kernel --no-mirror
|
|
|
204
205
|
|
|
205
206
|
覆写配置支持以下特殊操作符:
|
|
206
207
|
|
|
207
|
-
| 语法
|
|
208
|
-
|
|
209
|
-
| `key!`
|
|
210
|
-
| `+key`
|
|
211
|
-
| `key+`
|
|
212
|
-
| `<+key>` | 键名以 `+` 开头时转义
|
|
208
|
+
| 语法 | 作用 | 示例 |
|
|
209
|
+
| -------- | ------------------------------ | -------------------- |
|
|
210
|
+
| `key!` | 强制覆盖整个对象(不深度合并) | `dns!`: { ... } |
|
|
211
|
+
| `+key` | 数组前置插入 | `+proxies`: [...] |
|
|
212
|
+
| `key+` | 数组追加 | `rules+`: [...] |
|
|
213
|
+
| `<+key>` | 键名以 `+` 开头时转义 | `<+.google.cn>`: ... |
|
|
213
214
|
|
|
214
215
|
### 示例
|
|
215
216
|
|
|
@@ -225,18 +226,18 @@ dns!:
|
|
|
225
226
|
|
|
226
227
|
# 追加规则
|
|
227
228
|
rules+:
|
|
228
|
-
-
|
|
229
|
+
- 'DOMAIN-SUFFIX,example.com,DIRECT'
|
|
229
230
|
```
|
|
230
231
|
|
|
231
232
|
## Web UI
|
|
232
233
|
|
|
233
234
|
内置三个常用 Web UI:
|
|
234
235
|
|
|
235
|
-
| 名称 | 地址
|
|
236
|
-
|
|
237
|
-
| zash | https://board.zash.run.place
|
|
238
|
-
| dash | https://metacubex.github.io/metacubexd | MetaCubeX 官方 UI
|
|
239
|
-
| yacd | https://yacd.metacubex.one
|
|
236
|
+
| 名称 | 地址 | 说明 |
|
|
237
|
+
| ---- | ---------------------------------------- | -------------------- |
|
|
238
|
+
| zash | <https://board.zash.run.place> | 现代简洁界面(默认) |
|
|
239
|
+
| dash | <https://metacubex.github.io/metacubexd> | MetaCubeX 官方 UI |
|
|
240
|
+
| yacd | <https://yacd.metacubex.one> | 经典 YACD 界面 |
|
|
240
241
|
|
|
241
242
|
## 故障排除
|
|
242
243
|
|
|
@@ -261,6 +262,7 @@ sudo pkill -9 mihomo
|
|
|
261
262
|
### 端口被占用
|
|
262
263
|
|
|
263
264
|
默认端口(取决于订阅配置):
|
|
265
|
+
|
|
264
266
|
- Mixed 端口: `7890`
|
|
265
267
|
- 外部控制器: `127.0.0.1:9090`
|
|
266
268
|
|
package/index.js
CHANGED
|
@@ -33,7 +33,7 @@ process.on('SIGTERM', () => {
|
|
|
33
33
|
process.exit(0);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
process.on('uncaughtException',
|
|
36
|
+
process.on('uncaughtException', e => {
|
|
37
37
|
console.error('\n未捕获的异常: ' + e.message);
|
|
38
38
|
if (e.stack) {
|
|
39
39
|
console.error(e.stack.split('\n').slice(1).join('\n'));
|
|
@@ -41,80 +41,79 @@ process.on('uncaughtException', (e) => {
|
|
|
41
41
|
process.exit(1);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
process.on('unhandledRejection',
|
|
44
|
+
process.on('unhandledRejection', reason => {
|
|
45
45
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
46
46
|
console.error('\n未处理的 Promise 拒绝: ' + msg);
|
|
47
47
|
process.exit(1);
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
function printShortHelp() {
|
|
51
|
-
console.log('\nmihomo-cli v' + VERSION);
|
|
52
|
-
console.log(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
' logs 日志列表\n' +
|
|
60
|
-
' subscription 订阅管理(别名 sub)\n' +
|
|
61
|
-
' overwrite [on|off] 覆写配置(别名 ow)\n' +
|
|
62
|
-
' directory 数据目录(别名 dir)\n' +
|
|
63
|
-
' kernel 更新内核\n' +
|
|
64
|
-
' reset 重置配置\n' +
|
|
65
|
-
' version 版本信息\n');
|
|
51
|
+
console.log('\nmihomo-cli v' + VERSION + ' (mihomo help 查看完整帮助)\n');
|
|
52
|
+
console.log(
|
|
53
|
+
'常用命令:\n' +
|
|
54
|
+
' start [tun|mixed] 启动/切换代理\n' +
|
|
55
|
+
' ui [zash|dash|yacd] 打开 Web UI\n' +
|
|
56
|
+
' ow [on|off] 覆写配置\n' +
|
|
57
|
+
' sub [use|update] 订阅管理\n',
|
|
58
|
+
);
|
|
66
59
|
}
|
|
67
60
|
|
|
68
61
|
function printHelp() {
|
|
69
|
-
console.log(
|
|
70
|
-
'\
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
62
|
+
console.log(
|
|
63
|
+
'\nmihomo-cli v' +
|
|
64
|
+
VERSION +
|
|
65
|
+
'\n' +
|
|
66
|
+
'\n' +
|
|
67
|
+
'命令别名: mihomo, mmc, mh\n' +
|
|
68
|
+
'\n' +
|
|
69
|
+
'用法:\n' +
|
|
70
|
+
' mihomo <命令> [选项]\n' +
|
|
71
|
+
'\n' +
|
|
72
|
+
'控制:\n' +
|
|
73
|
+
' start [tun|mixed] 启动/切换代理 (默认 mixed)\n' +
|
|
74
|
+
' stop 停止代理\n' +
|
|
75
|
+
' status 查看状态\n' +
|
|
76
|
+
'\n' +
|
|
77
|
+
'界面:\n' +
|
|
78
|
+
' ui [zash|dash|yacd] 打开 Web UI (默认 zash)\n' +
|
|
79
|
+
' log [-o] 实时日志(-o 打开文件)\n' +
|
|
80
|
+
' logs [编号] [-n N] [-o] 日志列表(0=当前,1+=归档)\n' +
|
|
81
|
+
'\n' +
|
|
82
|
+
'订阅:\n' +
|
|
83
|
+
' subscription 列出所有订阅(别名 sub)\n' +
|
|
84
|
+
' subscription add <url> [name] 添加订阅\n' +
|
|
85
|
+
' subscription update [name] 更新订阅(无参更新所有)\n' +
|
|
86
|
+
' subscription use <name> 切换默认订阅\n' +
|
|
87
|
+
' subscription web [name] 打开订阅页面\n' +
|
|
88
|
+
'\n' +
|
|
89
|
+
'配置:\n' +
|
|
90
|
+
' overwrite 查看覆写状态(别名 ow)\n' +
|
|
91
|
+
' overwrite on|off 启用/禁用覆写配置\n' +
|
|
92
|
+
' directory 显示数据目录位置(别名 dir)\n' +
|
|
93
|
+
' directory open [target] 打开目录: root|subs|logs|overwrites|...\n' +
|
|
94
|
+
'\n' +
|
|
95
|
+
'系统:\n' +
|
|
96
|
+
' kernel [镜像|--no-mirror] 更新内核\n' +
|
|
97
|
+
' reset [--full] 重置用户数据 (--full 同时删除内核)\n' +
|
|
98
|
+
' help, -h 显示帮助\n' +
|
|
99
|
+
' version, -v 显示版本\n' +
|
|
100
|
+
'\n' +
|
|
101
|
+
'示例:\n' +
|
|
102
|
+
' mihomo start # 启动/重启 Mixed 模式\n' +
|
|
103
|
+
' mihomo start tun # 切换到 TUN 透明代理模式\n' +
|
|
104
|
+
' mihomo sub add <url> # 添加订阅 (sub 是 subscription 别名)\n' +
|
|
105
|
+
' mihomo ui # 打开 Web UI\n' +
|
|
106
|
+
'\n' +
|
|
107
|
+
'模式说明:\n' +
|
|
108
|
+
' mixed HTTP + SOCKS5 混合端口 (默认)\n' +
|
|
109
|
+
' tun 透明代理,全局自动路由,需要 sudo\n' +
|
|
110
|
+
'\n' +
|
|
111
|
+
'数据目录:\n' +
|
|
112
|
+
' 环境变量 MIHOMO_CLI_DIR 可自定义位置\n' +
|
|
113
|
+
' 默认: ' +
|
|
114
|
+
config.USER_DATA_DIR +
|
|
115
|
+
'\n',
|
|
116
|
+
);
|
|
118
117
|
}
|
|
119
118
|
|
|
120
119
|
function printVersion() {
|
|
@@ -126,7 +125,7 @@ function printVersion() {
|
|
|
126
125
|
|
|
127
126
|
function printStatus() {
|
|
128
127
|
const status = processMgr.getStatus();
|
|
129
|
-
const info =
|
|
128
|
+
const info = config.getConfigInfo();
|
|
130
129
|
const owEnabled = overwrite.isOverwriteEnabled();
|
|
131
130
|
const owFiles = overwrite.listOverwriteFiles().files;
|
|
132
131
|
const activeSub = getActiveSubscription();
|
|
@@ -160,12 +159,7 @@ function printStatus() {
|
|
|
160
159
|
if (activeSub) {
|
|
161
160
|
let subLine = '订阅: ' + activeSub.name;
|
|
162
161
|
if (info) {
|
|
163
|
-
|
|
164
|
-
if (info.proxyGroups && info.proxyGroups > 0) {
|
|
165
|
-
parts.push(info.proxyGroups + ' 组');
|
|
166
|
-
}
|
|
167
|
-
parts.push(info.proxies + ' 节点');
|
|
168
|
-
subLine += ' (' + parts.join(', ') + ')';
|
|
162
|
+
subLine += ' (' + subscription.formatProxySummary(info) + ')';
|
|
169
163
|
}
|
|
170
164
|
console.log(subLine);
|
|
171
165
|
} else {
|
|
@@ -213,7 +207,7 @@ function findSubscriptionFuzzy(subs, pattern) {
|
|
|
213
207
|
return includes;
|
|
214
208
|
}
|
|
215
209
|
|
|
216
|
-
function pickSingleSubscription(subs, pattern
|
|
210
|
+
function pickSingleSubscription(subs, pattern) {
|
|
217
211
|
if (subs.length === 0) {
|
|
218
212
|
console.error('错误: 未找到匹配 "' + pattern + '" 的订阅');
|
|
219
213
|
process.exit(1);
|
|
@@ -291,7 +285,7 @@ function viewLogWithTail(logPath, options) {
|
|
|
291
285
|
const tail = spawn('tail', tailArgs, { stdio: 'inherit' });
|
|
292
286
|
|
|
293
287
|
tail.on('close', () => process.exit(0));
|
|
294
|
-
tail.on('error',
|
|
288
|
+
tail.on('error', e => {
|
|
295
289
|
console.error('无法读取日志: ' + e.message);
|
|
296
290
|
process.exit(1);
|
|
297
291
|
});
|
|
@@ -344,10 +338,7 @@ async function cmdStart(args) {
|
|
|
344
338
|
}
|
|
345
339
|
|
|
346
340
|
const modeLabel = targetMode === 'tun' ? 'TUN' : 'Mixed';
|
|
347
|
-
|
|
348
|
-
if (cfgInfo.proxyGroups && cfgInfo.proxyGroups > 0) parts.push(cfgInfo.proxyGroups + ' 组');
|
|
349
|
-
parts.push(cfgInfo.proxies + ' 节点');
|
|
350
|
-
console.log([modeLabel, sub.name, parts.join(', ')].join(' · '));
|
|
341
|
+
console.log([modeLabel, sub.name, subscription.formatProxySummary(cfgInfo)].join(' · '));
|
|
351
342
|
|
|
352
343
|
try {
|
|
353
344
|
const result = await processMgr.start(targetMode);
|
|
@@ -377,7 +368,7 @@ async function cmdStop() {
|
|
|
377
368
|
console.log('已停止');
|
|
378
369
|
}
|
|
379
370
|
|
|
380
|
-
function
|
|
371
|
+
function cmdUI(args) {
|
|
381
372
|
const uiName = args[1] || 'zash';
|
|
382
373
|
const url = UI_URLS[uiName];
|
|
383
374
|
|
|
@@ -396,7 +387,6 @@ function cmdUi(args) {
|
|
|
396
387
|
}
|
|
397
388
|
}
|
|
398
389
|
|
|
399
|
-
|
|
400
390
|
function cmdLog(args) {
|
|
401
391
|
const logPath = processMgr.getLogPath();
|
|
402
392
|
|
|
@@ -419,7 +409,20 @@ function cmdLogs(args) {
|
|
|
419
409
|
if (targetName === 'current' || targetName === '0') {
|
|
420
410
|
logPath = processMgr.getLogPath();
|
|
421
411
|
} else {
|
|
422
|
-
|
|
412
|
+
// 纯数字 1+ 表示归档日志的位置(最新=1)
|
|
413
|
+
const parsedIdx = parseInt(targetName);
|
|
414
|
+
if (!isNaN(parsedIdx) && parsedIdx > 0 && String(parsedIdx) === targetName) {
|
|
415
|
+
const archiveLogs = processMgr.listLogs();
|
|
416
|
+
const archive = archiveLogs.archives[parsedIdx - 1];
|
|
417
|
+
if (!archive) {
|
|
418
|
+
console.error('错误: 未找到日志 "' + targetName + '"');
|
|
419
|
+
console.log('使用 "mihomo logs" 查看可用日志列表');
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
logPath = archive.path;
|
|
423
|
+
} else {
|
|
424
|
+
logPath = processMgr.getLogPathByName(targetName);
|
|
425
|
+
}
|
|
423
426
|
}
|
|
424
427
|
|
|
425
428
|
if (!logPath) {
|
|
@@ -454,8 +457,15 @@ function cmdLogs(args) {
|
|
|
454
457
|
console.log('日志列表:');
|
|
455
458
|
console.log('');
|
|
456
459
|
|
|
457
|
-
|
|
458
|
-
|
|
460
|
+
let archiveCounter = 0;
|
|
461
|
+
all.forEach(log => {
|
|
462
|
+
let num;
|
|
463
|
+
if (log.isCurrent) {
|
|
464
|
+
num = ' 0';
|
|
465
|
+
} else {
|
|
466
|
+
archiveCounter++;
|
|
467
|
+
num = archiveCounter < 10 ? ' ' + archiveCounter : '' + archiveCounter;
|
|
468
|
+
}
|
|
459
469
|
const time = subscription.formatDate(log.mtime);
|
|
460
470
|
const size = subscription.formatBytes(log.size);
|
|
461
471
|
const name = log.isCurrent ? 'mihomo.log (当前运行中)' : log.name;
|
|
@@ -463,16 +473,16 @@ function cmdLogs(args) {
|
|
|
463
473
|
console.log(' ' + num + '. ' + name);
|
|
464
474
|
console.log(' 时间: ' + time + ' 大小: ' + size);
|
|
465
475
|
if (!log.isCurrent) {
|
|
466
|
-
console.log(' 查看: mihomo logs ' +
|
|
476
|
+
console.log(' 查看: mihomo logs ' + archiveCounter + ' 或 mihomo logs ' + archiveCounter + ' -o');
|
|
467
477
|
}
|
|
468
478
|
console.log('');
|
|
469
479
|
});
|
|
470
480
|
|
|
471
481
|
console.log('用法:');
|
|
472
482
|
console.log(' mihomo logs 0 # 查看当前日志 (最后 100 行)');
|
|
473
|
-
console.log(' mihomo logs 1 # 查看第 1
|
|
483
|
+
console.log(' mihomo logs 1 # 查看第 1 个归档日志(最新)');
|
|
474
484
|
console.log(' mihomo logs 1 -n 200 # 查看 200 行');
|
|
475
|
-
console.log(' mihomo logs 1 -o
|
|
485
|
+
console.log(' mihomo logs 1 -o # 用系统默认程序打开');
|
|
476
486
|
console.log('');
|
|
477
487
|
}
|
|
478
488
|
|
|
@@ -542,11 +552,8 @@ async function cmdKernel(args) {
|
|
|
542
552
|
|
|
543
553
|
console.log('\n可用镜像:');
|
|
544
554
|
config.AVAILABLE_MIRRORS.forEach(m => {
|
|
545
|
-
const isCurrent =
|
|
546
|
-
effectiveMirror.includes('//' + m + '/') ||
|
|
547
|
-
effectiveMirror.includes('//' + m + ':') ||
|
|
548
|
-
effectiveMirror.endsWith('//' + m)
|
|
549
|
-
);
|
|
555
|
+
const isCurrent =
|
|
556
|
+
effectiveMirror && (effectiveMirror.includes('//' + m + '/') || effectiveMirror.includes('//' + m + ':') || effectiveMirror.endsWith('//' + m));
|
|
550
557
|
console.log(' ' + m + (isCurrent ? ' (当前)' : ''));
|
|
551
558
|
});
|
|
552
559
|
|
|
@@ -568,9 +575,9 @@ async function cmdKernel(args) {
|
|
|
568
575
|
}
|
|
569
576
|
|
|
570
577
|
console.log('\n正在下载...');
|
|
571
|
-
const result = await kernel.downloadKernel(
|
|
578
|
+
const result = await kernel.downloadKernel(msg => {
|
|
572
579
|
console.log(msg);
|
|
573
|
-
}, mirrorInfo.mirror);
|
|
580
|
+
}, mirrorInfo.mirror); // 传递镜像参数(undefined = 用配置,null = 禁用)
|
|
574
581
|
console.log('已更新到 ' + result.version);
|
|
575
582
|
} catch (e) {
|
|
576
583
|
console.error('更新失败: ' + e.message);
|
|
@@ -625,6 +632,7 @@ async function printSubscriptionList() {
|
|
|
625
632
|
console.log('切换默认: mihomo sub use <name>');
|
|
626
633
|
console.log('更新订阅: mihomo sub update [name]');
|
|
627
634
|
console.log('打开页面: mihomo sub web [name]');
|
|
635
|
+
console.log('新增订阅: mihomo sub add <url> [name]');
|
|
628
636
|
console.log('');
|
|
629
637
|
}
|
|
630
638
|
|
|
@@ -649,10 +657,7 @@ async function cmdSubscription(args) {
|
|
|
649
657
|
try {
|
|
650
658
|
config.addSubscription(url, name);
|
|
651
659
|
const info = await subscription.downloadSubscription(url, name);
|
|
652
|
-
|
|
653
|
-
if (info.proxyGroups && info.proxyGroups > 0) parts.push(info.proxyGroups + ' 组');
|
|
654
|
-
parts.push(info.proxies + ' 节点');
|
|
655
|
-
console.log('已添加 (' + parts.join(', ') + ')');
|
|
660
|
+
console.log('已添加 (' + subscription.formatProxySummary(info) + ')');
|
|
656
661
|
} catch (e) {
|
|
657
662
|
console.error('添加失败: ' + e.message);
|
|
658
663
|
process.exit(1);
|
|
@@ -678,10 +683,7 @@ async function cmdSubscription(args) {
|
|
|
678
683
|
results.forEach(r => {
|
|
679
684
|
if (r.success) {
|
|
680
685
|
ok++;
|
|
681
|
-
|
|
682
|
-
if (r.proxyGroups && r.proxyGroups > 0) parts.push(r.proxyGroups + ' 组');
|
|
683
|
-
parts.push(r.proxies + ' 节点');
|
|
684
|
-
console.log('✓ ' + r.name + ': 已更新 (' + parts.join(', ') + ')');
|
|
686
|
+
console.log('✓ ' + r.name + ': 已更新 (' + subscription.formatProxySummary(r) + ')');
|
|
685
687
|
} else {
|
|
686
688
|
console.log('✗ ' + r.name + ': 失败 (' + r.error.split('\n')[0] + ')');
|
|
687
689
|
}
|
|
@@ -693,15 +695,12 @@ async function cmdSubscription(args) {
|
|
|
693
695
|
}
|
|
694
696
|
|
|
695
697
|
const matches = findSubscriptionFuzzy(subs, name);
|
|
696
|
-
const target = pickSingleSubscription(matches, name
|
|
698
|
+
const target = pickSingleSubscription(matches, name);
|
|
697
699
|
|
|
698
700
|
console.log('更新订阅: ' + target.name);
|
|
699
701
|
try {
|
|
700
702
|
const info = await subscription.downloadSubscription(target.url, target.name);
|
|
701
|
-
|
|
702
|
-
if (info.proxyGroups && info.proxyGroups > 0) parts.push(info.proxyGroups + ' 组');
|
|
703
|
-
parts.push(info.proxies + ' 节点');
|
|
704
|
-
console.log('已更新 (' + parts.join(', ') + ')');
|
|
703
|
+
console.log('已更新 (' + subscription.formatProxySummary(info) + ')');
|
|
705
704
|
} catch (e) {
|
|
706
705
|
console.error('更新失败: ' + e.message);
|
|
707
706
|
process.exit(1);
|
|
@@ -725,7 +724,7 @@ async function cmdSubscription(args) {
|
|
|
725
724
|
}
|
|
726
725
|
|
|
727
726
|
const matches = findSubscriptionFuzzy(subs, name);
|
|
728
|
-
const target = pickSingleSubscription(matches, name
|
|
727
|
+
const target = pickSingleSubscription(matches, name);
|
|
729
728
|
|
|
730
729
|
// 检查是否已是当前默认订阅
|
|
731
730
|
const currentDefault = getActiveSubscription();
|
|
@@ -775,7 +774,7 @@ async function cmdSubscription(args) {
|
|
|
775
774
|
let target;
|
|
776
775
|
if (name) {
|
|
777
776
|
const matches = findSubscriptionFuzzy(subs, name);
|
|
778
|
-
target = pickSingleSubscription(matches, name
|
|
777
|
+
target = pickSingleSubscription(matches, name);
|
|
779
778
|
} else {
|
|
780
779
|
target = subs[0];
|
|
781
780
|
}
|
|
@@ -833,11 +832,11 @@ async function cmdReset(args) {
|
|
|
833
832
|
const readline = require('readline');
|
|
834
833
|
const rl = readline.createInterface({
|
|
835
834
|
input: process.stdin,
|
|
836
|
-
output: process.stdout
|
|
835
|
+
output: process.stdout,
|
|
837
836
|
});
|
|
838
837
|
|
|
839
838
|
const answer = await new Promise(resolve => {
|
|
840
|
-
rl.question('确认? (y/N) ',
|
|
839
|
+
rl.question('确认? (y/N) ', a => {
|
|
841
840
|
rl.close();
|
|
842
841
|
resolve(a);
|
|
843
842
|
});
|
|
@@ -943,14 +942,14 @@ async function cmdOverwrite(args) {
|
|
|
943
942
|
|
|
944
943
|
// 目录目标映射(精确匹配)
|
|
945
944
|
const DIRECTORY_TARGETS = {
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
945
|
+
root: { path: null, label: '根目录' },
|
|
946
|
+
subs: { path: config.DIRS.subscriptions, label: '订阅目录' },
|
|
947
|
+
logs: { path: config.DIRS.logs, label: '日志目录' },
|
|
948
|
+
data: { path: config.DIRS.data, label: 'mihomo 数据目录' },
|
|
949
|
+
runtime: { path: config.DIRS.runtime, label: '运行时目录' },
|
|
950
|
+
overwrites: { path: config.DIRS.overwrites, label: '覆写目录' },
|
|
951
|
+
settings: { path: config.PATHS.settingsFile, label: '设置文件' },
|
|
952
|
+
kernel: { path: config.DIRS.core, label: '内核目录' },
|
|
954
953
|
};
|
|
955
954
|
|
|
956
955
|
function cmdDirectory(args) {
|
|
@@ -1055,7 +1054,7 @@ async function main() {
|
|
|
1055
1054
|
cmdLogs(args);
|
|
1056
1055
|
break;
|
|
1057
1056
|
case 'ui':
|
|
1058
|
-
|
|
1057
|
+
cmdUI(args);
|
|
1059
1058
|
break;
|
|
1060
1059
|
case 'kernel':
|
|
1061
1060
|
await cmdKernel(args);
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -34,8 +34,6 @@ const DIRS = {
|
|
|
34
34
|
|
|
35
35
|
const PATHS = {
|
|
36
36
|
root: DIRS.root,
|
|
37
|
-
data: DIRS.data,
|
|
38
|
-
userDataDir: USER_DATA_DIR,
|
|
39
37
|
mihomoBinary: path.join(DIRS.core, 'mihomo'),
|
|
40
38
|
settingsFile: path.join(USER_DATA_DIR, 'settings.json'),
|
|
41
39
|
subscriptionsCacheFile: path.join(DIRS.subscriptions, 'cache.json'),
|
|
@@ -77,36 +75,41 @@ function maskUrl(url) {
|
|
|
77
75
|
}
|
|
78
76
|
}
|
|
79
77
|
|
|
78
|
+
let _settingsCache = null;
|
|
79
|
+
|
|
80
80
|
function readSettings() {
|
|
81
|
+
if (_settingsCache !== null) return _settingsCache;
|
|
81
82
|
ensureDirs();
|
|
82
83
|
if (fs.existsSync(PATHS.settingsFile)) {
|
|
83
84
|
try {
|
|
84
85
|
const content = fs.readFileSync(PATHS.settingsFile, 'utf8');
|
|
85
|
-
|
|
86
|
+
_settingsCache = JSON.parse(content);
|
|
87
|
+
return _settingsCache;
|
|
86
88
|
} catch (e) {
|
|
87
|
-
|
|
89
|
+
_settingsCache = {};
|
|
90
|
+
return _settingsCache;
|
|
88
91
|
}
|
|
89
92
|
}
|
|
90
|
-
|
|
93
|
+
_settingsCache = {};
|
|
94
|
+
return _settingsCache;
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
function writeSettings(settings) {
|
|
94
98
|
ensureDirs();
|
|
95
99
|
const existing = readSettings();
|
|
96
100
|
const merged = { ...existing, ...settings };
|
|
101
|
+
// undefined 值表示删除该键
|
|
102
|
+
for (const key of Object.keys(settings)) {
|
|
103
|
+
if (settings[key] === undefined) delete merged[key];
|
|
104
|
+
}
|
|
97
105
|
fs.writeFileSync(PATHS.settingsFile, JSON.stringify(merged, null, 2), { mode: 0o600 });
|
|
106
|
+
_settingsCache = merged;
|
|
98
107
|
return merged;
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
// GitHub 镜像配置
|
|
102
111
|
const DEFAULT_GITHUB_MIRROR = 'https://v6.gh-proxy.org/';
|
|
103
|
-
const AVAILABLE_MIRRORS = [
|
|
104
|
-
'v6.gh-proxy.org',
|
|
105
|
-
'gh-proxy.org',
|
|
106
|
-
'hk.gh-proxy.org',
|
|
107
|
-
'cdn.gh-proxy.org',
|
|
108
|
-
'edgeone.gh-proxy.org',
|
|
109
|
-
];
|
|
112
|
+
const AVAILABLE_MIRRORS = ['v6.gh-proxy.org', 'gh-proxy.org', 'hk.gh-proxy.org', 'cdn.gh-proxy.org', 'edgeone.gh-proxy.org'];
|
|
110
113
|
|
|
111
114
|
function getGitHubMirror() {
|
|
112
115
|
const settings = readSettings();
|
|
@@ -125,9 +128,7 @@ function setGitHubMirror(mirror) {
|
|
|
125
128
|
// - null 或 undefined: 恢复默认
|
|
126
129
|
|
|
127
130
|
if (mirror === null || mirror === undefined) {
|
|
128
|
-
|
|
129
|
-
delete settings.github_mirror;
|
|
130
|
-
writeSettings(settings);
|
|
131
|
+
writeSettings({ github_mirror: undefined });
|
|
131
132
|
return DEFAULT_GITHUB_MIRROR;
|
|
132
133
|
}
|
|
133
134
|
|
|
@@ -239,24 +240,28 @@ function hasKernel() {
|
|
|
239
240
|
return fs.existsSync(PATHS.mihomoBinary);
|
|
240
241
|
}
|
|
241
242
|
|
|
243
|
+
let _kernelVersionCache = undefined;
|
|
244
|
+
|
|
242
245
|
function getKernelVersion() {
|
|
243
246
|
if (!hasKernel()) {
|
|
247
|
+
_kernelVersionCache = undefined;
|
|
244
248
|
return null;
|
|
245
249
|
}
|
|
250
|
+
if (_kernelVersionCache !== undefined) return _kernelVersionCache;
|
|
246
251
|
try {
|
|
247
252
|
const output = execSync('"' + PATHS.mihomoBinary + '" -v 2>&1 || true', {
|
|
248
253
|
encoding: 'utf8',
|
|
249
254
|
}).trim();
|
|
250
255
|
if (output) {
|
|
251
256
|
const match = output.match(/v?[\d]+\.[\d]+\.[\d]+/);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
return output;
|
|
257
|
+
_kernelVersionCache = match ? match[0] : output;
|
|
258
|
+
return _kernelVersionCache;
|
|
256
259
|
}
|
|
257
|
-
|
|
260
|
+
_kernelVersionCache = 'unknown';
|
|
261
|
+
return _kernelVersionCache;
|
|
258
262
|
} catch (e) {
|
|
259
|
-
|
|
263
|
+
_kernelVersionCache = 'unknown';
|
|
264
|
+
return _kernelVersionCache;
|
|
260
265
|
}
|
|
261
266
|
}
|
|
262
267
|
|
|
@@ -377,13 +382,7 @@ function resetUserData(options) {
|
|
|
377
382
|
if (options === undefined) options = {};
|
|
378
383
|
const keepKernel = options.keepKernel !== false;
|
|
379
384
|
|
|
380
|
-
const itemsToRemove = [
|
|
381
|
-
PATHS.settingsFile,
|
|
382
|
-
DIRS.subscriptions,
|
|
383
|
-
DIRS.logs,
|
|
384
|
-
DIRS.data,
|
|
385
|
-
DIRS.runtime,
|
|
386
|
-
];
|
|
385
|
+
const itemsToRemove = [PATHS.settingsFile, DIRS.subscriptions, DIRS.logs, DIRS.data, DIRS.runtime];
|
|
387
386
|
|
|
388
387
|
if (!keepKernel) {
|
|
389
388
|
itemsToRemove.push(DIRS.core);
|
|
@@ -402,37 +401,35 @@ function resetUserData(options) {
|
|
|
402
401
|
}
|
|
403
402
|
|
|
404
403
|
ensureDirs();
|
|
404
|
+
_settingsCache = null;
|
|
405
405
|
return removedCount;
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
module.exports = {
|
|
409
409
|
PATHS,
|
|
410
410
|
DIRS,
|
|
411
|
-
PROJECT_ROOT,
|
|
412
411
|
USER_DATA_DIR,
|
|
413
|
-
IS_PKG,
|
|
414
412
|
ensureDirs,
|
|
415
413
|
readSettings,
|
|
416
414
|
writeSettings,
|
|
417
415
|
readSubscriptionsCache,
|
|
418
|
-
writeSubscriptionsCache,
|
|
419
416
|
saveSubscriptionCache,
|
|
420
417
|
maskUrl,
|
|
421
418
|
getSubscriptions,
|
|
422
419
|
getSubscriptionsWithCache,
|
|
423
420
|
addSubscription,
|
|
424
421
|
setDefaultSubscription,
|
|
425
|
-
getSubRawConfigPath,
|
|
426
422
|
saveSubRawConfig,
|
|
427
423
|
readSubRawConfig,
|
|
428
424
|
hasKernel,
|
|
429
425
|
getKernelVersion,
|
|
426
|
+
clearKernelVersionCache: () => {
|
|
427
|
+
_kernelVersionCache = undefined;
|
|
428
|
+
},
|
|
430
429
|
getGitHubMirror,
|
|
431
430
|
setGitHubMirror,
|
|
432
431
|
DEFAULT_GITHUB_MIRROR,
|
|
433
432
|
AVAILABLE_MIRRORS,
|
|
434
|
-
TUN_CONFIG,
|
|
435
|
-
BASE_CONFIG,
|
|
436
433
|
parseYamlOrJson,
|
|
437
434
|
buildConfig,
|
|
438
435
|
writeMihomoConfig,
|
package/src/kernel.js
CHANGED
|
@@ -31,8 +31,7 @@ function getArch() {
|
|
|
31
31
|
function findMatchingAsset(assets, platform, arch) {
|
|
32
32
|
const prefix = 'mihomo-' + platform + '-' + arch;
|
|
33
33
|
const matchingAssets = assets.filter(a => {
|
|
34
|
-
return (a.name.startsWith(prefix) && a.name.endsWith('.gz')) ||
|
|
35
|
-
(a.name.startsWith(prefix + '-') && a.name.endsWith('.gz'));
|
|
34
|
+
return (a.name.startsWith(prefix) && a.name.endsWith('.gz')) || (a.name.startsWith(prefix + '-') && a.name.endsWith('.gz'));
|
|
36
35
|
});
|
|
37
36
|
|
|
38
37
|
if (matchingAssets.length === 0) {
|
|
@@ -67,11 +66,12 @@ async function getLatestRelease(repo) {
|
|
|
67
66
|
throw new Error('无法获取版本信息');
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
const stableReleases = releases.filter(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
const stableReleases = releases.filter(
|
|
70
|
+
r =>
|
|
71
|
+
!r.prerelease &&
|
|
72
|
+
!r.tag_name.toLowerCase().includes('alpha') &&
|
|
73
|
+
!r.tag_name.toLowerCase().includes('beta') &&
|
|
74
|
+
!r.tag_name.toLowerCase().includes('prerelease'),
|
|
75
75
|
);
|
|
76
76
|
|
|
77
77
|
if (stableReleases.length > 0) {
|
|
@@ -194,14 +194,18 @@ async function downloadKernel(progressCallback, mirror) {
|
|
|
194
194
|
extractedBinary = outputPath;
|
|
195
195
|
}
|
|
196
196
|
} catch (e) {
|
|
197
|
-
try {
|
|
197
|
+
try {
|
|
198
|
+
fs.unlinkSync(tempPath);
|
|
199
|
+
} catch {}
|
|
198
200
|
throw new Error('解压失败: ' + e.message);
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
const foundBinary = extractedBinary || findBinaryInDir(extractPath);
|
|
202
204
|
|
|
203
205
|
if (!foundBinary) {
|
|
204
|
-
try {
|
|
206
|
+
try {
|
|
207
|
+
fs.unlinkSync(tempPath);
|
|
208
|
+
} catch {}
|
|
205
209
|
throw new Error('解压后未找到可执行文件');
|
|
206
210
|
}
|
|
207
211
|
|
|
@@ -223,6 +227,9 @@ async function downloadKernel(progressCallback, mirror) {
|
|
|
223
227
|
fs.unlinkSync(tempPath);
|
|
224
228
|
} catch (e) {}
|
|
225
229
|
|
|
230
|
+
// 内核已更新,清除版本缓存
|
|
231
|
+
config.clearKernelVersionCache();
|
|
232
|
+
|
|
226
233
|
return {
|
|
227
234
|
version: latest.tag_name,
|
|
228
235
|
path: targetPath,
|
|
@@ -230,9 +237,6 @@ async function downloadKernel(progressCallback, mirror) {
|
|
|
230
237
|
}
|
|
231
238
|
|
|
232
239
|
module.exports = {
|
|
233
|
-
getArch,
|
|
234
|
-
getLatestRelease,
|
|
235
|
-
findMatchingAsset,
|
|
236
240
|
checkUpdate,
|
|
237
241
|
downloadKernel,
|
|
238
242
|
};
|
package/src/process.js
CHANGED
|
@@ -3,6 +3,11 @@ const path = require('path');
|
|
|
3
3
|
const { spawn, execSync } = require('child_process');
|
|
4
4
|
const config = require('./config');
|
|
5
5
|
|
|
6
|
+
const _sharedBuf = new Int32Array(new SharedArrayBuffer(4));
|
|
7
|
+
function sleepSync(ms) {
|
|
8
|
+
Atomics.wait(_sharedBuf, 0, 0, ms);
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
function clearRuntime() {
|
|
7
12
|
if (fs.existsSync(config.DIRS.runtime)) {
|
|
8
13
|
config.rmrf(config.DIRS.runtime);
|
|
@@ -47,7 +52,10 @@ function getAllMihomoPids() {
|
|
|
47
52
|
encoding: 'utf8',
|
|
48
53
|
}).trim();
|
|
49
54
|
if (!output) return [];
|
|
50
|
-
return output
|
|
55
|
+
return output
|
|
56
|
+
.split('\n')
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.map(p => parseInt(p));
|
|
51
59
|
} catch {
|
|
52
60
|
return [];
|
|
53
61
|
}
|
|
@@ -208,9 +216,7 @@ function cleanupAll(forceSudo) {
|
|
|
208
216
|
|
|
209
217
|
for (let i = 0; i < 50; i++) {
|
|
210
218
|
if (getAllMihomoPids().length === 0) break;
|
|
211
|
-
|
|
212
|
-
execSync('sleep 0.1', { stdio: 'ignore' });
|
|
213
|
-
} catch (e) {}
|
|
219
|
+
sleepSync(100);
|
|
214
220
|
}
|
|
215
221
|
|
|
216
222
|
clearPid();
|
|
@@ -227,14 +233,25 @@ function createTunLaunchScript() {
|
|
|
227
233
|
const configFile = config.PATHS.configFile;
|
|
228
234
|
const logFile = config.PATHS.logFile;
|
|
229
235
|
const pidFile = config.PATHS.pidFile;
|
|
230
|
-
const dataDir = config.
|
|
231
|
-
|
|
232
|
-
const scriptContent =
|
|
233
|
-
'
|
|
234
|
-
'
|
|
235
|
-
|
|
236
|
-
'
|
|
237
|
-
'
|
|
236
|
+
const dataDir = config.DIRS.data;
|
|
237
|
+
|
|
238
|
+
const scriptContent =
|
|
239
|
+
'#!/bin/bash\n' +
|
|
240
|
+
'BINARY="' +
|
|
241
|
+
binary +
|
|
242
|
+
'"\n' +
|
|
243
|
+
'CONFIG_FILE="' +
|
|
244
|
+
configFile +
|
|
245
|
+
'"\n' +
|
|
246
|
+
'LOG_FILE="' +
|
|
247
|
+
logFile +
|
|
248
|
+
'"\n' +
|
|
249
|
+
'PID_FILE="' +
|
|
250
|
+
pidFile +
|
|
251
|
+
'"\n' +
|
|
252
|
+
'DATA_DIR="' +
|
|
253
|
+
dataDir +
|
|
254
|
+
'"\n' +
|
|
238
255
|
'\n' +
|
|
239
256
|
'# 终止旧进程\n' +
|
|
240
257
|
'pkill -9 -f "${BINARY}" 2>/dev/null || true\n' +
|
|
@@ -361,10 +378,7 @@ async function startMixedMode(staleState) {
|
|
|
361
378
|
throw new Error('未找到配置文件,请先添加订阅并启动');
|
|
362
379
|
}
|
|
363
380
|
|
|
364
|
-
const args = [
|
|
365
|
-
'-d', config.PATHS.data,
|
|
366
|
-
'-f', configFile,
|
|
367
|
-
];
|
|
381
|
+
const args = ['-d', config.DIRS.data, '-f', configFile];
|
|
368
382
|
|
|
369
383
|
const out = fs.openSync(logFile, 'a');
|
|
370
384
|
const err = fs.openSync(logFile, 'a');
|
|
@@ -389,7 +403,12 @@ async function startMixedMode(staleState) {
|
|
|
389
403
|
try {
|
|
390
404
|
const logs = fs.readFileSync(logFile, 'utf8').slice(-3000);
|
|
391
405
|
if (logs.trim()) {
|
|
392
|
-
errorMsg +=
|
|
406
|
+
errorMsg +=
|
|
407
|
+
'\n最近的日志:\n' +
|
|
408
|
+
logs
|
|
409
|
+
.split('\n')
|
|
410
|
+
.map(l => ' ' + l)
|
|
411
|
+
.join('\n');
|
|
393
412
|
}
|
|
394
413
|
} catch {}
|
|
395
414
|
}
|
|
@@ -427,14 +446,18 @@ async function startTunMode(staleState) {
|
|
|
427
446
|
timeout: 60000,
|
|
428
447
|
});
|
|
429
448
|
} catch (e) {
|
|
430
|
-
try {
|
|
449
|
+
try {
|
|
450
|
+
fs.unlinkSync(launchScript);
|
|
451
|
+
} catch (e2) {}
|
|
431
452
|
if (e.status === 1) {
|
|
432
453
|
throw new Error('密码错误或取消');
|
|
433
454
|
}
|
|
434
455
|
throw new Error(e.message);
|
|
435
456
|
}
|
|
436
457
|
|
|
437
|
-
try {
|
|
458
|
+
try {
|
|
459
|
+
fs.unlinkSync(launchScript);
|
|
460
|
+
} catch (e) {}
|
|
438
461
|
|
|
439
462
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
440
463
|
|
|
@@ -446,7 +469,7 @@ async function startTunMode(staleState) {
|
|
|
446
469
|
return { success: true, pid: finalPid, mode: 'tun' };
|
|
447
470
|
}
|
|
448
471
|
|
|
449
|
-
function stop(
|
|
472
|
+
function stop(forceSudo) {
|
|
450
473
|
const allPids = getAllMihomoPids();
|
|
451
474
|
if (allPids.length === 0) {
|
|
452
475
|
clearPid();
|
|
@@ -454,7 +477,7 @@ function stop(wasTunMode) {
|
|
|
454
477
|
return { success: true, notRunning: true };
|
|
455
478
|
}
|
|
456
479
|
|
|
457
|
-
const result = cleanupAll(
|
|
480
|
+
const result = cleanupAll(forceSudo);
|
|
458
481
|
|
|
459
482
|
const remaining = getAllMihomoPids();
|
|
460
483
|
if (remaining.length > 0) {
|
|
@@ -490,10 +513,7 @@ function rotateLog() {
|
|
|
490
513
|
return null;
|
|
491
514
|
}
|
|
492
515
|
|
|
493
|
-
const timestamp = new Date().toISOString()
|
|
494
|
-
.replace(/T/, '_')
|
|
495
|
-
.replace(/:/g, '-')
|
|
496
|
-
.replace(/\..+/, '');
|
|
516
|
+
const timestamp = new Date().toISOString().replace(/T/, '_').replace(/:/g, '-').replace(/\..+/, '');
|
|
497
517
|
|
|
498
518
|
const rotatedName = `mihomo.${timestamp}.log`;
|
|
499
519
|
const rotatedPath = path.join(config.DIRS.logs, rotatedName);
|
|
@@ -616,33 +636,6 @@ function getLogPathByName(name) {
|
|
|
616
636
|
return null;
|
|
617
637
|
}
|
|
618
638
|
|
|
619
|
-
function readLog(lines) {
|
|
620
|
-
if (lines === undefined) lines = 100;
|
|
621
|
-
if (!fs.existsSync(config.PATHS.logFile)) {
|
|
622
|
-
return '(暂无日志)';
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
try {
|
|
626
|
-
const content = fs.readFileSync(config.PATHS.logFile, 'utf8');
|
|
627
|
-
const allLines = content.split('\n');
|
|
628
|
-
return allLines.slice(-lines).join('\n');
|
|
629
|
-
} catch (e) {
|
|
630
|
-
return '(读取日志失败: ' + e.message + ')';
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function clearLog() {
|
|
635
|
-
if (fs.existsSync(config.PATHS.logFile)) {
|
|
636
|
-
try {
|
|
637
|
-
fs.writeFileSync(config.PATHS.logFile, '');
|
|
638
|
-
return true;
|
|
639
|
-
} catch (e) {
|
|
640
|
-
return false;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
return true;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
639
|
function openUrl(url) {
|
|
647
640
|
try {
|
|
648
641
|
spawn('open', [url], { stdio: 'ignore', detached: true });
|
|
@@ -653,21 +646,12 @@ function openUrl(url) {
|
|
|
653
646
|
}
|
|
654
647
|
|
|
655
648
|
module.exports = {
|
|
656
|
-
getPid,
|
|
657
|
-
isRunning,
|
|
658
|
-
isProcessRunning,
|
|
659
649
|
getAllMihomoPids,
|
|
660
|
-
isProcessRoot,
|
|
661
|
-
checkStaleState,
|
|
662
650
|
cleanupAll,
|
|
663
651
|
getStatus,
|
|
664
652
|
start,
|
|
665
653
|
stop,
|
|
666
654
|
getLogPath,
|
|
667
|
-
readLog,
|
|
668
|
-
clearLog,
|
|
669
|
-
rotateLog,
|
|
670
|
-
cleanupOldLogs,
|
|
671
655
|
listLogs,
|
|
672
656
|
getLogPathByName,
|
|
673
657
|
openUrl,
|
package/src/subscription.js
CHANGED
|
@@ -67,6 +67,13 @@ function formatDate(dateOrIso) {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
function formatProxySummary(info) {
|
|
71
|
+
const parts = [];
|
|
72
|
+
if (info && info.proxyGroups > 0) parts.push(info.proxyGroups + ' 组');
|
|
73
|
+
parts.push(((info && info.proxies) || 0) + ' 节点');
|
|
74
|
+
return parts.join(', ');
|
|
75
|
+
}
|
|
76
|
+
|
|
70
77
|
async function downloadSubscription(url, subName) {
|
|
71
78
|
if (subName === undefined) subName = 'default';
|
|
72
79
|
|
|
@@ -159,7 +166,7 @@ function needsAutoUpdate(sub) {
|
|
|
159
166
|
if (isNaN(lastUpdate)) return true;
|
|
160
167
|
const intervalHours = sub.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
|
|
161
168
|
const intervalMs = intervalHours * 60 * 60 * 1000;
|
|
162
|
-
return
|
|
169
|
+
return Date.now() - lastUpdate > intervalMs;
|
|
163
170
|
}
|
|
164
171
|
|
|
165
172
|
async function tryUpdateOne(sub) {
|
|
@@ -193,10 +200,7 @@ async function autoUpdateStaleSubscriptions() {
|
|
|
193
200
|
results.forEach(r => {
|
|
194
201
|
if (r.success) {
|
|
195
202
|
updatedCount++;
|
|
196
|
-
|
|
197
|
-
if (r.proxyGroups && r.proxyGroups > 0) parts.push(r.proxyGroups + ' 组');
|
|
198
|
-
parts.push(r.proxies + ' 节点');
|
|
199
|
-
console.log('✓ ' + r.name + ': 已更新 (' + parts.join(', ') + ')');
|
|
203
|
+
console.log('✓ ' + r.name + ': 已更新 (' + formatProxySummary(r) + ')');
|
|
200
204
|
} else {
|
|
201
205
|
console.log('✗ ' + r.name + ': 失败 (' + r.error.split('\n')[0] + ')');
|
|
202
206
|
}
|
|
@@ -213,11 +217,10 @@ module.exports = {
|
|
|
213
217
|
DEFAULT_UPDATE_INTERVAL_HOURS,
|
|
214
218
|
downloadSubscription,
|
|
215
219
|
prepareConfigForStart,
|
|
216
|
-
hasConfig: config.hasConfig,
|
|
217
|
-
getConfigInfo: config.getConfigInfo,
|
|
218
220
|
formatBytes,
|
|
219
221
|
formatTimestamp,
|
|
220
222
|
formatDate,
|
|
223
|
+
formatProxySummary,
|
|
221
224
|
tryUpdateOne,
|
|
222
225
|
autoUpdateStaleSubscriptions,
|
|
223
226
|
};
|