mihomo-cli 1.0.0-alpha.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -105
- package/index.js +435 -77
- package/package.json +12 -4
- package/src/config.js +129 -24
- package/src/kernel.js +6 -65
- package/src/process.js +151 -9
- package/src/subscription.js +159 -15
package/src/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const os = require('os');
|
|
4
|
+
const yaml = require('js-yaml');
|
|
4
5
|
const { execSync } = require('child_process');
|
|
5
6
|
|
|
6
7
|
const IS_PKG = typeof process.pkg !== 'undefined';
|
|
@@ -36,6 +37,7 @@ const PATHS = {
|
|
|
36
37
|
userDataDir: USER_DATA_DIR,
|
|
37
38
|
mihomoBinary: path.join(DIRS.core, 'mihomo'),
|
|
38
39
|
settingsFile: path.join(USER_DATA_DIR, 'settings.json'),
|
|
40
|
+
subsCacheFile: path.join(USER_DATA_DIR, 'subs-cache.json'),
|
|
39
41
|
configFile: path.join(DIRS.runtime, 'config.yaml'),
|
|
40
42
|
logFile: path.join(DIRS.logs, 'mihomo.log'),
|
|
41
43
|
pidFile: path.join(DIRS.runtime, 'pid'),
|
|
@@ -95,11 +97,96 @@ function writeSettings(settings) {
|
|
|
95
97
|
return merged;
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
// GitHub 镜像配置
|
|
101
|
+
const DEFAULT_GITHUB_MIRROR = 'https://v6.gh-proxy.org/';
|
|
102
|
+
const AVAILABLE_MIRRORS = [
|
|
103
|
+
'v6.gh-proxy.org',
|
|
104
|
+
'gh-proxy.org',
|
|
105
|
+
'hk.gh-proxy.org',
|
|
106
|
+
'cdn.gh-proxy.org',
|
|
107
|
+
'edgeone.gh-proxy.org',
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
function getGitHubMirror() {
|
|
111
|
+
const settings = readSettings();
|
|
112
|
+
// 空字符串或 false 表示禁用镜像
|
|
113
|
+
if (settings.githubMirror === '' || settings.githubMirror === false) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return settings.githubMirror || DEFAULT_GITHUB_MIRROR;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function setGitHubMirror(mirror) {
|
|
120
|
+
// mirror 取值:
|
|
121
|
+
// - 完整 URL: 'https://hk.gh-proxy.org/'
|
|
122
|
+
// - 短域名: 'hk.gh-proxy.org'
|
|
123
|
+
// - '' 或 false: 禁用镜像
|
|
124
|
+
// - null 或 undefined: 恢复默认
|
|
125
|
+
|
|
126
|
+
if (mirror === null || mirror === undefined) {
|
|
127
|
+
const settings = readSettings();
|
|
128
|
+
delete settings.githubMirror;
|
|
129
|
+
writeSettings(settings);
|
|
130
|
+
return DEFAULT_GITHUB_MIRROR;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (mirror === '' || mirror === false) {
|
|
134
|
+
writeSettings({ githubMirror: '' });
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let mirrorUrl = mirror;
|
|
139
|
+
if (!mirrorUrl.startsWith('http')) {
|
|
140
|
+
mirrorUrl = 'https://' + mirrorUrl;
|
|
141
|
+
}
|
|
142
|
+
if (!mirrorUrl.endsWith('/')) {
|
|
143
|
+
mirrorUrl += '/';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
writeSettings({ githubMirror: mirrorUrl });
|
|
147
|
+
return mirrorUrl;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 订阅缓存读写(动态数据:流量、用户名等)
|
|
151
|
+
function readSubsCache() {
|
|
152
|
+
ensureDirs();
|
|
153
|
+
if (fs.existsSync(PATHS.subsCacheFile)) {
|
|
154
|
+
try {
|
|
155
|
+
const content = fs.readFileSync(PATHS.subsCacheFile, 'utf8');
|
|
156
|
+
return JSON.parse(content);
|
|
157
|
+
} catch (e) {
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function writeSubsCache(cache) {
|
|
165
|
+
ensureDirs();
|
|
166
|
+
fs.writeFileSync(PATHS.subsCacheFile, JSON.stringify(cache, null, 2), { mode: 0o600 });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function saveSubCache(subName, data) {
|
|
170
|
+
const cache = readSubsCache();
|
|
171
|
+
cache[subName] = { ...cache[subName], ...data };
|
|
172
|
+
writeSubsCache(cache);
|
|
173
|
+
}
|
|
174
|
+
|
|
98
175
|
function getSubscriptions() {
|
|
99
176
|
const settings = readSettings();
|
|
100
177
|
return settings.subscriptions || [];
|
|
101
178
|
}
|
|
102
179
|
|
|
180
|
+
// 获取合并了缓存数据的订阅列表
|
|
181
|
+
function getSubscriptionsWithCache() {
|
|
182
|
+
const subs = getSubscriptions();
|
|
183
|
+
const cache = readSubsCache();
|
|
184
|
+
return subs.map(s => ({
|
|
185
|
+
...s,
|
|
186
|
+
...(cache[s.name] || {}),
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
|
|
103
190
|
function addSubscription(url, name) {
|
|
104
191
|
if (name === undefined) name = 'default';
|
|
105
192
|
const settings = readSettings();
|
|
@@ -113,6 +200,22 @@ function addSubscription(url, name) {
|
|
|
113
200
|
writeSettings({ subscriptions: subs });
|
|
114
201
|
}
|
|
115
202
|
|
|
203
|
+
function setDefaultSubscription(name) {
|
|
204
|
+
const settings = readSettings();
|
|
205
|
+
const subs = settings.subscriptions || [];
|
|
206
|
+
const idx = subs.findIndex(s => s.name === name);
|
|
207
|
+
if (idx < 0) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
if (idx === 0) {
|
|
211
|
+
return true; // 已经是第一个
|
|
212
|
+
}
|
|
213
|
+
const [sub] = subs.splice(idx, 1);
|
|
214
|
+
subs.unshift(sub);
|
|
215
|
+
writeSettings({ subscriptions: subs });
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
116
219
|
function getSubRawConfigPath(subName) {
|
|
117
220
|
return path.join(DIRS.subs, subName + '.yaml');
|
|
118
221
|
}
|
|
@@ -180,19 +283,23 @@ const BASE_CONFIG = {
|
|
|
180
283
|
},
|
|
181
284
|
};
|
|
182
285
|
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
286
|
+
function parseYamlOrJson(content, errorMsg) {
|
|
287
|
+
if (!content || !content.trim()) {
|
|
288
|
+
throw new Error((errorMsg || '内容') + '为空');
|
|
289
|
+
}
|
|
187
290
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
291
|
+
const result = yaml.load(content);
|
|
292
|
+
if (result !== undefined) return result;
|
|
293
|
+
} catch (e) {}
|
|
294
|
+
try {
|
|
295
|
+
return JSON.parse(content);
|
|
296
|
+
} catch (e2) {
|
|
297
|
+
throw new Error((errorMsg || '内容') + '格式错误,无法解析为 YAML 或 JSON');
|
|
195
298
|
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function buildConfig(subRawContent, mode) {
|
|
302
|
+
const baseConfig = parseYamlOrJson(subRawContent, '订阅内容');
|
|
196
303
|
|
|
197
304
|
if (!baseConfig) {
|
|
198
305
|
throw new Error('订阅内容为空');
|
|
@@ -216,7 +323,6 @@ function buildConfig(subRawContent, mode) {
|
|
|
216
323
|
}
|
|
217
324
|
|
|
218
325
|
function writeMihomoConfig(configObj) {
|
|
219
|
-
const yaml = require('js-yaml');
|
|
220
326
|
ensureDirs();
|
|
221
327
|
const content = yaml.dump(configObj, {
|
|
222
328
|
indent: 2,
|
|
@@ -236,7 +342,6 @@ function getConfigInfo() {
|
|
|
236
342
|
}
|
|
237
343
|
|
|
238
344
|
try {
|
|
239
|
-
const yaml = require('js-yaml');
|
|
240
345
|
const content = fs.readFileSync(PATHS.configFile, 'utf8');
|
|
241
346
|
const cfg = yaml.load(content);
|
|
242
347
|
|
|
@@ -255,17 +360,7 @@ function getConfigInfo() {
|
|
|
255
360
|
}
|
|
256
361
|
|
|
257
362
|
function rmrf(dir) {
|
|
258
|
-
|
|
259
|
-
const stat = fs.statSync(dir);
|
|
260
|
-
if (stat.isDirectory()) {
|
|
261
|
-
const files = fs.readdirSync(dir);
|
|
262
|
-
for (const f of files) {
|
|
263
|
-
rmrf(path.join(dir, f));
|
|
264
|
-
}
|
|
265
|
-
fs.rmdirSync(dir);
|
|
266
|
-
} else {
|
|
267
|
-
fs.unlinkSync(dir);
|
|
268
|
-
}
|
|
363
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
269
364
|
}
|
|
270
365
|
|
|
271
366
|
function resetUserData(options) {
|
|
@@ -309,16 +404,26 @@ module.exports = {
|
|
|
309
404
|
ensureDirs,
|
|
310
405
|
readSettings,
|
|
311
406
|
writeSettings,
|
|
407
|
+
readSubsCache,
|
|
408
|
+
writeSubsCache,
|
|
409
|
+
saveSubCache,
|
|
312
410
|
maskUrl,
|
|
313
411
|
getSubscriptions,
|
|
412
|
+
getSubscriptionsWithCache,
|
|
314
413
|
addSubscription,
|
|
414
|
+
setDefaultSubscription,
|
|
315
415
|
getSubRawConfigPath,
|
|
316
416
|
saveSubRawConfig,
|
|
317
417
|
readSubRawConfig,
|
|
318
418
|
hasKernel,
|
|
319
419
|
getKernelVersion,
|
|
420
|
+
getGitHubMirror,
|
|
421
|
+
setGitHubMirror,
|
|
422
|
+
DEFAULT_GITHUB_MIRROR,
|
|
423
|
+
AVAILABLE_MIRRORS,
|
|
320
424
|
TUN_CONFIG,
|
|
321
425
|
BASE_CONFIG,
|
|
426
|
+
parseYamlOrJson,
|
|
322
427
|
buildConfig,
|
|
323
428
|
writeMihomoConfig,
|
|
324
429
|
hasConfig,
|
package/src/kernel.js
CHANGED
|
@@ -6,13 +6,11 @@ const { compareVersions } = require('compare-versions');
|
|
|
6
6
|
const config = require('./config');
|
|
7
7
|
|
|
8
8
|
const GITHUB_REPO = 'MetaCubeX/mihomo';
|
|
9
|
-
const GEODATA_REPO = 'MetaCubeX/meta-rules-dat';
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return GITHUB_DOWNLOAD_MIRROR + url;
|
|
10
|
+
function withMirror(url, overrideMirror) {
|
|
11
|
+
const mirror = overrideMirror !== undefined ? overrideMirror : config.getGitHubMirror();
|
|
12
|
+
if (mirror && url.startsWith('https://github.com/')) {
|
|
13
|
+
return mirror + url;
|
|
16
14
|
}
|
|
17
15
|
return url;
|
|
18
16
|
}
|
|
@@ -23,11 +21,6 @@ const HTTP_CLIENT = axios.create({
|
|
|
23
21
|
maxContentLength: 200 * 1024 * 1024,
|
|
24
22
|
});
|
|
25
23
|
|
|
26
|
-
const GEODATA_FILES = [
|
|
27
|
-
{ name: 'geosite-lite.dat', targetName: 'geosite.dat', url: 'https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite-lite.dat' },
|
|
28
|
-
{ name: 'country-lite.mmdb', targetName: 'Country.mmdb', url: 'https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/country-lite.mmdb' },
|
|
29
|
-
];
|
|
30
|
-
|
|
31
24
|
function getArch() {
|
|
32
25
|
const arch = process.arch;
|
|
33
26
|
if (arch === 'arm64') return 'arm64';
|
|
@@ -88,57 +81,6 @@ async function getLatestRelease(repo) {
|
|
|
88
81
|
return releases[0];
|
|
89
82
|
}
|
|
90
83
|
|
|
91
|
-
async function downloadFile(url, destPath, progressCallback) {
|
|
92
|
-
if (progressCallback) {
|
|
93
|
-
progressCallback('下载 ' + path.basename(destPath));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const response = await HTTP_CLIENT({
|
|
97
|
-
method: 'get',
|
|
98
|
-
url: url,
|
|
99
|
-
responseType: 'stream',
|
|
100
|
-
timeout: 180000,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const writer = fs.createWriteStream(destPath);
|
|
104
|
-
response.data.pipe(writer);
|
|
105
|
-
|
|
106
|
-
await new Promise((resolve, reject) => {
|
|
107
|
-
writer.on('finish', resolve);
|
|
108
|
-
writer.on('error', reject);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
return destPath;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function downloadGeodata(progressCallback) {
|
|
115
|
-
config.ensureDirs();
|
|
116
|
-
const dataDir = config.DIRS.data;
|
|
117
|
-
|
|
118
|
-
const results = [];
|
|
119
|
-
for (const file of GEODATA_FILES) {
|
|
120
|
-
const destPath = path.join(dataDir, file.name);
|
|
121
|
-
const targetPath = file.targetName ? path.join(dataDir, file.targetName) : destPath;
|
|
122
|
-
try {
|
|
123
|
-
await downloadFile(file.url, destPath, progressCallback);
|
|
124
|
-
|
|
125
|
-
if (file.targetName && destPath !== targetPath) {
|
|
126
|
-
if (fs.existsSync(targetPath)) {
|
|
127
|
-
fs.unlinkSync(targetPath);
|
|
128
|
-
}
|
|
129
|
-
fs.renameSync(destPath, targetPath);
|
|
130
|
-
results.push({ name: file.targetName, success: true });
|
|
131
|
-
} else {
|
|
132
|
-
results.push({ name: file.name, success: true });
|
|
133
|
-
}
|
|
134
|
-
} catch (e) {
|
|
135
|
-
results.push({ name: file.name, success: false, error: e.message });
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return results;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
84
|
async function checkUpdate() {
|
|
143
85
|
const currentVersion = config.getKernelVersion();
|
|
144
86
|
const latest = await getLatestRelease(GITHUB_REPO);
|
|
@@ -190,7 +132,7 @@ function findBinaryInDir(dir) {
|
|
|
190
132
|
return null;
|
|
191
133
|
}
|
|
192
134
|
|
|
193
|
-
async function downloadKernel(progressCallback) {
|
|
135
|
+
async function downloadKernel(progressCallback, mirror) {
|
|
194
136
|
config.ensureDirs();
|
|
195
137
|
|
|
196
138
|
const latest = await getLatestRelease(GITHUB_REPO);
|
|
@@ -208,7 +150,7 @@ async function downloadKernel(progressCallback) {
|
|
|
208
150
|
throw new Error('未找到匹配的内核文件\n 平台: ' + platform + ', 架构: ' + arch + hint);
|
|
209
151
|
}
|
|
210
152
|
|
|
211
|
-
const downloadUrl = withMirror(asset.browser_download_url);
|
|
153
|
+
const downloadUrl = withMirror(asset.browser_download_url, mirror);
|
|
212
154
|
const tempPath = path.join(config.DIRS.core, asset.name);
|
|
213
155
|
|
|
214
156
|
if (progressCallback) {
|
|
@@ -293,5 +235,4 @@ module.exports = {
|
|
|
293
235
|
findMatchingAsset,
|
|
294
236
|
checkUpdate,
|
|
295
237
|
downloadKernel,
|
|
296
|
-
downloadGeodata,
|
|
297
238
|
};
|
package/src/process.js
CHANGED
|
@@ -100,14 +100,13 @@ function clearPid() {
|
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
102
|
if (isPidFileOwnedByRoot()) {
|
|
103
|
-
console.log(' PID 文件由 root 创建,需要管理员权限删除');
|
|
104
103
|
try {
|
|
105
104
|
execSync('sudo rm -f "' + config.PATHS.pidFile + '" 2>/dev/null', {
|
|
106
105
|
stdio: 'inherit',
|
|
107
106
|
timeout: 10000,
|
|
108
107
|
});
|
|
109
108
|
} catch (e) {
|
|
110
|
-
|
|
109
|
+
// 忽略失败,后续操作可能会检测到问题
|
|
111
110
|
}
|
|
112
111
|
} else {
|
|
113
112
|
try {
|
|
@@ -186,14 +185,10 @@ function cleanupAll(forceSudo) {
|
|
|
186
185
|
let failedPids = [];
|
|
187
186
|
|
|
188
187
|
if (needsSudo) {
|
|
189
|
-
console.log(' 发现 ' + pids.length + ' 个 mihomo 进程(部分需要 root 权限)');
|
|
190
|
-
console.log(' 正在请求管理员权限终止进程...');
|
|
191
188
|
const success = killAllMihomo(true);
|
|
192
189
|
if (success) {
|
|
193
190
|
killedCount = pids.length;
|
|
194
191
|
} else {
|
|
195
|
-
console.log(' 部分进程可能需要手动清理');
|
|
196
|
-
console.log(' 手动清理命令: sudo pkill -9 mihomo');
|
|
197
192
|
failedPids = pids;
|
|
198
193
|
}
|
|
199
194
|
} else {
|
|
@@ -246,8 +241,8 @@ function createTunLaunchScript() {
|
|
|
246
241
|
'sleep 0.2\n' +
|
|
247
242
|
'rm -f "${PID_FILE}" 2>/dev/null || true\n' +
|
|
248
243
|
'\n' +
|
|
249
|
-
'#
|
|
250
|
-
'echo "=== TUN 启动: $(date) ==="
|
|
244
|
+
'# 写入启动标记\n' +
|
|
245
|
+
'echo "=== TUN 启动: $(date) ===" >> "${LOG_FILE}"\n' +
|
|
251
246
|
'\n' +
|
|
252
247
|
'# 启动\n' +
|
|
253
248
|
'cd /tmp\n' +
|
|
@@ -259,7 +254,6 @@ function createTunLaunchScript() {
|
|
|
259
254
|
'for i in 1 2 3 4 5; do\n' +
|
|
260
255
|
' sleep 0.4\n' +
|
|
261
256
|
' if kill -0 ${NEW_PID} 2>/dev/null; then\n' +
|
|
262
|
-
' echo "TUN 启动成功, PID ${NEW_PID}"\n' +
|
|
263
257
|
' exit 0\n' +
|
|
264
258
|
' fi\n' +
|
|
265
259
|
'done\n' +
|
|
@@ -353,6 +347,7 @@ async function startMixedMode(staleState) {
|
|
|
353
347
|
}
|
|
354
348
|
|
|
355
349
|
config.ensureDirs();
|
|
350
|
+
rotateAndCleanupLogs();
|
|
356
351
|
|
|
357
352
|
const binary = config.PATHS.mihomoBinary;
|
|
358
353
|
if (!fs.existsSync(binary)) {
|
|
@@ -406,6 +401,7 @@ async function startMixedMode(staleState) {
|
|
|
406
401
|
|
|
407
402
|
async function startTunMode(staleState) {
|
|
408
403
|
config.ensureDirs();
|
|
404
|
+
rotateAndCleanupLogs();
|
|
409
405
|
|
|
410
406
|
const binary = config.PATHS.mihomoBinary;
|
|
411
407
|
if (!fs.existsSync(binary)) {
|
|
@@ -474,10 +470,152 @@ function stop(wasTunMode) {
|
|
|
474
470
|
return { success: true, killed: result.killed };
|
|
475
471
|
}
|
|
476
472
|
|
|
473
|
+
function rotateAndCleanupLogs() {
|
|
474
|
+
rotateLog();
|
|
475
|
+
cleanupOldLogs(7);
|
|
476
|
+
}
|
|
477
|
+
|
|
477
478
|
function getLogPath() {
|
|
478
479
|
return config.PATHS.logFile;
|
|
479
480
|
}
|
|
480
481
|
|
|
482
|
+
function rotateLog() {
|
|
483
|
+
const logFile = config.PATHS.logFile;
|
|
484
|
+
if (!fs.existsSync(logFile)) {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const stat = fs.statSync(logFile);
|
|
489
|
+
if (stat.size === 0) {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const timestamp = new Date().toISOString()
|
|
494
|
+
.replace(/T/, '_')
|
|
495
|
+
.replace(/:/g, '-')
|
|
496
|
+
.replace(/\..+/, '');
|
|
497
|
+
|
|
498
|
+
const rotatedName = `mihomo.${timestamp}.log`;
|
|
499
|
+
const rotatedPath = path.join(config.DIRS.logs, rotatedName);
|
|
500
|
+
|
|
501
|
+
fs.renameSync(logFile, rotatedPath);
|
|
502
|
+
return rotatedPath;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function cleanupOldLogs(maxAgeDays) {
|
|
506
|
+
if (maxAgeDays === undefined) maxAgeDays = 7;
|
|
507
|
+
const logsDir = config.DIRS.logs;
|
|
508
|
+
|
|
509
|
+
if (!fs.existsSync(logsDir)) {
|
|
510
|
+
return { deleted: 0, errors: 0 };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const files = fs.readdirSync(logsDir);
|
|
514
|
+
const now = Date.now();
|
|
515
|
+
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
|
516
|
+
|
|
517
|
+
let deleted = 0;
|
|
518
|
+
let errors = 0;
|
|
519
|
+
|
|
520
|
+
for (const file of files) {
|
|
521
|
+
if (!file.match(/^mihomo\.\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.log$/)) {
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
const filePath = path.join(logsDir, file);
|
|
527
|
+
const stat = fs.statSync(filePath);
|
|
528
|
+
const ageMs = now - stat.mtimeMs;
|
|
529
|
+
|
|
530
|
+
if (ageMs > maxAgeMs) {
|
|
531
|
+
fs.unlinkSync(filePath);
|
|
532
|
+
deleted++;
|
|
533
|
+
}
|
|
534
|
+
} catch (e) {
|
|
535
|
+
errors++;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return { deleted, errors };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function listLogs() {
|
|
543
|
+
const logsDir = config.DIRS.logs;
|
|
544
|
+
const result = {
|
|
545
|
+
current: null,
|
|
546
|
+
archives: [],
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
if (fs.existsSync(config.PATHS.logFile)) {
|
|
550
|
+
const stat = fs.statSync(config.PATHS.logFile);
|
|
551
|
+
result.current = {
|
|
552
|
+
name: 'mihomo.log (当前)',
|
|
553
|
+
path: config.PATHS.logFile,
|
|
554
|
+
size: stat.size,
|
|
555
|
+
mtime: stat.mtime,
|
|
556
|
+
isCurrent: true,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (!fs.existsSync(logsDir)) {
|
|
561
|
+
return result;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const files = fs.readdirSync(logsDir);
|
|
565
|
+
for (const file of files) {
|
|
566
|
+
const match = file.match(/^mihomo\.(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})\.log$/);
|
|
567
|
+
if (!match) continue;
|
|
568
|
+
|
|
569
|
+
try {
|
|
570
|
+
const filePath = path.join(logsDir, file);
|
|
571
|
+
const stat = fs.statSync(filePath);
|
|
572
|
+
result.archives.push({
|
|
573
|
+
name: file,
|
|
574
|
+
timestamp: match[1],
|
|
575
|
+
path: filePath,
|
|
576
|
+
size: stat.size,
|
|
577
|
+
mtime: stat.mtime,
|
|
578
|
+
isCurrent: false,
|
|
579
|
+
});
|
|
580
|
+
} catch (e) {
|
|
581
|
+
// ignore
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
result.archives.sort((a, b) => b.mtime - a.mtime);
|
|
586
|
+
return result;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function getLogPathByName(name) {
|
|
590
|
+
const logsDir = config.DIRS.logs;
|
|
591
|
+
|
|
592
|
+
// 处理部分匹配(用户只输入时间戳部分)
|
|
593
|
+
let targetName = name;
|
|
594
|
+
if (!name.endsWith('.log')) {
|
|
595
|
+
targetName = 'mihomo.' + name + '.log';
|
|
596
|
+
}
|
|
597
|
+
if (!targetName.startsWith('mihomo.')) {
|
|
598
|
+
targetName = 'mihomo.' + targetName;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const filePath = path.join(logsDir, targetName);
|
|
602
|
+
if (fs.existsSync(filePath)) {
|
|
603
|
+
return filePath;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// 尝试模糊匹配
|
|
607
|
+
if (fs.existsSync(logsDir)) {
|
|
608
|
+
const files = fs.readdirSync(logsDir);
|
|
609
|
+
for (const file of files) {
|
|
610
|
+
if (file.includes(name)) {
|
|
611
|
+
return path.join(logsDir, file);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
|
|
481
619
|
function readLog(lines) {
|
|
482
620
|
if (lines === undefined) lines = 100;
|
|
483
621
|
if (!fs.existsSync(config.PATHS.logFile)) {
|
|
@@ -528,5 +666,9 @@ module.exports = {
|
|
|
528
666
|
getLogPath,
|
|
529
667
|
readLog,
|
|
530
668
|
clearLog,
|
|
669
|
+
rotateLog,
|
|
670
|
+
cleanupOldLogs,
|
|
671
|
+
listLogs,
|
|
672
|
+
getLogPathByName,
|
|
531
673
|
openUrl,
|
|
532
674
|
};
|