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/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
- return JSON.parse(content);
86
+ _settingsCache = JSON.parse(content);
87
+ return _settingsCache;
86
88
  } catch (e) {
87
- return {};
89
+ _settingsCache = {};
90
+ return _settingsCache;
88
91
  }
89
92
  }
90
- return {};
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
- const settings = readSettings();
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
- if (match) {
253
- return match[0];
254
- }
255
- return output;
257
+ _kernelVersionCache = match ? match[0] : output;
258
+ return _kernelVersionCache;
256
259
  }
257
- return 'unknown';
260
+ _kernelVersionCache = 'unknown';
261
+ return _kernelVersionCache;
258
262
  } catch (e) {
259
- return 'unknown';
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.applyOverwrites(baseConfig);
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(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')
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 { fs.unlinkSync(tempPath); } catch {}
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 { fs.unlinkSync(tempPath); } catch {}
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 loadOverwriteFiles() {
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 applyOverwrites(baseConfig) {
210
+ function applyOverwrite(baseConfig) {
211
211
  if (!isOverwriteEnabled()) {
212
212
  return baseConfig;
213
213
  }
214
214
 
215
- const overwriteFiles = loadOverwriteFiles();
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 listOverwriteFiles() {
234
- const files = loadOverwriteFiles();
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
- applyOverwrites,
253
- listOverwriteFiles,
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.split('\n').filter(Boolean).map(p => parseInt(p));
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
- try {
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.PATHS.data;
231
-
232
- const scriptContent = '#!/bin/bash\n' +
233
- 'BINARY="' + binary + '"\n' +
234
- 'CONFIG_FILE="' + configFile + '"\n' +
235
- 'LOG_FILE="' + logFile + '"\n' +
236
- 'PID_FILE="' + pidFile + '"\n' +
237
- 'DATA_DIR="' + dataDir + '"\n' +
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 += '\n最近的日志:\n' + logs.split('\n').map(l => ' ' + l).join('\n');
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 { fs.unlinkSync(launchScript); } catch (e2) {}
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 { fs.unlinkSync(launchScript); } catch (e) {}
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(wasTunMode) {
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(wasTunMode);
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
- return path.join(logsDir, file);
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,
@@ -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 formatBytes(bytes) {
42
- if (bytes === undefined || bytes === null) return '未知';
43
- if (bytes === 0) return '0 B';
44
- const k = 1024;
45
- const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
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 (Date.now() - lastUpdate) > intervalMs;
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 autoUpdateStaleSubscriptions() {
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
- const parts = [];
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
- hasConfig: config.hasConfig,
217
- getConfigInfo: config.getConfigInfo,
218
- formatBytes,
219
- formatTimestamp,
220
- formatDate,
190
+ formatProxySummary,
221
191
  tryUpdateOne,
222
- autoUpdateStaleSubscriptions,
192
+ autoUpdateStaleSubscription,
223
193
  };