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.
@@ -1,15 +1,20 @@
1
- const axios = require('axios');
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
- const HTTP_CLIENT = axios.create({
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.saveSubRawConfig(subName, content);
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.readSubRawConfig(subName);
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
- const _sleepBuf = new Int32Array(1);
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(_sleepBuf, 0, 0, ms);
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 (e) {
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 (e) {
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
  };