mihomo-cli 1.2.3 → 1.2.5
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 +27 -0
- package/README.md +1 -0
- package/index.js +42 -40
- package/package.json +1 -1
- package/src/config.js +1 -1
- package/src/overwrite.js +8 -8
- package/src/process.js +18 -36
- package/src/subscription.js +2 -35
- package/src/utils.js +101 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.5] - 2026-04-07
|
|
4
|
+
|
|
5
|
+
### 新增功能
|
|
6
|
+
|
|
7
|
+
- **update 命令**:新增 `mihomo update` 命令,执行 `npm install -g mihomo-cli` 快速更新 CLI 版本
|
|
8
|
+
- 支持别名:`update`、`upd`、`upgrade`
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## [1.2.4] - 2026-04-07
|
|
13
|
+
|
|
14
|
+
### 修复
|
|
15
|
+
|
|
16
|
+
- **路径安全**:`getLogPathByName()` 增加 `isPathUnderDir()` 校验,防止潜在的路径遍历风险
|
|
17
|
+
- **错误提示**:覆写配置文件解析失败时显示警告日志,不再静默忽略
|
|
18
|
+
|
|
19
|
+
### 重构
|
|
20
|
+
|
|
21
|
+
- **代码结构**:工具函数(`sleepSync`、`formatBytes`、`isProcessRunning` 等)提取到 `utils.js` 模块,简化各模块依赖
|
|
22
|
+
- **命名规范**:统一函数命名为全称单数
|
|
23
|
+
- `autoUpdateStaleSubscriptions` → `autoUpdateStaleSubscription`
|
|
24
|
+
- `applyOverwrites` → `applyOverwrite`
|
|
25
|
+
- `loadOverwriteFiles` → `loadOverwriteFile`
|
|
26
|
+
- `listOverwriteFiles` → `listOverwriteFile`
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
3
30
|
## [1.2.3] - 2026-04-07
|
|
4
31
|
|
|
5
32
|
### 优化
|
package/README.md
CHANGED
|
@@ -112,6 +112,7 @@ mihomo ui yacd # YACD
|
|
|
112
112
|
| 命令 | 说明 |
|
|
113
113
|
| ----------------------------------- | ------------------------------------------------------- |
|
|
114
114
|
| `mihomo kernel [镜像\|--no-mirror]` | 更新内核 |
|
|
115
|
+
| `mihomo update` | 更新 mihomo-cli (npm install -g) |
|
|
115
116
|
| `mihomo ui [zash\|dash\|yacd]` | 打开 Web UI |
|
|
116
117
|
| `mihomo dir` | 显示数据目录位置 |
|
|
117
118
|
| `mihomo dir open [target]` | 打开指定目录(`root`, `subs`, `logs`, `overwrites` 等) |
|
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const kernel = require('./src/kernel');
|
|
|
8
8
|
const subscription = require('./src/subscription');
|
|
9
9
|
const processMgr = require('./src/process');
|
|
10
10
|
const overwrite = require('./src/overwrite');
|
|
11
|
+
const utils = require('./src/utils');
|
|
11
12
|
|
|
12
13
|
const VERSION = require('./package.json').version;
|
|
13
14
|
|
|
@@ -94,6 +95,7 @@ function printHelp() {
|
|
|
94
95
|
'\n' +
|
|
95
96
|
'系统:\n' +
|
|
96
97
|
' kernel [镜像|--no-mirror] 更新内核\n' +
|
|
98
|
+
' update 更新 mihomo-cli (npm install -g)\n' +
|
|
97
99
|
' reset [--full] 重置用户数据 (--full 同时删除内核)\n' +
|
|
98
100
|
' help, -h 显示帮助\n' +
|
|
99
101
|
' version, -v 显示版本\n' +
|
|
@@ -127,7 +129,7 @@ function printStatus() {
|
|
|
127
129
|
const status = processMgr.getStatus();
|
|
128
130
|
const info = config.getConfigInfo();
|
|
129
131
|
const owEnabled = overwrite.isOverwriteEnabled();
|
|
130
|
-
const owFiles = overwrite.
|
|
132
|
+
const owFiles = overwrite.listOverwriteFile().files;
|
|
131
133
|
const activeSub = getActiveSubscription();
|
|
132
134
|
|
|
133
135
|
console.log('');
|
|
@@ -221,32 +223,6 @@ function pickSingleSubscription(subs, pattern) {
|
|
|
221
223
|
process.exit(1);
|
|
222
224
|
}
|
|
223
225
|
|
|
224
|
-
function hasFlag(args, short, long) {
|
|
225
|
-
return args && (args.includes(short) || args.includes(long));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function parseIntArg(args, short, long, defaultValue) {
|
|
229
|
-
if (!args) return defaultValue;
|
|
230
|
-
for (let i = 0; i < args.length; i++) {
|
|
231
|
-
if (args[i] === short || args[i] === long) {
|
|
232
|
-
if (i + 1 < args.length) {
|
|
233
|
-
const val = parseInt(args[i + 1]);
|
|
234
|
-
return isNaN(val) ? defaultValue : val;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
return defaultValue;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function getNonFlagArg(args, startIdx) {
|
|
242
|
-
if (!args) return null;
|
|
243
|
-
for (let i = startIdx; i < args.length; i++) {
|
|
244
|
-
if (!args[i].startsWith('-')) {
|
|
245
|
-
return args[i];
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
226
|
|
|
251
227
|
function openLogFile(logPath, label) {
|
|
252
228
|
const displayLabel = label || logPath;
|
|
@@ -305,7 +281,7 @@ async function cmdStart(args) {
|
|
|
305
281
|
process.exit(1);
|
|
306
282
|
}
|
|
307
283
|
|
|
308
|
-
await subscription.
|
|
284
|
+
await subscription.autoUpdateStaleSubscription();
|
|
309
285
|
|
|
310
286
|
// 每次 start 都先确保完全干净的状态(停止进程 + 清理运行时文件)
|
|
311
287
|
const status = processMgr.getStatus();
|
|
@@ -390,7 +366,7 @@ function cmdUI(args) {
|
|
|
390
366
|
function cmdLog(args) {
|
|
391
367
|
const logPath = processMgr.getLogPath();
|
|
392
368
|
|
|
393
|
-
if (hasFlag(args, '-o', '--open')) {
|
|
369
|
+
if (utils.hasFlag(args, '-o', '--open')) {
|
|
394
370
|
openLogFile(logPath);
|
|
395
371
|
return;
|
|
396
372
|
}
|
|
@@ -399,9 +375,9 @@ function cmdLog(args) {
|
|
|
399
375
|
}
|
|
400
376
|
|
|
401
377
|
function cmdLogs(args) {
|
|
402
|
-
const targetName = getNonFlagArg(args, 1);
|
|
403
|
-
const lines = parseIntArg(args, '-n', '--lines', 100);
|
|
404
|
-
const openInViewer = hasFlag(args, '-o', '--open');
|
|
378
|
+
const targetName = utils.getNonFlagArg(args, 1);
|
|
379
|
+
const lines = utils.parseIntArg(args, '-n', '--lines', 100);
|
|
380
|
+
const openInViewer = utils.hasFlag(args, '-o', '--open');
|
|
405
381
|
|
|
406
382
|
if (targetName) {
|
|
407
383
|
let logPath;
|
|
@@ -466,8 +442,8 @@ function cmdLogs(args) {
|
|
|
466
442
|
archiveCounter++;
|
|
467
443
|
num = archiveCounter < 10 ? ' ' + archiveCounter : '' + archiveCounter;
|
|
468
444
|
}
|
|
469
|
-
const time =
|
|
470
|
-
const size =
|
|
445
|
+
const time = utils.formatDate(log.mtime);
|
|
446
|
+
const size = utils.formatBytes(log.size);
|
|
471
447
|
const name = log.isCurrent ? 'mihomo.log (当前运行中)' : log.name;
|
|
472
448
|
|
|
473
449
|
console.log(' ' + num + '. ' + name);
|
|
@@ -586,7 +562,7 @@ async function cmdKernel(args) {
|
|
|
586
562
|
}
|
|
587
563
|
|
|
588
564
|
async function printSubscriptionList() {
|
|
589
|
-
const updateResult = await subscription.
|
|
565
|
+
const updateResult = await subscription.autoUpdateStaleSubscription();
|
|
590
566
|
if (updateResult.total > 0) {
|
|
591
567
|
console.log('');
|
|
592
568
|
}
|
|
@@ -601,7 +577,7 @@ async function printSubscriptionList() {
|
|
|
601
577
|
}
|
|
602
578
|
console.log('订阅列表:');
|
|
603
579
|
subs.forEach((s, i) => {
|
|
604
|
-
const time =
|
|
580
|
+
const time = utils.formatDate(s.updated_at);
|
|
605
581
|
const defaultMark = i === 0 ? ' [默认]' : '';
|
|
606
582
|
const interval = s.update_interval || subscription.DEFAULT_UPDATE_INTERVAL_HOURS;
|
|
607
583
|
console.log(' ' + (i + 1) + '. ' + s.name + defaultMark);
|
|
@@ -612,8 +588,8 @@ async function printSubscriptionList() {
|
|
|
612
588
|
}
|
|
613
589
|
if (s.download !== undefined || s.total !== undefined) {
|
|
614
590
|
const used = (s.upload || 0) + (s.download || 0);
|
|
615
|
-
const usedStr =
|
|
616
|
-
const totalStr =
|
|
591
|
+
const usedStr = utils.formatBytes(used);
|
|
592
|
+
const totalStr = utils.formatBytes(s.total);
|
|
617
593
|
let percentStr = '';
|
|
618
594
|
if (s.total && s.total > 0) {
|
|
619
595
|
const percent = Math.min((used / s.total) * 100, 100);
|
|
@@ -622,7 +598,7 @@ async function printSubscriptionList() {
|
|
|
622
598
|
console.log(' 流量: ' + usedStr + ' / ' + totalStr + percentStr);
|
|
623
599
|
}
|
|
624
600
|
if (s.expire !== undefined) {
|
|
625
|
-
console.log(' 到期: ' +
|
|
601
|
+
console.log(' 到期: ' + utils.formatTimestamp(s.expire));
|
|
626
602
|
}
|
|
627
603
|
if (s.web_page_url) {
|
|
628
604
|
console.log(' 页面: ' + s.web_page_url);
|
|
@@ -811,6 +787,27 @@ async function cmdSubscription(args) {
|
|
|
811
787
|
process.exit(1);
|
|
812
788
|
}
|
|
813
789
|
|
|
790
|
+
function cmdUpdate() {
|
|
791
|
+
console.log('更新 mihomo-cli...');
|
|
792
|
+
console.log('');
|
|
793
|
+
|
|
794
|
+
const npm = spawn('npm', ['install', '-g', 'mihomo-cli'], { stdio: 'inherit' });
|
|
795
|
+
|
|
796
|
+
npm.on('close', code => {
|
|
797
|
+
if (code === 0) {
|
|
798
|
+
console.log('');
|
|
799
|
+
console.log('更新完成');
|
|
800
|
+
} else {
|
|
801
|
+
process.exit(code);
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
npm.on('error', e => {
|
|
806
|
+
console.error('执行失败: ' + e.message);
|
|
807
|
+
process.exit(1);
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
814
811
|
async function cmdReset(args) {
|
|
815
812
|
const fullReset = args && (args.includes('--full') || args.includes('-f'));
|
|
816
813
|
const skipConfirm = args && (args.includes('--yes') || args.includes('-y'));
|
|
@@ -853,7 +850,7 @@ async function cmdReset(args) {
|
|
|
853
850
|
}
|
|
854
851
|
|
|
855
852
|
function printOverwriteList() {
|
|
856
|
-
const info = overwrite.
|
|
853
|
+
const info = overwrite.listOverwriteFile();
|
|
857
854
|
console.log('状态: ' + (info.enabled ? '已启用' : '已禁用'));
|
|
858
855
|
console.log('目录: ' + info.dir);
|
|
859
856
|
console.log('');
|
|
@@ -1059,6 +1056,11 @@ async function main() {
|
|
|
1059
1056
|
case 'kernel':
|
|
1060
1057
|
await cmdKernel(args);
|
|
1061
1058
|
break;
|
|
1059
|
+
case 'upd':
|
|
1060
|
+
case 'update':
|
|
1061
|
+
case 'upgrade':
|
|
1062
|
+
cmdUpdate();
|
|
1063
|
+
break;
|
|
1062
1064
|
case 'sub':
|
|
1063
1065
|
case 'subscription':
|
|
1064
1066
|
case 'subscriptions':
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -315,7 +315,7 @@ function buildConfig(subRawContent, mode) {
|
|
|
315
315
|
const overwrite = require('./overwrite');
|
|
316
316
|
|
|
317
317
|
// 应用覆写配置
|
|
318
|
-
const withOverwrites = overwrite.
|
|
318
|
+
const withOverwrites = overwrite.applyOverwrite(baseConfig);
|
|
319
319
|
|
|
320
320
|
// 合并 BASE_CONFIG(优先级高于覆写)
|
|
321
321
|
const merged = { ...withOverwrites, ...BASE_CONFIG };
|
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,11 +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
|
-
|
|
6
|
-
const _sharedBuf = new Int32Array(new SharedArrayBuffer(4));
|
|
7
|
-
function sleepSync(ms) {
|
|
8
|
-
Atomics.wait(_sharedBuf, 0, 0, ms);
|
|
9
|
-
}
|
|
5
|
+
const utils = require('./utils');
|
|
10
6
|
|
|
11
7
|
function clearRuntime() {
|
|
12
8
|
if (fs.existsSync(config.DIRS.runtime)) {
|
|
@@ -27,21 +23,9 @@ function getPid() {
|
|
|
27
23
|
}
|
|
28
24
|
}
|
|
29
25
|
|
|
30
|
-
function isProcessRunning(pid) {
|
|
31
|
-
if (!pid) return false;
|
|
32
|
-
try {
|
|
33
|
-
const output = execSync('ps -p ' + pid + ' -o pid= 2>/dev/null || true', {
|
|
34
|
-
encoding: 'utf8',
|
|
35
|
-
}).trim();
|
|
36
|
-
return output.length > 0;
|
|
37
|
-
} catch (e) {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
26
|
function isRunning() {
|
|
43
27
|
const pid = getPid();
|
|
44
|
-
return pid ? isProcessRunning(pid) : false;
|
|
28
|
+
return pid ? utils.isProcessRunning(pid) : false;
|
|
45
29
|
}
|
|
46
30
|
|
|
47
31
|
function getAllMihomoPids() {
|
|
@@ -61,17 +45,6 @@ function getAllMihomoPids() {
|
|
|
61
45
|
}
|
|
62
46
|
}
|
|
63
47
|
|
|
64
|
-
function isProcessRoot(pid) {
|
|
65
|
-
try {
|
|
66
|
-
const uidOutput = execSync('ps -p ' + pid + ' -o uid= 2>/dev/null || true', {
|
|
67
|
-
encoding: 'utf8',
|
|
68
|
-
}).trim();
|
|
69
|
-
return uidOutput === '0';
|
|
70
|
-
} catch (e) {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
48
|
function isPidFileOwnedByRoot() {
|
|
76
49
|
if (!fs.existsSync(config.PATHS.pidFile)) {
|
|
77
50
|
return false;
|
|
@@ -86,7 +59,7 @@ function isPidFileOwnedByRoot() {
|
|
|
86
59
|
|
|
87
60
|
function checkStaleState() {
|
|
88
61
|
const allPids = getAllMihomoPids();
|
|
89
|
-
const hasRootProcess = allPids.some(p => isProcessRoot(p));
|
|
62
|
+
const hasRootProcess = allPids.some(p => utils.isProcessRoot(p));
|
|
90
63
|
const hasRootPidFile = isPidFileOwnedByRoot();
|
|
91
64
|
|
|
92
65
|
return {
|
|
@@ -184,7 +157,7 @@ function cleanupAll(forceSudo) {
|
|
|
184
157
|
return { killed: 0, failed: 0, remaining: [] };
|
|
185
158
|
}
|
|
186
159
|
|
|
187
|
-
const hasRootProcess = pids.some(p => isProcessRoot(p));
|
|
160
|
+
const hasRootProcess = pids.some(p => utils.isProcessRoot(p));
|
|
188
161
|
const hasRootPidFile = isPidFileOwnedByRoot();
|
|
189
162
|
const needsSudo = hasRootProcess;
|
|
190
163
|
const allowSudo = forceSudo || hasRootProcess || hasRootPidFile;
|
|
@@ -216,7 +189,7 @@ function cleanupAll(forceSudo) {
|
|
|
216
189
|
|
|
217
190
|
for (let i = 0; i < 50; i++) {
|
|
218
191
|
if (getAllMihomoPids().length === 0) break;
|
|
219
|
-
sleepSync(100);
|
|
192
|
+
utils.sleepSync(100);
|
|
220
193
|
}
|
|
221
194
|
|
|
222
195
|
clearPid();
|
|
@@ -306,7 +279,7 @@ function getProcessInfo(pid) {
|
|
|
306
279
|
pid,
|
|
307
280
|
memory: rss ? (rss / 1024).toFixed(1) + ' MB' : '未知',
|
|
308
281
|
cpu: pcpu ? pcpu.toFixed(1) + '%' : '未知',
|
|
309
|
-
isRoot: isProcessRoot(pid),
|
|
282
|
+
isRoot: utils.isProcessRoot(pid),
|
|
310
283
|
};
|
|
311
284
|
} catch (e) {
|
|
312
285
|
return { pid, memory: '未知', cpu: '未知', isRoot: false };
|
|
@@ -606,6 +579,12 @@ function listLogs() {
|
|
|
606
579
|
return result;
|
|
607
580
|
}
|
|
608
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
|
+
|
|
609
588
|
function getLogPathByName(name) {
|
|
610
589
|
const logsDir = config.DIRS.logs;
|
|
611
590
|
|
|
@@ -619,16 +598,19 @@ function getLogPathByName(name) {
|
|
|
619
598
|
}
|
|
620
599
|
|
|
621
600
|
const filePath = path.join(logsDir, targetName);
|
|
622
|
-
if (fs.existsSync(filePath)) {
|
|
601
|
+
if (fs.existsSync(filePath) && isPathUnderDir(filePath, logsDir)) {
|
|
623
602
|
return filePath;
|
|
624
603
|
}
|
|
625
604
|
|
|
626
|
-
//
|
|
605
|
+
// 尝试模糊匹配(readdirSync 返回的文件名已是安全的,但为了一致性仍校验)
|
|
627
606
|
if (fs.existsSync(logsDir)) {
|
|
628
607
|
const files = fs.readdirSync(logsDir);
|
|
629
608
|
for (const file of files) {
|
|
630
609
|
if (file.includes(name)) {
|
|
631
|
-
|
|
610
|
+
const candidatePath = path.join(logsDir, file);
|
|
611
|
+
if (isPathUnderDir(candidatePath, logsDir)) {
|
|
612
|
+
return candidatePath;
|
|
613
|
+
}
|
|
632
614
|
}
|
|
633
615
|
}
|
|
634
616
|
}
|
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,35 +37,6 @@ 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
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
40
|
function formatProxySummary(info) {
|
|
71
41
|
const parts = [];
|
|
72
42
|
if (info && info.proxyGroups > 0) parts.push(info.proxyGroups + ' 组');
|
|
@@ -178,7 +148,7 @@ async function tryUpdateOne(sub) {
|
|
|
178
148
|
}
|
|
179
149
|
}
|
|
180
150
|
|
|
181
|
-
async function
|
|
151
|
+
async function autoUpdateStaleSubscription() {
|
|
182
152
|
const allSubs = config.getSubscriptionsWithCache();
|
|
183
153
|
const staleSubs = allSubs.filter(needsAutoUpdate);
|
|
184
154
|
|
|
@@ -217,10 +187,7 @@ module.exports = {
|
|
|
217
187
|
DEFAULT_UPDATE_INTERVAL_HOURS,
|
|
218
188
|
downloadSubscription,
|
|
219
189
|
prepareConfigForStart,
|
|
220
|
-
formatBytes,
|
|
221
|
-
formatTimestamp,
|
|
222
|
-
formatDate,
|
|
223
190
|
formatProxySummary,
|
|
224
191
|
tryUpdateOne,
|
|
225
|
-
|
|
192
|
+
autoUpdateStaleSubscription,
|
|
226
193
|
};
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
|
|
3
|
+
const _sleepBuf = new Int32Array(1);
|
|
4
|
+
|
|
5
|
+
function sleepSync(ms) {
|
|
6
|
+
Atomics.wait(_sleepBuf, 0, 0, ms);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatBytes(bytes) {
|
|
10
|
+
if (bytes === undefined || bytes === null) return '未知';
|
|
11
|
+
const num = Number(bytes);
|
|
12
|
+
if (isNaN(num) || num < 0) return '未知';
|
|
13
|
+
if (num === 0) return '0 B';
|
|
14
|
+
const k = 1024;
|
|
15
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
16
|
+
const i = Math.floor(Math.log(num) / Math.log(k));
|
|
17
|
+
return parseFloat((num / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatTimestamp(ts) {
|
|
21
|
+
if (ts === undefined || ts === null) return '未知';
|
|
22
|
+
try {
|
|
23
|
+
return new Date(ts * 1000).toLocaleString('zh-CN');
|
|
24
|
+
} catch {
|
|
25
|
+
return '未知';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatDate(dateOrIso) {
|
|
30
|
+
if (dateOrIso === undefined || dateOrIso === null) return '未知';
|
|
31
|
+
try {
|
|
32
|
+
const d = dateOrIso instanceof Date ? dateOrIso : new Date(dateOrIso);
|
|
33
|
+
if (isNaN(d.getTime())) return '未知';
|
|
34
|
+
return d.toLocaleString('zh-CN');
|
|
35
|
+
} catch {
|
|
36
|
+
return '未知';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hasFlag(args, short, long) {
|
|
41
|
+
return args && (args.includes(short) || args.includes(long));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseIntArg(args, short, long, defaultValue) {
|
|
45
|
+
if (!args) return defaultValue;
|
|
46
|
+
for (let i = 0; i < args.length; i++) {
|
|
47
|
+
if (args[i] === short || args[i] === long) {
|
|
48
|
+
if (i + 1 < args.length) {
|
|
49
|
+
const val = parseInt(args[i + 1]);
|
|
50
|
+
return isNaN(val) ? defaultValue : val;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return defaultValue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getNonFlagArg(args, startIdx) {
|
|
58
|
+
if (!args) return null;
|
|
59
|
+
for (let i = startIdx; i < args.length; i++) {
|
|
60
|
+
if (!args[i].startsWith('-')) {
|
|
61
|
+
return args[i];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isProcessRunning(pid) {
|
|
68
|
+
if (!pid) return false;
|
|
69
|
+
try {
|
|
70
|
+
const output = execSync('ps -p ' + pid + ' -o pid= 2>/dev/null || true', {
|
|
71
|
+
encoding: 'utf8',
|
|
72
|
+
}).trim();
|
|
73
|
+
return output.length > 0;
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isProcessRoot(pid) {
|
|
80
|
+
if (!pid) return false;
|
|
81
|
+
try {
|
|
82
|
+
const uidOutput = execSync('ps -p ' + pid + ' -o uid= 2>/dev/null || true', {
|
|
83
|
+
encoding: 'utf8',
|
|
84
|
+
}).trim();
|
|
85
|
+
return uidOutput === '0';
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
sleepSync,
|
|
93
|
+
formatBytes,
|
|
94
|
+
formatTimestamp,
|
|
95
|
+
formatDate,
|
|
96
|
+
hasFlag,
|
|
97
|
+
parseIntArg,
|
|
98
|
+
getNonFlagArg,
|
|
99
|
+
isProcessRunning,
|
|
100
|
+
isProcessRoot,
|
|
101
|
+
};
|