mihomo-cli 1.0.0-alpha.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/LICENSE +21 -0
- package/README.md +244 -0
- package/index.js +480 -0
- package/package.json +37 -0
- package/src/config.js +328 -0
- package/src/kernel.js +297 -0
- package/src/process.js +532 -0
- package/src/subscription.js +89 -0
package/src/config.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
const IS_PKG = typeof process.pkg !== 'undefined';
|
|
7
|
+
|
|
8
|
+
let PROJECT_ROOT;
|
|
9
|
+
if (IS_PKG) {
|
|
10
|
+
PROJECT_ROOT = path.dirname(process.execPath);
|
|
11
|
+
} else {
|
|
12
|
+
PROJECT_ROOT = path.join(__dirname, '..');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getUserDataDir() {
|
|
16
|
+
if (process.env.MIHOMO_CLI_DIR) {
|
|
17
|
+
return process.env.MIHOMO_CLI_DIR;
|
|
18
|
+
}
|
|
19
|
+
return path.join(os.homedir(), '.mihomo-cli');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const USER_DATA_DIR = getUserDataDir();
|
|
23
|
+
|
|
24
|
+
const DIRS = {
|
|
25
|
+
root: PROJECT_ROOT,
|
|
26
|
+
core: path.join(USER_DATA_DIR, 'core'),
|
|
27
|
+
subs: path.join(USER_DATA_DIR, 'subs'),
|
|
28
|
+
logs: path.join(USER_DATA_DIR, 'logs'),
|
|
29
|
+
data: path.join(USER_DATA_DIR, 'data'),
|
|
30
|
+
runtime: path.join(USER_DATA_DIR, '.runtime'),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const PATHS = {
|
|
34
|
+
root: DIRS.root,
|
|
35
|
+
data: DIRS.data,
|
|
36
|
+
userDataDir: USER_DATA_DIR,
|
|
37
|
+
mihomoBinary: path.join(DIRS.core, 'mihomo'),
|
|
38
|
+
settingsFile: path.join(USER_DATA_DIR, 'settings.json'),
|
|
39
|
+
configFile: path.join(DIRS.runtime, 'config.yaml'),
|
|
40
|
+
logFile: path.join(DIRS.logs, 'mihomo.log'),
|
|
41
|
+
pidFile: path.join(DIRS.runtime, 'pid'),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function ensureDirs() {
|
|
45
|
+
Object.values(DIRS).forEach(dir => {
|
|
46
|
+
if (!fs.existsSync(dir)) {
|
|
47
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function maskUrl(url) {
|
|
53
|
+
if (!url) return url;
|
|
54
|
+
try {
|
|
55
|
+
const parsed = new URL(url);
|
|
56
|
+
const tokenKeys = ['token', 'key', 'secret', 'pass', 'password', 'auth', 'access_token', 'api_key'];
|
|
57
|
+
for (const key of tokenKeys) {
|
|
58
|
+
if (parsed.searchParams.has(key)) {
|
|
59
|
+
parsed.searchParams.set(key, '***');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (parsed.username) {
|
|
63
|
+
parsed.username = '***';
|
|
64
|
+
}
|
|
65
|
+
if (parsed.password) {
|
|
66
|
+
parsed.password = '***';
|
|
67
|
+
}
|
|
68
|
+
return parsed.toString();
|
|
69
|
+
} catch {
|
|
70
|
+
if (url.length > 30) {
|
|
71
|
+
return url.slice(0, 15) + '...' + url.slice(-10);
|
|
72
|
+
}
|
|
73
|
+
return url;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readSettings() {
|
|
78
|
+
ensureDirs();
|
|
79
|
+
if (fs.existsSync(PATHS.settingsFile)) {
|
|
80
|
+
try {
|
|
81
|
+
const content = fs.readFileSync(PATHS.settingsFile, 'utf8');
|
|
82
|
+
return JSON.parse(content);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function writeSettings(settings) {
|
|
91
|
+
ensureDirs();
|
|
92
|
+
const existing = readSettings();
|
|
93
|
+
const merged = { ...existing, ...settings };
|
|
94
|
+
fs.writeFileSync(PATHS.settingsFile, JSON.stringify(merged, null, 2), { mode: 0o600 });
|
|
95
|
+
return merged;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getSubscriptions() {
|
|
99
|
+
const settings = readSettings();
|
|
100
|
+
return settings.subscriptions || [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function addSubscription(url, name) {
|
|
104
|
+
if (name === undefined) name = 'default';
|
|
105
|
+
const settings = readSettings();
|
|
106
|
+
const subs = settings.subscriptions || [];
|
|
107
|
+
const existingIndex = subs.findIndex(s => s.name === name);
|
|
108
|
+
if (existingIndex >= 0) {
|
|
109
|
+
subs[existingIndex] = { name, url, updatedAt: new Date().toISOString() };
|
|
110
|
+
} else {
|
|
111
|
+
subs.push({ name, url, updatedAt: null });
|
|
112
|
+
}
|
|
113
|
+
writeSettings({ subscriptions: subs });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getSubRawConfigPath(subName) {
|
|
117
|
+
return path.join(DIRS.subs, subName + '.yaml');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function saveSubRawConfig(subName, content) {
|
|
121
|
+
ensureDirs();
|
|
122
|
+
const filePath = getSubRawConfigPath(subName);
|
|
123
|
+
fs.writeFileSync(filePath, content, { mode: 0o600 });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readSubRawConfig(subName) {
|
|
127
|
+
const filePath = getSubRawConfigPath(subName);
|
|
128
|
+
if (!fs.existsSync(filePath)) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function hasKernel() {
|
|
135
|
+
return fs.existsSync(PATHS.mihomoBinary);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getKernelVersion() {
|
|
139
|
+
if (!hasKernel()) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const output = execSync('"' + PATHS.mihomoBinary + '" -v 2>&1 || true', {
|
|
144
|
+
encoding: 'utf8',
|
|
145
|
+
}).trim();
|
|
146
|
+
if (output) {
|
|
147
|
+
const match = output.match(/v?[\d]+\.[\d]+\.[\d]+/);
|
|
148
|
+
if (match) {
|
|
149
|
+
return match[0];
|
|
150
|
+
}
|
|
151
|
+
return output;
|
|
152
|
+
}
|
|
153
|
+
return 'unknown';
|
|
154
|
+
} catch (e) {
|
|
155
|
+
return 'unknown';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const TUN_CONFIG = {
|
|
160
|
+
tun: {
|
|
161
|
+
enable: true,
|
|
162
|
+
stack: 'mixed',
|
|
163
|
+
'dns-hijack': ['0.0.0.0:53'],
|
|
164
|
+
'auto-route': true,
|
|
165
|
+
'auto-detect-interface': true,
|
|
166
|
+
'strict-route': true,
|
|
167
|
+
},
|
|
168
|
+
ipv6: false,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const BASE_CONFIG = {
|
|
172
|
+
'log-level': 'warning',
|
|
173
|
+
'geodata-mode': true,
|
|
174
|
+
'geo-update-interval': 24,
|
|
175
|
+
'geox-url': {
|
|
176
|
+
geoip: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip-lite.dat',
|
|
177
|
+
geosite: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite-lite.dat',
|
|
178
|
+
mmdb: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/country-lite.mmdb',
|
|
179
|
+
asn: 'https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/GeoLite2-ASN.mmdb',
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
function buildConfig(subRawContent, mode) {
|
|
184
|
+
const yaml = require('js-yaml');
|
|
185
|
+
|
|
186
|
+
let baseConfig;
|
|
187
|
+
try {
|
|
188
|
+
baseConfig = yaml.load(subRawContent);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
try {
|
|
191
|
+
baseConfig = JSON.parse(subRawContent);
|
|
192
|
+
} catch (e2) {
|
|
193
|
+
throw new Error('订阅内容格式错误,无法解析为 YAML 或 JSON');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!baseConfig) {
|
|
198
|
+
throw new Error('订阅内容为空');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const merged = { ...baseConfig, ...BASE_CONFIG };
|
|
202
|
+
|
|
203
|
+
if (mode === 'tun') {
|
|
204
|
+
// 合并 TUN 配置
|
|
205
|
+
merged.tun = TUN_CONFIG.tun;
|
|
206
|
+
merged.ipv6 = TUN_CONFIG.ipv6;
|
|
207
|
+
|
|
208
|
+
// 确保 DNS 配置与 TUN 模式兼容(保留订阅的 DNS 服务器)
|
|
209
|
+
merged.dns = merged.dns || {};
|
|
210
|
+
merged.dns.enable = true;
|
|
211
|
+
merged.dns['enhanced-mode'] = 'fake-ip';
|
|
212
|
+
merged.dns['fake-ip-range'] = merged.dns['fake-ip-range'] || '198.18.0.1/16';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return merged;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function writeMihomoConfig(configObj) {
|
|
219
|
+
const yaml = require('js-yaml');
|
|
220
|
+
ensureDirs();
|
|
221
|
+
const content = yaml.dump(configObj, {
|
|
222
|
+
indent: 2,
|
|
223
|
+
lineWidth: -1,
|
|
224
|
+
noCompat: true,
|
|
225
|
+
});
|
|
226
|
+
fs.writeFileSync(PATHS.configFile, content, { mode: 0o600 });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function hasConfig() {
|
|
230
|
+
return fs.existsSync(PATHS.configFile);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function getConfigInfo() {
|
|
234
|
+
if (!hasConfig()) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const yaml = require('js-yaml');
|
|
240
|
+
const content = fs.readFileSync(PATHS.configFile, 'utf8');
|
|
241
|
+
const cfg = yaml.load(content);
|
|
242
|
+
|
|
243
|
+
if (!cfg) return null;
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
proxies: cfg.proxies ? cfg.proxies.length : 0,
|
|
247
|
+
proxyGroups: cfg['proxy-groups'] ? cfg['proxy-groups'].length : 0,
|
|
248
|
+
mode: cfg.mode || 'rule',
|
|
249
|
+
port: cfg.port || cfg['mixed-port'] || '未知',
|
|
250
|
+
tun: cfg.tun ? cfg.tun.enable : false,
|
|
251
|
+
};
|
|
252
|
+
} catch (e) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function rmrf(dir) {
|
|
258
|
+
if (!fs.existsSync(dir)) return;
|
|
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
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function resetUserData(options) {
|
|
272
|
+
if (options === undefined) options = {};
|
|
273
|
+
const keepKernel = options.keepKernel !== false;
|
|
274
|
+
|
|
275
|
+
const itemsToRemove = [
|
|
276
|
+
PATHS.settingsFile,
|
|
277
|
+
DIRS.subs,
|
|
278
|
+
DIRS.logs,
|
|
279
|
+
DIRS.data,
|
|
280
|
+
DIRS.runtime,
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
if (!keepKernel) {
|
|
284
|
+
itemsToRemove.push(DIRS.core);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let removedCount = 0;
|
|
288
|
+
for (const item of itemsToRemove) {
|
|
289
|
+
if (fs.existsSync(item)) {
|
|
290
|
+
try {
|
|
291
|
+
rmrf(item);
|
|
292
|
+
removedCount++;
|
|
293
|
+
} catch (e) {
|
|
294
|
+
console.warn(' 警告: 无法删除 ' + item + ': ' + e.message);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
ensureDirs();
|
|
300
|
+
return removedCount;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = {
|
|
304
|
+
PATHS,
|
|
305
|
+
DIRS,
|
|
306
|
+
PROJECT_ROOT,
|
|
307
|
+
USER_DATA_DIR,
|
|
308
|
+
IS_PKG,
|
|
309
|
+
ensureDirs,
|
|
310
|
+
readSettings,
|
|
311
|
+
writeSettings,
|
|
312
|
+
maskUrl,
|
|
313
|
+
getSubscriptions,
|
|
314
|
+
addSubscription,
|
|
315
|
+
getSubRawConfigPath,
|
|
316
|
+
saveSubRawConfig,
|
|
317
|
+
readSubRawConfig,
|
|
318
|
+
hasKernel,
|
|
319
|
+
getKernelVersion,
|
|
320
|
+
TUN_CONFIG,
|
|
321
|
+
BASE_CONFIG,
|
|
322
|
+
buildConfig,
|
|
323
|
+
writeMihomoConfig,
|
|
324
|
+
hasConfig,
|
|
325
|
+
getConfigInfo,
|
|
326
|
+
resetUserData,
|
|
327
|
+
rmrf,
|
|
328
|
+
};
|
package/src/kernel.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const { compareVersions } = require('compare-versions');
|
|
6
|
+
const config = require('./config');
|
|
7
|
+
|
|
8
|
+
const GITHUB_REPO = 'MetaCubeX/mihomo';
|
|
9
|
+
const GEODATA_REPO = 'MetaCubeX/meta-rules-dat';
|
|
10
|
+
|
|
11
|
+
const GITHUB_DOWNLOAD_MIRROR = 'https://gh-proxy.org/';
|
|
12
|
+
|
|
13
|
+
function withMirror(url) {
|
|
14
|
+
if (GITHUB_DOWNLOAD_MIRROR && url.startsWith('https://github.com/')) {
|
|
15
|
+
return GITHUB_DOWNLOAD_MIRROR + url;
|
|
16
|
+
}
|
|
17
|
+
return url;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const HTTP_CLIENT = axios.create({
|
|
21
|
+
timeout: 120000,
|
|
22
|
+
headers: { 'User-Agent': 'mihomo-cli' },
|
|
23
|
+
maxContentLength: 200 * 1024 * 1024,
|
|
24
|
+
});
|
|
25
|
+
|
|
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
|
+
function getArch() {
|
|
32
|
+
const arch = process.arch;
|
|
33
|
+
if (arch === 'arm64') return 'arm64';
|
|
34
|
+
if (arch === 'x64') return 'amd64';
|
|
35
|
+
return arch;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findMatchingAsset(assets, platform, arch) {
|
|
39
|
+
const prefix = 'mihomo-' + platform + '-' + arch;
|
|
40
|
+
const matchingAssets = assets.filter(a => {
|
|
41
|
+
return (a.name.startsWith(prefix) && a.name.endsWith('.gz')) ||
|
|
42
|
+
(a.name.startsWith(prefix + '-') && a.name.endsWith('.gz'));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (matchingAssets.length === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (matchingAssets.length === 1) {
|
|
50
|
+
return matchingAssets[0];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const standardAsset = matchingAssets.find(a => {
|
|
54
|
+
const nameWithoutGz = a.name.slice(0, -3);
|
|
55
|
+
const parts = nameWithoutGz.split('-');
|
|
56
|
+
const lastPart = parts[parts.length - 1];
|
|
57
|
+
return /^v?\d+\.\d+\.\d+/.test(lastPart) && !nameWithoutGz.includes('-go');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (standardAsset) {
|
|
61
|
+
return standardAsset;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return matchingAssets[0];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function getLatestRelease(repo) {
|
|
68
|
+
const url = 'https://api.github.com/repos/' + repo + '/releases';
|
|
69
|
+
const response = await HTTP_CLIENT.get(url);
|
|
70
|
+
|
|
71
|
+
const releases = response.data;
|
|
72
|
+
|
|
73
|
+
if (!Array.isArray(releases) || releases.length === 0) {
|
|
74
|
+
throw new Error('无法获取版本信息');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const stableReleases = releases.filter(r =>
|
|
78
|
+
!r.prerelease &&
|
|
79
|
+
!r.tag_name.toLowerCase().includes('alpha') &&
|
|
80
|
+
!r.tag_name.toLowerCase().includes('beta') &&
|
|
81
|
+
!r.tag_name.toLowerCase().includes('prerelease')
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (stableReleases.length > 0) {
|
|
85
|
+
return stableReleases[0];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return releases[0];
|
|
89
|
+
}
|
|
90
|
+
|
|
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
|
+
async function checkUpdate() {
|
|
143
|
+
const currentVersion = config.getKernelVersion();
|
|
144
|
+
const latest = await getLatestRelease(GITHUB_REPO);
|
|
145
|
+
const latestVersion = latest.tag_name;
|
|
146
|
+
|
|
147
|
+
let needsUpdate = false;
|
|
148
|
+
let currentDisplay = currentVersion || '未安装';
|
|
149
|
+
|
|
150
|
+
if (!currentVersion) {
|
|
151
|
+
needsUpdate = true;
|
|
152
|
+
} else {
|
|
153
|
+
try {
|
|
154
|
+
needsUpdate = compareVersions(latestVersion.replace(/^v/, ''), currentVersion.replace(/^v/, '')) > 0;
|
|
155
|
+
} catch (e) {
|
|
156
|
+
needsUpdate = latestVersion !== currentVersion;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
current: currentDisplay,
|
|
162
|
+
latest: latestVersion,
|
|
163
|
+
needsUpdate,
|
|
164
|
+
assets: latest.assets,
|
|
165
|
+
htmlUrl: latest.html_url,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function findBinaryInDir(dir) {
|
|
170
|
+
const files = fs.readdirSync(dir);
|
|
171
|
+
|
|
172
|
+
for (const f of files) {
|
|
173
|
+
const fullPath = path.join(dir, f);
|
|
174
|
+
const stat = fs.statSync(fullPath);
|
|
175
|
+
|
|
176
|
+
if (stat.isDirectory()) {
|
|
177
|
+
const found = findBinaryInDir(fullPath);
|
|
178
|
+
if (found) return found;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (f === 'mihomo') {
|
|
183
|
+
return fullPath;
|
|
184
|
+
}
|
|
185
|
+
if (f.includes('mihomo') && !f.endsWith('.gz')) {
|
|
186
|
+
return fullPath;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function downloadKernel(progressCallback) {
|
|
194
|
+
config.ensureDirs();
|
|
195
|
+
|
|
196
|
+
const latest = await getLatestRelease(GITHUB_REPO);
|
|
197
|
+
const arch = getArch();
|
|
198
|
+
const platform = 'darwin';
|
|
199
|
+
|
|
200
|
+
const asset = findMatchingAsset(latest.assets, platform, arch);
|
|
201
|
+
|
|
202
|
+
if (!asset) {
|
|
203
|
+
const available = latest.assets.map(a => a.name).join(', ');
|
|
204
|
+
let hint = '';
|
|
205
|
+
if (available) {
|
|
206
|
+
hint = '\n 可用版本: ' + available;
|
|
207
|
+
}
|
|
208
|
+
throw new Error('未找到匹配的内核文件\n 平台: ' + platform + ', 架构: ' + arch + hint);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const downloadUrl = withMirror(asset.browser_download_url);
|
|
212
|
+
const tempPath = path.join(config.DIRS.core, asset.name);
|
|
213
|
+
|
|
214
|
+
if (progressCallback) {
|
|
215
|
+
const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
|
|
216
|
+
progressCallback('下载内核: ' + asset.name + ' (' + sizeMB + ' MB)');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const response = await HTTP_CLIENT({
|
|
220
|
+
method: 'get',
|
|
221
|
+
url: downloadUrl,
|
|
222
|
+
responseType: 'stream',
|
|
223
|
+
timeout: 180000,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const writer = fs.createWriteStream(tempPath);
|
|
227
|
+
response.data.pipe(writer);
|
|
228
|
+
|
|
229
|
+
await new Promise((resolve, reject) => {
|
|
230
|
+
writer.on('finish', resolve);
|
|
231
|
+
writer.on('error', reject);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (progressCallback) {
|
|
235
|
+
progressCallback('解压内核...');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const extractPath = config.DIRS.core;
|
|
239
|
+
let extractedBinary = null;
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
if (tempPath.endsWith('.tar.gz') || tempPath.endsWith('.tgz')) {
|
|
243
|
+
execSync('tar -xzf "' + tempPath + '" -C "' + extractPath + '"', {
|
|
244
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
245
|
+
});
|
|
246
|
+
} else if (tempPath.endsWith('.gz')) {
|
|
247
|
+
const baseName = path.basename(tempPath, '.gz');
|
|
248
|
+
const outputPath = path.join(extractPath, baseName);
|
|
249
|
+
execSync('gzip -dc "' + tempPath + '" > "' + outputPath + '"', {
|
|
250
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
251
|
+
});
|
|
252
|
+
extractedBinary = outputPath;
|
|
253
|
+
}
|
|
254
|
+
} catch (e) {
|
|
255
|
+
try { fs.unlinkSync(tempPath); } catch {}
|
|
256
|
+
throw new Error('解压失败: ' + e.message);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const foundBinary = extractedBinary || findBinaryInDir(extractPath);
|
|
260
|
+
|
|
261
|
+
if (!foundBinary) {
|
|
262
|
+
try { fs.unlinkSync(tempPath); } catch {}
|
|
263
|
+
throw new Error('解压后未找到可执行文件');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const targetPath = config.PATHS.mihomoBinary;
|
|
267
|
+
|
|
268
|
+
if (foundBinary !== targetPath) {
|
|
269
|
+
if (fs.existsSync(targetPath)) {
|
|
270
|
+
fs.chmodSync(targetPath, 0o755);
|
|
271
|
+
try {
|
|
272
|
+
fs.unlinkSync(targetPath);
|
|
273
|
+
} catch {}
|
|
274
|
+
}
|
|
275
|
+
fs.renameSync(foundBinary, targetPath);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
fs.chmodSync(targetPath, 0o755);
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
fs.unlinkSync(tempPath);
|
|
282
|
+
} catch (e) {}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
version: latest.tag_name,
|
|
286
|
+
path: targetPath,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
module.exports = {
|
|
291
|
+
getArch,
|
|
292
|
+
getLatestRelease,
|
|
293
|
+
findMatchingAsset,
|
|
294
|
+
checkUpdate,
|
|
295
|
+
downloadKernel,
|
|
296
|
+
downloadGeodata,
|
|
297
|
+
};
|