mihomo-cli 1.5.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +3 -2
- package/dist/index.js +5138 -0
- package/package.json +23 -16
- package/index.js +0 -1176
- package/src/config.js +0 -516
- package/src/kernel.js +0 -250
- package/src/overwrite.js +0 -258
- package/src/process.js +0 -691
- package/src/subscription.js +0 -257
- package/src/utils.js +0 -202
package/index.js
DELETED
|
@@ -1,1176 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// 内置模块
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { spawn, exec } = require('child_process');
|
|
6
|
-
const { promisify } = require('util');
|
|
7
|
-
const readline = require('readline');
|
|
8
|
-
|
|
9
|
-
// 第三方模块
|
|
10
|
-
// (无第三方模块依赖)
|
|
11
|
-
|
|
12
|
-
// 本地模块
|
|
13
|
-
const config = require('./src/config');
|
|
14
|
-
const kernel = require('./src/kernel');
|
|
15
|
-
const subscription = require('./src/subscription');
|
|
16
|
-
const processManager = require('./src/process');
|
|
17
|
-
const overwrite = require('./src/overwrite');
|
|
18
|
-
const utils = require('./src/utils');
|
|
19
|
-
|
|
20
|
-
const execAsync = promisify(exec);
|
|
21
|
-
const VERSION = utils.VERSION;
|
|
22
|
-
const { colors } = utils;
|
|
23
|
-
|
|
24
|
-
const UI_URLS = {
|
|
25
|
-
zash: 'https://board.zash.run.place',
|
|
26
|
-
dash: 'https://metacubex.github.io/metacubexd',
|
|
27
|
-
yacd: 'https://yacd.metacubex.one',
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
let exiting = false;
|
|
31
|
-
|
|
32
|
-
process.on('SIGINT', () => {
|
|
33
|
-
if (exiting) {
|
|
34
|
-
console.log('\n强制退出');
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
exiting = true;
|
|
38
|
-
console.log('\n正在退出...');
|
|
39
|
-
process.exit(0);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
process.on('SIGTERM', () => {
|
|
43
|
-
process.exit(0);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
process.on('uncaughtException', e => {
|
|
47
|
-
console.error('\n未捕获的异常: ' + e.message);
|
|
48
|
-
if (e.stack) {
|
|
49
|
-
console.error(e.stack.split('\n').slice(1).join('\n'));
|
|
50
|
-
}
|
|
51
|
-
process.exit(1);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
process.on('unhandledRejection', reason => {
|
|
55
|
-
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
56
|
-
console.error('\n未处理的 Promise 拒绝: ' + msg);
|
|
57
|
-
process.exit(1);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
function printShortHelp() {
|
|
61
|
-
console.log('\n' + colors.cyan(colors.bold('mihomo-cli v' + VERSION)) + ' (mihomo help 查看完整帮助)\n');
|
|
62
|
-
console.log(
|
|
63
|
-
'常用命令:\n' +
|
|
64
|
-
' ' +
|
|
65
|
-
colors.bold('start') +
|
|
66
|
-
' [tun|mixed] 启动/切换代理\n' +
|
|
67
|
-
' ' +
|
|
68
|
-
colors.bold('sub') +
|
|
69
|
-
' [use|update] 订阅管理\n' +
|
|
70
|
-
' ' +
|
|
71
|
-
colors.bold('ow') +
|
|
72
|
-
' [on|off] 覆写配置\n' +
|
|
73
|
-
' ' +
|
|
74
|
-
colors.bold('ui') +
|
|
75
|
-
' [zash|dash|yacd] 打开 Web UI\n',
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function printHelp() {
|
|
80
|
-
console.log(
|
|
81
|
-
'\n' +
|
|
82
|
-
colors.cyan(colors.bold('mihomo-cli v' + VERSION)) +
|
|
83
|
-
'\n' +
|
|
84
|
-
'\n' +
|
|
85
|
-
'命令别名: mihomo, mmc, mh\n' +
|
|
86
|
-
'\n' +
|
|
87
|
-
'用法:\n' +
|
|
88
|
-
' mihomo <命令> [选项]\n' +
|
|
89
|
-
'\n' +
|
|
90
|
-
colors.cyan('控制:') +
|
|
91
|
-
'\n' +
|
|
92
|
-
' ' +
|
|
93
|
-
colors.bold('start') +
|
|
94
|
-
' [tun|mixed] 启动/切换代理 (默认 mixed)\n' +
|
|
95
|
-
' ' +
|
|
96
|
-
colors.bold('stop') +
|
|
97
|
-
' 停止代理\n' +
|
|
98
|
-
' ' +
|
|
99
|
-
colors.bold('status') +
|
|
100
|
-
' 查看状态\n' +
|
|
101
|
-
'\n' +
|
|
102
|
-
colors.cyan('界面:') +
|
|
103
|
-
'\n' +
|
|
104
|
-
' ' +
|
|
105
|
-
colors.bold('ui') +
|
|
106
|
-
' [zash|dash|yacd] 打开 Web UI (默认 zash)\n' +
|
|
107
|
-
' ' +
|
|
108
|
-
colors.bold('log') +
|
|
109
|
-
' [-o] 实时日志(-o 打开文件)\n' +
|
|
110
|
-
' ' +
|
|
111
|
-
colors.bold('logs') +
|
|
112
|
-
' [编号] [-n N] [-o] 日志列表(0=当前,1+=归档)\n' +
|
|
113
|
-
'\n' +
|
|
114
|
-
colors.cyan('订阅:') +
|
|
115
|
-
'\n' +
|
|
116
|
-
' ' +
|
|
117
|
-
colors.bold('subscription') +
|
|
118
|
-
' 列出所有订阅(别名 sub)\n' +
|
|
119
|
-
' ' +
|
|
120
|
-
colors.bold('subscription') +
|
|
121
|
-
' add <url> [name] 添加订阅\n' +
|
|
122
|
-
' ' +
|
|
123
|
-
colors.bold('subscription') +
|
|
124
|
-
' update [name] 更新订阅(无参更新所有)\n' +
|
|
125
|
-
' ' +
|
|
126
|
-
colors.bold('subscription') +
|
|
127
|
-
' use <name> 切换当前订阅\n' +
|
|
128
|
-
' ' +
|
|
129
|
-
colors.bold('subscription') +
|
|
130
|
-
' web [name] 打开订阅页面\n' +
|
|
131
|
-
'\n' +
|
|
132
|
-
colors.cyan('配置:') +
|
|
133
|
-
'\n' +
|
|
134
|
-
' ' +
|
|
135
|
-
colors.bold('overwrite') +
|
|
136
|
-
' 查看覆写状态(别名 ow)\n' +
|
|
137
|
-
' ' +
|
|
138
|
-
colors.bold('overwrite') +
|
|
139
|
-
' on|off 启用/禁用覆写配置\n' +
|
|
140
|
-
' ' +
|
|
141
|
-
colors.bold('directory') +
|
|
142
|
-
' 显示数据目录位置(别名 dir)\n' +
|
|
143
|
-
' ' +
|
|
144
|
-
colors.bold('directory') +
|
|
145
|
-
' open [target] 打开目录: root|subs|logs|runtime|...\n' +
|
|
146
|
-
'\n' +
|
|
147
|
-
colors.cyan('系统:') +
|
|
148
|
-
'\n' +
|
|
149
|
-
' ' +
|
|
150
|
-
colors.bold('kernel') +
|
|
151
|
-
' [--mirror [镜像]] 更新内核(默认直连,--mirror 使用 v6)\n' +
|
|
152
|
-
' ' +
|
|
153
|
-
colors.bold('update') +
|
|
154
|
-
' 更新 mihomo-cli (npm install -g)\n' +
|
|
155
|
-
' ' +
|
|
156
|
-
colors.bold('reset') +
|
|
157
|
-
' [目标...] [--full] 重置: 留空保留设置/内核/覆写, 指定目标删对应项, --full 删全部\n' +
|
|
158
|
-
' ' +
|
|
159
|
-
colors.bold('help') +
|
|
160
|
-
', -h 显示帮助\n' +
|
|
161
|
-
' ' +
|
|
162
|
-
colors.bold('version') +
|
|
163
|
-
', -v 显示版本\n' +
|
|
164
|
-
'\n' +
|
|
165
|
-
colors.cyan('示例:') +
|
|
166
|
-
'\n' +
|
|
167
|
-
' mihomo start # 启动/重启 Mixed 模式\n' +
|
|
168
|
-
' mihomo start tun # 切换到 TUN 透明代理模式\n' +
|
|
169
|
-
' mihomo sub add <url> # 添加订阅 (sub 是 subscription 别名)\n' +
|
|
170
|
-
' mihomo ui # 打开 Web UI\n' +
|
|
171
|
-
'\n' +
|
|
172
|
-
colors.cyan('模式说明:') +
|
|
173
|
-
'\n' +
|
|
174
|
-
' mixed HTTP + SOCKS5 混合端口 (默认)\n' +
|
|
175
|
-
' tun 透明代理,全局自动路由,需要 sudo\n' +
|
|
176
|
-
'\n' +
|
|
177
|
-
colors.cyan('数据目录:') +
|
|
178
|
-
'\n' +
|
|
179
|
-
' 环境变量 MIHOMO_CLI_DIR 可自定义位置\n' +
|
|
180
|
-
' 默认: ' +
|
|
181
|
-
config.USER_DATA_DIR +
|
|
182
|
-
'\n',
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function printVersion() {
|
|
187
|
-
const kv = config.getKernelVersion() || '未安装';
|
|
188
|
-
console.log(colors.cyan(colors.bold('mihomo-cli v' + VERSION)));
|
|
189
|
-
console.log(colors.gray('内核: ') + kv);
|
|
190
|
-
console.log(colors.gray('数据目录: ') + config.USER_DATA_DIR);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function printStatus() {
|
|
194
|
-
const status = processManager.getStatus();
|
|
195
|
-
const info = config.getConfigInfo();
|
|
196
|
-
const overwriteEnabled = overwrite.isOverwriteEnabled();
|
|
197
|
-
const overwriteFiles = overwrite.listOverwriteFile().files;
|
|
198
|
-
const activeSub = subscription.getActiveSubscription();
|
|
199
|
-
|
|
200
|
-
console.log('');
|
|
201
|
-
let modeLabel = '';
|
|
202
|
-
if (info && status.running) {
|
|
203
|
-
modeLabel = colors.cyan(info.tun ? ' (TUN)' : ' (Mixed)');
|
|
204
|
-
}
|
|
205
|
-
const statusText = status.running ? colors.green('● 运行中') : colors.yellow('不在运行');
|
|
206
|
-
console.log(colors.gray('状态: ') + statusText + modeLabel);
|
|
207
|
-
console.log(colors.gray('内核: ') + (status.kernelVersion || '未安装'));
|
|
208
|
-
|
|
209
|
-
if (status.pid) {
|
|
210
|
-
console.log(colors.gray('PID: ') + status.pid);
|
|
211
|
-
if (status.processInfo) {
|
|
212
|
-
console.log(colors.gray('内存: ') + status.processInfo.memory);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (info) {
|
|
217
|
-
if (info.mixedPort) {
|
|
218
|
-
console.log(colors.gray('端口: ') + info.mixedPort);
|
|
219
|
-
} else {
|
|
220
|
-
let ports = [];
|
|
221
|
-
if (info.httpPort) ports.push('HTTP:' + info.httpPort);
|
|
222
|
-
if (info.socksPort) ports.push('SOCKS:' + info.socksPort);
|
|
223
|
-
console.log(colors.gray('端口: ') + (ports.length > 0 ? ports.join(', ') : '未知'));
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (activeSub) {
|
|
228
|
-
let subLine = colors.gray('订阅: ') + activeSub.name;
|
|
229
|
-
if (info) {
|
|
230
|
-
subLine += ' (' + subscription.formatProxySummary(info) + ')';
|
|
231
|
-
}
|
|
232
|
-
console.log(subLine);
|
|
233
|
-
} else {
|
|
234
|
-
console.log(colors.gray('订阅: ') + '未配置');
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (overwriteEnabled && overwriteFiles.length > 0) {
|
|
238
|
-
const names = overwriteFiles.map(f => f.name.replace(/^overwrite\.?/, '')).join(', ');
|
|
239
|
-
console.log(colors.gray('覆写: ') + colors.green('已启用') + ' (' + names + ')');
|
|
240
|
-
} else if (overwriteEnabled) {
|
|
241
|
-
console.log(colors.gray('覆写: ') + colors.green('已启用') + ' (无文件)');
|
|
242
|
-
} else {
|
|
243
|
-
console.log(colors.gray('覆写: ') + colors.yellow('已禁用'));
|
|
244
|
-
}
|
|
245
|
-
console.log('');
|
|
246
|
-
}
|
|
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
|
-
|
|
256
|
-
async function cmdStart(args) {
|
|
257
|
-
if (!config.hasKernel()) {
|
|
258
|
-
console.error('错误: 未找到内核,请运行 "mihomo kernel"');
|
|
259
|
-
process.exit(1);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const targetMode = args[1] === 'tun' ? 'tun' : 'mixed';
|
|
263
|
-
|
|
264
|
-
const sub = subscription.getActiveSubscription();
|
|
265
|
-
if (!sub) {
|
|
266
|
-
console.error('错误: 没有订阅,请先添加订阅');
|
|
267
|
-
process.exit(1);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
await subscription.autoUpdateStaleSubscription();
|
|
271
|
-
|
|
272
|
-
const status = processManager.getStatus();
|
|
273
|
-
const hasProcess = status.running || status.allProcesses.length > 0;
|
|
274
|
-
|
|
275
|
-
if (hasProcess) {
|
|
276
|
-
const count = status.allProcesses.length > 0 ? status.allProcesses.length : 1;
|
|
277
|
-
console.log('停止 ' + count + ' 个进程...');
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
handleStopResult(processManager.stop(true));
|
|
281
|
-
|
|
282
|
-
if (hasProcess) {
|
|
283
|
-
console.log(colors.green('已停止进程') + '\n');
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
let configInfo;
|
|
287
|
-
try {
|
|
288
|
-
configInfo = subscription.prepareConfigForStart(targetMode, sub.name);
|
|
289
|
-
} catch (e) {
|
|
290
|
-
console.error(colors.red('配置错误:') + ' ' + e.message);
|
|
291
|
-
process.exit(1);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const modeLabel = targetMode === 'tun' ? 'TUN' : 'Mixed';
|
|
295
|
-
console.log([colors.cyan(modeLabel), sub.name, subscription.formatProxySummary(configInfo)].join(' · '));
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
const result = await processManager.start(targetMode);
|
|
299
|
-
console.log(colors.green('已启动') + ' (PID ' + result.pid + ')');
|
|
300
|
-
printStatus();
|
|
301
|
-
} catch (e) {
|
|
302
|
-
console.error(colors.red('启动失败:') + ' ' + e.message.split('\n')[0]);
|
|
303
|
-
process.exit(1);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
async function cmdStop() {
|
|
308
|
-
const pids = processManager.getAllMihomoPids();
|
|
309
|
-
if (pids.length === 0) {
|
|
310
|
-
console.log(colors.yellow('不在运行'));
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
console.log('停止 ' + pids.length + ' 个进程...');
|
|
315
|
-
handleStopResult(processManager.stop(true));
|
|
316
|
-
console.log(colors.green('已停止进程'));
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function cmdUI(args) {
|
|
320
|
-
const uiName = args[1] || 'zash';
|
|
321
|
-
const url = UI_URLS[uiName];
|
|
322
|
-
|
|
323
|
-
if (!url) {
|
|
324
|
-
console.error('错误: 未知的 UI "' + uiName + '"');
|
|
325
|
-
console.error('可用 UI: zash (默认), dash, yacd');
|
|
326
|
-
process.exit(1);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
console.log('打开 Web UI: ' + uiName);
|
|
330
|
-
console.log('地址: ' + url);
|
|
331
|
-
|
|
332
|
-
const success = processManager.openUrl(url);
|
|
333
|
-
if (!success) {
|
|
334
|
-
console.log('请手动访问上面的地址');
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function cmdLog(args) {
|
|
339
|
-
const logPath = processManager.getLogPath();
|
|
340
|
-
|
|
341
|
-
if (utils.hasFlag(args, '-o', '--open')) {
|
|
342
|
-
processManager.openLogFile(logPath);
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
processManager.viewLogWithTail(logPath, { follow: true, lines: 50 });
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function cmdLogs(args) {
|
|
350
|
-
const targetName = utils.getNonFlagArg(args, 1);
|
|
351
|
-
const lines = utils.parseIntArg(args, '-n', '--lines', 100);
|
|
352
|
-
const openInViewer = utils.hasFlag(args, '-o', '--open');
|
|
353
|
-
|
|
354
|
-
if (targetName) {
|
|
355
|
-
let logPath;
|
|
356
|
-
|
|
357
|
-
if (targetName === 'current' || targetName === '0') {
|
|
358
|
-
logPath = processManager.getLogPath();
|
|
359
|
-
} else {
|
|
360
|
-
const parsedIdx = parseInt(targetName);
|
|
361
|
-
if (!isNaN(parsedIdx) && parsedIdx > 0 && String(parsedIdx) === targetName) {
|
|
362
|
-
const archiveLogs = processManager.listLogs();
|
|
363
|
-
const archive = archiveLogs.archives[parsedIdx - 1];
|
|
364
|
-
if (!archive) {
|
|
365
|
-
console.error('错误: 未找到日志 "' + targetName + '"');
|
|
366
|
-
console.log('使用 "mihomo logs" 查看可用日志列表');
|
|
367
|
-
process.exit(1);
|
|
368
|
-
}
|
|
369
|
-
logPath = archive.path;
|
|
370
|
-
} else {
|
|
371
|
-
logPath = processManager.getLogPathByName(targetName);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (!logPath) {
|
|
376
|
-
console.error('错误: 未找到日志 "' + targetName + '"');
|
|
377
|
-
console.log('使用 "mihomo logs" 查看可用日志列表');
|
|
378
|
-
process.exit(1);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
if (openInViewer) {
|
|
382
|
-
processManager.openLogFile(logPath);
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
processManager.viewLogWithTail(logPath, { follow: false, lines });
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const logs = processManager.listLogs();
|
|
391
|
-
const all = [];
|
|
392
|
-
|
|
393
|
-
if (logs.current) {
|
|
394
|
-
all.push(logs.current);
|
|
395
|
-
}
|
|
396
|
-
all.push(...logs.archives);
|
|
397
|
-
|
|
398
|
-
if (all.length === 0) {
|
|
399
|
-
console.log('暂无日志');
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
console.log('');
|
|
404
|
-
console.log('日志列表:');
|
|
405
|
-
console.log('');
|
|
406
|
-
|
|
407
|
-
let archiveCounter = 0;
|
|
408
|
-
all.forEach(log => {
|
|
409
|
-
let num;
|
|
410
|
-
if (log.isCurrent) {
|
|
411
|
-
num = ' 0';
|
|
412
|
-
} else {
|
|
413
|
-
archiveCounter++;
|
|
414
|
-
num = archiveCounter < 10 ? ' ' + archiveCounter : '' + archiveCounter;
|
|
415
|
-
}
|
|
416
|
-
const time = utils.formatDate(log.mtime);
|
|
417
|
-
const size = utils.formatBytes(log.size);
|
|
418
|
-
const name = log.isCurrent ? 'mihomo.log (当前运行中)' : log.name;
|
|
419
|
-
|
|
420
|
-
console.log(' ' + num + '. ' + name);
|
|
421
|
-
console.log(' 时间: ' + time + ' 大小: ' + size);
|
|
422
|
-
if (!log.isCurrent) {
|
|
423
|
-
console.log(' 查看: mihomo logs ' + archiveCounter + ' 或 mihomo logs ' + archiveCounter + ' -o');
|
|
424
|
-
}
|
|
425
|
-
console.log('');
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
console.log('用法:');
|
|
429
|
-
console.log(' mihomo logs 0 # 查看当前日志 (最后 100 行)');
|
|
430
|
-
console.log(' mihomo logs 1 # 查看第 1 个归档日志(最新)');
|
|
431
|
-
console.log(' mihomo logs 1 -n 200 # 查看 200 行');
|
|
432
|
-
console.log(' mihomo logs 1 -o # 用系统默认程序打开');
|
|
433
|
-
console.log('');
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
async function cmdKernel(args) {
|
|
437
|
-
const mirrorInfo = utils.parseMirrorArg(args);
|
|
438
|
-
const effectiveMirror = mirrorInfo.mirror;
|
|
439
|
-
|
|
440
|
-
if (effectiveMirror) {
|
|
441
|
-
let mirrorDesc = mirrorInfo.type === 'all' ? ' (API和下载均使用镜像)' : ' (下载时使用镜像)';
|
|
442
|
-
console.log('镜像: ' + effectiveMirror + mirrorDesc);
|
|
443
|
-
}
|
|
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
|
-
|
|
453
|
-
console.log('\n可用镜像:');
|
|
454
|
-
config.AVAILABLE_MIRRORS.forEach(m => {
|
|
455
|
-
const isCurrent =
|
|
456
|
-
effectiveMirror && (effectiveMirror.includes('//' + m + '/') || effectiveMirror.includes('//' + m + ':') || effectiveMirror.endsWith('//' + m));
|
|
457
|
-
console.log(' ' + m + (isCurrent ? ' (当前)' : ''));
|
|
458
|
-
});
|
|
459
|
-
console.log('');
|
|
460
|
-
|
|
461
|
-
console.log('检查内核更新...');
|
|
462
|
-
|
|
463
|
-
try {
|
|
464
|
-
const apiMirror = mirrorInfo.type === 'all' ? effectiveMirror : null;
|
|
465
|
-
const info = await kernel.checkUpdate(apiMirror);
|
|
466
|
-
console.log('当前: ' + info.current);
|
|
467
|
-
console.log('最新: ' + info.latest);
|
|
468
|
-
|
|
469
|
-
if (!info.needsUpdate) {
|
|
470
|
-
console.log('已是最新版本');
|
|
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);
|
|
481
|
-
}
|
|
482
|
-
} catch (e) {
|
|
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
|
-
}
|
|
492
|
-
process.exit(1);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
async function printSubscriptionList() {
|
|
497
|
-
const updateResult = await subscription.autoUpdateStaleSubscription();
|
|
498
|
-
if (updateResult.total > 0) {
|
|
499
|
-
console.log('');
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const subs = config.getSubscriptionsWithCache();
|
|
503
|
-
if (subs.length === 0) {
|
|
504
|
-
console.log('没有订阅');
|
|
505
|
-
console.log('');
|
|
506
|
-
console.log('添加订阅: mihomo sub add <url> [name]');
|
|
507
|
-
console.log('');
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
const activeSub = subscription.getActiveSubscription();
|
|
511
|
-
console.log(colors.cyan('订阅列表:'));
|
|
512
|
-
subs.forEach((s, i) => {
|
|
513
|
-
const time = utils.formatDate(s.updated_at);
|
|
514
|
-
const defaultMark = activeSub && s.name === activeSub.name ? colors.green(' [使用中]') : '';
|
|
515
|
-
const interval = s.update_interval || subscription.DEFAULT_UPDATE_INTERVAL_HOURS;
|
|
516
|
-
console.log(' ' + (i + 1) + '. ' + s.name + defaultMark);
|
|
517
|
-
console.log(' ' + colors.gray('更新: ') + time + ' (间隔: ' + interval + 'h)');
|
|
518
|
-
|
|
519
|
-
if (s.username) {
|
|
520
|
-
console.log(' ' + colors.gray('用户: ') + s.username);
|
|
521
|
-
}
|
|
522
|
-
if (s.download !== undefined || s.total !== undefined) {
|
|
523
|
-
const used = (s.upload || 0) + (s.download || 0);
|
|
524
|
-
const usedStr = utils.formatBytes(used);
|
|
525
|
-
const totalStr = utils.formatBytes(s.total);
|
|
526
|
-
let percentStr = '';
|
|
527
|
-
if (s.total && s.total > 0) {
|
|
528
|
-
const percent = Math.min((used / s.total) * 100, 100);
|
|
529
|
-
percentStr = ' (' + percent.toFixed(1) + '%)';
|
|
530
|
-
}
|
|
531
|
-
console.log(' ' + colors.gray('流量: ') + usedStr + ' / ' + totalStr + percentStr);
|
|
532
|
-
}
|
|
533
|
-
if (s.expire !== undefined) {
|
|
534
|
-
console.log(' ' + colors.gray('到期: ') + utils.formatTimestamp(s.expire));
|
|
535
|
-
}
|
|
536
|
-
if (s.web_page_url) {
|
|
537
|
-
console.log(' ' + colors.gray('页面: ') + s.web_page_url);
|
|
538
|
-
}
|
|
539
|
-
});
|
|
540
|
-
console.log('');
|
|
541
|
-
console.log('切换订阅: mihomo sub use <name>');
|
|
542
|
-
console.log('更新订阅: mihomo sub update [name]');
|
|
543
|
-
console.log('打开页面: mihomo sub web [name]');
|
|
544
|
-
console.log('新增订阅: mihomo sub add <url> [name]');
|
|
545
|
-
console.log('');
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
async function cmdSubscription(args) {
|
|
549
|
-
const action = args[1];
|
|
550
|
-
|
|
551
|
-
if (!action || action === 'list') {
|
|
552
|
-
await printSubscriptionList();
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
if (action === 'add') {
|
|
557
|
-
const url = args[2];
|
|
558
|
-
const name = args[3] || 'default';
|
|
559
|
-
|
|
560
|
-
if (!url || !url.startsWith('http')) {
|
|
561
|
-
console.error('错误: 请提供有效的订阅 URL');
|
|
562
|
-
process.exit(1);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
console.log('添加订阅: ' + name);
|
|
566
|
-
try {
|
|
567
|
-
config.addSubscription(url, name);
|
|
568
|
-
const info = await subscription.downloadSubscription(url, name);
|
|
569
|
-
console.log('已添加 (' + subscription.formatProxySummary(info) + ')');
|
|
570
|
-
} catch (e) {
|
|
571
|
-
console.error('添加失败: ' + e.message);
|
|
572
|
-
process.exit(1);
|
|
573
|
-
}
|
|
574
|
-
console.log('');
|
|
575
|
-
await printSubscriptionList();
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
if (action === 'update') {
|
|
580
|
-
const name = args[2];
|
|
581
|
-
const subs = config.getSubscriptions();
|
|
582
|
-
|
|
583
|
-
if (subs.length === 0) {
|
|
584
|
-
console.error('错误: 没有订阅');
|
|
585
|
-
process.exit(1);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
if (!name) {
|
|
589
|
-
console.log('更新所有 ' + subs.length + ' 个订阅...');
|
|
590
|
-
const results = await Promise.all(subs.map(subscription.tryUpdateOne));
|
|
591
|
-
let ok = 0;
|
|
592
|
-
results.forEach(r => {
|
|
593
|
-
if (r.success) {
|
|
594
|
-
ok++;
|
|
595
|
-
console.log(colors.green('✓') + ' ' + r.name + ': ' + colors.green('已更新') + ' (' + subscription.formatProxySummary(r) + ')');
|
|
596
|
-
} else {
|
|
597
|
-
console.log(colors.red('✗') + ' ' + r.name + ': ' + colors.red('失败') + ' (' + r.error.split('\n')[0] + ')');
|
|
598
|
-
}
|
|
599
|
-
});
|
|
600
|
-
if (ok === 0) process.exit(1);
|
|
601
|
-
console.log('');
|
|
602
|
-
await printSubscriptionList();
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
const matches = subscription.findSubscriptionFuzzy(subs, name);
|
|
607
|
-
const target = subscription.pickSingleSubscription(matches, name);
|
|
608
|
-
|
|
609
|
-
console.log('更新订阅: ' + target.name);
|
|
610
|
-
try {
|
|
611
|
-
const info = await subscription.downloadSubscription(target.url, target.name);
|
|
612
|
-
console.log('已更新 (' + subscription.formatProxySummary(info) + ')');
|
|
613
|
-
} catch (e) {
|
|
614
|
-
console.error('更新失败: ' + e.message);
|
|
615
|
-
process.exit(1);
|
|
616
|
-
}
|
|
617
|
-
console.log('');
|
|
618
|
-
await printSubscriptionList();
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
if (action === 'use') {
|
|
623
|
-
const name = args[2];
|
|
624
|
-
const subs = config.getSubscriptions();
|
|
625
|
-
|
|
626
|
-
if (!name) {
|
|
627
|
-
console.error('错误: 请指定订阅名称');
|
|
628
|
-
if (subs.length > 0) {
|
|
629
|
-
console.log('\n可用订阅:');
|
|
630
|
-
subs.forEach(s => console.log(' ' + s.name));
|
|
631
|
-
}
|
|
632
|
-
process.exit(1);
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
const matches = subscription.findSubscriptionFuzzy(subs, name);
|
|
636
|
-
const target = subscription.pickSingleSubscription(matches, name);
|
|
637
|
-
|
|
638
|
-
const currentDefault = subscription.getActiveSubscription();
|
|
639
|
-
const isAlreadyDefault = currentDefault && currentDefault.name === target.name;
|
|
640
|
-
|
|
641
|
-
if (isAlreadyDefault) {
|
|
642
|
-
console.log('"' + target.name + '" 已是当前使用的订阅');
|
|
643
|
-
console.log('');
|
|
644
|
-
await printSubscriptionList();
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
const status = processManager.getStatus();
|
|
649
|
-
const configInfo = config.getConfigInfo();
|
|
650
|
-
const currentMode = configInfo && configInfo.tun ? 'tun' : 'mixed';
|
|
651
|
-
|
|
652
|
-
const success = config.setDefaultSubscription(target.name);
|
|
653
|
-
if (success) {
|
|
654
|
-
console.log('已切换到 "' + target.name + '"');
|
|
655
|
-
} else {
|
|
656
|
-
console.error('错误: 未找到订阅 "' + name + '"');
|
|
657
|
-
process.exit(1);
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
if (status.running) {
|
|
661
|
-
console.log('');
|
|
662
|
-
await cmdStart(['start', currentMode]);
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
console.log('');
|
|
667
|
-
await printSubscriptionList();
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
if (action === 'web' || action === 'open') {
|
|
672
|
-
const name = args[2];
|
|
673
|
-
const subs = config.getSubscriptionsWithCache();
|
|
674
|
-
|
|
675
|
-
if (subs.length === 0) {
|
|
676
|
-
console.error('错误: 没有订阅');
|
|
677
|
-
process.exit(1);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
let target;
|
|
681
|
-
if (name) {
|
|
682
|
-
const matches = subscription.findSubscriptionFuzzy(subs, name);
|
|
683
|
-
target = subscription.pickSingleSubscription(matches, name);
|
|
684
|
-
} else {
|
|
685
|
-
target = subs[0];
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
let webPageUrl = target.web_page_url;
|
|
689
|
-
if (!webPageUrl) {
|
|
690
|
-
console.log('订阅信息中缺少页面地址,正在更新订阅...');
|
|
691
|
-
try {
|
|
692
|
-
await subscription.downloadSubscription(target.url, target.name);
|
|
693
|
-
const cache = config.readSubscriptionCache();
|
|
694
|
-
if (cache[target.name] && cache[target.name].web_page_url) {
|
|
695
|
-
webPageUrl = cache[target.name].web_page_url;
|
|
696
|
-
} else {
|
|
697
|
-
console.error('错误: 该订阅没有提供页面地址');
|
|
698
|
-
process.exit(1);
|
|
699
|
-
}
|
|
700
|
-
} catch (e) {
|
|
701
|
-
console.error('更新失败: ' + e.message);
|
|
702
|
-
process.exit(1);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
console.log('打开订阅页面: ' + webPageUrl);
|
|
707
|
-
const opened = processManager.openUrl(webPageUrl);
|
|
708
|
-
if (!opened) {
|
|
709
|
-
console.log('请手动访问上面的地址');
|
|
710
|
-
}
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
console.error('错误: 未知的订阅命令');
|
|
715
|
-
console.log('用法: mihomo sub [list|add|update|use|web]');
|
|
716
|
-
process.exit(1);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
async function cmdUpdate() {
|
|
720
|
-
console.log('当前版本: ' + colors.cyan(VERSION));
|
|
721
|
-
console.log('');
|
|
722
|
-
console.log('正在更新 mihomo-cli...');
|
|
723
|
-
console.log('');
|
|
724
|
-
|
|
725
|
-
await new Promise(resolve => {
|
|
726
|
-
const npm = spawn('npm', ['install', '-g', 'mihomo-cli'], { stdio: 'inherit' });
|
|
727
|
-
|
|
728
|
-
npm.on('close', code => {
|
|
729
|
-
if (code === 0) {
|
|
730
|
-
resolve();
|
|
731
|
-
} else {
|
|
732
|
-
process.exit(code);
|
|
733
|
-
}
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
npm.on('error', e => {
|
|
737
|
-
console.error('执行失败: ' + e.message);
|
|
738
|
-
process.exit(1);
|
|
739
|
-
});
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
try {
|
|
743
|
-
const { stdout } = await execAsync('npm list -g mihomo-cli --json --depth=0');
|
|
744
|
-
const result = JSON.parse(stdout);
|
|
745
|
-
const newVersion = result.dependencies?.['mihomo-cli']?.version;
|
|
746
|
-
|
|
747
|
-
console.log('');
|
|
748
|
-
if (newVersion) {
|
|
749
|
-
console.log('更新完成,最新版本: ' + colors.green(newVersion));
|
|
750
|
-
} else {
|
|
751
|
-
console.log('更新完成');
|
|
752
|
-
}
|
|
753
|
-
} catch {
|
|
754
|
-
console.log('');
|
|
755
|
-
console.log('更新完成');
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
async function confirmPrompt(question) {
|
|
760
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
761
|
-
const answer = await new Promise(resolve => {
|
|
762
|
-
rl.question(question + ' (y/N) ', a => {
|
|
763
|
-
rl.close();
|
|
764
|
-
resolve(a);
|
|
765
|
-
});
|
|
766
|
-
});
|
|
767
|
-
return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
const RESET_TARGETS = [
|
|
771
|
-
{
|
|
772
|
-
id: 'subs',
|
|
773
|
-
aliases: ['sub', 'subs', 'subscription', 'subscriptions'],
|
|
774
|
-
label: '订阅',
|
|
775
|
-
paths: () => [config.DIRS.subscriptions],
|
|
776
|
-
needsStop: true,
|
|
777
|
-
},
|
|
778
|
-
{
|
|
779
|
-
id: 'logs',
|
|
780
|
-
aliases: ['log', 'logs'],
|
|
781
|
-
label: '日志',
|
|
782
|
-
paths: () => [config.DIRS.logs],
|
|
783
|
-
needsStop: false,
|
|
784
|
-
},
|
|
785
|
-
{
|
|
786
|
-
id: 'data',
|
|
787
|
-
aliases: ['data'],
|
|
788
|
-
label: '运行数据',
|
|
789
|
-
paths: () => [config.DIRS.data],
|
|
790
|
-
needsStop: true,
|
|
791
|
-
},
|
|
792
|
-
{
|
|
793
|
-
id: 'runtime',
|
|
794
|
-
aliases: ['runtime'],
|
|
795
|
-
label: '运行时',
|
|
796
|
-
paths: () => [config.DIRS.runtime],
|
|
797
|
-
needsStop: true,
|
|
798
|
-
},
|
|
799
|
-
{
|
|
800
|
-
id: 'settings',
|
|
801
|
-
aliases: ['setting', 'settings', 'config'],
|
|
802
|
-
label: '设置',
|
|
803
|
-
paths: () => [config.PATHS.settingsFile],
|
|
804
|
-
needsStop: false,
|
|
805
|
-
},
|
|
806
|
-
{
|
|
807
|
-
id: 'kernel',
|
|
808
|
-
aliases: ['kernel', 'core'],
|
|
809
|
-
label: '内核',
|
|
810
|
-
paths: () => [config.DIRS.kernel],
|
|
811
|
-
needsStop: false,
|
|
812
|
-
onAfter: () => config.clearKernelVersionCache(),
|
|
813
|
-
checkEmpty: () => !config.hasKernel(),
|
|
814
|
-
emptyMsg: '内核未安装,无需删除',
|
|
815
|
-
warnIfRunning: true,
|
|
816
|
-
},
|
|
817
|
-
{
|
|
818
|
-
id: 'overwrites',
|
|
819
|
-
aliases: ['overwrite', 'overwrites', 'ow'],
|
|
820
|
-
label: '覆写',
|
|
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
|
-
},
|
|
830
|
-
needsStop: false,
|
|
831
|
-
},
|
|
832
|
-
];
|
|
833
|
-
|
|
834
|
-
function resolveResetTargets(names) {
|
|
835
|
-
const matched = [];
|
|
836
|
-
const unmatched = [];
|
|
837
|
-
for (const name of names) {
|
|
838
|
-
const t = RESET_TARGETS.find(t => t.aliases.includes(name.toLowerCase()));
|
|
839
|
-
if (t) {
|
|
840
|
-
if (!matched.find(m => m.id === t.id)) matched.push(t);
|
|
841
|
-
} else {
|
|
842
|
-
unmatched.push(name);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
return { matched, unmatched };
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
async function cmdReset(args) {
|
|
849
|
-
const flags = (args || []).filter(a => a.startsWith('-'));
|
|
850
|
-
const names = (args || []).slice(1).filter(a => !a.startsWith('-'));
|
|
851
|
-
const fullReset = flags.includes('--full') || flags.includes('-f');
|
|
852
|
-
const skipConfirm = flags.includes('--yes') || flags.includes('-y');
|
|
853
|
-
|
|
854
|
-
let targets;
|
|
855
|
-
|
|
856
|
-
if (fullReset) {
|
|
857
|
-
targets = RESET_TARGETS;
|
|
858
|
-
} else if (names.length > 0) {
|
|
859
|
-
const { matched, unmatched } = resolveResetTargets(names);
|
|
860
|
-
if (unmatched.length > 0) {
|
|
861
|
-
console.error('错误: 未知的重置目标: ' + unmatched.join(', '));
|
|
862
|
-
console.log('');
|
|
863
|
-
console.log('可用目标: ' + RESET_TARGETS.map(t => t.aliases[0]).join(', '));
|
|
864
|
-
console.log('');
|
|
865
|
-
console.log('示例:');
|
|
866
|
-
console.log(' mihomo reset sub log # 删除订阅和日志');
|
|
867
|
-
console.log(' mihomo reset kernel # 只删内核');
|
|
868
|
-
console.log(' mihomo reset --full # 删除全部');
|
|
869
|
-
console.log(' mihomo reset # 删除全部(保留设置、内核、覆写)');
|
|
870
|
-
process.exit(1);
|
|
871
|
-
}
|
|
872
|
-
targets = matched;
|
|
873
|
-
} else {
|
|
874
|
-
targets = RESET_TARGETS.filter(t => !['settings', 'kernel', 'overwrites'].includes(t.id));
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
for (const t of targets) {
|
|
878
|
-
if (t.checkEmpty && t.checkEmpty()) {
|
|
879
|
-
if (targets.length === 1) {
|
|
880
|
-
console.log(t.emptyMsg);
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
const needsStop = targets.some(t => t.needsStop);
|
|
887
|
-
const warnRunning = targets.some(t => t.warnIfRunning);
|
|
888
|
-
|
|
889
|
-
const pids = needsStop || warnRunning ? processManager.getAllMihomoPids() : [];
|
|
890
|
-
|
|
891
|
-
if (needsStop && pids.length > 0) {
|
|
892
|
-
console.log('停止 ' + pids.length + ' 个进程...');
|
|
893
|
-
processManager.cleanupAll(true);
|
|
894
|
-
for (let i = 0; i < processManager.PROCESS_WAIT_ATTEMPTS; i++) {
|
|
895
|
-
if (processManager.getAllMihomoPids().length === 0) break;
|
|
896
|
-
await new Promise(r => setTimeout(r, processManager.PROCESS_WAIT_INTERVAL));
|
|
897
|
-
}
|
|
898
|
-
} else if (warnRunning && pids.length > 0) {
|
|
899
|
-
console.log(colors.yellow('警告: mihomo 正在运行 (PID ' + pids.join(', ') + '),删除内核后将无法重新启动'));
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
console.log('将删除: ' + targets.map(t => t.label).join('、'));
|
|
903
|
-
|
|
904
|
-
if (!skipConfirm && !(await confirmPrompt('确认?'))) {
|
|
905
|
-
console.log('已取消');
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
for (const t of targets) {
|
|
910
|
-
for (const p of t.paths()) {
|
|
911
|
-
if (config.fsExistsSync(p)) {
|
|
912
|
-
try {
|
|
913
|
-
config.rmrf(p);
|
|
914
|
-
} catch (e) {
|
|
915
|
-
console.warn(' 警告: 无法删除 ' + p + ': ' + e.message);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
if (t.onAfter) t.onAfter();
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
config.ensureDirs();
|
|
923
|
-
if (targets.some(t => t.id === 'settings')) {
|
|
924
|
-
config.invalidateSettingsCache();
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
console.log(colors.green('已重置: ' + targets.map(t => t.label).join('、')));
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
function printOverwriteList() {
|
|
931
|
-
const info = overwrite.listOverwriteFile();
|
|
932
|
-
const statusText = info.enabled ? colors.green('已启用') : colors.yellow('已禁用');
|
|
933
|
-
console.log(colors.gray('状态: ') + statusText);
|
|
934
|
-
console.log(colors.gray('位置: ') + info.dir);
|
|
935
|
-
console.log('');
|
|
936
|
-
if (info.files.length === 0) {
|
|
937
|
-
console.log('暂无覆写文件');
|
|
938
|
-
console.log('');
|
|
939
|
-
console.log('用法示例: 创建文件 ' + path.join(info.dir, 'overwrite.yaml'));
|
|
940
|
-
console.log(' 或 ' + path.join(info.dir, 'overwrite.dns.yaml'));
|
|
941
|
-
console.log('');
|
|
942
|
-
} else {
|
|
943
|
-
console.log(colors.cyan('覆写文件') + ' (' + info.files.length + ' 个,按顺序加载):');
|
|
944
|
-
console.log('');
|
|
945
|
-
info.files.forEach((f, i) => {
|
|
946
|
-
const num = i < 10 ? ' ' + i : '' + i;
|
|
947
|
-
console.log(' ' + num + '. ' + f.name);
|
|
948
|
-
if (f.keys.length > 0) {
|
|
949
|
-
console.log(' ' + colors.gray('字段: ') + f.keys.join(', '));
|
|
950
|
-
}
|
|
951
|
-
});
|
|
952
|
-
console.log('');
|
|
953
|
-
}
|
|
954
|
-
console.log('启用覆写: mihomo ow on');
|
|
955
|
-
console.log('禁用覆写: mihomo ow off');
|
|
956
|
-
console.log('');
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
async function cmdOverwrite(args) {
|
|
960
|
-
const action = args && args[1];
|
|
961
|
-
|
|
962
|
-
const status = processManager.getStatus();
|
|
963
|
-
const configInfo = config.getConfigInfo();
|
|
964
|
-
const currentMode = configInfo && configInfo.tun ? 'tun' : 'mixed';
|
|
965
|
-
|
|
966
|
-
if (action === 'on' || action === 'enable') {
|
|
967
|
-
if (overwrite.isOverwriteEnabled()) {
|
|
968
|
-
console.log('覆写配置已是启用状态');
|
|
969
|
-
console.log('');
|
|
970
|
-
printOverwriteList();
|
|
971
|
-
return;
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
overwrite.setOverwriteEnabled(true);
|
|
975
|
-
console.log('已启用覆写配置');
|
|
976
|
-
|
|
977
|
-
if (status.running) {
|
|
978
|
-
console.log('');
|
|
979
|
-
await cmdStart(['start', currentMode]);
|
|
980
|
-
return;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
console.log('');
|
|
984
|
-
printOverwriteList();
|
|
985
|
-
return;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
if (action === 'off' || action === 'disable') {
|
|
989
|
-
if (!overwrite.isOverwriteEnabled()) {
|
|
990
|
-
console.log('覆写配置已是禁用状态');
|
|
991
|
-
console.log('');
|
|
992
|
-
printOverwriteList();
|
|
993
|
-
return;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
overwrite.setOverwriteEnabled(false);
|
|
997
|
-
console.log('已禁用覆写配置');
|
|
998
|
-
|
|
999
|
-
if (status.running) {
|
|
1000
|
-
console.log('');
|
|
1001
|
-
await cmdStart(['start', currentMode]);
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
console.log('');
|
|
1006
|
-
printOverwriteList();
|
|
1007
|
-
return;
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
console.log('');
|
|
1011
|
-
printOverwriteList();
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
function cmdDirectory(args) {
|
|
1015
|
-
const action = args && args[1];
|
|
1016
|
-
|
|
1017
|
-
if (action === 'open') {
|
|
1018
|
-
const target = args[2];
|
|
1019
|
-
|
|
1020
|
-
if (!target || target === 'root') {
|
|
1021
|
-
const displayLabel = '根目录';
|
|
1022
|
-
console.log('正在打开: ' + displayLabel);
|
|
1023
|
-
const success = processManager.openUrl(config.USER_DATA_DIR);
|
|
1024
|
-
if (!success) {
|
|
1025
|
-
console.log('请手动打开: ' + config.USER_DATA_DIR);
|
|
1026
|
-
}
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
const targetInfo = config.DIRECTORY_TARGETS[target.toLowerCase()];
|
|
1031
|
-
if (targetInfo) {
|
|
1032
|
-
const targetPath = targetInfo.path || config.USER_DATA_DIR;
|
|
1033
|
-
console.log('正在打开: ' + targetInfo.label);
|
|
1034
|
-
const success = processManager.openUrl(targetPath);
|
|
1035
|
-
if (!success) {
|
|
1036
|
-
console.log('请手动打开: ' + targetPath);
|
|
1037
|
-
}
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
console.error('错误: 未知的目录目标 "' + target + '"');
|
|
1042
|
-
console.log('');
|
|
1043
|
-
console.log('可用目标:');
|
|
1044
|
-
console.log(' root (默认) 根目录');
|
|
1045
|
-
Object.entries(config.DIRECTORY_TARGETS).forEach(([key, val]) => {
|
|
1046
|
-
if (key !== 'root') {
|
|
1047
|
-
console.log(' ' + key.padEnd(14) + val.label);
|
|
1048
|
-
}
|
|
1049
|
-
});
|
|
1050
|
-
console.log('');
|
|
1051
|
-
process.exit(1);
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
console.log('');
|
|
1055
|
-
console.log('数据目录位置:');
|
|
1056
|
-
console.log(' 根目录: ' + config.USER_DATA_DIR);
|
|
1057
|
-
console.log(' 全局设置: ' + config.PATHS.settingsFile);
|
|
1058
|
-
console.log(' 内核目录: ' + config.DIRS.kernel);
|
|
1059
|
-
console.log(' 内核文件: ' + config.PATHS.mihomoBinary);
|
|
1060
|
-
console.log(' 订阅目录: ' + config.DIRS.subscriptions);
|
|
1061
|
-
console.log(' - cache.json (订阅缓存:更新时间、流量等)');
|
|
1062
|
-
console.log(' - xxx.yaml (订阅原始配置)');
|
|
1063
|
-
console.log(' 运行时目录: ' + config.DIRS.runtime);
|
|
1064
|
-
console.log(' - config.yaml (启动时生成,stop 自动清除)');
|
|
1065
|
-
console.log(' - pid (PID 文件,stop 自动清除)');
|
|
1066
|
-
console.log(' 日志文件: ' + config.PATHS.logFile);
|
|
1067
|
-
console.log(' mihomo 数据: ' + config.DIRS.data);
|
|
1068
|
-
console.log(' - cache.db, Geo*.dat 等 (mihomo 自行管理)');
|
|
1069
|
-
console.log('');
|
|
1070
|
-
console.log('打开目录:');
|
|
1071
|
-
console.log(' mihomo dir open 打开根目录');
|
|
1072
|
-
console.log(' mihomo dir open subs 打开订阅目录');
|
|
1073
|
-
console.log(' mihomo dir open logs 打开日志目录');
|
|
1074
|
-
console.log(' mihomo dir open runtime 打开运行时目录');
|
|
1075
|
-
console.log(' mihomo dir open kernel 打开内核目录');
|
|
1076
|
-
console.log('');
|
|
1077
|
-
console.log('环境变量:');
|
|
1078
|
-
console.log(' MIHOMO_CLI_DIR: 自定义根目录位置');
|
|
1079
|
-
console.log('');
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
async function main() {
|
|
1083
|
-
config.ensureDirs();
|
|
1084
|
-
|
|
1085
|
-
const args = process.argv.slice(2);
|
|
1086
|
-
|
|
1087
|
-
if (args.length === 0) {
|
|
1088
|
-
printStatus();
|
|
1089
|
-
printShortHelp();
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
const cmd = args[0].toLowerCase();
|
|
1094
|
-
|
|
1095
|
-
if (['help', '-h', '--help'].includes(cmd)) {
|
|
1096
|
-
printHelp();
|
|
1097
|
-
return;
|
|
1098
|
-
}
|
|
1099
|
-
if (['version', '-v', '--version'].includes(cmd)) {
|
|
1100
|
-
printVersion();
|
|
1101
|
-
return;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
switch (cmd) {
|
|
1105
|
-
case 'up':
|
|
1106
|
-
case 'start':
|
|
1107
|
-
await cmdStart(args);
|
|
1108
|
-
break;
|
|
1109
|
-
case 'tun':
|
|
1110
|
-
await cmdStart(['start', 'tun']);
|
|
1111
|
-
break;
|
|
1112
|
-
case 'down':
|
|
1113
|
-
case 'stop':
|
|
1114
|
-
await cmdStop();
|
|
1115
|
-
break;
|
|
1116
|
-
case 'status':
|
|
1117
|
-
printStatus();
|
|
1118
|
-
break;
|
|
1119
|
-
case 'log':
|
|
1120
|
-
cmdLog(args);
|
|
1121
|
-
break;
|
|
1122
|
-
case 'logs':
|
|
1123
|
-
cmdLogs(args);
|
|
1124
|
-
break;
|
|
1125
|
-
case 'open':
|
|
1126
|
-
cmdDirectory(['dir', 'open', ...args.slice(1)]);
|
|
1127
|
-
break;
|
|
1128
|
-
case 'ui':
|
|
1129
|
-
cmdUI(args);
|
|
1130
|
-
break;
|
|
1131
|
-
case 'kernel':
|
|
1132
|
-
await cmdKernel(args);
|
|
1133
|
-
break;
|
|
1134
|
-
case 'upd':
|
|
1135
|
-
case 'update':
|
|
1136
|
-
case 'upgrade':
|
|
1137
|
-
await cmdUpdate();
|
|
1138
|
-
break;
|
|
1139
|
-
case 'use':
|
|
1140
|
-
await cmdSubscription(['sub', 'use', ...args.slice(1)]);
|
|
1141
|
-
break;
|
|
1142
|
-
case 'sub':
|
|
1143
|
-
case 'subscription':
|
|
1144
|
-
case 'subscriptions':
|
|
1145
|
-
await cmdSubscription(args);
|
|
1146
|
-
break;
|
|
1147
|
-
case 'dir':
|
|
1148
|
-
case 'dirs':
|
|
1149
|
-
case 'directory':
|
|
1150
|
-
case 'directories':
|
|
1151
|
-
cmdDirectory(args);
|
|
1152
|
-
break;
|
|
1153
|
-
case 'reset':
|
|
1154
|
-
await cmdReset(args);
|
|
1155
|
-
break;
|
|
1156
|
-
case 'on':
|
|
1157
|
-
await cmdOverwrite(['ow', 'on']);
|
|
1158
|
-
break;
|
|
1159
|
-
case 'off':
|
|
1160
|
-
await cmdOverwrite(['ow', 'off']);
|
|
1161
|
-
break;
|
|
1162
|
-
case 'ow':
|
|
1163
|
-
case 'overwrite':
|
|
1164
|
-
await cmdOverwrite(args);
|
|
1165
|
-
break;
|
|
1166
|
-
default:
|
|
1167
|
-
console.error('未知命令: ' + cmd);
|
|
1168
|
-
console.error('使用 "mihomo help" 查看帮助');
|
|
1169
|
-
process.exit(1);
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
main().catch(e => {
|
|
1174
|
-
console.error('错误:', e.message);
|
|
1175
|
-
process.exit(1);
|
|
1176
|
-
});
|