mihomo-cli 1.2.2 → 1.2.4
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 +41 -0
- package/README.md +44 -42
- package/index.js +134 -160
- package/package.json +1 -1
- package/src/config.js +32 -35
- package/src/kernel.js +16 -12
- package/src/overwrite.js +8 -8
- package/src/process.js +57 -91
- package/src/subscription.js +10 -40
- package/src/utils.js +101 -0
package/src/config.js
CHANGED
|
@@ -34,8 +34,6 @@ const DIRS = {
|
|
|
34
34
|
|
|
35
35
|
const PATHS = {
|
|
36
36
|
root: DIRS.root,
|
|
37
|
-
data: DIRS.data,
|
|
38
|
-
userDataDir: USER_DATA_DIR,
|
|
39
37
|
mihomoBinary: path.join(DIRS.core, 'mihomo'),
|
|
40
38
|
settingsFile: path.join(USER_DATA_DIR, 'settings.json'),
|
|
41
39
|
subscriptionsCacheFile: path.join(DIRS.subscriptions, 'cache.json'),
|
|
@@ -77,36 +75,41 @@ function maskUrl(url) {
|
|
|
77
75
|
}
|
|
78
76
|
}
|
|
79
77
|
|
|
78
|
+
let _settingsCache = null;
|
|
79
|
+
|
|
80
80
|
function readSettings() {
|
|
81
|
+
if (_settingsCache !== null) return _settingsCache;
|
|
81
82
|
ensureDirs();
|
|
82
83
|
if (fs.existsSync(PATHS.settingsFile)) {
|
|
83
84
|
try {
|
|
84
85
|
const content = fs.readFileSync(PATHS.settingsFile, 'utf8');
|
|
85
|
-
|
|
86
|
+
_settingsCache = JSON.parse(content);
|
|
87
|
+
return _settingsCache;
|
|
86
88
|
} catch (e) {
|
|
87
|
-
|
|
89
|
+
_settingsCache = {};
|
|
90
|
+
return _settingsCache;
|
|
88
91
|
}
|
|
89
92
|
}
|
|
90
|
-
|
|
93
|
+
_settingsCache = {};
|
|
94
|
+
return _settingsCache;
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
function writeSettings(settings) {
|
|
94
98
|
ensureDirs();
|
|
95
99
|
const existing = readSettings();
|
|
96
100
|
const merged = { ...existing, ...settings };
|
|
101
|
+
// undefined 值表示删除该键
|
|
102
|
+
for (const key of Object.keys(settings)) {
|
|
103
|
+
if (settings[key] === undefined) delete merged[key];
|
|
104
|
+
}
|
|
97
105
|
fs.writeFileSync(PATHS.settingsFile, JSON.stringify(merged, null, 2), { mode: 0o600 });
|
|
106
|
+
_settingsCache = merged;
|
|
98
107
|
return merged;
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
// GitHub 镜像配置
|
|
102
111
|
const DEFAULT_GITHUB_MIRROR = 'https://v6.gh-proxy.org/';
|
|
103
|
-
const AVAILABLE_MIRRORS = [
|
|
104
|
-
'v6.gh-proxy.org',
|
|
105
|
-
'gh-proxy.org',
|
|
106
|
-
'hk.gh-proxy.org',
|
|
107
|
-
'cdn.gh-proxy.org',
|
|
108
|
-
'edgeone.gh-proxy.org',
|
|
109
|
-
];
|
|
112
|
+
const AVAILABLE_MIRRORS = ['v6.gh-proxy.org', 'gh-proxy.org', 'hk.gh-proxy.org', 'cdn.gh-proxy.org', 'edgeone.gh-proxy.org'];
|
|
110
113
|
|
|
111
114
|
function getGitHubMirror() {
|
|
112
115
|
const settings = readSettings();
|
|
@@ -125,9 +128,7 @@ function setGitHubMirror(mirror) {
|
|
|
125
128
|
// - null 或 undefined: 恢复默认
|
|
126
129
|
|
|
127
130
|
if (mirror === null || mirror === undefined) {
|
|
128
|
-
|
|
129
|
-
delete settings.github_mirror;
|
|
130
|
-
writeSettings(settings);
|
|
131
|
+
writeSettings({ github_mirror: undefined });
|
|
131
132
|
return DEFAULT_GITHUB_MIRROR;
|
|
132
133
|
}
|
|
133
134
|
|
|
@@ -239,24 +240,28 @@ function hasKernel() {
|
|
|
239
240
|
return fs.existsSync(PATHS.mihomoBinary);
|
|
240
241
|
}
|
|
241
242
|
|
|
243
|
+
let _kernelVersionCache = undefined;
|
|
244
|
+
|
|
242
245
|
function getKernelVersion() {
|
|
243
246
|
if (!hasKernel()) {
|
|
247
|
+
_kernelVersionCache = undefined;
|
|
244
248
|
return null;
|
|
245
249
|
}
|
|
250
|
+
if (_kernelVersionCache !== undefined) return _kernelVersionCache;
|
|
246
251
|
try {
|
|
247
252
|
const output = execSync('"' + PATHS.mihomoBinary + '" -v 2>&1 || true', {
|
|
248
253
|
encoding: 'utf8',
|
|
249
254
|
}).trim();
|
|
250
255
|
if (output) {
|
|
251
256
|
const match = output.match(/v?[\d]+\.[\d]+\.[\d]+/);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
return output;
|
|
257
|
+
_kernelVersionCache = match ? match[0] : output;
|
|
258
|
+
return _kernelVersionCache;
|
|
256
259
|
}
|
|
257
|
-
|
|
260
|
+
_kernelVersionCache = 'unknown';
|
|
261
|
+
return _kernelVersionCache;
|
|
258
262
|
} catch (e) {
|
|
259
|
-
|
|
263
|
+
_kernelVersionCache = 'unknown';
|
|
264
|
+
return _kernelVersionCache;
|
|
260
265
|
}
|
|
261
266
|
}
|
|
262
267
|
|
|
@@ -310,7 +315,7 @@ function buildConfig(subRawContent, mode) {
|
|
|
310
315
|
const overwrite = require('./overwrite');
|
|
311
316
|
|
|
312
317
|
// 应用覆写配置
|
|
313
|
-
const withOverwrites = overwrite.
|
|
318
|
+
const withOverwrites = overwrite.applyOverwrite(baseConfig);
|
|
314
319
|
|
|
315
320
|
// 合并 BASE_CONFIG(优先级高于覆写)
|
|
316
321
|
const merged = { ...withOverwrites, ...BASE_CONFIG };
|
|
@@ -377,13 +382,7 @@ function resetUserData(options) {
|
|
|
377
382
|
if (options === undefined) options = {};
|
|
378
383
|
const keepKernel = options.keepKernel !== false;
|
|
379
384
|
|
|
380
|
-
const itemsToRemove = [
|
|
381
|
-
PATHS.settingsFile,
|
|
382
|
-
DIRS.subscriptions,
|
|
383
|
-
DIRS.logs,
|
|
384
|
-
DIRS.data,
|
|
385
|
-
DIRS.runtime,
|
|
386
|
-
];
|
|
385
|
+
const itemsToRemove = [PATHS.settingsFile, DIRS.subscriptions, DIRS.logs, DIRS.data, DIRS.runtime];
|
|
387
386
|
|
|
388
387
|
if (!keepKernel) {
|
|
389
388
|
itemsToRemove.push(DIRS.core);
|
|
@@ -402,37 +401,35 @@ function resetUserData(options) {
|
|
|
402
401
|
}
|
|
403
402
|
|
|
404
403
|
ensureDirs();
|
|
404
|
+
_settingsCache = null;
|
|
405
405
|
return removedCount;
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
module.exports = {
|
|
409
409
|
PATHS,
|
|
410
410
|
DIRS,
|
|
411
|
-
PROJECT_ROOT,
|
|
412
411
|
USER_DATA_DIR,
|
|
413
|
-
IS_PKG,
|
|
414
412
|
ensureDirs,
|
|
415
413
|
readSettings,
|
|
416
414
|
writeSettings,
|
|
417
415
|
readSubscriptionsCache,
|
|
418
|
-
writeSubscriptionsCache,
|
|
419
416
|
saveSubscriptionCache,
|
|
420
417
|
maskUrl,
|
|
421
418
|
getSubscriptions,
|
|
422
419
|
getSubscriptionsWithCache,
|
|
423
420
|
addSubscription,
|
|
424
421
|
setDefaultSubscription,
|
|
425
|
-
getSubRawConfigPath,
|
|
426
422
|
saveSubRawConfig,
|
|
427
423
|
readSubRawConfig,
|
|
428
424
|
hasKernel,
|
|
429
425
|
getKernelVersion,
|
|
426
|
+
clearKernelVersionCache: () => {
|
|
427
|
+
_kernelVersionCache = undefined;
|
|
428
|
+
},
|
|
430
429
|
getGitHubMirror,
|
|
431
430
|
setGitHubMirror,
|
|
432
431
|
DEFAULT_GITHUB_MIRROR,
|
|
433
432
|
AVAILABLE_MIRRORS,
|
|
434
|
-
TUN_CONFIG,
|
|
435
|
-
BASE_CONFIG,
|
|
436
433
|
parseYamlOrJson,
|
|
437
434
|
buildConfig,
|
|
438
435
|
writeMihomoConfig,
|
package/src/kernel.js
CHANGED
|
@@ -31,8 +31,7 @@ function getArch() {
|
|
|
31
31
|
function findMatchingAsset(assets, platform, arch) {
|
|
32
32
|
const prefix = 'mihomo-' + platform + '-' + arch;
|
|
33
33
|
const matchingAssets = assets.filter(a => {
|
|
34
|
-
return (a.name.startsWith(prefix) && a.name.endsWith('.gz')) ||
|
|
35
|
-
(a.name.startsWith(prefix + '-') && a.name.endsWith('.gz'));
|
|
34
|
+
return (a.name.startsWith(prefix) && a.name.endsWith('.gz')) || (a.name.startsWith(prefix + '-') && a.name.endsWith('.gz'));
|
|
36
35
|
});
|
|
37
36
|
|
|
38
37
|
if (matchingAssets.length === 0) {
|
|
@@ -67,11 +66,12 @@ async function getLatestRelease(repo) {
|
|
|
67
66
|
throw new Error('无法获取版本信息');
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
const stableReleases = releases.filter(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
const stableReleases = releases.filter(
|
|
70
|
+
r =>
|
|
71
|
+
!r.prerelease &&
|
|
72
|
+
!r.tag_name.toLowerCase().includes('alpha') &&
|
|
73
|
+
!r.tag_name.toLowerCase().includes('beta') &&
|
|
74
|
+
!r.tag_name.toLowerCase().includes('prerelease'),
|
|
75
75
|
);
|
|
76
76
|
|
|
77
77
|
if (stableReleases.length > 0) {
|
|
@@ -194,14 +194,18 @@ async function downloadKernel(progressCallback, mirror) {
|
|
|
194
194
|
extractedBinary = outputPath;
|
|
195
195
|
}
|
|
196
196
|
} catch (e) {
|
|
197
|
-
try {
|
|
197
|
+
try {
|
|
198
|
+
fs.unlinkSync(tempPath);
|
|
199
|
+
} catch {}
|
|
198
200
|
throw new Error('解压失败: ' + e.message);
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
const foundBinary = extractedBinary || findBinaryInDir(extractPath);
|
|
202
204
|
|
|
203
205
|
if (!foundBinary) {
|
|
204
|
-
try {
|
|
206
|
+
try {
|
|
207
|
+
fs.unlinkSync(tempPath);
|
|
208
|
+
} catch {}
|
|
205
209
|
throw new Error('解压后未找到可执行文件');
|
|
206
210
|
}
|
|
207
211
|
|
|
@@ -223,6 +227,9 @@ async function downloadKernel(progressCallback, mirror) {
|
|
|
223
227
|
fs.unlinkSync(tempPath);
|
|
224
228
|
} catch (e) {}
|
|
225
229
|
|
|
230
|
+
// 内核已更新,清除版本缓存
|
|
231
|
+
config.clearKernelVersionCache();
|
|
232
|
+
|
|
226
233
|
return {
|
|
227
234
|
version: latest.tag_name,
|
|
228
235
|
path: targetPath,
|
|
@@ -230,9 +237,6 @@ async function downloadKernel(progressCallback, mirror) {
|
|
|
230
237
|
}
|
|
231
238
|
|
|
232
239
|
module.exports = {
|
|
233
|
-
getArch,
|
|
234
|
-
getLatestRelease,
|
|
235
|
-
findMatchingAsset,
|
|
236
240
|
checkUpdate,
|
|
237
241
|
downloadKernel,
|
|
238
242
|
};
|
package/src/overwrite.js
CHANGED
|
@@ -171,7 +171,7 @@ function setOverwriteEnabled(enabled) {
|
|
|
171
171
|
* 读取 overwrites 目录下的所有 yaml 文件
|
|
172
172
|
* 按文件名排序返回
|
|
173
173
|
*/
|
|
174
|
-
function
|
|
174
|
+
function loadOverwriteFile() {
|
|
175
175
|
const dir = getOverwritesDir();
|
|
176
176
|
|
|
177
177
|
if (!fs.existsSync(dir)) {
|
|
@@ -197,7 +197,7 @@ function loadOverwriteFiles() {
|
|
|
197
197
|
});
|
|
198
198
|
}
|
|
199
199
|
} catch (e) {
|
|
200
|
-
|
|
200
|
+
console.warn('警告: 覆写文件 "' + file + '" 解析失败: ' + e.message);
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
@@ -207,12 +207,12 @@ function loadOverwriteFiles() {
|
|
|
207
207
|
/**
|
|
208
208
|
* 应用所有覆写配置到基础配置
|
|
209
209
|
*/
|
|
210
|
-
function
|
|
210
|
+
function applyOverwrite(baseConfig) {
|
|
211
211
|
if (!isOverwriteEnabled()) {
|
|
212
212
|
return baseConfig;
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
const overwriteFiles =
|
|
215
|
+
const overwriteFiles = loadOverwriteFile();
|
|
216
216
|
|
|
217
217
|
if (overwriteFiles.length === 0) {
|
|
218
218
|
return baseConfig;
|
|
@@ -230,8 +230,8 @@ function applyOverwrites(baseConfig) {
|
|
|
230
230
|
/**
|
|
231
231
|
* 列出覆写文件信息
|
|
232
232
|
*/
|
|
233
|
-
function
|
|
234
|
-
const files =
|
|
233
|
+
function listOverwriteFile() {
|
|
234
|
+
const files = loadOverwriteFile();
|
|
235
235
|
const enabled = isOverwriteEnabled();
|
|
236
236
|
const dir = getOverwritesDir();
|
|
237
237
|
|
|
@@ -249,6 +249,6 @@ function listOverwriteFiles() {
|
|
|
249
249
|
module.exports = {
|
|
250
250
|
isOverwriteEnabled,
|
|
251
251
|
setOverwriteEnabled,
|
|
252
|
-
|
|
253
|
-
|
|
252
|
+
applyOverwrite,
|
|
253
|
+
listOverwriteFile,
|
|
254
254
|
};
|
package/src/process.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { spawn, execSync } = require('child_process');
|
|
4
4
|
const config = require('./config');
|
|
5
|
+
const utils = require('./utils');
|
|
5
6
|
|
|
6
7
|
function clearRuntime() {
|
|
7
8
|
if (fs.existsSync(config.DIRS.runtime)) {
|
|
@@ -22,21 +23,9 @@ function getPid() {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
function isProcessRunning(pid) {
|
|
26
|
-
if (!pid) return false;
|
|
27
|
-
try {
|
|
28
|
-
const output = execSync('ps -p ' + pid + ' -o pid= 2>/dev/null || true', {
|
|
29
|
-
encoding: 'utf8',
|
|
30
|
-
}).trim();
|
|
31
|
-
return output.length > 0;
|
|
32
|
-
} catch (e) {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
26
|
function isRunning() {
|
|
38
27
|
const pid = getPid();
|
|
39
|
-
return pid ? isProcessRunning(pid) : false;
|
|
28
|
+
return pid ? utils.isProcessRunning(pid) : false;
|
|
40
29
|
}
|
|
41
30
|
|
|
42
31
|
function getAllMihomoPids() {
|
|
@@ -47,23 +36,15 @@ function getAllMihomoPids() {
|
|
|
47
36
|
encoding: 'utf8',
|
|
48
37
|
}).trim();
|
|
49
38
|
if (!output) return [];
|
|
50
|
-
return output
|
|
39
|
+
return output
|
|
40
|
+
.split('\n')
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.map(p => parseInt(p));
|
|
51
43
|
} catch {
|
|
52
44
|
return [];
|
|
53
45
|
}
|
|
54
46
|
}
|
|
55
47
|
|
|
56
|
-
function isProcessRoot(pid) {
|
|
57
|
-
try {
|
|
58
|
-
const uidOutput = execSync('ps -p ' + pid + ' -o uid= 2>/dev/null || true', {
|
|
59
|
-
encoding: 'utf8',
|
|
60
|
-
}).trim();
|
|
61
|
-
return uidOutput === '0';
|
|
62
|
-
} catch (e) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
48
|
function isPidFileOwnedByRoot() {
|
|
68
49
|
if (!fs.existsSync(config.PATHS.pidFile)) {
|
|
69
50
|
return false;
|
|
@@ -78,7 +59,7 @@ function isPidFileOwnedByRoot() {
|
|
|
78
59
|
|
|
79
60
|
function checkStaleState() {
|
|
80
61
|
const allPids = getAllMihomoPids();
|
|
81
|
-
const hasRootProcess = allPids.some(p => isProcessRoot(p));
|
|
62
|
+
const hasRootProcess = allPids.some(p => utils.isProcessRoot(p));
|
|
82
63
|
const hasRootPidFile = isPidFileOwnedByRoot();
|
|
83
64
|
|
|
84
65
|
return {
|
|
@@ -176,7 +157,7 @@ function cleanupAll(forceSudo) {
|
|
|
176
157
|
return { killed: 0, failed: 0, remaining: [] };
|
|
177
158
|
}
|
|
178
159
|
|
|
179
|
-
const hasRootProcess = pids.some(p => isProcessRoot(p));
|
|
160
|
+
const hasRootProcess = pids.some(p => utils.isProcessRoot(p));
|
|
180
161
|
const hasRootPidFile = isPidFileOwnedByRoot();
|
|
181
162
|
const needsSudo = hasRootProcess;
|
|
182
163
|
const allowSudo = forceSudo || hasRootProcess || hasRootPidFile;
|
|
@@ -208,9 +189,7 @@ function cleanupAll(forceSudo) {
|
|
|
208
189
|
|
|
209
190
|
for (let i = 0; i < 50; i++) {
|
|
210
191
|
if (getAllMihomoPids().length === 0) break;
|
|
211
|
-
|
|
212
|
-
execSync('sleep 0.1', { stdio: 'ignore' });
|
|
213
|
-
} catch (e) {}
|
|
192
|
+
utils.sleepSync(100);
|
|
214
193
|
}
|
|
215
194
|
|
|
216
195
|
clearPid();
|
|
@@ -227,14 +206,25 @@ function createTunLaunchScript() {
|
|
|
227
206
|
const configFile = config.PATHS.configFile;
|
|
228
207
|
const logFile = config.PATHS.logFile;
|
|
229
208
|
const pidFile = config.PATHS.pidFile;
|
|
230
|
-
const dataDir = config.
|
|
231
|
-
|
|
232
|
-
const scriptContent =
|
|
233
|
-
'
|
|
234
|
-
'
|
|
235
|
-
|
|
236
|
-
'
|
|
237
|
-
'
|
|
209
|
+
const dataDir = config.DIRS.data;
|
|
210
|
+
|
|
211
|
+
const scriptContent =
|
|
212
|
+
'#!/bin/bash\n' +
|
|
213
|
+
'BINARY="' +
|
|
214
|
+
binary +
|
|
215
|
+
'"\n' +
|
|
216
|
+
'CONFIG_FILE="' +
|
|
217
|
+
configFile +
|
|
218
|
+
'"\n' +
|
|
219
|
+
'LOG_FILE="' +
|
|
220
|
+
logFile +
|
|
221
|
+
'"\n' +
|
|
222
|
+
'PID_FILE="' +
|
|
223
|
+
pidFile +
|
|
224
|
+
'"\n' +
|
|
225
|
+
'DATA_DIR="' +
|
|
226
|
+
dataDir +
|
|
227
|
+
'"\n' +
|
|
238
228
|
'\n' +
|
|
239
229
|
'# 终止旧进程\n' +
|
|
240
230
|
'pkill -9 -f "${BINARY}" 2>/dev/null || true\n' +
|
|
@@ -289,7 +279,7 @@ function getProcessInfo(pid) {
|
|
|
289
279
|
pid,
|
|
290
280
|
memory: rss ? (rss / 1024).toFixed(1) + ' MB' : '未知',
|
|
291
281
|
cpu: pcpu ? pcpu.toFixed(1) + '%' : '未知',
|
|
292
|
-
isRoot: isProcessRoot(pid),
|
|
282
|
+
isRoot: utils.isProcessRoot(pid),
|
|
293
283
|
};
|
|
294
284
|
} catch (e) {
|
|
295
285
|
return { pid, memory: '未知', cpu: '未知', isRoot: false };
|
|
@@ -361,10 +351,7 @@ async function startMixedMode(staleState) {
|
|
|
361
351
|
throw new Error('未找到配置文件,请先添加订阅并启动');
|
|
362
352
|
}
|
|
363
353
|
|
|
364
|
-
const args = [
|
|
365
|
-
'-d', config.PATHS.data,
|
|
366
|
-
'-f', configFile,
|
|
367
|
-
];
|
|
354
|
+
const args = ['-d', config.DIRS.data, '-f', configFile];
|
|
368
355
|
|
|
369
356
|
const out = fs.openSync(logFile, 'a');
|
|
370
357
|
const err = fs.openSync(logFile, 'a');
|
|
@@ -389,7 +376,12 @@ async function startMixedMode(staleState) {
|
|
|
389
376
|
try {
|
|
390
377
|
const logs = fs.readFileSync(logFile, 'utf8').slice(-3000);
|
|
391
378
|
if (logs.trim()) {
|
|
392
|
-
errorMsg +=
|
|
379
|
+
errorMsg +=
|
|
380
|
+
'\n最近的日志:\n' +
|
|
381
|
+
logs
|
|
382
|
+
.split('\n')
|
|
383
|
+
.map(l => ' ' + l)
|
|
384
|
+
.join('\n');
|
|
393
385
|
}
|
|
394
386
|
} catch {}
|
|
395
387
|
}
|
|
@@ -427,14 +419,18 @@ async function startTunMode(staleState) {
|
|
|
427
419
|
timeout: 60000,
|
|
428
420
|
});
|
|
429
421
|
} catch (e) {
|
|
430
|
-
try {
|
|
422
|
+
try {
|
|
423
|
+
fs.unlinkSync(launchScript);
|
|
424
|
+
} catch (e2) {}
|
|
431
425
|
if (e.status === 1) {
|
|
432
426
|
throw new Error('密码错误或取消');
|
|
433
427
|
}
|
|
434
428
|
throw new Error(e.message);
|
|
435
429
|
}
|
|
436
430
|
|
|
437
|
-
try {
|
|
431
|
+
try {
|
|
432
|
+
fs.unlinkSync(launchScript);
|
|
433
|
+
} catch (e) {}
|
|
438
434
|
|
|
439
435
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
440
436
|
|
|
@@ -446,7 +442,7 @@ async function startTunMode(staleState) {
|
|
|
446
442
|
return { success: true, pid: finalPid, mode: 'tun' };
|
|
447
443
|
}
|
|
448
444
|
|
|
449
|
-
function stop(
|
|
445
|
+
function stop(forceSudo) {
|
|
450
446
|
const allPids = getAllMihomoPids();
|
|
451
447
|
if (allPids.length === 0) {
|
|
452
448
|
clearPid();
|
|
@@ -454,7 +450,7 @@ function stop(wasTunMode) {
|
|
|
454
450
|
return { success: true, notRunning: true };
|
|
455
451
|
}
|
|
456
452
|
|
|
457
|
-
const result = cleanupAll(
|
|
453
|
+
const result = cleanupAll(forceSudo);
|
|
458
454
|
|
|
459
455
|
const remaining = getAllMihomoPids();
|
|
460
456
|
if (remaining.length > 0) {
|
|
@@ -490,10 +486,7 @@ function rotateLog() {
|
|
|
490
486
|
return null;
|
|
491
487
|
}
|
|
492
488
|
|
|
493
|
-
const timestamp = new Date().toISOString()
|
|
494
|
-
.replace(/T/, '_')
|
|
495
|
-
.replace(/:/g, '-')
|
|
496
|
-
.replace(/\..+/, '');
|
|
489
|
+
const timestamp = new Date().toISOString().replace(/T/, '_').replace(/:/g, '-').replace(/\..+/, '');
|
|
497
490
|
|
|
498
491
|
const rotatedName = `mihomo.${timestamp}.log`;
|
|
499
492
|
const rotatedPath = path.join(config.DIRS.logs, rotatedName);
|
|
@@ -586,6 +579,12 @@ function listLogs() {
|
|
|
586
579
|
return result;
|
|
587
580
|
}
|
|
588
581
|
|
|
582
|
+
function isPathUnderDir(filePath, baseDir) {
|
|
583
|
+
const resolvedPath = path.resolve(filePath);
|
|
584
|
+
const resolvedBase = path.resolve(baseDir);
|
|
585
|
+
return resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + path.sep);
|
|
586
|
+
}
|
|
587
|
+
|
|
589
588
|
function getLogPathByName(name) {
|
|
590
589
|
const logsDir = config.DIRS.logs;
|
|
591
590
|
|
|
@@ -599,16 +598,19 @@ function getLogPathByName(name) {
|
|
|
599
598
|
}
|
|
600
599
|
|
|
601
600
|
const filePath = path.join(logsDir, targetName);
|
|
602
|
-
if (fs.existsSync(filePath)) {
|
|
601
|
+
if (fs.existsSync(filePath) && isPathUnderDir(filePath, logsDir)) {
|
|
603
602
|
return filePath;
|
|
604
603
|
}
|
|
605
604
|
|
|
606
|
-
//
|
|
605
|
+
// 尝试模糊匹配(readdirSync 返回的文件名已是安全的,但为了一致性仍校验)
|
|
607
606
|
if (fs.existsSync(logsDir)) {
|
|
608
607
|
const files = fs.readdirSync(logsDir);
|
|
609
608
|
for (const file of files) {
|
|
610
609
|
if (file.includes(name)) {
|
|
611
|
-
|
|
610
|
+
const candidatePath = path.join(logsDir, file);
|
|
611
|
+
if (isPathUnderDir(candidatePath, logsDir)) {
|
|
612
|
+
return candidatePath;
|
|
613
|
+
}
|
|
612
614
|
}
|
|
613
615
|
}
|
|
614
616
|
}
|
|
@@ -616,33 +618,6 @@ function getLogPathByName(name) {
|
|
|
616
618
|
return null;
|
|
617
619
|
}
|
|
618
620
|
|
|
619
|
-
function readLog(lines) {
|
|
620
|
-
if (lines === undefined) lines = 100;
|
|
621
|
-
if (!fs.existsSync(config.PATHS.logFile)) {
|
|
622
|
-
return '(暂无日志)';
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
try {
|
|
626
|
-
const content = fs.readFileSync(config.PATHS.logFile, 'utf8');
|
|
627
|
-
const allLines = content.split('\n');
|
|
628
|
-
return allLines.slice(-lines).join('\n');
|
|
629
|
-
} catch (e) {
|
|
630
|
-
return '(读取日志失败: ' + e.message + ')';
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function clearLog() {
|
|
635
|
-
if (fs.existsSync(config.PATHS.logFile)) {
|
|
636
|
-
try {
|
|
637
|
-
fs.writeFileSync(config.PATHS.logFile, '');
|
|
638
|
-
return true;
|
|
639
|
-
} catch (e) {
|
|
640
|
-
return false;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
return true;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
621
|
function openUrl(url) {
|
|
647
622
|
try {
|
|
648
623
|
spawn('open', [url], { stdio: 'ignore', detached: true });
|
|
@@ -653,21 +628,12 @@ function openUrl(url) {
|
|
|
653
628
|
}
|
|
654
629
|
|
|
655
630
|
module.exports = {
|
|
656
|
-
getPid,
|
|
657
|
-
isRunning,
|
|
658
|
-
isProcessRunning,
|
|
659
631
|
getAllMihomoPids,
|
|
660
|
-
isProcessRoot,
|
|
661
|
-
checkStaleState,
|
|
662
632
|
cleanupAll,
|
|
663
633
|
getStatus,
|
|
664
634
|
start,
|
|
665
635
|
stop,
|
|
666
636
|
getLogPath,
|
|
667
|
-
readLog,
|
|
668
|
-
clearLog,
|
|
669
|
-
rotateLog,
|
|
670
|
-
cleanupOldLogs,
|
|
671
637
|
listLogs,
|
|
672
638
|
getLogPathByName,
|
|
673
639
|
openUrl,
|
package/src/subscription.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
|
-
const yaml = require('js-yaml');
|
|
3
2
|
const config = require('./config');
|
|
4
3
|
|
|
5
4
|
const DEFAULT_UPDATE_INTERVAL_HOURS = 12;
|
|
@@ -38,33 +37,11 @@ function parseUsernameFromContentDisposition(header) {
|
|
|
38
37
|
return parts[parts.length - 1] || null;
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
47
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function formatTimestamp(ts) {
|
|
51
|
-
if (!ts) return '未知';
|
|
52
|
-
try {
|
|
53
|
-
return new Date(ts * 1000).toLocaleString('zh-CN');
|
|
54
|
-
} catch {
|
|
55
|
-
return '未知';
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function formatDate(dateOrIso) {
|
|
60
|
-
if (!dateOrIso) return '未知';
|
|
61
|
-
try {
|
|
62
|
-
const d = dateOrIso instanceof Date ? dateOrIso : new Date(dateOrIso);
|
|
63
|
-
if (isNaN(d.getTime())) return '未知';
|
|
64
|
-
return d.toLocaleString('zh-CN');
|
|
65
|
-
} catch {
|
|
66
|
-
return '未知';
|
|
67
|
-
}
|
|
40
|
+
function formatProxySummary(info) {
|
|
41
|
+
const parts = [];
|
|
42
|
+
if (info && info.proxyGroups > 0) parts.push(info.proxyGroups + ' 组');
|
|
43
|
+
parts.push(((info && info.proxies) || 0) + ' 节点');
|
|
44
|
+
return parts.join(', ');
|
|
68
45
|
}
|
|
69
46
|
|
|
70
47
|
async function downloadSubscription(url, subName) {
|
|
@@ -159,7 +136,7 @@ function needsAutoUpdate(sub) {
|
|
|
159
136
|
if (isNaN(lastUpdate)) return true;
|
|
160
137
|
const intervalHours = sub.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
|
|
161
138
|
const intervalMs = intervalHours * 60 * 60 * 1000;
|
|
162
|
-
return
|
|
139
|
+
return Date.now() - lastUpdate > intervalMs;
|
|
163
140
|
}
|
|
164
141
|
|
|
165
142
|
async function tryUpdateOne(sub) {
|
|
@@ -171,7 +148,7 @@ async function tryUpdateOne(sub) {
|
|
|
171
148
|
}
|
|
172
149
|
}
|
|
173
150
|
|
|
174
|
-
async function
|
|
151
|
+
async function autoUpdateStaleSubscription() {
|
|
175
152
|
const allSubs = config.getSubscriptionsWithCache();
|
|
176
153
|
const staleSubs = allSubs.filter(needsAutoUpdate);
|
|
177
154
|
|
|
@@ -193,10 +170,7 @@ async function autoUpdateStaleSubscriptions() {
|
|
|
193
170
|
results.forEach(r => {
|
|
194
171
|
if (r.success) {
|
|
195
172
|
updatedCount++;
|
|
196
|
-
|
|
197
|
-
if (r.proxyGroups && r.proxyGroups > 0) parts.push(r.proxyGroups + ' 组');
|
|
198
|
-
parts.push(r.proxies + ' 节点');
|
|
199
|
-
console.log('✓ ' + r.name + ': 已更新 (' + parts.join(', ') + ')');
|
|
173
|
+
console.log('✓ ' + r.name + ': 已更新 (' + formatProxySummary(r) + ')');
|
|
200
174
|
} else {
|
|
201
175
|
console.log('✗ ' + r.name + ': 失败 (' + r.error.split('\n')[0] + ')');
|
|
202
176
|
}
|
|
@@ -213,11 +187,7 @@ module.exports = {
|
|
|
213
187
|
DEFAULT_UPDATE_INTERVAL_HOURS,
|
|
214
188
|
downloadSubscription,
|
|
215
189
|
prepareConfigForStart,
|
|
216
|
-
|
|
217
|
-
getConfigInfo: config.getConfigInfo,
|
|
218
|
-
formatBytes,
|
|
219
|
-
formatTimestamp,
|
|
220
|
-
formatDate,
|
|
190
|
+
formatProxySummary,
|
|
221
191
|
tryUpdateOne,
|
|
222
|
-
|
|
192
|
+
autoUpdateStaleSubscription,
|
|
223
193
|
};
|