mihomo-cli 1.5.0 → 2.0.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.
@@ -1,257 +0,0 @@
1
- // 内置模块
2
- // (无内置模块依赖)
3
-
4
- // 第三方模块
5
- // (无第三方模块依赖,axios 通过 utils.createHttpClient 间接使用)
6
-
7
- // 本地模块
8
- const config = require('./config');
9
- const utils = require('./utils');
10
-
11
- const { colors } = utils;
12
- const DEFAULT_UPDATE_INTERVAL_HOURS = 12;
13
-
14
- // 订阅专用 HTTP 客户端(超时较短,适合下载订阅配置)
15
- const HTTP_CLIENT = utils.createHttpClient({
16
- timeout: 60000,
17
- maxContentLength: 50 * 1024 * 1024,
18
- });
19
-
20
- function parseUserInfo(header) {
21
- if (!header) return null;
22
- const info = {};
23
- const parts = header.split(';').map(p => p.trim());
24
- for (const part of parts) {
25
- const [key, val] = part.split('=').map(s => s.trim());
26
- if (key && val !== undefined) {
27
- const numVal = parseFloat(val);
28
- info[key] = isNaN(numVal) ? val : numVal;
29
- }
30
- }
31
- return info;
32
- }
33
-
34
- function parseUsernameFromContentDisposition(header) {
35
- if (!header) return null;
36
- const match = header.match(/filename\s*=\s*["']?([^"';\s]+)["']?/i);
37
- if (!match) return null;
38
- const filename = match[1];
39
- const parts = filename.split('/');
40
- return parts[parts.length - 1] || null;
41
- }
42
-
43
- function formatProxySummary(info) {
44
- const parts = [];
45
- if (info && info.proxyGroups > 0) parts.push(info.proxyGroups + ' 组');
46
- parts.push(((info && info.proxies) || 0) + ' 节点');
47
- return parts.join(', ');
48
- }
49
-
50
- /**
51
- * 获取当前使用的订阅
52
- */
53
- function getActiveSubscription() {
54
- const subs = config.getSubscriptions();
55
- if (subs.length === 0) {
56
- return null;
57
- }
58
- const settings = config.readSettings();
59
- const activeName = settings.active_subscription;
60
- if (activeName) {
61
- const found = subs.find(s => s.name === activeName);
62
- if (found) return found;
63
- }
64
- return subs[0];
65
- }
66
-
67
- /**
68
- * 模糊查找订阅(从 index.js 移入)
69
- */
70
- function findSubscriptionFuzzy(subs, pattern) {
71
- const lowerPattern = pattern.toLowerCase();
72
- let exact = [];
73
- let prefix = [];
74
- let includes = [];
75
-
76
- for (const s of subs) {
77
- const name = s.name.toLowerCase();
78
- if (name === lowerPattern) {
79
- exact.push(s);
80
- } else if (name.startsWith(lowerPattern)) {
81
- prefix.push(s);
82
- } else if (name.includes(lowerPattern)) {
83
- includes.push(s);
84
- }
85
- }
86
-
87
- if (exact.length > 0) return exact;
88
- if (prefix.length > 0) return prefix;
89
- return includes;
90
- }
91
-
92
- /**
93
- * 从匹配列表中选择单个订阅(从 index.js 移入)
94
- * 如果匹配多个,打印错误并退出进程
95
- */
96
- function pickSingleSubscription(subs, pattern) {
97
- if (subs.length === 0) {
98
- console.error('错误: 未找到匹配 "' + pattern + '" 的订阅');
99
- process.exit(1);
100
- }
101
- if (subs.length === 1) {
102
- return subs[0];
103
- }
104
- console.error('错误: 匹配到多个订阅,请更精确指定');
105
- console.log('\n匹配的订阅:');
106
- subs.forEach(s => console.log(' ' + s.name));
107
- process.exit(1);
108
- }
109
-
110
- async function downloadSubscription(url, subName) {
111
- if (subName === undefined) subName = 'default';
112
-
113
- let response;
114
- try {
115
- response = await HTTP_CLIENT.get(url, {
116
- responseType: 'text',
117
- });
118
- } catch (e) {
119
- const maskedUrl = config.maskUrl(url);
120
- let errorMsg = '获取订阅失败: ' + e.message;
121
- if (e.response) {
122
- errorMsg += ' (HTTP ' + e.response.status + ')';
123
- }
124
- errorMsg += '\n URL: ' + maskedUrl;
125
- throw new Error(errorMsg);
126
- }
127
-
128
- const content = response.data;
129
- if (!content || !content.trim()) {
130
- throw new Error('订阅内容为空');
131
- }
132
-
133
- const parsed = config.parseYamlOrJson(content, '订阅内容');
134
- if (!parsed) {
135
- throw new Error('订阅内容为空');
136
- }
137
-
138
- config.saveSubscriptionRawConfig(subName, content);
139
-
140
- const headers = response.headers;
141
- const userInfo = parseUserInfo(headers['subscription-userinfo']);
142
- const updateInterval = headers['profile-update-interval'] ? parseInt(headers['profile-update-interval']) : null;
143
- const webPageUrl = headers['profile-web-page-url'] || null;
144
- const username = parseUsernameFromContentDisposition(headers['content-disposition']);
145
-
146
- const cacheData = {
147
- updated_at: new Date().toISOString(),
148
- };
149
- if (userInfo) {
150
- cacheData.upload = userInfo.upload;
151
- cacheData.download = userInfo.download;
152
- cacheData.total = userInfo.total;
153
- cacheData.expire = userInfo.expire;
154
- }
155
- if (updateInterval) {
156
- cacheData.update_interval = updateInterval;
157
- }
158
- if (webPageUrl) {
159
- cacheData.web_page_url = webPageUrl;
160
- }
161
- if (username) {
162
- cacheData.username = username;
163
- }
164
- config.saveSubscriptionCache(subName, cacheData);
165
-
166
- return {
167
- proxies: parsed.proxies ? parsed.proxies.length : 0,
168
- proxyGroups: parsed['proxy-groups'] ? parsed['proxy-groups'].length : 0,
169
- userInfo,
170
- updateInterval,
171
- webPageUrl,
172
- username,
173
- };
174
- }
175
-
176
- function prepareConfigForStart(mode, subName) {
177
- if (subName === undefined) subName = 'default';
178
-
179
- const rawContent = config.readSubscriptionRawConfig(subName);
180
- if (!rawContent) {
181
- throw new Error('未找到订阅配置 "' + subName + '",请先添加订阅');
182
- }
183
-
184
- const buildResult = config.buildConfig(rawContent, mode);
185
- config.writeMihomoConfig(buildResult.config);
186
- config.writeDebugConfig(buildResult);
187
-
188
- return {
189
- proxies: buildResult.config.proxies ? buildResult.config.proxies.length : 0,
190
- proxyGroups: buildResult.config['proxy-groups'] ? buildResult.config['proxy-groups'].length : 0,
191
- };
192
- }
193
-
194
- function needsAutoUpdate(sub) {
195
- if (!sub.updated_at) return true;
196
- const lastUpdate = new Date(sub.updated_at).getTime();
197
- if (isNaN(lastUpdate)) return true;
198
- const intervalHours = sub.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
199
- const intervalMs = intervalHours * 60 * 60 * 1000;
200
- return Date.now() - lastUpdate > intervalMs;
201
- }
202
-
203
- async function tryUpdateOne(sub) {
204
- try {
205
- const info = await downloadSubscription(sub.url, sub.name);
206
- return { name: sub.name, success: true, proxies: info.proxies, proxyGroups: info.proxyGroups };
207
- } catch (e) {
208
- return { name: sub.name, success: false, error: e.message };
209
- }
210
- }
211
-
212
- async function autoUpdateStaleSubscription() {
213
- const allSubs = config.getSubscriptionsWithCache();
214
- const staleSubs = allSubs.filter(needsAutoUpdate);
215
-
216
- if (staleSubs.length === 0) {
217
- return { total: 0, updated: 0, failed: 0 };
218
- }
219
-
220
- if (staleSubs.length === 1) {
221
- const sub = staleSubs[0];
222
- const interval = sub.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
223
- console.log('订阅 "' + sub.name + '" 超过 ' + interval + ' 小时未更新,正在更新...');
224
- } else {
225
- console.log('检查到 ' + staleSubs.length + ' 个订阅需要更新,正在并行更新...');
226
- }
227
-
228
- const results = await Promise.all(staleSubs.map(tryUpdateOne));
229
- let updatedCount = 0;
230
-
231
- results.forEach(r => {
232
- if (r.success) {
233
- updatedCount++;
234
- console.log(colors.green('✓') + ' ' + r.name + ': ' + colors.green('已更新') + ' (' + formatProxySummary(r) + ')');
235
- } else {
236
- console.log(colors.red('✗') + ' ' + r.name + ': ' + colors.red('失败') + ' (' + r.error.split('\n')[0] + ')');
237
- }
238
- });
239
-
240
- return {
241
- total: staleSubs.length,
242
- updated: updatedCount,
243
- failed: staleSubs.length - updatedCount,
244
- };
245
- }
246
-
247
- module.exports = {
248
- DEFAULT_UPDATE_INTERVAL_HOURS,
249
- getActiveSubscription,
250
- findSubscriptionFuzzy,
251
- pickSingleSubscription,
252
- downloadSubscription,
253
- prepareConfigForStart,
254
- formatProxySummary,
255
- tryUpdateOne,
256
- autoUpdateStaleSubscription,
257
- };
package/src/utils.js DELETED
@@ -1,202 +0,0 @@
1
- // 内置模块
2
- const { execSync } = require('child_process');
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
- };
29
-
30
- function sleepSync(ms) {
31
- Atomics.wait(sleepBuf, 0, 0, ms);
32
- }
33
-
34
- function formatBytes(bytes) {
35
- if (bytes === undefined || bytes === null) return '未知';
36
- const num = Number(bytes);
37
- if (isNaN(num) || num < 0) return '未知';
38
- if (num === 0) return '0 B';
39
- const k = 1024;
40
- const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
41
- const i = Math.floor(Math.log(num) / Math.log(k));
42
- return parseFloat((num / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
43
- }
44
-
45
- function formatTimestamp(ts) {
46
- if (ts === undefined || ts === null) return '未知';
47
- try {
48
- return new Date(ts * 1000).toLocaleString('zh-CN');
49
- } catch {
50
- return '未知';
51
- }
52
- }
53
-
54
- function formatDate(dateOrIso) {
55
- if (dateOrIso === undefined || dateOrIso === null) return '未知';
56
- try {
57
- const d = dateOrIso instanceof Date ? dateOrIso : new Date(dateOrIso);
58
- if (isNaN(d.getTime())) return '未知';
59
- return d.toLocaleString('zh-CN');
60
- } catch {
61
- return '未知';
62
- }
63
- }
64
-
65
- function hasFlag(args, short, long) {
66
- return args && (args.includes(short) || args.includes(long));
67
- }
68
-
69
- function parseIntArg(args, short, long, defaultValue) {
70
- if (!args) return defaultValue;
71
- for (let i = 0; i < args.length; i++) {
72
- if (args[i] === short || args[i] === long) {
73
- if (i + 1 < args.length) {
74
- const val = parseInt(args[i + 1]);
75
- return isNaN(val) ? defaultValue : val;
76
- }
77
- }
78
- }
79
- return defaultValue;
80
- }
81
-
82
- function getNonFlagArg(args, startIdx) {
83
- if (!args) return null;
84
- for (let i = startIdx; i < args.length; i++) {
85
- if (!args[i].startsWith('-')) {
86
- return args[i];
87
- }
88
- }
89
- return null;
90
- }
91
-
92
- function isProcessRunning(pid) {
93
- if (!pid) return false;
94
- try {
95
- const output = execSync('ps -p ' + pid + ' -o pid= 2>/dev/null || true', {
96
- encoding: 'utf8',
97
- }).trim();
98
- return output.length > 0;
99
- } catch (_e) {
100
- return false;
101
- }
102
- }
103
-
104
- function isProcessRoot(pid) {
105
- if (!pid) return false;
106
- try {
107
- const uidOutput = execSync('ps -p ' + pid + ' -o uid= 2>/dev/null || true', {
108
- encoding: 'utf8',
109
- }).trim();
110
- return uidOutput === '0';
111
- } catch (_e) {
112
- return false;
113
- }
114
- }
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, type: 'download'|'all' }
153
- * mirror = null 表示禁用镜像(直连)
154
- * mirror = undefined 表示使用默认/配置
155
- * type: download=仅下载用镜像, all=API和下载都用镜像
156
- */
157
- function parseMirrorArg(args) {
158
- if (!args || args.length < 2) {
159
- return { mirror: null, isOverride: false, type: 'download' };
160
- }
161
-
162
- if (args.includes('--no-mirror') || args.includes('--direct')) {
163
- return { mirror: null, isOverride: true, type: 'download' };
164
- }
165
-
166
- const mirrorAllIdx = args.indexOf('--mirror-all');
167
- if (mirrorAllIdx >= 0) {
168
- const nextArg = args[mirrorAllIdx + 1];
169
- if (!nextArg || nextArg.startsWith('-')) {
170
- return { mirror: 'https://v6.gh-proxy.org/', isOverride: true, type: 'all' };
171
- }
172
- return { mirror: normalizeMirrorUrl(nextArg), isOverride: true, type: 'all' };
173
- }
174
-
175
- const mirrorIdx = args.indexOf('--mirror');
176
- if (mirrorIdx >= 0) {
177
- const nextArg = args[mirrorIdx + 1];
178
- if (!nextArg || nextArg.startsWith('-')) {
179
- return { mirror: 'https://v6.gh-proxy.org/', isOverride: true, type: 'download' };
180
- }
181
- return { mirror: normalizeMirrorUrl(nextArg), isOverride: true, type: 'download' };
182
- }
183
-
184
- return { mirror: null, isOverride: false, type: 'download' };
185
- }
186
-
187
- module.exports = {
188
- VERSION,
189
- sleepSync,
190
- formatBytes,
191
- formatTimestamp,
192
- formatDate,
193
- hasFlag,
194
- parseIntArg,
195
- getNonFlagArg,
196
- isProcessRunning,
197
- isProcessRoot,
198
- colors,
199
- createHttpClient,
200
- normalizeMirrorUrl,
201
- parseMirrorArg,
202
- };