mihomo-cli 1.5.0 → 2.0.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/src/config.js DELETED
@@ -1,516 +0,0 @@
1
- // 内置模块
2
- const path = require('path');
3
- const fs = require('fs');
4
- const os = require('os');
5
- const { execSync } = require('child_process');
6
-
7
- // 第三方模块
8
- const yaml = require('js-yaml');
9
-
10
- // 本地模块
11
- // (无额外本地模块,overwrite.js 在 buildConfig 中延迟加载以避免循环依赖)
12
-
13
- const IS_PKG = typeof process.pkg !== 'undefined';
14
-
15
- let PROJECT_ROOT;
16
- if (IS_PKG) {
17
- PROJECT_ROOT = path.dirname(process.execPath);
18
- } else {
19
- PROJECT_ROOT = path.join(__dirname, '..');
20
- }
21
-
22
- function getUserDataDir() {
23
- if (process.env.MIHOMO_CLI_DIR) {
24
- return process.env.MIHOMO_CLI_DIR;
25
- }
26
- return path.join(os.homedir(), '.mihomo-cli');
27
- }
28
-
29
- const USER_DATA_DIR = getUserDataDir();
30
-
31
- const DIRS = {
32
- root: PROJECT_ROOT,
33
- kernel: path.join(USER_DATA_DIR, 'kernel'),
34
- subscriptions: path.join(USER_DATA_DIR, 'subscriptions'),
35
- logs: path.join(USER_DATA_DIR, 'logs'),
36
- data: path.join(USER_DATA_DIR, 'data'),
37
- runtime: path.join(USER_DATA_DIR, 'runtime'),
38
- };
39
-
40
- const PATHS = {
41
- root: DIRS.root,
42
- mihomoBinary: path.join(DIRS.kernel, 'mihomo'),
43
- settingsFile: path.join(USER_DATA_DIR, 'settings.json'),
44
- subscriptionsCacheFile: path.join(DIRS.subscriptions, 'cache.json'),
45
- configFile: path.join(DIRS.runtime, 'config.yaml'),
46
- logFile: path.join(DIRS.logs, 'mihomo.log'),
47
- pidFile: path.join(DIRS.runtime, 'pid'),
48
- configStage1Subscription: path.join(DIRS.runtime, '1.subscription.yaml'),
49
- configStage2Overwrite: path.join(DIRS.runtime, '2.overwrite.yaml'),
50
- configStage3System: path.join(DIRS.runtime, '3.system.yaml'),
51
- };
52
-
53
- function ensureDirs() {
54
- Object.values(DIRS).forEach(dir => {
55
- if (!fs.existsSync(dir)) {
56
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
57
- }
58
- });
59
- }
60
-
61
- function maskUrl(url) {
62
- if (!url) return url;
63
- try {
64
- const parsed = new URL(url);
65
- const tokenKeys = ['token', 'key', 'secret', 'pass', 'password', 'auth', 'access_token', 'api_key'];
66
- for (const key of tokenKeys) {
67
- if (parsed.searchParams.has(key)) {
68
- parsed.searchParams.set(key, '***');
69
- }
70
- }
71
- if (parsed.username) {
72
- parsed.username = '***';
73
- }
74
- if (parsed.password) {
75
- parsed.password = '***';
76
- }
77
- return parsed.toString();
78
- } catch {
79
- if (url.length > 30) {
80
- return url.slice(0, 15) + '...' + url.slice(-10);
81
- }
82
- return url;
83
- }
84
- }
85
-
86
- let settingsCache = null;
87
-
88
- function readSettings() {
89
- if (settingsCache !== null) return settingsCache;
90
- ensureDirs();
91
- if (fs.existsSync(PATHS.settingsFile)) {
92
- try {
93
- const content = fs.readFileSync(PATHS.settingsFile, 'utf8');
94
- settingsCache = JSON.parse(content);
95
- return settingsCache;
96
- } catch (_e) {
97
- console.warn('警告: settings.json 格式损坏,使用默认设置(原文件已保留)');
98
- settingsCache = {};
99
- return settingsCache;
100
- }
101
- }
102
- settingsCache = {};
103
- return settingsCache;
104
- }
105
-
106
- function writeSettings(settings) {
107
- ensureDirs();
108
- const existing = readSettings();
109
- const merged = { ...existing, ...settings };
110
- // undefined 值表示删除该键
111
- for (const key of Object.keys(settings)) {
112
- if (settings[key] === undefined) delete merged[key];
113
- }
114
- fs.writeFileSync(PATHS.settingsFile, JSON.stringify(merged, null, 2), { mode: 0o600 });
115
- settingsCache = merged;
116
- return merged;
117
- }
118
-
119
- // GitHub 镜像配置
120
- const DEFAULT_GITHUB_MIRROR = 'https://v6.gh-proxy.org/';
121
- const AVAILABLE_MIRRORS = ['gh-proxy.org', 'v6.gh-proxy.org', 'hk.gh-proxy.org', 'cdn.gh-proxy.org'];
122
-
123
- function getGitHubMirror() {
124
- const settings = readSettings();
125
- // 空字符串或 false 表示禁用镜像
126
- if (settings.github_mirror === '' || settings.github_mirror === false) {
127
- return null;
128
- }
129
- return settings.github_mirror || DEFAULT_GITHUB_MIRROR;
130
- }
131
-
132
- function setGitHubMirror(mirror) {
133
- // mirror 取值:
134
- // - 完整 URL: 'https://hk.gh-proxy.org/'
135
- // - 短域名: 'hk.gh-proxy.org'
136
- // - '' 或 false: 禁用镜像
137
- // - null 或 undefined: 恢复默认
138
-
139
- if (mirror === null || mirror === undefined) {
140
- writeSettings({ github_mirror: undefined });
141
- return DEFAULT_GITHUB_MIRROR;
142
- }
143
-
144
- if (mirror === '' || mirror === false) {
145
- writeSettings({ github_mirror: '' });
146
- return null;
147
- }
148
-
149
- let mirrorUrl = mirror;
150
- if (!mirrorUrl.startsWith('http')) {
151
- mirrorUrl = 'https://' + mirrorUrl;
152
- }
153
- if (!mirrorUrl.endsWith('/')) {
154
- mirrorUrl += '/';
155
- }
156
-
157
- writeSettings({ github_mirror: mirrorUrl });
158
- return mirrorUrl;
159
- }
160
-
161
- // 订阅缓存读写(动态数据:流量、用户名、更新时间等)
162
- function readSubscriptionCache() {
163
- ensureDirs();
164
- if (fs.existsSync(PATHS.subscriptionsCacheFile)) {
165
- try {
166
- const content = fs.readFileSync(PATHS.subscriptionsCacheFile, 'utf8');
167
- return JSON.parse(content);
168
- } catch (_e) {
169
- return {};
170
- }
171
- }
172
- return {};
173
- }
174
-
175
- function writeSubscriptionCache(cache) {
176
- ensureDirs();
177
- fs.writeFileSync(PATHS.subscriptionsCacheFile, JSON.stringify(cache, null, 2), { mode: 0o600 });
178
- }
179
-
180
- function saveSubscriptionCache(subName, data) {
181
- const cache = readSubscriptionCache();
182
- cache[subName] = { ...cache[subName], ...data };
183
- writeSubscriptionCache(cache);
184
- }
185
-
186
- function getSubscriptions() {
187
- const settings = readSettings();
188
- return settings.subscriptions || [];
189
- }
190
-
191
- // 获取合并了缓存数据的订阅列表
192
- function getSubscriptionsWithCache() {
193
- const subs = getSubscriptions();
194
- const cache = readSubscriptionCache();
195
- return subs.map(s => ({
196
- ...s,
197
- ...(cache[s.name] || {}),
198
- }));
199
- }
200
-
201
- function addSubscription(url, name) {
202
- if (name === undefined) name = 'default';
203
- const settings = readSettings();
204
- const subs = settings.subscriptions || [];
205
- const existingIndex = subs.findIndex(s => s.name === name);
206
- if (existingIndex >= 0) {
207
- subs[existingIndex] = { name, url };
208
- } else {
209
- subs.push({ name, url });
210
- }
211
- const updates = { subscriptions: subs };
212
- if (!settings.active_subscription && subs.length === 1) {
213
- updates.active_subscription = name;
214
- }
215
- writeSettings(updates);
216
- }
217
-
218
- function setDefaultSubscription(name) {
219
- const settings = readSettings();
220
- const subs = settings.subscriptions || [];
221
- const idx = subs.findIndex(s => s.name === name);
222
- if (idx < 0) {
223
- return false;
224
- }
225
- writeSettings({ active_subscription: name });
226
- return true;
227
- }
228
-
229
- function getSubscriptionRawConfigPath(subName) {
230
- return path.join(DIRS.subscriptions, subName + '.yaml');
231
- }
232
-
233
- function saveSubscriptionRawConfig(subName, content) {
234
- ensureDirs();
235
- const filePath = getSubscriptionRawConfigPath(subName);
236
- fs.writeFileSync(filePath, content, { mode: 0o600 });
237
- }
238
-
239
- function readSubscriptionRawConfig(subName) {
240
- const filePath = getSubscriptionRawConfigPath(subName);
241
- if (!fs.existsSync(filePath)) {
242
- return null;
243
- }
244
- return fs.readFileSync(filePath, 'utf8');
245
- }
246
-
247
- function hasKernel() {
248
- return fs.existsSync(PATHS.mihomoBinary);
249
- }
250
-
251
- let kernelVersionCache = null;
252
- let kernelVersionCached = false;
253
-
254
- function getKernelVersion() {
255
- if (!hasKernel()) {
256
- kernelVersionCache = null;
257
- kernelVersionCached = false;
258
- return null;
259
- }
260
- if (kernelVersionCached) return kernelVersionCache;
261
- try {
262
- const output = execSync('"' + PATHS.mihomoBinary + '" -v 2>&1 || true', {
263
- encoding: 'utf8',
264
- }).trim();
265
- if (output) {
266
- const match = output.match(/v?[\d]+\.[\d]+\.[\d]+/);
267
- kernelVersionCache = match ? match[0] : output;
268
- } else {
269
- kernelVersionCache = 'unknown';
270
- }
271
- } catch (_e) {
272
- kernelVersionCache = 'unknown';
273
- }
274
- kernelVersionCached = true;
275
- return kernelVersionCache;
276
- }
277
-
278
- const TUN_CONFIG = {
279
- tun: {
280
- enable: true,
281
- stack: 'mixed',
282
- 'dns-hijack': ['0.0.0.0:53'],
283
- 'auto-route': true,
284
- 'auto-detect-interface': true,
285
- 'strict-route': true,
286
- },
287
- };
288
-
289
- const BASE_CONFIG = {
290
- 'log-level': 'warning',
291
- 'geodata-mode': true,
292
- 'geo-update-interval': 24,
293
- 'geox-url': {
294
- geoip: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip-lite.dat',
295
- geosite: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite-lite.dat',
296
- mmdb: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/country-lite.mmdb',
297
- asn: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/GeoLite2-ASN.mmdb',
298
- },
299
- };
300
-
301
- function parseYamlOrJson(content, errorMsg) {
302
- if (!content || !content.trim()) {
303
- throw new Error((errorMsg || '内容') + '为空');
304
- }
305
- try {
306
- const result = yaml.load(content);
307
- if (result !== undefined) return result;
308
- } catch (_e) {}
309
- try {
310
- return JSON.parse(content);
311
- } catch (_e2) {
312
- throw new Error((errorMsg || '内容') + '格式错误,无法解析为 YAML 或 JSON');
313
- }
314
- }
315
-
316
- function buildConfig(subRawContent, mode) {
317
- const subscriptionConfig = parseYamlOrJson(subRawContent, '订阅内容');
318
-
319
- if (!subscriptionConfig) {
320
- throw new Error('订阅内容为空');
321
- }
322
-
323
- // 延迟加载以避免循环依赖
324
- const overwrite = require('./overwrite');
325
-
326
- // 应用覆写配置
327
- const overwriteEnabled = overwrite.isOverwriteEnabled();
328
- const withOverwrites = overwrite.applyOverwrite(subscriptionConfig);
329
- const overwriteFiles = overwriteEnabled ? overwrite.loadOverwriteFile() : [];
330
-
331
- // 构建系统覆盖值(BASE_CONFIG + 可选 TUN)
332
- // 只补充订阅中缺失的字段,不覆盖已有值
333
- const systemConfig = {};
334
- for (const [key, value] of Object.entries(BASE_CONFIG)) {
335
- if (!(key in withOverwrites)) {
336
- systemConfig[key] = value;
337
- }
338
- }
339
-
340
- if (mode === 'tun') {
341
- // tun 块始终由系统控制
342
- systemConfig.tun = TUN_CONFIG.tun;
343
- // dns 只补充 TUN 必需的字段
344
- const subDns = withOverwrites.dns || {};
345
- systemConfig.dns = {};
346
- if (!subDns.enable) systemConfig.dns.enable = true;
347
- if (!subDns['enhanced-mode']) systemConfig.dns['enhanced-mode'] = 'fake-ip';
348
- if (!subDns['fake-ip-range']) systemConfig.dns['fake-ip-range'] = '198.18.0.1/16';
349
- // 如果没有需要补充的 dns 字段,则不设置
350
- if (Object.keys(systemConfig.dns).length === 0) {
351
- delete systemConfig.dns;
352
- }
353
- }
354
-
355
- // 合并:订阅(+overwrite) → 系统补充
356
- const merged = { ...withOverwrites, ...systemConfig };
357
-
358
- // dns 需要深度合并:保留订阅的 DNS 服务器,叠加系统补充
359
- if (systemConfig.dns) {
360
- merged.dns = { ...(withOverwrites.dns || {}), ...systemConfig.dns };
361
- }
362
-
363
- return {
364
- config: merged,
365
- subscriptionConfig,
366
- overwriteFiles,
367
- systemConfig,
368
- };
369
- }
370
-
371
- function writeMihomoConfig(configObj) {
372
- ensureDirs();
373
- const content = yaml.dump(configObj, {
374
- indent: 2,
375
- lineWidth: -1,
376
- noCompat: true,
377
- });
378
- fs.writeFileSync(PATHS.configFile, content, { mode: 0o600 });
379
- }
380
-
381
- function writeDebugConfig(buildResult) {
382
- ensureDirs();
383
- const dumpOpts = { indent: 2, lineWidth: -1, noCompat: true };
384
-
385
- // 1. 订阅原始配置
386
- fs.writeFileSync(PATHS.configStage1Subscription, yaml.dump(buildResult.subscriptionConfig, dumpOpts), { mode: 0o600 });
387
-
388
- // 2. overwrite 覆写内容(禁用时写空文件)
389
- const overwriteMerged = {};
390
- for (const f of buildResult.overwriteFiles) {
391
- Object.assign(overwriteMerged, f.config);
392
- }
393
- const overwriteContent = buildResult.overwriteFiles.length > 0 ? yaml.dump(overwriteMerged, dumpOpts) : '# overwrite 已禁用或无覆写文件\n';
394
- fs.writeFileSync(PATHS.configStage2Overwrite, overwriteContent, { mode: 0o600 });
395
-
396
- // 3. 系统覆盖值(BASE_CONFIG + TUN_CONFIG)
397
- fs.writeFileSync(PATHS.configStage3System, yaml.dump(buildResult.systemConfig, dumpOpts), { mode: 0o600 });
398
- }
399
-
400
- function hasConfig() {
401
- return fs.existsSync(PATHS.configFile);
402
- }
403
-
404
- function getConfigInfo() {
405
- if (!hasConfig()) {
406
- return null;
407
- }
408
-
409
- try {
410
- const content = fs.readFileSync(PATHS.configFile, 'utf8');
411
- const cfg = yaml.load(content);
412
-
413
- if (!cfg) return null;
414
-
415
- return {
416
- proxies: cfg.proxies ? cfg.proxies.length : 0,
417
- proxyGroups: cfg['proxy-groups'] ? cfg['proxy-groups'].length : 0,
418
- mode: cfg.mode || 'rule',
419
- mixedPort: cfg['mixed-port'] || null,
420
- httpPort: cfg.port || null,
421
- socksPort: cfg['socks-port'] || null,
422
- tun: cfg.tun ? cfg.tun.enable : false,
423
- };
424
- } catch (_e) {
425
- return null;
426
- }
427
- }
428
-
429
- function rmrf(dir) {
430
- fs.rmSync(dir, { recursive: true, force: true });
431
- }
432
-
433
- function resetUserData(options) {
434
- if (options === undefined) options = {};
435
- const keepKernel = options.keepKernel !== false;
436
- const kernelOnly = options.kernelOnly === true;
437
-
438
- let itemsToRemove;
439
- if (kernelOnly) {
440
- itemsToRemove = [DIRS.kernel];
441
- } else {
442
- itemsToRemove = [PATHS.settingsFile, DIRS.subscriptions, DIRS.logs, DIRS.data, DIRS.runtime];
443
- if (!keepKernel) {
444
- itemsToRemove.push(DIRS.kernel);
445
- }
446
- }
447
-
448
- let removedCount = 0;
449
- for (const item of itemsToRemove) {
450
- if (fs.existsSync(item)) {
451
- try {
452
- rmrf(item);
453
- removedCount++;
454
- } catch (e) {
455
- console.warn(' 警告: 无法删除 ' + item + ': ' + e.message);
456
- }
457
- }
458
- }
459
-
460
- if (!kernelOnly) {
461
- ensureDirs();
462
- settingsCache = null;
463
- }
464
- return removedCount;
465
- }
466
-
467
- // 目录目标映射(从 index.js 移入,精确匹配)
468
- const DIRECTORY_TARGETS = {
469
- root: { path: null, label: '根目录' },
470
- subs: { path: DIRS.subscriptions, label: '订阅目录' },
471
- logs: { path: DIRS.logs, label: '日志目录' },
472
- data: { path: DIRS.data, label: 'mihomo 数据目录' },
473
- runtime: { path: DIRS.runtime, label: '运行时目录' },
474
- kernel: { path: DIRS.kernel, label: '内核目录' },
475
- };
476
-
477
- module.exports = {
478
- PATHS,
479
- DIRS,
480
- USER_DATA_DIR,
481
- DIRECTORY_TARGETS,
482
- ensureDirs,
483
- readSettings,
484
- writeSettings,
485
- readSubscriptionCache,
486
- saveSubscriptionCache,
487
- maskUrl,
488
- getSubscriptions,
489
- getSubscriptionsWithCache,
490
- addSubscription,
491
- setDefaultSubscription,
492
- saveSubscriptionRawConfig,
493
- readSubscriptionRawConfig,
494
- hasKernel,
495
- getKernelVersion,
496
- clearKernelVersionCache: () => {
497
- kernelVersionCache = null;
498
- kernelVersionCached = false;
499
- },
500
- getGitHubMirror,
501
- setGitHubMirror,
502
- DEFAULT_GITHUB_MIRROR,
503
- AVAILABLE_MIRRORS,
504
- parseYamlOrJson,
505
- buildConfig,
506
- writeMihomoConfig,
507
- writeDebugConfig,
508
- hasConfig,
509
- getConfigInfo,
510
- resetUserData,
511
- invalidateSettingsCache: () => {
512
- settingsCache = null;
513
- },
514
- fsExistsSync: p => fs.existsSync(p),
515
- rmrf,
516
- };