mihomo-cli 1.0.2 → 1.1.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 +46 -0
- package/README.md +17 -16
- package/index.js +400 -212
- package/package.json +1 -1
- package/src/config.js +36 -26
- package/src/overwrite.js +258 -0
- package/src/process.js +10 -10
- package/src/subscription.js +19 -29
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -25,10 +25,11 @@ const USER_DATA_DIR = getUserDataDir();
|
|
|
25
25
|
const DIRS = {
|
|
26
26
|
root: PROJECT_ROOT,
|
|
27
27
|
core: path.join(USER_DATA_DIR, 'core'),
|
|
28
|
-
|
|
28
|
+
subscriptions: path.join(USER_DATA_DIR, 'subscriptions'),
|
|
29
29
|
logs: path.join(USER_DATA_DIR, 'logs'),
|
|
30
30
|
data: path.join(USER_DATA_DIR, 'data'),
|
|
31
31
|
runtime: path.join(USER_DATA_DIR, '.runtime'),
|
|
32
|
+
overwrites: path.join(USER_DATA_DIR, 'overwrites'),
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
const PATHS = {
|
|
@@ -37,7 +38,7 @@ const PATHS = {
|
|
|
37
38
|
userDataDir: USER_DATA_DIR,
|
|
38
39
|
mihomoBinary: path.join(DIRS.core, 'mihomo'),
|
|
39
40
|
settingsFile: path.join(USER_DATA_DIR, 'settings.json'),
|
|
40
|
-
|
|
41
|
+
subscriptionsCacheFile: path.join(DIRS.subscriptions, 'cache.json'),
|
|
41
42
|
configFile: path.join(DIRS.runtime, 'config.yaml'),
|
|
42
43
|
logFile: path.join(DIRS.logs, 'mihomo.log'),
|
|
43
44
|
pidFile: path.join(DIRS.runtime, 'pid'),
|
|
@@ -110,10 +111,10 @@ const AVAILABLE_MIRRORS = [
|
|
|
110
111
|
function getGitHubMirror() {
|
|
111
112
|
const settings = readSettings();
|
|
112
113
|
// 空字符串或 false 表示禁用镜像
|
|
113
|
-
if (settings.
|
|
114
|
+
if (settings.github_mirror === '' || settings.github_mirror === false) {
|
|
114
115
|
return null;
|
|
115
116
|
}
|
|
116
|
-
return settings.
|
|
117
|
+
return settings.github_mirror || DEFAULT_GITHUB_MIRROR;
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
function setGitHubMirror(mirror) {
|
|
@@ -125,13 +126,13 @@ function setGitHubMirror(mirror) {
|
|
|
125
126
|
|
|
126
127
|
if (mirror === null || mirror === undefined) {
|
|
127
128
|
const settings = readSettings();
|
|
128
|
-
delete settings.
|
|
129
|
+
delete settings.github_mirror;
|
|
129
130
|
writeSettings(settings);
|
|
130
131
|
return DEFAULT_GITHUB_MIRROR;
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
if (mirror === '' || mirror === false) {
|
|
134
|
-
writeSettings({
|
|
135
|
+
writeSettings({ github_mirror: '' });
|
|
135
136
|
return null;
|
|
136
137
|
}
|
|
137
138
|
|
|
@@ -143,16 +144,16 @@ function setGitHubMirror(mirror) {
|
|
|
143
144
|
mirrorUrl += '/';
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
writeSettings({
|
|
147
|
+
writeSettings({ github_mirror: mirrorUrl });
|
|
147
148
|
return mirrorUrl;
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
//
|
|
151
|
-
function
|
|
151
|
+
// 订阅缓存读写(动态数据:流量、用户名、更新时间等)
|
|
152
|
+
function readSubscriptionsCache() {
|
|
152
153
|
ensureDirs();
|
|
153
|
-
if (fs.existsSync(PATHS.
|
|
154
|
+
if (fs.existsSync(PATHS.subscriptionsCacheFile)) {
|
|
154
155
|
try {
|
|
155
|
-
const content = fs.readFileSync(PATHS.
|
|
156
|
+
const content = fs.readFileSync(PATHS.subscriptionsCacheFile, 'utf8');
|
|
156
157
|
return JSON.parse(content);
|
|
157
158
|
} catch (e) {
|
|
158
159
|
return {};
|
|
@@ -161,15 +162,15 @@ function readSubsCache() {
|
|
|
161
162
|
return {};
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
function
|
|
165
|
+
function writeSubscriptionsCache(cache) {
|
|
165
166
|
ensureDirs();
|
|
166
|
-
fs.writeFileSync(PATHS.
|
|
167
|
+
fs.writeFileSync(PATHS.subscriptionsCacheFile, JSON.stringify(cache, null, 2), { mode: 0o600 });
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
function
|
|
170
|
-
const cache =
|
|
170
|
+
function saveSubscriptionCache(subName, data) {
|
|
171
|
+
const cache = readSubscriptionsCache();
|
|
171
172
|
cache[subName] = { ...cache[subName], ...data };
|
|
172
|
-
|
|
173
|
+
writeSubscriptionsCache(cache);
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
function getSubscriptions() {
|
|
@@ -180,7 +181,7 @@ function getSubscriptions() {
|
|
|
180
181
|
// 获取合并了缓存数据的订阅列表
|
|
181
182
|
function getSubscriptionsWithCache() {
|
|
182
183
|
const subs = getSubscriptions();
|
|
183
|
-
const cache =
|
|
184
|
+
const cache = readSubscriptionsCache();
|
|
184
185
|
return subs.map(s => ({
|
|
185
186
|
...s,
|
|
186
187
|
...(cache[s.name] || {}),
|
|
@@ -193,9 +194,9 @@ function addSubscription(url, name) {
|
|
|
193
194
|
const subs = settings.subscriptions || [];
|
|
194
195
|
const existingIndex = subs.findIndex(s => s.name === name);
|
|
195
196
|
if (existingIndex >= 0) {
|
|
196
|
-
subs[existingIndex] = { name, url
|
|
197
|
+
subs[existingIndex] = { name, url };
|
|
197
198
|
} else {
|
|
198
|
-
subs.push({ name, url
|
|
199
|
+
subs.push({ name, url });
|
|
199
200
|
}
|
|
200
201
|
writeSettings({ subscriptions: subs });
|
|
201
202
|
}
|
|
@@ -217,7 +218,7 @@ function setDefaultSubscription(name) {
|
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
function getSubRawConfigPath(subName) {
|
|
220
|
-
return path.join(DIRS.
|
|
221
|
+
return path.join(DIRS.subscriptions, subName + '.yaml');
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
function saveSubRawConfig(subName, content) {
|
|
@@ -305,7 +306,14 @@ function buildConfig(subRawContent, mode) {
|
|
|
305
306
|
throw new Error('订阅内容为空');
|
|
306
307
|
}
|
|
307
308
|
|
|
308
|
-
|
|
309
|
+
// 延迟加载以避免循环依赖
|
|
310
|
+
const overwrite = require('./overwrite');
|
|
311
|
+
|
|
312
|
+
// 应用覆写配置
|
|
313
|
+
const withOverwrites = overwrite.applyOverwrites(baseConfig);
|
|
314
|
+
|
|
315
|
+
// 合并 BASE_CONFIG(优先级高于覆写)
|
|
316
|
+
const merged = { ...withOverwrites, ...BASE_CONFIG };
|
|
309
317
|
|
|
310
318
|
if (mode === 'tun') {
|
|
311
319
|
// 合并 TUN 配置
|
|
@@ -351,7 +359,9 @@ function getConfigInfo() {
|
|
|
351
359
|
proxies: cfg.proxies ? cfg.proxies.length : 0,
|
|
352
360
|
proxyGroups: cfg['proxy-groups'] ? cfg['proxy-groups'].length : 0,
|
|
353
361
|
mode: cfg.mode || 'rule',
|
|
354
|
-
|
|
362
|
+
mixedPort: cfg['mixed-port'] || null,
|
|
363
|
+
httpPort: cfg.port || null,
|
|
364
|
+
socksPort: cfg['socks-port'] || null,
|
|
355
365
|
tun: cfg.tun ? cfg.tun.enable : false,
|
|
356
366
|
};
|
|
357
367
|
} catch (e) {
|
|
@@ -369,7 +379,7 @@ function resetUserData(options) {
|
|
|
369
379
|
|
|
370
380
|
const itemsToRemove = [
|
|
371
381
|
PATHS.settingsFile,
|
|
372
|
-
DIRS.
|
|
382
|
+
DIRS.subscriptions,
|
|
373
383
|
DIRS.logs,
|
|
374
384
|
DIRS.data,
|
|
375
385
|
DIRS.runtime,
|
|
@@ -404,9 +414,9 @@ module.exports = {
|
|
|
404
414
|
ensureDirs,
|
|
405
415
|
readSettings,
|
|
406
416
|
writeSettings,
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
417
|
+
readSubscriptionsCache,
|
|
418
|
+
writeSubscriptionsCache,
|
|
419
|
+
saveSubscriptionCache,
|
|
410
420
|
maskUrl,
|
|
411
421
|
getSubscriptions,
|
|
412
422
|
getSubscriptionsWithCache,
|
package/src/overwrite.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
const config = require('./config');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 解析覆写键名
|
|
8
|
+
* 支持的格式:
|
|
9
|
+
* - key! → 强制覆盖整个对象
|
|
10
|
+
* - +key → 数组前置插入
|
|
11
|
+
* - key+ → 数组追加
|
|
12
|
+
* - <+key> → 实际键名是 +key(当键名以 + 开头/结尾时)
|
|
13
|
+
* - +<+key> → 为键名 +key 执行前置插入
|
|
14
|
+
* - <+key>+ → 为键名 +key 执行追加
|
|
15
|
+
*
|
|
16
|
+
* 返回: { key: string, forceOverwrite: boolean, arrayPrepend: boolean, arrayAppend: boolean }
|
|
17
|
+
*/
|
|
18
|
+
function parseOverrideKey(key) {
|
|
19
|
+
let actualKey = key;
|
|
20
|
+
let forceOverwrite = false;
|
|
21
|
+
let arrayPrepend = false;
|
|
22
|
+
let arrayAppend = false;
|
|
23
|
+
|
|
24
|
+
// 1. 检查强制覆盖标记 (! 后缀,不在 <> 内时)
|
|
25
|
+
// 只有当 ! 是最后一个字符,且前面没有未闭合的 < 时才是标记
|
|
26
|
+
const lastChar = key[key.length - 1];
|
|
27
|
+
const openAngleCount = (key.match(/</g) || []).length;
|
|
28
|
+
const closeAngleCount = (key.match(/>/g) || []).length;
|
|
29
|
+
|
|
30
|
+
if (lastChar === '!' && openAngleCount === closeAngleCount) {
|
|
31
|
+
forceOverwrite = true;
|
|
32
|
+
actualKey = key.slice(0, -1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. 解析 <> 包裹的键名和 + 操作符
|
|
36
|
+
// 支持的模式:
|
|
37
|
+
// +<key> → prepend, key
|
|
38
|
+
// <key>+ → append, key
|
|
39
|
+
// <+key> → 无操作, 实际键名是 +key
|
|
40
|
+
// +<+key> → prepend, 实际键名是 +key
|
|
41
|
+
// <+key>+ → append, 实际键名是 +key
|
|
42
|
+
|
|
43
|
+
const wrappedMatch = actualKey.match(/^(\+)?(<[^>]+>)(\+)?$/);
|
|
44
|
+
if (wrappedMatch) {
|
|
45
|
+
const prefixPlus = wrappedMatch[1] === '+';
|
|
46
|
+
const wrappedPart = wrappedMatch[2];
|
|
47
|
+
const suffixPlus = wrappedMatch[3] === '+';
|
|
48
|
+
|
|
49
|
+
// 提取 <> 内的内容作为实际键名
|
|
50
|
+
const unwrapped = wrappedPart.slice(1, -1);
|
|
51
|
+
|
|
52
|
+
if (prefixPlus || suffixPlus) {
|
|
53
|
+
// 有 + 操作符,<> 内是实际键名
|
|
54
|
+
actualKey = unwrapped;
|
|
55
|
+
if (prefixPlus) arrayPrepend = true;
|
|
56
|
+
if (suffixPlus) arrayAppend = true;
|
|
57
|
+
} else {
|
|
58
|
+
// 没有 + 操作符,<key> 形式本身就是为了表示键名含特殊字符
|
|
59
|
+
// 这种情况下不需要额外处理,actualKey 已经是 wrappedPart,但我们需要 unwrap
|
|
60
|
+
// 例如:<+.google.cn> 的实际键名就是 +.google.cn
|
|
61
|
+
actualKey = unwrapped;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
// 没有被 <> 完整包裹,检查开头和结尾的 +
|
|
65
|
+
if (actualKey.startsWith('+')) {
|
|
66
|
+
arrayPrepend = true;
|
|
67
|
+
actualKey = actualKey.slice(1);
|
|
68
|
+
}
|
|
69
|
+
if (actualKey.endsWith('+')) {
|
|
70
|
+
arrayAppend = true;
|
|
71
|
+
actualKey = actualKey.slice(0, -1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { key: actualKey, forceOverwrite, arrayPrepend, arrayAppend };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 深度合并带覆写规则
|
|
80
|
+
*/
|
|
81
|
+
function deepMergeWithOverrides(target, override) {
|
|
82
|
+
if (target === null || target === undefined) {
|
|
83
|
+
target = Array.isArray(override) ? [] : {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (override === null || override === undefined) {
|
|
87
|
+
return target;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 如果 override 不是对象,直接返回 override(覆盖)
|
|
91
|
+
if (typeof override !== 'object') {
|
|
92
|
+
return override;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 如果 override 是数组,target 也必须是数组
|
|
96
|
+
if (Array.isArray(override)) {
|
|
97
|
+
return override;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 此时 override 是普通对象
|
|
101
|
+
|
|
102
|
+
const result = { ...target };
|
|
103
|
+
|
|
104
|
+
for (const [rawKey, value] of Object.entries(override)) {
|
|
105
|
+
const { key, forceOverwrite, arrayPrepend, arrayAppend } = parseOverrideKey(rawKey);
|
|
106
|
+
|
|
107
|
+
const existingValue = result[key];
|
|
108
|
+
|
|
109
|
+
// 处理数组操作
|
|
110
|
+
if (arrayPrepend || arrayAppend) {
|
|
111
|
+
const existingArr = Array.isArray(existingValue) ? existingValue : [];
|
|
112
|
+
const overrideArr = Array.isArray(value) ? value : [value];
|
|
113
|
+
|
|
114
|
+
if (arrayPrepend) {
|
|
115
|
+
result[key] = [...overrideArr, ...existingArr];
|
|
116
|
+
} else {
|
|
117
|
+
result[key] = [...existingArr, ...overrideArr];
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 处理强制覆盖
|
|
123
|
+
if (forceOverwrite) {
|
|
124
|
+
result[key] = value;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 递归合并对象
|
|
129
|
+
if (
|
|
130
|
+
value !== null &&
|
|
131
|
+
typeof value === 'object' &&
|
|
132
|
+
!Array.isArray(value) &&
|
|
133
|
+
existingValue !== null &&
|
|
134
|
+
typeof existingValue === 'object' &&
|
|
135
|
+
!Array.isArray(existingValue)
|
|
136
|
+
) {
|
|
137
|
+
result[key] = deepMergeWithOverrides(existingValue, value);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 其他情况直接覆盖
|
|
142
|
+
result[key] = value;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 获取覆写目录路径
|
|
150
|
+
*/
|
|
151
|
+
function getOverwritesDir() {
|
|
152
|
+
return config.DIRS.overwrites;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 检查覆写功能是否启用
|
|
157
|
+
*/
|
|
158
|
+
function isOverwriteEnabled() {
|
|
159
|
+
const settings = config.readSettings();
|
|
160
|
+
return settings.overwrite_enabled !== false; // 默认启用
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 启用/禁用覆写功能
|
|
165
|
+
*/
|
|
166
|
+
function setOverwriteEnabled(enabled) {
|
|
167
|
+
config.writeSettings({ overwrite_enabled: enabled });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 读取 overwrites 目录下的所有 yaml 文件
|
|
172
|
+
* 按文件名排序返回
|
|
173
|
+
*/
|
|
174
|
+
function loadOverwriteFiles() {
|
|
175
|
+
const dir = getOverwritesDir();
|
|
176
|
+
|
|
177
|
+
if (!fs.existsSync(dir)) {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const files = fs.readdirSync(dir)
|
|
182
|
+
.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
|
|
183
|
+
.sort();
|
|
184
|
+
|
|
185
|
+
const results = [];
|
|
186
|
+
|
|
187
|
+
for (const file of files) {
|
|
188
|
+
const filePath = path.join(dir, file);
|
|
189
|
+
try {
|
|
190
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
191
|
+
const parsed = yaml.load(content);
|
|
192
|
+
if (parsed && typeof parsed === 'object') {
|
|
193
|
+
results.push({
|
|
194
|
+
name: file,
|
|
195
|
+
path: filePath,
|
|
196
|
+
config: parsed,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
} catch (e) {
|
|
200
|
+
// 忽略解析错误的文件
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return results;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 应用所有覆写配置到基础配置
|
|
209
|
+
*/
|
|
210
|
+
function applyOverwrites(baseConfig) {
|
|
211
|
+
if (!isOverwriteEnabled()) {
|
|
212
|
+
return baseConfig;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const overwriteFiles = loadOverwriteFiles();
|
|
216
|
+
|
|
217
|
+
if (overwriteFiles.length === 0) {
|
|
218
|
+
return baseConfig;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let result = { ...baseConfig };
|
|
222
|
+
|
|
223
|
+
for (const file of overwriteFiles) {
|
|
224
|
+
result = deepMergeWithOverrides(result, file.config);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 列出覆写文件信息
|
|
232
|
+
*/
|
|
233
|
+
function listOverwriteFiles() {
|
|
234
|
+
const files = loadOverwriteFiles();
|
|
235
|
+
const enabled = isOverwriteEnabled();
|
|
236
|
+
const dir = getOverwritesDir();
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
enabled,
|
|
240
|
+
dir,
|
|
241
|
+
files: files.map(f => ({
|
|
242
|
+
name: f.name,
|
|
243
|
+
path: f.path,
|
|
244
|
+
keys: Object.keys(f.config || {}),
|
|
245
|
+
})),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = {
|
|
250
|
+
parseOverrideKey,
|
|
251
|
+
deepMergeWithOverrides,
|
|
252
|
+
getOverwritesDir,
|
|
253
|
+
isOverwriteEnabled,
|
|
254
|
+
setOverwriteEnabled,
|
|
255
|
+
loadOverwriteFiles,
|
|
256
|
+
applyOverwrites,
|
|
257
|
+
listOverwriteFiles,
|
|
258
|
+
};
|
package/src/process.js
CHANGED
|
@@ -329,15 +329,15 @@ async function start(mode) {
|
|
|
329
329
|
async function startMixedMode(staleState) {
|
|
330
330
|
if (staleState.needsCleanup) {
|
|
331
331
|
if (staleState.needsSudo) {
|
|
332
|
-
console.log('\n
|
|
333
|
-
console.log('
|
|
334
|
-
console.log('
|
|
332
|
+
console.log('\n发现需要 root 权限清理的残留进程/文件');
|
|
333
|
+
console.log('请先手动清理: sudo pkill -9 mihomo');
|
|
334
|
+
console.log('或者切换到 TUN 模式,启动时会自动清理');
|
|
335
335
|
throw new Error('存在需要 root 权限清理的残留');
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
const cleanupResult = cleanupAll();
|
|
339
339
|
if (cleanupResult.killed > 0) {
|
|
340
|
-
console.log('
|
|
340
|
+
console.log('清理了 ' + cleanupResult.killed + ' 个残留进程');
|
|
341
341
|
}
|
|
342
342
|
}
|
|
343
343
|
|
|
@@ -389,7 +389,7 @@ async function startMixedMode(staleState) {
|
|
|
389
389
|
try {
|
|
390
390
|
const logs = fs.readFileSync(logFile, 'utf8').slice(-3000);
|
|
391
391
|
if (logs.trim()) {
|
|
392
|
-
errorMsg += '\n
|
|
392
|
+
errorMsg += '\n最近的日志:\n' + logs.split('\n').map(l => ' ' + l).join('\n');
|
|
393
393
|
}
|
|
394
394
|
} catch {}
|
|
395
395
|
}
|
|
@@ -417,9 +417,9 @@ async function startTunMode(staleState) {
|
|
|
417
417
|
const launchScript = createTunLaunchScript();
|
|
418
418
|
|
|
419
419
|
if (staleState.needsCleanup) {
|
|
420
|
-
console.log('
|
|
420
|
+
console.log('清理 ' + staleState.allPids.length + ' 个残留进程...');
|
|
421
421
|
}
|
|
422
|
-
console.log('
|
|
422
|
+
console.log('TUN 模式需要 sudo 权限...');
|
|
423
423
|
|
|
424
424
|
try {
|
|
425
425
|
execSync('sudo "' + launchScript + '"', {
|
|
@@ -459,9 +459,9 @@ function stop(wasTunMode) {
|
|
|
459
459
|
const remaining = getAllMihomoPids();
|
|
460
460
|
if (remaining.length > 0) {
|
|
461
461
|
console.log('');
|
|
462
|
-
console.log('
|
|
463
|
-
console.log('
|
|
464
|
-
console.log('
|
|
462
|
+
console.log('仍有进程残留,需要手动清理:');
|
|
463
|
+
console.log('进程 PID: ' + remaining.join(', '));
|
|
464
|
+
console.log('手动命令: sudo pkill -9 mihomo');
|
|
465
465
|
console.log('');
|
|
466
466
|
return { success: true, warning: '部分进程未终止', remaining };
|
|
467
467
|
}
|
package/src/subscription.js
CHANGED
|
@@ -104,23 +104,11 @@ async function downloadSubscription(url, subName) {
|
|
|
104
104
|
const webPageUrl = headers['profile-web-page-url'] || null;
|
|
105
105
|
const username = parseUsernameFromContentDisposition(headers['content-disposition']);
|
|
106
106
|
|
|
107
|
-
//
|
|
108
|
-
// 同时清理旧的动态字段(迁移到缓存文件)
|
|
109
|
-
const subs = config.getSubscriptions();
|
|
110
|
-
const subIndex = subs.findIndex(s => s.name === subName);
|
|
111
|
-
if (subIndex >= 0) {
|
|
112
|
-
// 只保留核心配置字段,动态字段已迁移到缓存
|
|
113
|
-
const cleanedSub = {
|
|
114
|
-
name: subs[subIndex].name,
|
|
115
|
-
url: subs[subIndex].url,
|
|
116
|
-
updatedAt: new Date().toISOString(),
|
|
117
|
-
};
|
|
118
|
-
subs[subIndex] = cleanedSub;
|
|
119
|
-
config.writeSettings({ subscriptions: subs });
|
|
120
|
-
}
|
|
107
|
+
// 2. 动态数据 + updated_at 保存到缓存文件(settings 只存 name/url)
|
|
121
108
|
|
|
122
|
-
|
|
123
|
-
|
|
109
|
+
const cacheData = {
|
|
110
|
+
updated_at: new Date().toISOString(),
|
|
111
|
+
};
|
|
124
112
|
if (userInfo) {
|
|
125
113
|
cacheData.upload = userInfo.upload;
|
|
126
114
|
cacheData.download = userInfo.download;
|
|
@@ -128,15 +116,15 @@ async function downloadSubscription(url, subName) {
|
|
|
128
116
|
cacheData.expire = userInfo.expire;
|
|
129
117
|
}
|
|
130
118
|
if (updateInterval) {
|
|
131
|
-
cacheData.
|
|
119
|
+
cacheData.update_interval = updateInterval;
|
|
132
120
|
}
|
|
133
121
|
if (webPageUrl) {
|
|
134
|
-
cacheData.
|
|
122
|
+
cacheData.web_page_url = webPageUrl;
|
|
135
123
|
}
|
|
136
124
|
if (username) {
|
|
137
125
|
cacheData.username = username;
|
|
138
126
|
}
|
|
139
|
-
config.
|
|
127
|
+
config.saveSubscriptionCache(subName, cacheData);
|
|
140
128
|
|
|
141
129
|
return {
|
|
142
130
|
proxies: parsed.proxies ? parsed.proxies.length : 0,
|
|
@@ -166,10 +154,10 @@ function prepareConfigForStart(mode, subName) {
|
|
|
166
154
|
}
|
|
167
155
|
|
|
168
156
|
function needsAutoUpdate(sub) {
|
|
169
|
-
if (!sub.
|
|
170
|
-
const lastUpdate = new Date(sub.
|
|
157
|
+
if (!sub.updated_at) return true;
|
|
158
|
+
const lastUpdate = new Date(sub.updated_at).getTime();
|
|
171
159
|
if (isNaN(lastUpdate)) return true;
|
|
172
|
-
const intervalHours = sub.
|
|
160
|
+
const intervalHours = sub.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
|
|
173
161
|
const intervalMs = intervalHours * 60 * 60 * 1000;
|
|
174
162
|
return (Date.now() - lastUpdate) > intervalMs;
|
|
175
163
|
}
|
|
@@ -177,7 +165,7 @@ function needsAutoUpdate(sub) {
|
|
|
177
165
|
async function tryUpdateOne(sub) {
|
|
178
166
|
try {
|
|
179
167
|
const info = await downloadSubscription(sub.url, sub.name);
|
|
180
|
-
return { name: sub.name, success: true, proxies: info.proxies };
|
|
168
|
+
return { name: sub.name, success: true, proxies: info.proxies, proxyGroups: info.proxyGroups };
|
|
181
169
|
} catch (e) {
|
|
182
170
|
return { name: sub.name, success: false, error: e.message };
|
|
183
171
|
}
|
|
@@ -193,10 +181,10 @@ async function autoUpdateStaleSubscriptions() {
|
|
|
193
181
|
|
|
194
182
|
if (staleSubs.length === 1) {
|
|
195
183
|
const sub = staleSubs[0];
|
|
196
|
-
const interval = sub.
|
|
197
|
-
console.log('
|
|
184
|
+
const interval = sub.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
|
|
185
|
+
console.log('订阅 "' + sub.name + '" 超过 ' + interval + ' 小时未更新,正在更新...');
|
|
198
186
|
} else {
|
|
199
|
-
console.log('
|
|
187
|
+
console.log('检查到 ' + staleSubs.length + ' 个订阅需要更新,正在并行更新...');
|
|
200
188
|
}
|
|
201
189
|
|
|
202
190
|
const results = await Promise.all(staleSubs.map(tryUpdateOne));
|
|
@@ -205,10 +193,12 @@ async function autoUpdateStaleSubscriptions() {
|
|
|
205
193
|
results.forEach(r => {
|
|
206
194
|
if (r.success) {
|
|
207
195
|
updatedCount++;
|
|
208
|
-
|
|
196
|
+
const parts = [];
|
|
197
|
+
if (r.proxyGroups && r.proxyGroups > 0) parts.push(r.proxyGroups + ' 组');
|
|
198
|
+
parts.push(r.proxies + ' 节点');
|
|
199
|
+
console.log('✓ ' + r.name + ': 已更新 (' + parts.join(', ') + ')');
|
|
209
200
|
} else {
|
|
210
|
-
console.log('
|
|
211
|
-
console.log(' 原因: ' + r.error.split('\n')[0]);
|
|
201
|
+
console.log('✗ ' + r.name + ': 失败 (' + r.error.split('\n')[0] + ')');
|
|
212
202
|
}
|
|
213
203
|
});
|
|
214
204
|
|