mihomo-cli 1.3.1 → 1.4.0

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