mihomo-cli 1.2.4 → 1.3.0
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 +56 -0
- package/README.md +1 -0
- package/index.js +247 -287
- package/package.json +18 -11
- package/src/config.js +59 -40
- package/src/kernel.js +23 -11
- package/src/overwrite.js +7 -1
- package/src/process.js +87 -24
- package/src/subscription.js +72 -15
- package/src/utils.js +99 -4
package/src/subscription.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
// 内置模块
|
|
2
|
+
// (无内置模块依赖)
|
|
3
|
+
|
|
4
|
+
// 第三方模块
|
|
5
|
+
// (无第三方模块依赖,axios 通过 utils.createHttpClient 间接使用)
|
|
6
|
+
|
|
7
|
+
// 本地模块
|
|
2
8
|
const config = require('./config');
|
|
9
|
+
const utils = require('./utils');
|
|
3
10
|
|
|
11
|
+
const { colors } = utils;
|
|
4
12
|
const DEFAULT_UPDATE_INTERVAL_HOURS = 12;
|
|
5
13
|
|
|
6
|
-
|
|
14
|
+
// 订阅专用 HTTP 客户端(超时较短,适合下载订阅配置)
|
|
15
|
+
const HTTP_CLIENT = utils.createHttpClient({
|
|
7
16
|
timeout: 60000,
|
|
8
|
-
headers: {
|
|
9
|
-
'User-Agent': 'mihomo-cli/1.0',
|
|
10
|
-
},
|
|
11
17
|
maxContentLength: 50 * 1024 * 1024,
|
|
12
|
-
maxBodyLength: 50 * 1024 * 1024,
|
|
13
18
|
});
|
|
14
19
|
|
|
15
20
|
function parseUserInfo(header) {
|
|
@@ -28,11 +33,9 @@ function parseUserInfo(header) {
|
|
|
28
33
|
|
|
29
34
|
function parseUsernameFromContentDisposition(header) {
|
|
30
35
|
if (!header) return null;
|
|
31
|
-
// 匹配 filename="..." 或 filename='...'
|
|
32
36
|
const match = header.match(/filename\s*=\s*["']?([^"';\s]+)["']?/i);
|
|
33
37
|
if (!match) return null;
|
|
34
38
|
const filename = match[1];
|
|
35
|
-
// 可能是 "glados.one/user@example.com" 格式,取最后一部分
|
|
36
39
|
const parts = filename.split('/');
|
|
37
40
|
return parts[parts.length - 1] || null;
|
|
38
41
|
}
|
|
@@ -44,6 +47,60 @@ function formatProxySummary(info) {
|
|
|
44
47
|
return parts.join(', ');
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
/**
|
|
51
|
+
* 获取当前默认订阅(从 index.js 移入)
|
|
52
|
+
*/
|
|
53
|
+
function getActiveSubscription() {
|
|
54
|
+
const subs = config.getSubscriptions();
|
|
55
|
+
if (subs.length === 0) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return subs[0];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 模糊查找订阅(从 index.js 移入)
|
|
63
|
+
*/
|
|
64
|
+
function findSubscriptionFuzzy(subs, pattern) {
|
|
65
|
+
const lowerPattern = pattern.toLowerCase();
|
|
66
|
+
let exact = [];
|
|
67
|
+
let prefix = [];
|
|
68
|
+
let includes = [];
|
|
69
|
+
|
|
70
|
+
for (const s of subs) {
|
|
71
|
+
const name = s.name.toLowerCase();
|
|
72
|
+
if (name === lowerPattern) {
|
|
73
|
+
exact.push(s);
|
|
74
|
+
} else if (name.startsWith(lowerPattern)) {
|
|
75
|
+
prefix.push(s);
|
|
76
|
+
} else if (name.includes(lowerPattern)) {
|
|
77
|
+
includes.push(s);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (exact.length > 0) return exact;
|
|
82
|
+
if (prefix.length > 0) return prefix;
|
|
83
|
+
return includes;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 从匹配列表中选择单个订阅(从 index.js 移入)
|
|
88
|
+
* 如果匹配多个,打印错误并退出进程
|
|
89
|
+
*/
|
|
90
|
+
function pickSingleSubscription(subs, pattern) {
|
|
91
|
+
if (subs.length === 0) {
|
|
92
|
+
console.error('错误: 未找到匹配 "' + pattern + '" 的订阅');
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
if (subs.length === 1) {
|
|
96
|
+
return subs[0];
|
|
97
|
+
}
|
|
98
|
+
console.error('错误: 匹配到多个订阅,请更精确指定');
|
|
99
|
+
console.log('\n匹配的订阅:');
|
|
100
|
+
subs.forEach(s => console.log(' ' + s.name));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
47
104
|
async function downloadSubscription(url, subName) {
|
|
48
105
|
if (subName === undefined) subName = 'default';
|
|
49
106
|
|
|
@@ -72,17 +129,14 @@ async function downloadSubscription(url, subName) {
|
|
|
72
129
|
throw new Error('订阅内容为空');
|
|
73
130
|
}
|
|
74
131
|
|
|
75
|
-
config.
|
|
132
|
+
config.saveSubscriptionRawConfig(subName, content);
|
|
76
133
|
|
|
77
|
-
// 提取 response headers 中的订阅信息
|
|
78
134
|
const headers = response.headers;
|
|
79
135
|
const userInfo = parseUserInfo(headers['subscription-userinfo']);
|
|
80
136
|
const updateInterval = headers['profile-update-interval'] ? parseInt(headers['profile-update-interval']) : null;
|
|
81
137
|
const webPageUrl = headers['profile-web-page-url'] || null;
|
|
82
138
|
const username = parseUsernameFromContentDisposition(headers['content-disposition']);
|
|
83
139
|
|
|
84
|
-
// 2. 动态数据 + updated_at 保存到缓存文件(settings 只存 name/url)
|
|
85
|
-
|
|
86
140
|
const cacheData = {
|
|
87
141
|
updated_at: new Date().toISOString(),
|
|
88
142
|
};
|
|
@@ -116,7 +170,7 @@ async function downloadSubscription(url, subName) {
|
|
|
116
170
|
function prepareConfigForStart(mode, subName) {
|
|
117
171
|
if (subName === undefined) subName = 'default';
|
|
118
172
|
|
|
119
|
-
const rawContent = config.
|
|
173
|
+
const rawContent = config.readSubscriptionRawConfig(subName);
|
|
120
174
|
if (!rawContent) {
|
|
121
175
|
throw new Error('未找到订阅配置 "' + subName + '",请先添加订阅');
|
|
122
176
|
}
|
|
@@ -170,9 +224,9 @@ async function autoUpdateStaleSubscription() {
|
|
|
170
224
|
results.forEach(r => {
|
|
171
225
|
if (r.success) {
|
|
172
226
|
updatedCount++;
|
|
173
|
-
console.log('✓ ' + r.name + ': 已更新 (' + formatProxySummary(r) + ')');
|
|
227
|
+
console.log(colors.green('✓') + ' ' + r.name + ': ' + colors.green('已更新') + ' (' + formatProxySummary(r) + ')');
|
|
174
228
|
} else {
|
|
175
|
-
console.log('✗ ' + r.name + ': 失败 (' + r.error.split('\n')[0] + ')');
|
|
229
|
+
console.log(colors.red('✗') + ' ' + r.name + ': ' + colors.red('失败') + ' (' + r.error.split('\n')[0] + ')');
|
|
176
230
|
}
|
|
177
231
|
});
|
|
178
232
|
|
|
@@ -185,6 +239,9 @@ async function autoUpdateStaleSubscription() {
|
|
|
185
239
|
|
|
186
240
|
module.exports = {
|
|
187
241
|
DEFAULT_UPDATE_INTERVAL_HOURS,
|
|
242
|
+
getActiveSubscription,
|
|
243
|
+
findSubscriptionFuzzy,
|
|
244
|
+
pickSingleSubscription,
|
|
188
245
|
downloadSubscription,
|
|
189
246
|
prepareConfigForStart,
|
|
190
247
|
formatProxySummary,
|
package/src/utils.js
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
|
+
// 内置模块
|
|
1
2
|
const { execSync } = require('child_process');
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
// 第三方模块
|
|
5
|
+
const axios = require('axios');
|
|
6
|
+
|
|
7
|
+
// 本地模块
|
|
8
|
+
// (无本地模块依赖)
|
|
9
|
+
|
|
10
|
+
const VERSION = require('../package.json').version;
|
|
11
|
+
|
|
12
|
+
const sleepBuf = new Int32Array(1);
|
|
13
|
+
|
|
14
|
+
const NO_COLOR = process.env.NO_COLOR !== undefined || !process.stdout.isTTY;
|
|
15
|
+
|
|
16
|
+
function colorize(code, str) {
|
|
17
|
+
if (NO_COLOR) return String(str);
|
|
18
|
+
return code + String(str) + '\x1b[0m';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const colors = {
|
|
22
|
+
bold: s => colorize('\x1b[1m', s),
|
|
23
|
+
red: s => colorize('\x1b[31m', s),
|
|
24
|
+
green: s => colorize('\x1b[32m', s),
|
|
25
|
+
yellow: s => colorize('\x1b[33m', s),
|
|
26
|
+
cyan: s => colorize('\x1b[36m', s),
|
|
27
|
+
gray: s => colorize('\x1b[90m', s),
|
|
28
|
+
};
|
|
4
29
|
|
|
5
30
|
function sleepSync(ms) {
|
|
6
|
-
Atomics.wait(
|
|
31
|
+
Atomics.wait(sleepBuf, 0, 0, ms);
|
|
7
32
|
}
|
|
8
33
|
|
|
9
34
|
function formatBytes(bytes) {
|
|
@@ -71,7 +96,7 @@ function isProcessRunning(pid) {
|
|
|
71
96
|
encoding: 'utf8',
|
|
72
97
|
}).trim();
|
|
73
98
|
return output.length > 0;
|
|
74
|
-
} catch (
|
|
99
|
+
} catch (_e) {
|
|
75
100
|
return false;
|
|
76
101
|
}
|
|
77
102
|
}
|
|
@@ -83,12 +108,78 @@ function isProcessRoot(pid) {
|
|
|
83
108
|
encoding: 'utf8',
|
|
84
109
|
}).trim();
|
|
85
110
|
return uidOutput === '0';
|
|
86
|
-
} catch (
|
|
111
|
+
} catch (_e) {
|
|
87
112
|
return false;
|
|
88
113
|
}
|
|
89
114
|
}
|
|
90
115
|
|
|
116
|
+
/**
|
|
117
|
+
* 创建统一的 HTTP 客户端
|
|
118
|
+
*/
|
|
119
|
+
function createHttpClient(options) {
|
|
120
|
+
const opts = options || {};
|
|
121
|
+
const timeout = opts.timeout || 60000;
|
|
122
|
+
const maxContentLength = opts.maxContentLength || 50 * 1024 * 1024;
|
|
123
|
+
const userAgent = opts.userAgent || 'mihomo-cli/' + VERSION;
|
|
124
|
+
|
|
125
|
+
return axios.create({
|
|
126
|
+
timeout,
|
|
127
|
+
headers: { 'User-Agent': userAgent },
|
|
128
|
+
maxContentLength,
|
|
129
|
+
maxBodyLength: maxContentLength,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 规范化镜像 URL(从 index.js 移入)
|
|
135
|
+
*/
|
|
136
|
+
function normalizeMirrorUrl(val) {
|
|
137
|
+
if (!val) return null;
|
|
138
|
+
if (val === 'direct' || val === 'no' || val === 'none') return null;
|
|
139
|
+
|
|
140
|
+
let url = val;
|
|
141
|
+
if (!url.startsWith('http')) {
|
|
142
|
+
url = 'https://' + url;
|
|
143
|
+
}
|
|
144
|
+
if (!url.endsWith('/')) {
|
|
145
|
+
url += '/';
|
|
146
|
+
}
|
|
147
|
+
return url;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 解析镜像参数(从 index.js 移入)
|
|
152
|
+
* 返回: { mirror: 镜像URL|null, isOverride: boolean }
|
|
153
|
+
* mirror = null 表示禁用镜像
|
|
154
|
+
* mirror = undefined 表示使用默认/配置
|
|
155
|
+
*/
|
|
156
|
+
function parseMirrorArg(args) {
|
|
157
|
+
if (!args || args.length < 2) {
|
|
158
|
+
return { mirror: undefined, isOverride: false };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (args.includes('--no-mirror') || args.includes('--direct')) {
|
|
162
|
+
return { mirror: null, isOverride: true };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const mirrorIdx = args.indexOf('--mirror');
|
|
166
|
+
if (mirrorIdx >= 0 && mirrorIdx + 1 < args.length) {
|
|
167
|
+
let mirrorVal = args[mirrorIdx + 1];
|
|
168
|
+
return { mirror: normalizeMirrorUrl(mirrorVal), isOverride: true };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (let i = 1; i < args.length; i++) {
|
|
172
|
+
const arg = args[i];
|
|
173
|
+
if (!arg.startsWith('-')) {
|
|
174
|
+
return { mirror: normalizeMirrorUrl(arg), isOverride: true };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { mirror: undefined, isOverride: false };
|
|
179
|
+
}
|
|
180
|
+
|
|
91
181
|
module.exports = {
|
|
182
|
+
VERSION,
|
|
92
183
|
sleepSync,
|
|
93
184
|
formatBytes,
|
|
94
185
|
formatTimestamp,
|
|
@@ -98,4 +189,8 @@ module.exports = {
|
|
|
98
189
|
getNonFlagArg,
|
|
99
190
|
isProcessRunning,
|
|
100
191
|
isProcessRoot,
|
|
192
|
+
colors,
|
|
193
|
+
createHttpClient,
|
|
194
|
+
normalizeMirrorUrl,
|
|
195
|
+
parseMirrorArg,
|
|
101
196
|
};
|