mihomo-cli 1.0.3 → 1.1.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/index.js CHANGED
@@ -6,6 +6,7 @@ const config = require('./src/config');
6
6
  const kernel = require('./src/kernel');
7
7
  const subscription = require('./src/subscription');
8
8
  const processMgr = require('./src/process');
9
+ const overwrite = require('./src/overwrite');
9
10
 
10
11
  const VERSION = require('./package.json').version;
11
12
 
@@ -19,11 +20,11 @@ let exiting = false;
19
20
 
20
21
  process.on('SIGINT', () => {
21
22
  if (exiting) {
22
- console.log('\n 强制退出');
23
+ console.log('\n强制退出');
23
24
  process.exit(1);
24
25
  }
25
26
  exiting = true;
26
- console.log('\n 正在退出...');
27
+ console.log('\n正在退出...');
27
28
  process.exit(0);
28
29
  });
29
30
 
@@ -32,16 +33,16 @@ process.on('SIGTERM', () => {
32
33
  });
33
34
 
34
35
  process.on('uncaughtException', (e) => {
35
- console.error('\n 未捕获的异常: ' + e.message);
36
+ console.error('\n未捕获的异常: ' + e.message);
36
37
  if (e.stack) {
37
- console.error(' ' + e.stack.split('\n').slice(1).join('\n '));
38
+ console.error(e.stack.split('\n').slice(1).join('\n'));
38
39
  }
39
40
  process.exit(1);
40
41
  });
41
42
 
42
43
  process.on('unhandledRejection', (reason) => {
43
44
  const msg = reason instanceof Error ? reason.message : String(reason);
44
- console.error('\n 未处理的 Promise 拒绝: ' + msg);
45
+ console.error('\n未处理的 Promise 拒绝: ' + msg);
45
46
  process.exit(1);
46
47
  });
47
48
 
@@ -49,21 +50,20 @@ function printShortHelp() {
49
50
  console.log('\nmihomo-cli v' + VERSION);
50
51
  console.log('别名: mihomo, mmc, mh\n');
51
52
  console.log('命令:\n' +
52
- ' start [tun|mixed] 启动/切换代理(重复执行可切换模式)\n' +
53
- ' stop 停止代理\n' +
54
- ' status 查看状态\n' +
55
- ' log 实时日志(-o 打开文件)\n' +
56
- ' logs 日志列表(当前 + 历史归档)\n' +
57
- ' ui [zash|dash|yacd] Web 界面\n' +
58
- ' kernel 更新内核\n' +
59
- ' sub add <url> [name] 添加订阅\n' +
60
- ' sub update [name] 更新订阅(无参更新所有)\n' +
61
- ' sub use <name> 切换默认订阅\n' +
62
- ' sub web [name] 打开订阅页面\n' +
63
- ' sub list 列出订阅\n' +
64
- ' reset 重置配置\n' +
65
- ' dirs 数据目录\n' +
66
- ' version 版本信息\n');
53
+ ' start [tun|mixed] 启动/切换代理\n' +
54
+ ' stop 停止代理\n' +
55
+ ' status 查看状态\n' +
56
+ ' ui [zash|dash|yacd] Web 界面\n' +
57
+ ' log 实时日志\n' +
58
+ ' logs 日志列表\n' +
59
+ ' subscription add <url> 添加订阅(别名 sub)\n' +
60
+ ' subscription update 更新订阅\n' +
61
+ ' subscription use <name> 切换默认订阅\n' +
62
+ ' overwrite [on|off] 覆写配置(别名 ow)\n' +
63
+ ' directories 数据目录(别名 dir)\n' +
64
+ ' kernel 更新内核\n' +
65
+ ' reset 重置配置\n' +
66
+ ' version 版本信息\n');
67
67
  }
68
68
 
69
69
  function printHelp() {
@@ -74,30 +74,37 @@ function printHelp() {
74
74
  '用法:\n' +
75
75
  ' mihomo-cli <命令> [选项]\n' +
76
76
  '\n' +
77
- '命令:\n' +
78
- ' start [tun|mixed] 启动/切换代理 (默认 mixed, 重复执行可重启/切换模式)\n' +
79
- ' stop 停止代理\n' +
80
- ' status 查看状态\n' +
81
- ' log [-o] 实时日志(-o 用系统编辑器打开)\n' +
82
- ' logs [编号] [-n N] [-o] 列出/查看日志(0=当前,1+=归档,-n 指定行数,-o 打开)\n' +
83
- ' ui [zash|dash|yacd] 打开 Web UI (默认 zash)\n' +
84
- ' kernel [镜像|--no-mirror] 更新内核(指定镜像或 --no-mirror 直连)\n' +
85
- ' sub add <url> [name] 添加订阅\n' +
86
- ' sub update [name] 更新订阅 (无参更新所有)\n' +
87
- ' sub use <name> 设置默认订阅 (支持模糊匹配)\n' +
88
- ' sub web [name] 打开订阅页面\n' +
89
- ' sub list 列出订阅\n' +
90
- ' reset [--full] 重置用户数据 (--full 同时删除内核)\n' +
91
- ' dirs 显示数据目录位置\n' +
92
- ' help, -h 显示帮助\n' +
93
- ' version, -v 显示版本\n' +
77
+ '控制:\n' +
78
+ ' start [tun|mixed] 启动/切换代理 (默认 mixed)\n' +
79
+ ' stop 停止代理\n' +
80
+ ' status 查看状态\n' +
81
+ '\n' +
82
+ '界面:\n' +
83
+ ' ui [zash|dash|yacd] 打开 Web UI (默认 zash)\n' +
84
+ ' log [-o] 实时日志(-o 打开文件)\n' +
85
+ ' logs [编号] [-n N] [-o] 日志列表(0=当前,1+=归档)\n' +
86
+ '\n' +
87
+ '订阅:\n' +
88
+ ' subscription add <url> [name] 添加订阅(别名 sub)\n' +
89
+ ' subscription update [name] 更新订阅(无参更新所有)\n' +
90
+ ' subscription use <name> 设置默认订阅\n' +
91
+ ' subscription web [name] 打开订阅页面\n' +
92
+ '\n' +
93
+ '配置:\n' +
94
+ ' overwrite [on|off] 覆写配置(别名 ow)\n' +
95
+ ' directories [open] 数据目录(别名 dir)\n' +
96
+ '\n' +
97
+ '系统:\n' +
98
+ ' kernel [镜像|--no-mirror] 更新内核\n' +
99
+ ' reset [--full] 重置用户数据 (--full 同时删除内核)\n' +
100
+ ' help, -h 显示帮助\n' +
101
+ ' version, -v 显示版本\n' +
94
102
  '\n' +
95
103
  '示例:\n' +
96
- ' mihomo-cli start # 启动/重启 Mixed 模式\n' +
97
- ' mihomo-cli start tun # 启动/切换到 TUN 模式 (透明代理)\n' +
98
- ' mihomo-cli ui # 打开默认 UI (zash)\n' +
99
- ' mihomo-cli ui dash # 打开 metacubexd\n' +
100
- ' mihomo-cli ui yacd # 打开 YACD\n' +
104
+ ' mihomo-cli start # 启动/重启 Mixed 模式\n' +
105
+ ' mihomo-cli start tun # 切换到 TUN 透明代理模式\n' +
106
+ ' mihomo-cli sub add <url> # 添加订阅 (sub 是 subscription 别名)\n' +
107
+ ' mihomo-cli ui # 打开 Web UI\n' +
101
108
  '\n' +
102
109
  '模式说明:\n' +
103
110
  ' mixed HTTP + SOCKS5 混合端口 (默认)\n' +
@@ -118,24 +125,59 @@ function printVersion() {
118
125
  function printStatus() {
119
126
  const status = processMgr.getStatus();
120
127
  const info = subscription.getConfigInfo();
128
+ const owEnabled = overwrite.isOverwriteEnabled();
129
+ const owFiles = overwrite.listOverwriteFiles().files;
130
+ const activeSub = getActiveSubscription();
121
131
 
122
132
  console.log('');
123
- console.log(' 状态: ' + (status.running ? '运行中' : '已停止'));
133
+ let modeLabel = '';
134
+ if (info && status.running) {
135
+ modeLabel = info.tun ? ' (TUN)' : ' (Mixed)';
136
+ }
137
+ console.log('状态: ' + (status.running ? '运行中' : '已停止') + modeLabel);
138
+ console.log('内核: ' + (status.kernelVersion || '未安装'));
139
+
124
140
  if (status.pid) {
125
- console.log(' PID: ' + status.pid);
141
+ console.log('PID: ' + status.pid);
126
142
  if (status.processInfo) {
127
- console.log(' 内存: ' + status.processInfo.memory);
128
- if (status.processInfo.cpu) {
129
- console.log(' CPU: ' + status.processInfo.cpu);
130
- }
143
+ console.log('内存: ' + status.processInfo.memory);
131
144
  }
132
145
  }
146
+
133
147
  if (info) {
134
- console.log(' 节点: ' + info.proxies);
135
- console.log(' 端口: ' + info.port);
136
- console.log(' TUN: ' + (info.tun ? '启用' : '未启用'));
148
+ if (info.mixedPort) {
149
+ console.log('端口: ' + info.mixedPort);
150
+ } else {
151
+ let ports = [];
152
+ if (info.httpPort) ports.push('HTTP:' + info.httpPort);
153
+ if (info.socksPort) ports.push('SOCKS:' + info.socksPort);
154
+ console.log('端口: ' + (ports.length > 0 ? ports.join(', ') : '未知'));
155
+ }
156
+ }
157
+
158
+ if (activeSub) {
159
+ let subLine = '订阅: ' + activeSub.name;
160
+ if (info) {
161
+ let parts = [];
162
+ if (info.proxyGroups && info.proxyGroups > 0) {
163
+ parts.push(info.proxyGroups + ' 组');
164
+ }
165
+ parts.push(info.proxies + ' 节点');
166
+ subLine += ' (' + parts.join(', ') + ')';
167
+ }
168
+ console.log(subLine);
169
+ } else {
170
+ console.log('订阅: 未配置');
171
+ }
172
+
173
+ if (owEnabled && owFiles.length > 0) {
174
+ const names = owFiles.map(f => f.name).join(', ');
175
+ console.log('覆写: 已启用 (' + names + ')');
176
+ } else if (owEnabled) {
177
+ console.log('覆写: 已启用 (无文件)');
178
+ } else {
179
+ console.log('覆写: 已禁用');
137
180
  }
138
- console.log(' 内核: ' + (status.kernelVersion || '未安装'));
139
181
  console.log('');
140
182
  }
141
183
 
@@ -171,15 +213,15 @@ function findSubsFuzzy(subs, pattern) {
171
213
 
172
214
  function pickSingleSub(subs, pattern, actionName) {
173
215
  if (subs.length === 0) {
174
- console.error(' 错误: 未找到匹配 "' + pattern + '" 的订阅');
216
+ console.error('错误: 未找到匹配 "' + pattern + '" 的订阅');
175
217
  process.exit(1);
176
218
  }
177
219
  if (subs.length === 1) {
178
220
  return subs[0];
179
221
  }
180
- console.error(' 错误: 匹配到多个订阅,请更精确指定');
181
- console.log('\n 匹配的订阅:');
182
- subs.forEach(s => console.log(' ' + s.name));
222
+ console.error('错误: 匹配到多个订阅,请更精确指定');
223
+ console.log('\n匹配的订阅:');
224
+ subs.forEach(s => console.log(' ' + s.name));
183
225
  process.exit(1);
184
226
  }
185
227
 
@@ -212,10 +254,19 @@ function getNonFlagArg(args, startIdx) {
212
254
 
213
255
  function openLogFile(logPath, label) {
214
256
  const displayLabel = label || logPath;
215
- console.log(' 用系统默认程序打开: ' + displayLabel);
257
+ console.log('用系统默认程序打开: ' + displayLabel);
216
258
  const success = processMgr.openUrl(logPath);
217
259
  if (!success) {
218
- console.log(' 请手动打开: ' + logPath);
260
+ console.log('请手动打开: ' + logPath);
261
+ }
262
+ }
263
+
264
+ function openDir(dirPath, label) {
265
+ const displayLabel = label || dirPath;
266
+ console.log('正在打开: ' + displayLabel);
267
+ const success = processMgr.openUrl(dirPath);
268
+ if (!success) {
269
+ console.log('请手动打开: ' + dirPath);
219
270
  }
220
271
  }
221
272
 
@@ -223,11 +274,11 @@ function viewLogWithTail(logPath, options) {
223
274
  const follow = options && options.follow;
224
275
  const lines = (options && options.lines) || 100;
225
276
 
226
- console.log(' 日志: ' + logPath);
277
+ console.log('日志: ' + logPath);
227
278
  if (follow) {
228
- console.log(' 按 Ctrl+C 退出\n');
279
+ console.log('按 Ctrl+C 退出\n');
229
280
  } else {
230
- console.log(' 显示最后 ' + lines + ' 行\n');
281
+ console.log('显示最后 ' + lines + ' 行\n');
231
282
  }
232
283
 
233
284
  const tailArgs = [];
@@ -239,14 +290,14 @@ function viewLogWithTail(logPath, options) {
239
290
 
240
291
  tail.on('close', () => process.exit(0));
241
292
  tail.on('error', (e) => {
242
- console.error(' 无法读取日志: ' + e.message);
293
+ console.error('无法读取日志: ' + e.message);
243
294
  process.exit(1);
244
295
  });
245
296
  }
246
297
 
247
298
  async function cmdStart(args) {
248
299
  if (!config.hasKernel()) {
249
- console.error(' 错误: 未找到内核,请运行 \'mihomo-cli kernel\'');
300
+ console.error('错误: 未找到内核,请运行 \'mihomo-cli kernel\'');
250
301
  process.exit(1);
251
302
  }
252
303
 
@@ -254,7 +305,7 @@ async function cmdStart(args) {
254
305
 
255
306
  const sub = getActiveSubscription();
256
307
  if (!sub) {
257
- console.error(' 错误: 没有订阅,请先添加订阅');
308
+ console.error('错误: 没有订阅,请先添加订阅');
258
309
  process.exit(1);
259
310
  }
260
311
 
@@ -266,40 +317,42 @@ async function cmdStart(args) {
266
317
 
267
318
  if (hasProcess) {
268
319
  const count = status.allProcesses.length > 0 ? status.allProcesses.length : 1;
269
- console.log(' 停止 ' + count + ' 个进程...');
320
+ console.log('停止 ' + count + ' 个进程...');
270
321
  }
271
322
 
272
323
  // 总是调用 stop(即使没进程也会清理 PID 文件和运行时目录)
273
324
  const stopResult = processMgr.stop(true);
274
325
 
275
326
  if (stopResult.remaining && stopResult.remaining.length > 0) {
276
- console.error(' 部分进程无法终止: ' + stopResult.remaining.join(', '));
277
- console.error(' 请手动运行: sudo pkill -9 mihomo');
327
+ console.error('部分进程未终止: ' + stopResult.remaining.join(', '));
328
+ console.error('请手动运行: sudo pkill -9 mihomo');
278
329
  process.exit(1);
279
330
  }
280
331
 
281
332
  if (hasProcess) {
282
- console.log(' 已停止\n');
333
+ console.log('已停止\n');
283
334
  }
284
335
 
285
336
  let cfgInfo;
286
337
  try {
287
338
  cfgInfo = subscription.prepareConfigForStart(targetMode, sub.name);
288
339
  } catch (e) {
289
- console.error(' 配置错误: ' + e.message);
340
+ console.error('配置错误: ' + e.message);
290
341
  process.exit(1);
291
342
  }
292
343
 
293
344
  const modeLabel = targetMode === 'tun' ? 'TUN' : 'Mixed';
294
- const groupsLabel = cfgInfo.proxyGroups ? cfgInfo.proxyGroups + ' 组' : '';
295
- const countLabel = groupsLabel ? (groupsLabel + ' ' + cfgInfo.proxies + ' 节点') : (cfgInfo.proxies + ' 节点');
296
- console.log(' ' + [modeLabel, sub.name, countLabel].join(' · '));
345
+ const parts = [];
346
+ if (cfgInfo.proxyGroups && cfgInfo.proxyGroups > 0) parts.push(cfgInfo.proxyGroups + ' ');
347
+ parts.push(cfgInfo.proxies + ' 节点');
348
+ console.log([modeLabel, sub.name, parts.join(', ')].join(' · '));
297
349
 
298
350
  try {
299
351
  const result = await processMgr.start(targetMode);
300
- console.log(' 已启动 (PID ' + result.pid + ')');
352
+ console.log('已启动 (PID ' + result.pid + ')');
353
+ printStatus();
301
354
  } catch (e) {
302
- console.error(' 启动失败: ' + e.message.split('\n')[0]);
355
+ console.error('启动失败: ' + e.message.split('\n')[0]);
303
356
  process.exit(1);
304
357
  }
305
358
  }
@@ -307,19 +360,19 @@ async function cmdStart(args) {
307
360
  async function cmdStop() {
308
361
  const pids = processMgr.getAllMihomoPids();
309
362
  if (pids.length === 0) {
310
- console.log(' 未在运行');
363
+ console.log('未在运行');
311
364
  return;
312
365
  }
313
366
 
314
- console.log(' 停止 ' + pids.length + ' 个进程...');
367
+ console.log('停止 ' + pids.length + ' 个进程...');
315
368
  const result = processMgr.stop(true);
316
369
 
317
370
  if (result.remaining && result.remaining.length > 0) {
318
- console.error(' 部分进程未终止: ' + result.remaining.join(', '));
319
- console.error(' 请手动运行: sudo pkill -9 mihomo');
371
+ console.error('部分进程未终止: ' + result.remaining.join(', '));
372
+ console.error('请手动运行: sudo pkill -9 mihomo');
320
373
  process.exit(1);
321
374
  }
322
- console.log(' 已停止');
375
+ console.log('已停止');
323
376
  }
324
377
 
325
378
  function cmdUi(args) {
@@ -327,17 +380,17 @@ function cmdUi(args) {
327
380
  const url = UI_URLS[uiName];
328
381
 
329
382
  if (!url) {
330
- console.error(' 错误: 未知的 UI "' + uiName + '"');
331
- console.error(' 可用 UI: zash (默认), dash, yacd');
383
+ console.error('错误: 未知的 UI "' + uiName + '"');
384
+ console.error('可用 UI: zash (默认), dash, yacd');
332
385
  process.exit(1);
333
386
  }
334
387
 
335
- console.log(' 打开 Web UI: ' + uiName);
336
- console.log(' 地址: ' + url);
388
+ console.log('打开 Web UI: ' + uiName);
389
+ console.log('地址: ' + url);
337
390
 
338
391
  const success = processMgr.openUrl(url);
339
392
  if (!success) {
340
- console.log(' 请手动访问上面的地址');
393
+ console.log('请手动访问上面的地址');
341
394
  }
342
395
  }
343
396
 
@@ -368,8 +421,8 @@ function cmdLogs(args) {
368
421
  }
369
422
 
370
423
  if (!logPath) {
371
- console.error(' 错误: 未找到日志 "' + targetName + '"');
372
- console.log(' 使用 "mihomo-cli logs" 查看可用日志列表');
424
+ console.error('错误: 未找到日志 "' + targetName + '"');
425
+ console.log('使用 "mihomo-cli logs" 查看可用日志列表');
373
426
  process.exit(1);
374
427
  }
375
428
 
@@ -391,12 +444,12 @@ function cmdLogs(args) {
391
444
  all.push(...logs.archives);
392
445
 
393
446
  if (all.length === 0) {
394
- console.log(' 暂无日志');
447
+ console.log('暂无日志');
395
448
  return;
396
449
  }
397
450
 
398
451
  console.log('');
399
- console.log(' 日志列表:');
452
+ console.log('日志列表:');
400
453
  console.log('');
401
454
 
402
455
  all.forEach((log, idx) => {
@@ -405,19 +458,19 @@ function cmdLogs(args) {
405
458
  const size = subscription.formatBytes(log.size);
406
459
  const name = log.isCurrent ? 'mihomo.log (当前运行中)' : log.name;
407
460
 
408
- console.log(' ' + num + '. ' + name);
409
- console.log(' 时间: ' + time + ' 大小: ' + size);
461
+ console.log(' ' + num + '. ' + name);
462
+ console.log(' 时间: ' + time + ' 大小: ' + size);
410
463
  if (!log.isCurrent) {
411
- console.log(' 查看: mihomo-cli logs ' + idx + ' 或 mihomo-cli logs -o ' + idx);
464
+ console.log(' 查看: mihomo-cli logs ' + idx + ' 或 mihomo-cli logs -o ' + idx);
412
465
  }
413
466
  console.log('');
414
467
  });
415
468
 
416
- console.log(' 用法:');
417
- console.log(' mihomo-cli logs 0 # 查看当前日志 (最后 100 行)');
418
- console.log(' mihomo-cli logs 1 # 查看第 1 个归档日志');
419
- console.log(' mihomo-cli logs 1 -n 200 # 查看 200 行');
420
- console.log(' mihomo-cli logs 1 -o # 用系统默认程序打开');
469
+ console.log('用法:');
470
+ console.log(' mihomo-cli logs 0 # 查看当前日志 (最后 100 行)');
471
+ console.log(' mihomo-cli logs 1 # 查看第 1 个归档日志');
472
+ console.log(' mihomo-cli logs 1 -n 200 # 查看 200 行');
473
+ console.log(' mihomo-cli logs 1 -o # 用系统默认程序打开');
421
474
  console.log('');
422
475
  }
423
476
 
@@ -473,107 +526,111 @@ async function cmdKernel(args) {
473
526
  const effectiveMirror = mirrorInfo.isOverride ? mirrorInfo.mirror : config.getGitHubMirror();
474
527
  const isDefault = !mirrorInfo.isOverride && effectiveMirror === config.DEFAULT_GITHUB_MIRROR;
475
528
 
476
- console.log(' 检查内核更新...');
529
+ console.log('检查内核更新...');
477
530
 
478
531
  if (mirrorInfo.isOverride) {
479
532
  if (effectiveMirror === null) {
480
- console.log(' 镜像: 直连(命令行指定 --no-mirror)');
533
+ console.log('镜像: 直连(命令行指定 --no-mirror)');
481
534
  } else {
482
- console.log(' 镜像: ' + effectiveMirror + ' (命令行指定)');
535
+ console.log('镜像: ' + effectiveMirror + ' (命令行指定)');
483
536
  }
484
537
  } else {
485
- console.log(' 镜像: ' + (effectiveMirror || '直连(无镜像)') + (isDefault && effectiveMirror ? ' (默认)' : ''));
538
+ console.log('镜像: ' + (effectiveMirror || '直连(无镜像)') + (isDefault && effectiveMirror ? ' (默认)' : ''));
486
539
  }
487
540
 
488
- console.log('\n 可用镜像:');
541
+ console.log('\n可用镜像:');
489
542
  config.AVAILABLE_MIRRORS.forEach(m => {
490
543
  const isCurrent = effectiveMirror && (
491
544
  effectiveMirror.includes('//' + m + '/') ||
492
545
  effectiveMirror.includes('//' + m + ':') ||
493
546
  effectiveMirror.endsWith('//' + m)
494
547
  );
495
- console.log(' ' + m + (isCurrent ? ' (当前)' : ''));
548
+ console.log(' ' + m + (isCurrent ? ' (当前)' : ''));
496
549
  });
497
550
 
498
- console.log('\n 用法:');
499
- console.log(' mihomo-cli kernel # 使用默认镜像');
500
- console.log(' mihomo-cli kernel hk.gh-proxy.org # 使用指定镜像');
501
- console.log(' mihomo-cli kernel --mirror hk.gh-proxy.org');
502
- console.log(' mihomo-cli kernel --no-mirror # 直连,不使用镜像');
551
+ console.log('\n用法:');
552
+ console.log(' mihomo-cli kernel # 使用默认镜像');
553
+ console.log(' mihomo-cli kernel hk.gh-proxy.org # 使用指定镜像');
554
+ console.log(' mihomo-cli kernel --mirror hk.gh-proxy.org');
555
+ console.log(' mihomo-cli kernel --no-mirror # 直连,不使用镜像');
503
556
  console.log('');
504
557
 
505
558
  try {
506
559
  const info = await kernel.checkUpdate();
507
- console.log(' 当前: ' + info.current);
508
- console.log(' 最新: ' + info.latest);
560
+ console.log('当前: ' + info.current);
561
+ console.log('最新: ' + info.latest);
509
562
 
510
563
  if (!info.needsUpdate) {
511
- console.log(' 已是最新版本');
564
+ console.log('已是最新版本');
512
565
  return;
513
566
  }
514
567
 
515
- console.log('\n 正在下载...');
568
+ console.log('\n正在下载...');
516
569
  const result = await kernel.downloadKernel((msg) => {
517
- console.log(' ' + msg);
570
+ console.log(msg);
518
571
  }, mirrorInfo.mirror); // 传递镜像参数(undefined = 用配置,null = 禁用)
519
- console.log(' 已更新到 ' + result.version);
572
+ console.log('已更新到 ' + result.version);
520
573
  } catch (e) {
521
- console.error(' 更新失败: ' + e.message);
574
+ console.error('更新失败: ' + e.message);
522
575
  process.exit(1);
523
576
  }
524
577
  }
525
578
 
526
- async function cmdSub(args) {
527
- const action = args[1];
579
+ async function printSubList() {
580
+ const updateResult = await subscription.autoUpdateStaleSubscriptions();
581
+ if (updateResult.total > 0) {
582
+ console.log('');
583
+ }
528
584
 
529
- if (!action || action === 'list') {
530
- const updateResult = await subscription.autoUpdateStaleSubscriptions();
531
- if (updateResult.total > 0) {
532
- console.log('');
585
+ const subs = config.getSubscriptionsWithCache();
586
+ if (subs.length === 0) {
587
+ console.log('没有订阅');
588
+ console.log('');
589
+ console.log('添加订阅: mihomo-cli sub add <url> [name]');
590
+ console.log('');
591
+ return;
592
+ }
593
+ console.log('订阅列表:');
594
+ subs.forEach((s, i) => {
595
+ const time = subscription.formatDate(s.updated_at);
596
+ const defaultMark = i === 0 ? ' [默认]' : '';
597
+ const interval = s.update_interval || subscription.DEFAULT_UPDATE_INTERVAL_HOURS;
598
+ console.log(' ' + (i + 1) + '. ' + s.name + defaultMark);
599
+ console.log(' 更新: ' + time + ' (间隔: ' + interval + 'h)');
600
+
601
+ if (s.username) {
602
+ console.log(' 用户: ' + s.username);
533
603
  }
534
-
535
- const subs = config.getSubscriptionsWithCache();
536
- if (subs.length === 0) {
537
- console.log(' 没有订阅');
538
- console.log('\n 添加订阅:');
539
- console.log(' mihomo-cli sub add <url> [name]');
540
- return;
604
+ if (s.download !== undefined || s.total !== undefined) {
605
+ const used = (s.upload || 0) + (s.download || 0);
606
+ const usedStr = subscription.formatBytes(used);
607
+ const totalStr = subscription.formatBytes(s.total);
608
+ let percentStr = '';
609
+ if (s.total && s.total > 0) {
610
+ const percent = Math.min((used / s.total) * 100, 100);
611
+ percentStr = ' (' + percent.toFixed(1) + '%)';
612
+ }
613
+ console.log(' 流量: ' + usedStr + ' / ' + totalStr + percentStr);
614
+ }
615
+ if (s.expire !== undefined) {
616
+ console.log(' 到期: ' + subscription.formatTimestamp(s.expire));
541
617
  }
542
- console.log(' 订阅列表:');
543
- subs.forEach((s, i) => {
544
- const time = subscription.formatDate(s.updatedAt);
545
- const defaultMark = i === 0 ? ' [默认]' : '';
546
- const interval = s.updateInterval || subscription.DEFAULT_UPDATE_INTERVAL_HOURS;
547
- console.log(' ' + (i + 1) + '. ' + s.name + defaultMark);
548
- console.log(' 更新: ' + time + ' (间隔: ' + interval + 'h)');
618
+ if (s.web_page_url) {
619
+ console.log(' 页面: ' + s.web_page_url);
620
+ }
621
+ });
622
+ console.log('');
623
+ console.log('切换默认: sub use <name>');
624
+ console.log('更新订阅: sub update [name]');
625
+ console.log('打开页面: sub web [name]');
626
+ console.log('');
627
+ }
549
628
 
550
- if (s.username) {
551
- console.log(' 用户: ' + s.username);
552
- }
553
- if (s.download !== undefined || s.total !== undefined) {
554
- const used = (s.upload || 0) + (s.download || 0);
555
- const usedStr = subscription.formatBytes(used);
556
- const totalStr = subscription.formatBytes(s.total);
557
- let percentStr = '';
558
- if (s.total && s.total > 0) {
559
- const percent = Math.min((used / s.total) * 100, 100);
560
- percentStr = ' (' + percent.toFixed(1) + '%)';
561
- }
562
- console.log(' 流量: ' + usedStr + ' / ' + totalStr + percentStr);
563
- }
564
- if (s.expire !== undefined) {
565
- console.log(' 到期: ' + subscription.formatTimestamp(s.expire));
566
- }
567
- if (s.webPageUrl) {
568
- console.log(' 页面: ' + s.webPageUrl);
569
- }
570
- });
571
- console.log('\n 切换默认订阅:');
572
- console.log(' mihomo-cli sub use <name>');
573
- console.log(' 更新订阅:');
574
- console.log(' mihomo-cli sub update [name]');
575
- console.log(' 打开订阅页面:');
576
- console.log(' mihomo-cli sub web [name]');
629
+ async function cmdSub(args) {
630
+ const action = args[1];
631
+
632
+ if (!action || action === 'list') {
633
+ await printSubList();
577
634
  return;
578
635
  }
579
636
 
@@ -582,19 +639,24 @@ async function cmdSub(args) {
582
639
  const name = args[3] || 'default';
583
640
 
584
641
  if (!url || !url.startsWith('http')) {
585
- console.error(' 错误: 请提供有效的订阅 URL');
642
+ console.error('错误: 请提供有效的订阅 URL');
586
643
  process.exit(1);
587
644
  }
588
645
 
589
- console.log(' 添加订阅: ' + name);
646
+ console.log('添加订阅: ' + name);
590
647
  try {
591
648
  config.addSubscription(url, name);
592
649
  const info = await subscription.downloadSubscription(url, name);
593
- console.log(' 已添加 (节点: ' + info.proxies + ')');
650
+ const parts = [];
651
+ if (info.proxyGroups && info.proxyGroups > 0) parts.push(info.proxyGroups + ' 组');
652
+ parts.push(info.proxies + ' 节点');
653
+ console.log('已添加 (' + parts.join(', ') + ')');
594
654
  } catch (e) {
595
- console.error(' 添加失败: ' + e.message);
655
+ console.error('添加失败: ' + e.message);
596
656
  process.exit(1);
597
657
  }
658
+ console.log('');
659
+ await printSubList();
598
660
  return;
599
661
  }
600
662
 
@@ -603,37 +665,47 @@ async function cmdSub(args) {
603
665
  const subs = config.getSubscriptions();
604
666
 
605
667
  if (subs.length === 0) {
606
- console.error(' 错误: 没有订阅');
668
+ console.error('错误: 没有订阅');
607
669
  process.exit(1);
608
670
  }
609
671
 
610
672
  if (!name) {
611
- console.log(' 更新所有 ' + subs.length + ' 个订阅...');
673
+ console.log('更新所有 ' + subs.length + ' 个订阅...');
612
674
  const results = await Promise.all(subs.map(subscription.tryUpdateOne));
613
675
  let ok = 0;
614
676
  results.forEach(r => {
615
677
  if (r.success) {
616
678
  ok++;
617
- console.log(' ✓ ' + r.name + ': ' + r.proxies + ' 节点');
679
+ const parts = [];
680
+ if (r.proxyGroups && r.proxyGroups > 0) parts.push(r.proxyGroups + ' 组');
681
+ parts.push(r.proxies + ' 节点');
682
+ console.log('✓ ' + r.name + ': 已更新 (' + parts.join(', ') + ')');
618
683
  } else {
619
- console.log(' ✗ ' + r.name + ': 失败 (' + r.error.split('\n')[0] + ')');
684
+ console.log('✗ ' + r.name + ': 失败 (' + r.error.split('\n')[0] + ')');
620
685
  }
621
686
  });
622
687
  if (ok === 0) process.exit(1);
688
+ console.log('');
689
+ await printSubList();
623
690
  return;
624
691
  }
625
692
 
626
693
  const matches = findSubsFuzzy(subs, name);
627
694
  const target = pickSingleSub(matches, name, '更新');
628
695
 
629
- console.log(' 更新订阅: ' + target.name);
696
+ console.log('更新订阅: ' + target.name);
630
697
  try {
631
698
  const info = await subscription.downloadSubscription(target.url, target.name);
632
- console.log(' 已更新 (节点: ' + info.proxies + ')');
699
+ const parts = [];
700
+ if (info.proxyGroups && info.proxyGroups > 0) parts.push(info.proxyGroups + ' 组');
701
+ parts.push(info.proxies + ' 节点');
702
+ console.log('已更新 (' + parts.join(', ') + ')');
633
703
  } catch (e) {
634
- console.error(' 更新失败: ' + e.message);
704
+ console.error('更新失败: ' + e.message);
635
705
  process.exit(1);
636
706
  }
707
+ console.log('');
708
+ await printSubList();
637
709
  return;
638
710
  }
639
711
 
@@ -642,10 +714,10 @@ async function cmdSub(args) {
642
714
  const subs = config.getSubscriptions();
643
715
 
644
716
  if (!name) {
645
- console.error(' 错误: 请指定订阅名称');
717
+ console.error('错误: 请指定订阅名称');
646
718
  if (subs.length > 0) {
647
- console.log('\n 可用订阅:');
648
- subs.forEach(s => console.log(' ' + s.name));
719
+ console.log('\n可用订阅:');
720
+ subs.forEach(s => console.log(' ' + s.name));
649
721
  }
650
722
  process.exit(1);
651
723
  }
@@ -655,11 +727,13 @@ async function cmdSub(args) {
655
727
 
656
728
  const success = config.setDefaultSubscription(target.name);
657
729
  if (success) {
658
- console.log(' 已设置 "' + target.name + '" 为默认订阅');
730
+ console.log('已设置 "' + target.name + '" 为默认订阅');
659
731
  } else {
660
- console.error(' 错误: 未找到订阅 "' + name + '"');
732
+ console.error('错误: 未找到订阅 "' + name + '"');
661
733
  process.exit(1);
662
734
  }
735
+ console.log('');
736
+ await printSubList();
663
737
  return;
664
738
  }
665
739
 
@@ -668,7 +742,7 @@ async function cmdSub(args) {
668
742
  const subs = config.getSubscriptionsWithCache();
669
743
 
670
744
  if (subs.length === 0) {
671
- console.error(' 错误: 没有订阅');
745
+ console.error('错误: 没有订阅');
672
746
  process.exit(1);
673
747
  }
674
748
 
@@ -680,33 +754,35 @@ async function cmdSub(args) {
680
754
  target = subs[0];
681
755
  }
682
756
 
683
- let webPageUrl = target.webPageUrl;
757
+ let webPageUrl = target.web_page_url;
684
758
  if (!webPageUrl) {
685
- console.log(' 订阅信息中缺少页面地址,正在更新订阅...');
759
+ console.log('订阅信息中缺少页面地址,正在更新订阅...');
686
760
  try {
687
761
  const info = await subscription.downloadSubscription(target.url, target.name);
688
- if (info.webPageUrl) {
689
- webPageUrl = info.webPageUrl;
762
+ // 重新读取缓存获取 web_page_url
763
+ const cache = config.readSubscriptionsCache();
764
+ if (cache[target.name] && cache[target.name].web_page_url) {
765
+ webPageUrl = cache[target.name].web_page_url;
690
766
  } else {
691
- console.error(' 错误: 该订阅没有提供页面地址');
767
+ console.error('错误: 该订阅没有提供页面地址');
692
768
  process.exit(1);
693
769
  }
694
770
  } catch (e) {
695
- console.error(' 更新失败: ' + e.message);
771
+ console.error('更新失败: ' + e.message);
696
772
  process.exit(1);
697
773
  }
698
774
  }
699
775
 
700
- console.log(' 打开订阅页面: ' + webPageUrl);
776
+ console.log('打开订阅页面: ' + webPageUrl);
701
777
  const opened = processMgr.openUrl(webPageUrl);
702
778
  if (!opened) {
703
- console.log(' 请手动访问上面的地址');
779
+ console.log('请手动访问上面的地址');
704
780
  }
705
781
  return;
706
782
  }
707
783
 
708
- console.error(' 错误: 未知的订阅命令');
709
- console.log(' 用法: mihomo-cli sub [list|add|update|use|web]');
784
+ console.error('错误: 未知的订阅命令');
785
+ console.log('用法: mihomo-cli sub [list|add|update|use|web]');
710
786
  process.exit(1);
711
787
  }
712
788
 
@@ -716,7 +792,7 @@ async function cmdReset(args) {
716
792
 
717
793
  const pids = processMgr.getAllMihomoPids();
718
794
  if (pids.length > 0) {
719
- console.log(' 停止 ' + pids.length + ' 个进程...');
795
+ console.log('停止 ' + pids.length + ' 个进程...');
720
796
  processMgr.cleanupAll(true);
721
797
  for (let i = 0; i < 50; i++) {
722
798
  if (processMgr.getAllMihomoPids().length === 0) break;
@@ -725,7 +801,7 @@ async function cmdReset(args) {
725
801
  }
726
802
 
727
803
  const mode = fullReset ? '完整重置 (含内核)' : '重置配置';
728
- console.log(' ' + mode);
804
+ console.log(mode);
729
805
 
730
806
  if (!skipConfirm) {
731
807
  const readline = require('readline');
@@ -735,39 +811,143 @@ async function cmdReset(args) {
735
811
  });
736
812
 
737
813
  const answer = await new Promise(resolve => {
738
- rl.question(' 确认? (y/N) ', (a) => {
814
+ rl.question('确认? (y/N) ', (a) => {
739
815
  rl.close();
740
816
  resolve(a);
741
817
  });
742
818
  });
743
819
 
744
820
  if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
745
- console.log(' 已取消');
821
+ console.log('已取消');
746
822
  return;
747
823
  }
748
824
  }
749
825
 
750
826
  const count = config.resetUserData({ keepKernel: !fullReset });
751
- console.log(' 已重置 ' + count + ' 项');
827
+ console.log('已重置 ' + count + ' 项');
752
828
  }
753
829
 
754
- function cmdDirs() {
830
+ function printOverwriteList() {
831
+ const info = overwrite.listOverwriteFiles();
755
832
  console.log('');
756
- console.log(' 数据目录位置:');
757
- console.log(' 根目录: ' + config.USER_DATA_DIR);
758
- console.log(' 全局设置: ' + config.PATHS.settingsFile);
759
- console.log(' 内核文件: ' + config.PATHS.mihomoBinary);
760
- console.log(' 订阅目录: ' + config.DIRS.subs);
761
- console.log(' - xxx.yaml (订阅原始配置,不修改)');
762
- console.log(' 运行时目录: ' + config.DIRS.runtime);
763
- console.log(' - config.yaml (启动时生成,stop 自动清除)');
764
- console.log(' - pid (PID 文件,stop 自动清除)');
765
- console.log(' 日志文件: ' + config.PATHS.logFile);
766
- console.log(' mihomo 数据: ' + config.DIRS.data);
767
- console.log(' - cache.db, Geo*.dat 等 (mihomo 自行管理)');
833
+ console.log('状态: ' + (info.enabled ? '已启用' : '已禁用'));
834
+ console.log('目录: ' + info.dir);
768
835
  console.log('');
769
- console.log(' 环境变量:');
770
- console.log(' MIHOMO_CLI_DIR: 自定义根目录位置');
836
+ if (info.files.length === 0) {
837
+ console.log('暂无覆写文件');
838
+ console.log('');
839
+ console.log('用法示例: 创建文件 ' + path.join(info.dir, '01-custom.yaml'));
840
+ console.log('');
841
+ } else {
842
+ console.log('覆写文件 (' + info.files.length + ' 个,按顺序加载):');
843
+ console.log('');
844
+ info.files.forEach((f, i) => {
845
+ const num = i < 10 ? ' ' + i : '' + i;
846
+ console.log(' ' + num + '. ' + f.name);
847
+ if (f.keys.length > 0) {
848
+ console.log(' 字段: ' + f.keys.join(', '));
849
+ }
850
+ });
851
+ console.log('');
852
+ }
853
+ console.log('启用覆写: ow on');
854
+ console.log('禁用覆写: ow off');
855
+ console.log('');
856
+ }
857
+
858
+ function cmdOverwrite(args) {
859
+ const action = args && args[1];
860
+
861
+ if (action === 'on' || action === 'enable') {
862
+ overwrite.setOverwriteEnabled(true);
863
+ console.log('已启用覆写配置');
864
+ console.log('');
865
+ printOverwriteList();
866
+ return;
867
+ }
868
+
869
+ if (action === 'off' || action === 'disable') {
870
+ overwrite.setOverwriteEnabled(false);
871
+ console.log('已禁用覆写配置');
872
+ console.log('');
873
+ printOverwriteList();
874
+ return;
875
+ }
876
+
877
+ // 无参数、list、ls 都显示文件列表
878
+ printOverwriteList();
879
+ }
880
+
881
+ function cmdDirs(args) {
882
+ const action = args && args[1];
883
+
884
+ // dirs open [root|subs|logs|data|runtime|config|kernel]
885
+ if (action === 'open') {
886
+ const target = args[2];
887
+
888
+ if (!target || target === 'root') {
889
+ openDir(config.USER_DATA_DIR, '根目录');
890
+ return;
891
+ }
892
+
893
+ const dirMap = {
894
+ 'subs': { path: config.DIRS.subscriptions, label: '订阅目录' },
895
+ 'subscriptions': { path: config.DIRS.subscriptions, label: '订阅目录' },
896
+ 'logs': { path: config.DIRS.logs, label: '日志目录' },
897
+ 'data': { path: config.DIRS.data, label: 'mihomo 数据目录' },
898
+ 'runtime': { path: config.DIRS.runtime, label: '运行时目录' },
899
+ };
900
+
901
+ const fileMap = {
902
+ 'config': { path: config.PATHS.settingsFile, label: '设置文件' },
903
+ 'settings': { path: config.PATHS.settingsFile, label: '设置文件' },
904
+ 'kernel': { path: config.DIRS.core, label: '内核目录' },
905
+ };
906
+
907
+ const targetInfo = dirMap[target] || fileMap[target];
908
+ if (targetInfo) {
909
+ openDir(targetInfo.path, targetInfo.label);
910
+ return;
911
+ }
912
+
913
+ console.error('错误: 未知的目录目标 "' + target + '"');
914
+ console.log('');
915
+ console.log('可用目标:');
916
+ console.log(' root (默认) 根目录');
917
+ console.log(' subs 订阅目录');
918
+ console.log(' logs 日志目录');
919
+ console.log(' data mihomo 数据目录');
920
+ console.log(' runtime 运行时目录');
921
+ console.log(' config 设置文件 (settings.json)');
922
+ console.log(' kernel 内核目录');
923
+ console.log('');
924
+ process.exit(1);
925
+ }
926
+
927
+ // 无参数或未知参数:显示目录列表
928
+ console.log('');
929
+ console.log('数据目录位置:');
930
+ console.log(' 根目录: ' + config.USER_DATA_DIR);
931
+ console.log(' 全局设置: ' + config.PATHS.settingsFile);
932
+ console.log(' 内核文件: ' + config.PATHS.mihomoBinary);
933
+ console.log(' 订阅目录: ' + config.DIRS.subscriptions);
934
+ console.log(' - cache.json (订阅缓存:更新时间、流量等)');
935
+ console.log(' - xxx.yaml (订阅原始配置)');
936
+ console.log(' 运行时目录: ' + config.DIRS.runtime);
937
+ console.log(' - config.yaml (启动时生成,stop 自动清除)');
938
+ console.log(' - pid (PID 文件,stop 自动清除)');
939
+ console.log(' 日志文件: ' + config.PATHS.logFile);
940
+ console.log(' mihomo 数据: ' + config.DIRS.data);
941
+ console.log(' - cache.db, Geo*.dat 等 (mihomo 自行管理)');
942
+ console.log('');
943
+ console.log('打开目录:');
944
+ console.log(' mihomo-cli dirs open 打开根目录');
945
+ console.log(' mihomo-cli dirs open subscriptions 打开订阅目录');
946
+ console.log(' mihomo-cli dirs open logs 打开日志目录');
947
+ console.log(' mihomo-cli dirs open config 打开设置文件');
948
+ console.log('');
949
+ console.log('环境变量:');
950
+ console.log(' MIHOMO_CLI_DIR: 自定义根目录位置');
771
951
  console.log('');
772
952
  }
773
953
 
@@ -817,22 +997,30 @@ async function main() {
817
997
  break;
818
998
  case 'sub':
819
999
  case 'subscription':
1000
+ case 'subscriptions':
820
1001
  await cmdSub(args);
821
1002
  break;
1003
+ case 'dir':
822
1004
  case 'dirs':
823
- cmdDirs();
1005
+ case 'directory':
1006
+ case 'directories':
1007
+ cmdDirs(args);
824
1008
  break;
825
1009
  case 'reset':
826
1010
  await cmdReset(args);
827
1011
  break;
1012
+ case 'overwrite':
1013
+ case 'ow':
1014
+ cmdOverwrite(args);
1015
+ break;
828
1016
  default:
829
- console.error(' 未知命令: ' + cmd);
830
- console.error(' 使用 "mihomo-cli help" 查看帮助');
1017
+ console.error('未知命令: ' + cmd);
1018
+ console.error('使用 "mihomo-cli help" 查看帮助');
831
1019
  process.exit(1);
832
1020
  }
833
1021
  }
834
1022
 
835
1023
  main().catch(e => {
836
- console.error(' 错误:', e.message);
1024
+ console.error('错误:', e.message);
837
1025
  process.exit(1);
838
1026
  });