mihomo-cli 2.2.4 → 2.3.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/dist/index.js CHANGED
@@ -1,65 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/paths.ts
4
- import fs from "fs";
5
- import os from "os";
6
- import path from "path";
7
- function getUserDataDir() {
8
- if (process.env.MIHOMO_CLI_DIR) {
9
- return process.env.MIHOMO_CLI_DIR;
10
- }
11
- return path.join(os.homedir(), ".mihomo-cli");
12
- }
13
- var USER_DATA_DIR = getUserDataDir();
14
- var DIRS = {
15
- kernel: path.join(USER_DATA_DIR, "kernel"),
16
- subscriptions: path.join(USER_DATA_DIR, "subscriptions"),
17
- logs: path.join(USER_DATA_DIR, "logs"),
18
- data: path.join(USER_DATA_DIR, "data"),
19
- runtime: path.join(USER_DATA_DIR, "runtime")
20
- };
21
- var PATHS = {
22
- mihomoBinary: path.join(DIRS.kernel, "mihomo"),
23
- settingsFile: path.join(USER_DATA_DIR, "settings.json"),
24
- subscriptionsCacheFile: path.join(DIRS.subscriptions, "cache.json"),
25
- configFile: path.join(DIRS.runtime, "config.yaml"),
26
- logFile: path.join(DIRS.logs, "mihomo.log"),
27
- pidFile: path.join(DIRS.runtime, "pid"),
28
- configStage1Subscription: path.join(DIRS.runtime, "1.subscription.yaml"),
29
- configStage2Overwrite: path.join(DIRS.runtime, "2.overwrite.yaml"),
30
- configStage3System: path.join(DIRS.runtime, "3.system.yaml")
31
- };
32
- var DIRECTORY_TARGETS = {
33
- root: { path: null, label: "\u6839\u76EE\u5F55" },
34
- subs: { path: DIRS.subscriptions, label: "\u8BA2\u9605\u76EE\u5F55" },
35
- logs: { path: DIRS.logs, label: "\u65E5\u5FD7\u76EE\u5F55" },
36
- data: { path: DIRS.data, label: "mihomo \u6570\u636E\u76EE\u5F55" },
37
- runtime: { path: DIRS.runtime, label: "\u8FD0\u884C\u65F6\u76EE\u5F55" },
38
- kernel: { path: DIRS.kernel, label: "\u5185\u6838\u76EE\u5F55" }
39
- };
40
- function ensureDirs() {
41
- for (const dir of Object.values(DIRS)) {
42
- if (!fs.existsSync(dir)) {
43
- fs.mkdirSync(dir, { recursive: true, mode: 448 });
44
- }
45
- }
46
- }
47
- function fsExistsSync(p) {
48
- return fs.existsSync(p);
49
- }
50
- function rmrf(dir) {
51
- fs.rmSync(dir, { recursive: true, force: true });
52
- }
53
-
54
- // src/process.ts
55
- import { execSync as execSync3, spawn } from "child_process";
3
+ // src/bench.ts
4
+ import { spawn } from "child_process";
56
5
  import fs5 from "fs";
57
6
  import path3 from "path";
58
7
 
59
- // src/config.ts
60
- import { execSync } from "child_process";
61
- import fs4 from "fs";
62
-
63
8
  // node_modules/js-yaml/dist/js-yaml.mjs
64
9
  function isNothing(subject) {
65
10
  return typeof subject === "undefined" || subject === null;
@@ -2684,6 +2629,10 @@ var jsYaml = {
2684
2629
  safeDump
2685
2630
  };
2686
2631
 
2632
+ // src/config.ts
2633
+ import { execSync } from "child_process";
2634
+ import fs4 from "fs";
2635
+
2687
2636
  // src/constants.ts
2688
2637
  var AVAILABLE_MIRRORS = ["gh-proxy.org", "v6.gh-proxy.org", "hk.gh-proxy.org", "cdn.gh-proxy.org"];
2689
2638
  var UI_URLS = {
@@ -2701,6 +2650,44 @@ var TUN_CONFIG = {
2701
2650
  "strict-route": true
2702
2651
  }
2703
2652
  };
2653
+ function getFreeSubscriptionSources() {
2654
+ return [
2655
+ // 完整配置(DNS + 分组 + rule-provider, 29 组)
2656
+ { name: "FreeSubsCheck", url: "https://gh-proxy.org/raw.githubusercontent.com/kooker/FreeSubsCheck/main/mihomo.yaml" },
2657
+ { name: "shaoyouvip", url: "https://gh-proxy.org/raw.githubusercontent.com/shaoyouvip/free/main/mihomo.yaml" },
2658
+ { name: "freeSub", url: "https://gh-proxy.org/raw.githubusercontent.com/Ruk1ng001/freeSub/main/clash.yaml" },
2659
+ // 完整配置(13 组)
2660
+ { name: "PuddinCat", url: "https://gh-proxy.org/raw.githubusercontent.com/PuddinCat/BestClash/refs/heads/main/proxies.yaml" },
2661
+ { name: "cn-news", url: "https://gh-proxy.org/raw.githubusercontent.com/hello-world-1989/cn-news/refs/heads/main/clash.yaml" },
2662
+ // 基础分组(10-11 组)
2663
+ { name: "naidounode", url: "https://gh-proxy.org/raw.githubusercontent.com/xiaoji235/airport-free/main/clash/naidounode.txt" },
2664
+ { name: "v2rayshare", url: "https://gh-proxy.org/raw.githubusercontent.com/xiaoji235/airport-free/main/clash/v2rayshare.txt" },
2665
+ // 简单配置(2 组)
2666
+ { name: "proxypool", url: "https://gh-proxy.org/raw.githubusercontent.com/snakem982/proxypool/main/source/clash-meta.yaml" },
2667
+ { name: "chromego", url: "https://gh-proxy.org/raw.githubusercontent.com/Misaka-blog/chromego_merge/main/sub/merged_proxies_new.yaml" },
2668
+ // 纯节点列表
2669
+ { name: "awesome-vpn", url: "https://gh-proxy.org/raw.githubusercontent.com/awesome-vpn/awesome-vpn/master/clash.yaml" },
2670
+ { name: "V2RayAggregator", url: "https://gh-proxy.org/raw.githubusercontent.com/mahdibland/V2RayAggregator/master/Eternity.yml" },
2671
+ { name: "Pawdroid", url: "https://gh-proxy.org/raw.githubusercontent.com/Pawdroid/Free-servers/main/sub" },
2672
+ { name: "ermaozi", url: "https://gh-proxy.org/raw.githubusercontent.com/ermaozi/get_subscribe/main/subscribe/clash.yml" },
2673
+ { name: "v2rayfree", url: "https://gh-proxy.org/raw.githubusercontent.com/v2raynnodes/v2rayfree/main/nodes/clashmeta.yaml" },
2674
+ { name: "yudou66", url: "https://gh-proxy.org/raw.githubusercontent.com/Barabama/FreeNodes/main/nodes/yudou66.yaml" },
2675
+ { name: "wenode", url: "https://gh-proxy.org/raw.githubusercontent.com/Barabama/FreeNodes/main/nodes/wenode.yaml" },
2676
+ { name: "dongtai-sub", url: "https://gh-proxy.org/raw.githubusercontent.com/wenxig/dongtai-sub/refs/heads/main/data/sub.yaml" },
2677
+ { name: "kasesm", url: "https://gh-proxy.org/raw.githubusercontent.com/kasesm/Free-Config/refs/heads/main/all_raw.txt" },
2678
+ { name: "Au1rxx", url: "https://gh-proxy.org/raw.githubusercontent.com/Au1rxx/free-vpn-subscriptions/main/output/clash.yaml" },
2679
+ // 完整配置但需要完整版 GeoSite.dat(geosite-lite 不兼容, 22 组)
2680
+ { name: "NoMoreWalls", url: "https://gh-proxy.org/raw.githubusercontent.com/peasoft/NoMoreWalls/master/list.meta.yml" }
2681
+ ];
2682
+ }
2683
+ var BENCH_CONFIG = {
2684
+ "allow-lan": false,
2685
+ "external-controller": "127.0.0.1:19090",
2686
+ port: 17890,
2687
+ "socks-port": 17891,
2688
+ "log-level": "error",
2689
+ "geodata-mode": true
2690
+ };
2704
2691
  var BASE_CONFIG = {
2705
2692
  "allow-lan": false,
2706
2693
  "external-controller": "127.0.0.1:9090",
@@ -2721,6 +2708,57 @@ var BASE_CONFIG = {
2721
2708
  import fs3 from "fs";
2722
2709
  import path2 from "path";
2723
2710
 
2711
+ // src/paths.ts
2712
+ import fs from "fs";
2713
+ import os from "os";
2714
+ import path from "path";
2715
+ function getUserDataDir() {
2716
+ if (process.env.MIHOMO_CLI_DIR) {
2717
+ return process.env.MIHOMO_CLI_DIR;
2718
+ }
2719
+ return path.join(os.homedir(), ".mihomo-cli");
2720
+ }
2721
+ var USER_DATA_DIR = getUserDataDir();
2722
+ var DIRS = {
2723
+ kernel: path.join(USER_DATA_DIR, "kernel"),
2724
+ subscriptions: path.join(USER_DATA_DIR, "subscriptions"),
2725
+ logs: path.join(USER_DATA_DIR, "logs"),
2726
+ data: path.join(USER_DATA_DIR, "data"),
2727
+ runtime: path.join(USER_DATA_DIR, "runtime")
2728
+ };
2729
+ var PATHS = {
2730
+ mihomoBinary: path.join(DIRS.kernel, "mihomo"),
2731
+ settingsFile: path.join(USER_DATA_DIR, "settings.json"),
2732
+ subscriptionsCacheFile: path.join(DIRS.subscriptions, "cache.json"),
2733
+ configFile: path.join(DIRS.runtime, "config.yaml"),
2734
+ logFile: path.join(DIRS.logs, "mihomo.log"),
2735
+ pidFile: path.join(DIRS.runtime, "pid"),
2736
+ configStage1Subscription: path.join(DIRS.runtime, "1.subscription.yaml"),
2737
+ configStage2Overwrite: path.join(DIRS.runtime, "2.overwrite.yaml"),
2738
+ configStage3System: path.join(DIRS.runtime, "3.system.yaml")
2739
+ };
2740
+ var DIRECTORY_TARGETS = {
2741
+ root: { path: null, label: "\u6839\u76EE\u5F55" },
2742
+ subs: { path: DIRS.subscriptions, label: "\u8BA2\u9605\u76EE\u5F55" },
2743
+ logs: { path: DIRS.logs, label: "\u65E5\u5FD7\u76EE\u5F55" },
2744
+ data: { path: DIRS.data, label: "mihomo \u6570\u636E\u76EE\u5F55" },
2745
+ runtime: { path: DIRS.runtime, label: "\u8FD0\u884C\u65F6\u76EE\u5F55" },
2746
+ kernel: { path: DIRS.kernel, label: "\u5185\u6838\u76EE\u5F55" }
2747
+ };
2748
+ function ensureDirs() {
2749
+ for (const dir of Object.values(DIRS)) {
2750
+ if (!fs.existsSync(dir)) {
2751
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
2752
+ }
2753
+ }
2754
+ }
2755
+ function fsExistsSync(p) {
2756
+ return fs.existsSync(p);
2757
+ }
2758
+ function rmrf(dir) {
2759
+ fs.rmSync(dir, { recursive: true, force: true });
2760
+ }
2761
+
2724
2762
  // src/settings.ts
2725
2763
  import fs2 from "fs";
2726
2764
  var settingsCache = null;
@@ -2757,6 +2795,9 @@ function invalidateSettingsCache() {
2757
2795
  }
2758
2796
  function maskUrl(url) {
2759
2797
  if (!url) return url;
2798
+ if (url.includes(",")) {
2799
+ return url.split(",").map((u) => maskUrl(u.trim())).join(", ");
2800
+ }
2760
2801
  try {
2761
2802
  const parsed = new URL(url);
2762
2803
  const tokenKeys = ["token", "key", "secret", "pass", "password", "auth", "access_token", "api_key"];
@@ -3022,6 +3063,37 @@ function parseYamlOrJson(content, errorMsg) {
3022
3063
  throw new Error(`${errorMsg || "\u5185\u5BB9"}\u683C\u5F0F\u9519\u8BEF\uFF0C\u65E0\u6CD5\u89E3\u6790\u4E3A YAML \u6216 JSON`);
3023
3064
  }
3024
3065
  }
3066
+ function collectOverwriteProxyNames(overwriteFiles) {
3067
+ const names = [];
3068
+ for (const file of overwriteFiles) {
3069
+ for (const [key, value] of Object.entries(file.config)) {
3070
+ if ((key === "+proxies" || key === "proxies+") && Array.isArray(value)) {
3071
+ for (const proxy of value) {
3072
+ if (proxy && typeof proxy === "object" && "name" in proxy) {
3073
+ names.push(proxy.name);
3074
+ }
3075
+ }
3076
+ }
3077
+ }
3078
+ }
3079
+ return names;
3080
+ }
3081
+ function excludeOverwriteProxiesFromIncludeAll(config, overwriteFiles) {
3082
+ const injectedNames = collectOverwriteProxyNames(overwriteFiles);
3083
+ if (injectedNames.length === 0) return;
3084
+ const groups = config["proxy-groups"];
3085
+ if (!groups) return;
3086
+ const excludePattern = injectedNames.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
3087
+ for (const group of groups) {
3088
+ if (!group["include-all"] && !group["include-all-proxies"]) continue;
3089
+ const existing = group["exclude-filter"];
3090
+ if (existing) {
3091
+ group["exclude-filter"] = `${existing}|${excludePattern}`;
3092
+ } else {
3093
+ group["exclude-filter"] = excludePattern;
3094
+ }
3095
+ }
3096
+ }
3025
3097
  function buildConfig(subRawContent, mode) {
3026
3098
  const subscriptionConfig = parseYamlOrJson(subRawContent, "\u8BA2\u9605\u5185\u5BB9");
3027
3099
  if (!subscriptionConfig) {
@@ -3030,6 +3102,9 @@ function buildConfig(subRawContent, mode) {
3030
3102
  const overwriteEnabled = isOverwriteEnabled();
3031
3103
  const overwriteFiles = overwriteEnabled ? loadOverwriteFile() : [];
3032
3104
  const withOverwrites = applyOverwrite(subscriptionConfig, overwriteFiles);
3105
+ if (overwriteFiles.length > 0) {
3106
+ excludeOverwriteProxiesFromIncludeAll(withOverwrites, overwriteFiles);
3107
+ }
3033
3108
  const systemConfig = {};
3034
3109
  for (const [key, value] of Object.entries(BASE_CONFIG)) {
3035
3110
  if (!(key in withOverwrites)) {
@@ -3188,6 +3263,18 @@ function formatDate(dateOrIso) {
3188
3263
  return "\u672A\u77E5";
3189
3264
  }
3190
3265
  }
3266
+ function displayWidth(str2) {
3267
+ let w = 0;
3268
+ for (const ch of str2) {
3269
+ const code = ch.codePointAt(0);
3270
+ if (code >= 4352 && (code <= 4447 || code === 9001 || code === 9002 || code >= 11904 && code <= 42191 && code !== 12351 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65040 && code <= 65135 || code >= 65281 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 131072 && code <= 196605 || code >= 196608 && code <= 262141)) {
3271
+ w += 2;
3272
+ } else {
3273
+ w += 1;
3274
+ }
3275
+ }
3276
+ return w;
3277
+ }
3191
3278
  function hasFlag(args, short, long) {
3192
3279
  return !!args && (args.includes(short) || args.includes(long));
3193
3280
  }
@@ -3296,7 +3383,345 @@ function parseMirrorArg(args) {
3296
3383
  return { mirror: null, isOverride: false, type: "download" };
3297
3384
  }
3298
3385
 
3386
+ // src/bench.ts
3387
+ var BENCH_DIR = path3.join(USER_DATA_DIR, "bench");
3388
+ var BENCH_DIRS = {
3389
+ data: path3.join(BENCH_DIR, "data"),
3390
+ runtime: path3.join(BENCH_DIR, "runtime")
3391
+ };
3392
+ var BENCH_PATHS = {
3393
+ configFile: path3.join(BENCH_DIRS.runtime, "config.yaml"),
3394
+ pidFile: path3.join(BENCH_DIRS.runtime, "pid"),
3395
+ logFile: path3.join(BENCH_DIR, "bench.log")
3396
+ };
3397
+ var BENCH_API = `http://${BENCH_CONFIG["external-controller"]}`;
3398
+ var BENCH_TEST_URL = "http://www.gstatic.com/generate_204";
3399
+ function ensureBenchDirs() {
3400
+ for (const dir of Object.values(BENCH_DIRS)) {
3401
+ if (!fs5.existsSync(dir)) {
3402
+ fs5.mkdirSync(dir, { recursive: true, mode: 448 });
3403
+ }
3404
+ }
3405
+ }
3406
+ function cleanupBenchDir() {
3407
+ if (fs5.existsSync(BENCH_DIR)) {
3408
+ fs5.rmSync(BENCH_DIR, { recursive: true, force: true });
3409
+ }
3410
+ }
3411
+ function tryDecodeBase64Content(content) {
3412
+ const trimmed = content.trim();
3413
+ if (trimmed.startsWith("{") || trimmed.startsWith("proxies") || trimmed.includes("proxy-groups")) return null;
3414
+ try {
3415
+ const decoded = Buffer.from(trimmed, "base64").toString("utf8");
3416
+ if (decoded.includes("://")) return decoded;
3417
+ } catch {
3418
+ }
3419
+ return null;
3420
+ }
3421
+ function parseVmessUri(uri) {
3422
+ try {
3423
+ const b64 = uri.slice("vmess://".length);
3424
+ const json2 = JSON.parse(Buffer.from(b64, "base64").toString("utf8"));
3425
+ return {
3426
+ name: json2.ps || json2.add || "vmess",
3427
+ type: "vmess",
3428
+ server: json2.add,
3429
+ port: Number(json2.port),
3430
+ uuid: json2.id,
3431
+ alterId: Number(json2.aid) || 0,
3432
+ cipher: json2.security || "auto",
3433
+ tls: json2.tls === "tls",
3434
+ network: json2.net || "tcp",
3435
+ ...json2.net === "ws" && { "ws-opts": { path: json2.path || "/", headers: json2.host ? { Host: json2.host } : void 0 } }
3436
+ };
3437
+ } catch {
3438
+ return null;
3439
+ }
3440
+ }
3441
+ function parseSsUri(uri) {
3442
+ try {
3443
+ const hashIdx = uri.indexOf("#");
3444
+ const name = hashIdx >= 0 ? decodeURIComponent(uri.slice(hashIdx + 1)) : "ss";
3445
+ const main2 = uri.slice("ss://".length, hashIdx >= 0 ? hashIdx : void 0);
3446
+ let decoded;
3447
+ const atIdx = main2.indexOf("@");
3448
+ if (atIdx >= 0) {
3449
+ const methodPassword2 = Buffer.from(main2.slice(0, atIdx), "base64").toString("utf8");
3450
+ decoded = `${methodPassword2}@${main2.slice(atIdx + 1)}`;
3451
+ } else {
3452
+ decoded = Buffer.from(main2, "base64").toString("utf8");
3453
+ }
3454
+ const [methodPassword, serverPort] = decoded.split("@");
3455
+ if (!methodPassword || !serverPort) return null;
3456
+ const colonIdx = methodPassword.indexOf(":");
3457
+ const method = methodPassword.slice(0, colonIdx);
3458
+ const password = methodPassword.slice(colonIdx + 1);
3459
+ const lastColon = serverPort.lastIndexOf(":");
3460
+ const server = serverPort.slice(0, lastColon);
3461
+ const port = Number(serverPort.slice(lastColon + 1));
3462
+ return { name, type: "ss", server, port, cipher: method, password };
3463
+ } catch {
3464
+ return null;
3465
+ }
3466
+ }
3467
+ function parseTrojanUri(uri) {
3468
+ try {
3469
+ const hashIdx = uri.indexOf("#");
3470
+ const name = hashIdx >= 0 ? decodeURIComponent(uri.slice(hashIdx + 1)) : "trojan";
3471
+ const main2 = uri.slice("trojan://".length, hashIdx >= 0 ? hashIdx : void 0);
3472
+ const atIdx = main2.indexOf("@");
3473
+ if (atIdx < 0) return null;
3474
+ const password = main2.slice(0, atIdx);
3475
+ const rest = main2.slice(atIdx + 1).split("?")[0];
3476
+ const lastColon = rest.lastIndexOf(":");
3477
+ const server = rest.slice(0, lastColon);
3478
+ const port = Number(rest.slice(lastColon + 1));
3479
+ return { name, type: "trojan", server, port, password, sni: server };
3480
+ } catch {
3481
+ return null;
3482
+ }
3483
+ }
3484
+ function parseProxyUris(content) {
3485
+ const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
3486
+ const proxies = [];
3487
+ for (const line of lines) {
3488
+ let proxy = null;
3489
+ if (line.startsWith("vmess://")) proxy = parseVmessUri(line);
3490
+ else if (line.startsWith("ss://")) proxy = parseSsUri(line);
3491
+ else if (line.startsWith("trojan://")) proxy = parseTrojanUri(line);
3492
+ if (proxy?.name && proxy?.server) proxies.push(proxy);
3493
+ }
3494
+ return proxies;
3495
+ }
3496
+ async function downloadAllSources(sources, onProgress) {
3497
+ const savedProxy = { http: process.env.http_proxy, https: process.env.https_proxy, HTTP: process.env.HTTP_PROXY, HTTPS: process.env.HTTPS_PROXY };
3498
+ delete process.env.http_proxy;
3499
+ delete process.env.https_proxy;
3500
+ delete process.env.HTTP_PROXY;
3501
+ delete process.env.HTTPS_PROXY;
3502
+ try {
3503
+ const client = createHttpClient({ timeout: 3e4 });
3504
+ const tasks = sources.map(async (source) => {
3505
+ const entry = { name: source.name, url: source.url, proxies: [], proxyGroups: 0 };
3506
+ try {
3507
+ const response = await client.get(source.url, { responseType: "text" });
3508
+ const content = response.data;
3509
+ if (!content?.trim()) throw new Error("\u5185\u5BB9\u4E3A\u7A7A");
3510
+ let proxies;
3511
+ try {
3512
+ const parsed = parseYamlOrJson(content, "\u8BA2\u9605\u5185\u5BB9");
3513
+ proxies = parsed.proxies || [];
3514
+ const groups = parsed["proxy-groups"];
3515
+ if (groups) entry.proxyGroups = groups.length;
3516
+ } catch {
3517
+ const decoded = tryDecodeBase64Content(content);
3518
+ if (decoded) {
3519
+ proxies = parseProxyUris(decoded);
3520
+ } else {
3521
+ proxies = parseProxyUris(content);
3522
+ }
3523
+ if (proxies.length === 0) throw new Error("\u65E0\u6CD5\u89E3\u6790\u8BA2\u9605\u5185\u5BB9\uFF08\u975E YAML/JSON/Base64\uFF09");
3524
+ }
3525
+ entry.proxies = proxies.map((p) => ({ ...p, name: `[${source.name}] ${p.name}` }));
3526
+ onProgress?.(source.name, true, proxies.length, entry.proxyGroups);
3527
+ } catch (e) {
3528
+ entry.error = e.message;
3529
+ onProgress?.(source.name, false, 0, 0, entry.error);
3530
+ }
3531
+ return entry;
3532
+ });
3533
+ return await Promise.all(tasks);
3534
+ } finally {
3535
+ for (const [key, val] of Object.entries(savedProxy)) {
3536
+ if (val !== void 0) process.env[key] = val;
3537
+ }
3538
+ }
3539
+ }
3540
+ function isProxyValid(proxy) {
3541
+ if (!proxy.name || !proxy.server || !proxy.port) return false;
3542
+ if (!proxy.type) return false;
3543
+ if (proxy.type === "ss" && typeof proxy.cipher === "string" && proxy.cipher.startsWith("2022-blake3")) {
3544
+ const pw = String(proxy.password || "");
3545
+ if (!/^[A-Za-z0-9+/]+=*$/.test(pw) || pw.length < 20) return false;
3546
+ }
3547
+ return true;
3548
+ }
3549
+ function buildMergedBenchConfig(allProxies) {
3550
+ ensureBenchDirs();
3551
+ const validProxies = allProxies.filter(isProxyValid);
3552
+ const removed = allProxies.length - validProxies.length;
3553
+ const nameCount = /* @__PURE__ */ new Map();
3554
+ for (const proxy of validProxies) {
3555
+ const originalName = proxy.name;
3556
+ const count = (nameCount.get(originalName) || 0) + 1;
3557
+ nameCount.set(originalName, count);
3558
+ if (count > 1) {
3559
+ proxy.name = `${originalName} #${count}`;
3560
+ }
3561
+ }
3562
+ const config = {
3563
+ ...BENCH_CONFIG,
3564
+ proxies: validProxies,
3565
+ "proxy-groups": [
3566
+ {
3567
+ name: "PROXY",
3568
+ type: "select",
3569
+ proxies: validProxies.map((p) => p.name)
3570
+ }
3571
+ ],
3572
+ rules: ["MATCH,PROXY"]
3573
+ };
3574
+ const content = jsYaml.dump(config, { indent: 2, lineWidth: -1, noCompatMode: true });
3575
+ fs5.writeFileSync(BENCH_PATHS.configFile, content, { mode: 384 });
3576
+ allProxies.length = 0;
3577
+ allProxies.push(...validProxies);
3578
+ return removed;
3579
+ }
3580
+ async function startBenchInstance() {
3581
+ const binary2 = PATHS.mihomoBinary;
3582
+ if (!fs5.existsSync(binary2)) throw new Error("\u672A\u627E\u5230 mihomo \u5185\u6838");
3583
+ const logFd = fs5.openSync(BENCH_PATHS.logFile, "a");
3584
+ const child = spawn(binary2, ["-d", BENCH_DIRS.data, "-f", BENCH_PATHS.configFile], {
3585
+ detached: true,
3586
+ stdio: ["ignore", logFd, logFd]
3587
+ });
3588
+ fs5.closeSync(logFd);
3589
+ child.unref();
3590
+ const pid = child.pid;
3591
+ fs5.writeFileSync(BENCH_PATHS.pidFile, pid.toString(), { mode: 384 });
3592
+ const client = createHttpClient({ timeout: 2e3 });
3593
+ let ready = false;
3594
+ for (let i = 0; i < 60; i++) {
3595
+ await sleep(500);
3596
+ if (!isProcessRunning(pid)) break;
3597
+ try {
3598
+ await client.get(`${BENCH_API}/version`);
3599
+ ready = true;
3600
+ break;
3601
+ } catch {
3602
+ }
3603
+ }
3604
+ if (!isProcessRunning(pid)) {
3605
+ let errorDetail = "";
3606
+ if (fs5.existsSync(BENCH_PATHS.logFile)) {
3607
+ try {
3608
+ errorDetail = fs5.readFileSync(BENCH_PATHS.logFile, "utf8").slice(-1e3);
3609
+ } catch {
3610
+ }
3611
+ }
3612
+ throw new Error(`bench \u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
3613
+ ${errorDetail}` : ""}`);
3614
+ }
3615
+ if (!ready) {
3616
+ throw new Error("bench \u5B9E\u4F8B\u542F\u52A8\u8D85\u65F6\uFF0CAPI \u672A\u54CD\u5E94");
3617
+ }
3618
+ return pid;
3619
+ }
3620
+ function stopBenchInstance() {
3621
+ if (!fs5.existsSync(BENCH_PATHS.pidFile)) return;
3622
+ try {
3623
+ const pid = parseInt(fs5.readFileSync(BENCH_PATHS.pidFile, "utf8").trim(), 10);
3624
+ if (pid > 0 && isProcessRunning(pid)) {
3625
+ process.kill(pid, "SIGKILL");
3626
+ for (let i = 0; i < 20; i++) {
3627
+ if (!isProcessRunning(pid)) break;
3628
+ sleepSync(100);
3629
+ }
3630
+ }
3631
+ } catch {
3632
+ }
3633
+ try {
3634
+ fs5.unlinkSync(BENCH_PATHS.pidFile);
3635
+ } catch {
3636
+ }
3637
+ }
3638
+ async function testBenchProxy(proxyName, timeout, client) {
3639
+ const encodedName = encodeURIComponent(proxyName);
3640
+ const url = `${BENCH_API}/proxies/${encodedName}/delay?timeout=${timeout}&url=${encodeURIComponent(BENCH_TEST_URL)}`;
3641
+ try {
3642
+ const response = await client.get(url);
3643
+ const data = JSON.parse(response.data);
3644
+ if (data.delay && data.delay > 0) {
3645
+ return { name: proxyName, delay: data.delay };
3646
+ }
3647
+ return { name: proxyName, delay: null, error: data.message || "no delay" };
3648
+ } catch (e) {
3649
+ const err = e;
3650
+ let errorMsg = "timeout";
3651
+ if (err.response?.data?.message) {
3652
+ errorMsg = String(err.response.data.message);
3653
+ } else if (err.message) {
3654
+ errorMsg = err.message;
3655
+ }
3656
+ return { name: proxyName, delay: null, error: errorMsg };
3657
+ }
3658
+ }
3659
+ async function testBenchProxies(proxyNames, options = {}) {
3660
+ const { timeout = 3e3, concurrency = 100, onResult, onBatch } = options;
3661
+ const client = createHttpClient({ timeout: timeout + 3e3 });
3662
+ const results = [];
3663
+ let completedCount = 0;
3664
+ let aliveCount = 0;
3665
+ const delays = [];
3666
+ const totalBatches = Math.ceil(proxyNames.length / concurrency);
3667
+ for (let i = 0; i < proxyNames.length; i += concurrency) {
3668
+ const batch = proxyNames.slice(i, i + concurrency);
3669
+ const batchResults = await Promise.all(batch.map((name) => testBenchProxy(name, timeout, client)));
3670
+ for (const result of batchResults) {
3671
+ results.push(result);
3672
+ if (result.delay !== null) {
3673
+ aliveCount++;
3674
+ delays.push(result.delay);
3675
+ }
3676
+ onResult?.(result, completedCount, proxyNames.length);
3677
+ completedCount++;
3678
+ }
3679
+ delays.sort((a, b) => a - b);
3680
+ const median = delays.length > 0 ? delays[Math.floor(delays.length / 2)] : 0;
3681
+ onBatch?.(Math.floor(i / concurrency) + 1, totalBatches, aliveCount, completedCount, median);
3682
+ }
3683
+ return results;
3684
+ }
3685
+ function computeSourceResult(source, resultsByName) {
3686
+ const proxyNames = source.proxies.map((p) => p.name);
3687
+ const sourceResults = proxyNames.map((n) => resultsByName.get(n)).filter((r) => r !== void 0);
3688
+ const delays = sourceResults.filter((r) => r.delay !== null).map((r) => r.delay);
3689
+ const alive = delays.length;
3690
+ const dead = sourceResults.length - alive;
3691
+ if (alive === 0) {
3692
+ return {
3693
+ name: source.name,
3694
+ url: source.url,
3695
+ downloadOk: !source.error,
3696
+ downloadError: source.error,
3697
+ totalProxies: source.proxies.length,
3698
+ proxyGroups: source.proxyGroups,
3699
+ alive: 0,
3700
+ dead,
3701
+ avgDelay: 0,
3702
+ minDelay: 0,
3703
+ medianDelay: 0
3704
+ };
3705
+ }
3706
+ delays.sort((a, b) => a - b);
3707
+ return {
3708
+ name: source.name,
3709
+ url: source.url,
3710
+ downloadOk: true,
3711
+ totalProxies: source.proxies.length,
3712
+ proxyGroups: source.proxyGroups,
3713
+ alive,
3714
+ dead,
3715
+ avgDelay: Math.round(delays.reduce((sum, d) => sum + d, 0) / alive),
3716
+ minDelay: delays[0],
3717
+ medianDelay: delays[Math.floor(delays.length / 2)]
3718
+ };
3719
+ }
3720
+
3299
3721
  // src/process.ts
3722
+ import { execSync as execSync3, spawn as spawn2 } from "child_process";
3723
+ import fs6 from "fs";
3724
+ import path4 from "path";
3300
3725
  var PROCESS_WAIT_ATTEMPTS = 50;
3301
3726
  var PROCESS_WAIT_INTERVAL = 100;
3302
3727
  var STARTUP_WAIT_MS = 800;
@@ -3305,15 +3730,15 @@ var TUN_MODE_POST_WAIT_MS = 500;
3305
3730
  var BATCH_KILL_THRESHOLD = 3;
3306
3731
  var DEFAULT_LOG_RETENTION_DAYS = 7;
3307
3732
  function clearRuntime() {
3308
- if (fs5.existsSync(DIRS.runtime)) {
3733
+ if (fs6.existsSync(DIRS.runtime)) {
3309
3734
  rmrf(DIRS.runtime);
3310
3735
  }
3311
3736
  ensureDirs();
3312
3737
  }
3313
3738
  function getPid() {
3314
- if (!fs5.existsSync(PATHS.pidFile)) return null;
3739
+ if (!fs6.existsSync(PATHS.pidFile)) return null;
3315
3740
  try {
3316
- const pid = parseInt(fs5.readFileSync(PATHS.pidFile, "utf8").trim(), 10);
3741
+ const pid = parseInt(fs6.readFileSync(PATHS.pidFile, "utf8").trim(), 10);
3317
3742
  return pid > 0 ? pid : null;
3318
3743
  } catch {
3319
3744
  return null;
@@ -3334,9 +3759,9 @@ function getAllMihomoPids() {
3334
3759
  }
3335
3760
  }
3336
3761
  function isPidFileOwnedByRoot() {
3337
- if (!fs5.existsSync(PATHS.pidFile)) return false;
3762
+ if (!fs6.existsSync(PATHS.pidFile)) return false;
3338
3763
  try {
3339
- const stat = fs5.statSync(PATHS.pidFile);
3764
+ const stat = fs6.statSync(PATHS.pidFile);
3340
3765
  return stat.uid === 0;
3341
3766
  } catch {
3342
3767
  return false;
@@ -3356,10 +3781,10 @@ function checkStaleState() {
3356
3781
  }
3357
3782
  function savePid(pid) {
3358
3783
  ensureDirs();
3359
- fs5.writeFileSync(PATHS.pidFile, pid.toString(), { mode: 384 });
3784
+ fs6.writeFileSync(PATHS.pidFile, pid.toString(), { mode: 384 });
3360
3785
  }
3361
3786
  function clearPid() {
3362
- if (!fs5.existsSync(PATHS.pidFile)) return;
3787
+ if (!fs6.existsSync(PATHS.pidFile)) return;
3363
3788
  if (isPidFileOwnedByRoot()) {
3364
3789
  try {
3365
3790
  execSync3(`sudo rm -f "${PATHS.pidFile}" 2>/dev/null`, { stdio: "inherit", timeout: 1e4 });
@@ -3367,7 +3792,7 @@ function clearPid() {
3367
3792
  }
3368
3793
  } else {
3369
3794
  try {
3370
- fs5.unlinkSync(PATHS.pidFile);
3795
+ fs6.unlinkSync(PATHS.pidFile);
3371
3796
  } catch {
3372
3797
  }
3373
3798
  }
@@ -3492,8 +3917,8 @@ echo "--- \u65E5\u5FD7 ---"
3492
3917
  tail -25 "\${LOG_FILE}" 2>/dev/null
3493
3918
  exit 1
3494
3919
  `;
3495
- const scriptPath = path3.join(DIRS.runtime, "launch-tun.sh");
3496
- fs5.writeFileSync(scriptPath, scriptContent, { mode: 448 });
3920
+ const scriptPath = path4.join(DIRS.runtime, "launch-tun.sh");
3921
+ fs6.writeFileSync(scriptPath, scriptContent, { mode: 448 });
3497
3922
  return scriptPath;
3498
3923
  }
3499
3924
  function getProcessInfo(pid) {
@@ -3534,11 +3959,11 @@ async function start(mode = "mixed") {
3534
3959
  ensureDirs();
3535
3960
  rotateAndCleanupLogs();
3536
3961
  const binary2 = PATHS.mihomoBinary;
3537
- if (!fs5.existsSync(binary2)) {
3962
+ if (!fs6.existsSync(binary2)) {
3538
3963
  throw new Error("\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u4E0B\u8F7D\u5185\u6838");
3539
3964
  }
3540
3965
  const configFile = PATHS.configFile;
3541
- if (!fs5.existsSync(configFile)) {
3966
+ if (!fs6.existsSync(configFile)) {
3542
3967
  throw new Error("\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605\u5E76\u542F\u52A8");
3543
3968
  }
3544
3969
  const staleState = checkStaleState();
@@ -3567,12 +3992,12 @@ async function startMixedMode(staleState) {
3567
3992
  const configFile = PATHS.configFile;
3568
3993
  const logFile = PATHS.logFile;
3569
3994
  const args = ["-d", DIRS.data, "-f", configFile];
3570
- const logFd = fs5.openSync(logFile, "a");
3571
- const child = spawn(PATHS.mihomoBinary, args, {
3995
+ const logFd = fs6.openSync(logFile, "a");
3996
+ const child = spawn2(PATHS.mihomoBinary, args, {
3572
3997
  detached: true,
3573
3998
  stdio: ["ignore", logFd, logFd]
3574
3999
  });
3575
- fs5.closeSync(logFd);
4000
+ fs6.closeSync(logFd);
3576
4001
  child.unref();
3577
4002
  const pid = child.pid;
3578
4003
  savePid(pid);
@@ -3580,9 +4005,9 @@ async function startMixedMode(staleState) {
3580
4005
  if (!isRunning()) {
3581
4006
  clearPid();
3582
4007
  let errorMsg = "\u542F\u52A8\u5931\u8D25";
3583
- if (fs5.existsSync(logFile)) {
4008
+ if (fs6.existsSync(logFile)) {
3584
4009
  try {
3585
- const logs = fs5.readFileSync(logFile, "utf8").slice(-3e3);
4010
+ const logs = fs6.readFileSync(logFile, "utf8").slice(-3e3);
3586
4011
  if (logs.trim()) {
3587
4012
  errorMsg += "\n\u6700\u8FD1\u7684\u65E5\u5FD7:\n" + logs.split("\n").map((l) => ` ${l}`).join("\n");
3588
4013
  }
@@ -3603,7 +4028,7 @@ async function startTunMode(staleState) {
3603
4028
  execSync3(`sudo "${launchScript}"`, { stdio: "inherit", timeout: SUDO_TIMEOUT_MS });
3604
4029
  } catch (e) {
3605
4030
  try {
3606
- fs5.unlinkSync(launchScript);
4031
+ fs6.unlinkSync(launchScript);
3607
4032
  } catch {
3608
4033
  }
3609
4034
  if (e.status === 1) {
@@ -3612,7 +4037,7 @@ async function startTunMode(staleState) {
3612
4037
  throw new Error(e.message);
3613
4038
  }
3614
4039
  try {
3615
- fs5.unlinkSync(launchScript);
4040
+ fs6.unlinkSync(launchScript);
3616
4041
  } catch {
3617
4042
  }
3618
4043
  await new Promise((resolve) => setTimeout(resolve, TUN_MODE_POST_WAIT_MS));
@@ -3651,19 +4076,19 @@ function getLogPath() {
3651
4076
  }
3652
4077
  function rotateLog() {
3653
4078
  const logFile = PATHS.logFile;
3654
- if (!fs5.existsSync(logFile)) return null;
3655
- const stat = fs5.statSync(logFile);
4079
+ if (!fs6.existsSync(logFile)) return null;
4080
+ const stat = fs6.statSync(logFile);
3656
4081
  if (stat.size === 0) return null;
3657
4082
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/T/, "_").replace(/:/g, "-").replace(/\..+/, "");
3658
4083
  const rotatedName = `mihomo.${timestamp2}.log`;
3659
- const rotatedPath = path3.join(DIRS.logs, rotatedName);
3660
- fs5.renameSync(logFile, rotatedPath);
4084
+ const rotatedPath = path4.join(DIRS.logs, rotatedName);
4085
+ fs6.renameSync(logFile, rotatedPath);
3661
4086
  return rotatedPath;
3662
4087
  }
3663
4088
  function cleanupOldLogs(maxAgeDays = DEFAULT_LOG_RETENTION_DAYS) {
3664
4089
  const logsDir = DIRS.logs;
3665
- if (!fs5.existsSync(logsDir)) return { deleted: 0, errors: 0 };
3666
- const files = fs5.readdirSync(logsDir);
4090
+ if (!fs6.existsSync(logsDir)) return { deleted: 0, errors: 0 };
4091
+ const files = fs6.readdirSync(logsDir);
3667
4092
  const now = Date.now();
3668
4093
  const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
3669
4094
  let deleted = 0;
@@ -3671,10 +4096,10 @@ function cleanupOldLogs(maxAgeDays = DEFAULT_LOG_RETENTION_DAYS) {
3671
4096
  for (const file of files) {
3672
4097
  if (!file.match(/^mihomo\.\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.log$/)) continue;
3673
4098
  try {
3674
- const filePath = path3.join(logsDir, file);
3675
- const stat = fs5.statSync(filePath);
4099
+ const filePath = path4.join(logsDir, file);
4100
+ const stat = fs6.statSync(filePath);
3676
4101
  if (now - stat.mtimeMs > maxAgeMs) {
3677
- fs5.unlinkSync(filePath);
4102
+ fs6.unlinkSync(filePath);
3678
4103
  deleted++;
3679
4104
  }
3680
4105
  } catch {
@@ -3686,8 +4111,8 @@ function cleanupOldLogs(maxAgeDays = DEFAULT_LOG_RETENTION_DAYS) {
3686
4111
  function listLogs() {
3687
4112
  const logsDir = DIRS.logs;
3688
4113
  const result = { current: null, archives: [] };
3689
- if (fs5.existsSync(PATHS.logFile)) {
3690
- const stat = fs5.statSync(PATHS.logFile);
4114
+ if (fs6.existsSync(PATHS.logFile)) {
4115
+ const stat = fs6.statSync(PATHS.logFile);
3691
4116
  result.current = {
3692
4117
  name: "mihomo.log (\u5F53\u524D)",
3693
4118
  path: PATHS.logFile,
@@ -3696,14 +4121,14 @@ function listLogs() {
3696
4121
  isCurrent: true
3697
4122
  };
3698
4123
  }
3699
- if (!fs5.existsSync(logsDir)) return result;
3700
- const files = fs5.readdirSync(logsDir);
4124
+ if (!fs6.existsSync(logsDir)) return result;
4125
+ const files = fs6.readdirSync(logsDir);
3701
4126
  for (const file of files) {
3702
4127
  const match = file.match(/^mihomo\.(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})\.log$/);
3703
4128
  if (!match) continue;
3704
4129
  try {
3705
- const filePath = path3.join(logsDir, file);
3706
- const stat = fs5.statSync(filePath);
4130
+ const filePath = path4.join(logsDir, file);
4131
+ const stat = fs6.statSync(filePath);
3707
4132
  result.archives.push({
3708
4133
  name: file,
3709
4134
  path: filePath,
@@ -3718,22 +4143,22 @@ function listLogs() {
3718
4143
  return result;
3719
4144
  }
3720
4145
  function isPathUnderDir(filePath, baseDir) {
3721
- const resolvedPath = path3.resolve(filePath);
3722
- const resolvedBase = path3.resolve(baseDir);
3723
- return resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + path3.sep);
4146
+ const resolvedPath = path4.resolve(filePath);
4147
+ const resolvedBase = path4.resolve(baseDir);
4148
+ return resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + path4.sep);
3724
4149
  }
3725
4150
  function getLogPathByName(name) {
3726
4151
  const logsDir = DIRS.logs;
3727
4152
  let targetName = name;
3728
4153
  if (!name.endsWith(".log")) targetName = `mihomo.${name}.log`;
3729
4154
  if (!targetName.startsWith("mihomo.")) targetName = `mihomo.${targetName}`;
3730
- const filePath = path3.join(logsDir, targetName);
3731
- if (fs5.existsSync(filePath) && isPathUnderDir(filePath, logsDir)) return filePath;
3732
- if (fs5.existsSync(logsDir)) {
3733
- const files = fs5.readdirSync(logsDir);
4155
+ const filePath = path4.join(logsDir, targetName);
4156
+ if (fs6.existsSync(filePath) && isPathUnderDir(filePath, logsDir)) return filePath;
4157
+ if (fs6.existsSync(logsDir)) {
4158
+ const files = fs6.readdirSync(logsDir);
3734
4159
  for (const file of files) {
3735
4160
  if (file.includes(name)) {
3736
- const candidatePath = path3.join(logsDir, file);
4161
+ const candidatePath = path4.join(logsDir, file);
3737
4162
  if (isPathUnderDir(candidatePath, logsDir)) return candidatePath;
3738
4163
  }
3739
4164
  }
@@ -3742,7 +4167,7 @@ function getLogPathByName(name) {
3742
4167
  }
3743
4168
  function openUrl(url) {
3744
4169
  try {
3745
- const child = spawn("open", [url], { stdio: "ignore", detached: true });
4170
+ const child = spawn2("open", [url], { stdio: "ignore", detached: true });
3746
4171
  child.unref();
3747
4172
  child.on("error", () => {
3748
4173
  });
@@ -3773,7 +4198,7 @@ function viewLogWithTail(logPath, options) {
3773
4198
  if (follow) tailArgs.push("-f");
3774
4199
  tailArgs.push("-n", lines.toString());
3775
4200
  tailArgs.push(logPath);
3776
- const tail = spawn("tail", tailArgs, { stdio: "inherit" });
4201
+ const tail = spawn2("tail", tailArgs, { stdio: "inherit" });
3777
4202
  tail.on("close", () => process.exit(0));
3778
4203
  tail.on("error", (e) => {
3779
4204
  console.error(`\u65E0\u6CD5\u8BFB\u53D6\u65E5\u5FD7: ${e.message}`);
@@ -3781,1268 +4206,1588 @@ function viewLogWithTail(logPath, options) {
3781
4206
  });
3782
4207
  }
3783
4208
 
3784
- // src/commands/directory.ts
3785
- function cmdDirectory(args) {
3786
- const action = args?.[1];
3787
- if (action === "open") {
3788
- const target = args[2];
3789
- if (!target || target === "root") {
3790
- console.log("\u6B63\u5728\u6253\u5F00: \u6839\u76EE\u5F55");
3791
- const success = openUrl(USER_DATA_DIR);
3792
- if (!success) {
3793
- console.log(`\u8BF7\u624B\u52A8\u6253\u5F00: ${USER_DATA_DIR}`);
3794
- }
3795
- return;
3796
- }
3797
- const targetInfo = DIRECTORY_TARGETS[target.toLowerCase()];
3798
- if (targetInfo) {
3799
- const targetPath = targetInfo.path || USER_DATA_DIR;
3800
- console.log(`\u6B63\u5728\u6253\u5F00: ${targetInfo.label}`);
3801
- const success = openUrl(targetPath);
3802
- if (!success) {
3803
- console.log(`\u8BF7\u624B\u52A8\u6253\u5F00: ${targetPath}`);
3804
- }
3805
- return;
3806
- }
3807
- console.error(`\u9519\u8BEF: \u672A\u77E5\u7684\u76EE\u5F55\u76EE\u6807 "${target}"`);
3808
- console.log("");
3809
- console.log("\u53EF\u7528\u76EE\u6807:");
3810
- console.log(" root (\u9ED8\u8BA4) \u6839\u76EE\u5F55");
3811
- for (const [key, val] of Object.entries(DIRECTORY_TARGETS)) {
3812
- if (key !== "root") {
3813
- console.log(` ${key.padEnd(14)}${val.label}`);
3814
- }
4209
+ // src/subscription.ts
4210
+ var DEFAULT_UPDATE_INTERVAL_HOURS = 12;
4211
+ var YAML_DUMP_OPTS = { indent: 2, lineWidth: -1, noCompatMode: true };
4212
+ var HTTP_CLIENT = createHttpClient({ timeout: 6e4 });
4213
+ function isMultiUrl(url) {
4214
+ return url.includes(",");
4215
+ }
4216
+ function splitUrls(url) {
4217
+ return url.split(",").map((u) => u.trim()).filter(Boolean);
4218
+ }
4219
+ function loadSubscriptionConfig(subName) {
4220
+ const rawContent = readSubscriptionRawConfig(subName);
4221
+ if (!rawContent) {
4222
+ throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
4223
+ }
4224
+ const raw = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
4225
+ return {
4226
+ raw,
4227
+ proxies: raw.proxies || [],
4228
+ proxyGroups: raw["proxy-groups"] || []
4229
+ };
4230
+ }
4231
+ function saveSubscriptionConfig(subName, parsed) {
4232
+ normalizeProxyNamesBeforeSave(parsed);
4233
+ parsed.raw.proxies = parsed.proxies;
4234
+ parsed.raw["proxy-groups"] = parsed.proxyGroups;
4235
+ saveSubscriptionRawConfig(subName, jsYaml.dump(parsed.raw, YAML_DUMP_OPTS));
4236
+ }
4237
+ function parseUserInfo(header) {
4238
+ if (!header) return null;
4239
+ const info = {};
4240
+ const parts = header.split(";").map((p) => p.trim());
4241
+ for (const part of parts) {
4242
+ const [key, val] = part.split("=").map((s) => s.trim());
4243
+ if (key && val !== void 0) {
4244
+ const numVal = parseFloat(val);
4245
+ info[key] = Number.isNaN(numVal) ? 0 : numVal;
3815
4246
  }
3816
- console.log("");
3817
- process.exit(1);
3818
4247
  }
3819
- console.log("");
3820
- console.log("\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E:");
3821
- console.log(` \u6839\u76EE\u5F55: ${USER_DATA_DIR}`);
3822
- console.log(` \u5168\u5C40\u8BBE\u7F6E: ${PATHS.settingsFile}`);
3823
- console.log(` \u5185\u6838\u76EE\u5F55: ${DIRS.kernel}`);
3824
- console.log(` \u5185\u6838\u6587\u4EF6: ${PATHS.mihomoBinary}`);
3825
- console.log(` \u8BA2\u9605\u76EE\u5F55: ${DIRS.subscriptions}`);
3826
- console.log(" - cache.json (\u8BA2\u9605\u7F13\u5B58\uFF1A\u66F4\u65B0\u65F6\u95F4\u3001\u6D41\u91CF\u7B49)");
3827
- console.log(" - xxx.yaml (\u8BA2\u9605\u539F\u59CB\u914D\u7F6E)");
3828
- console.log(` \u8FD0\u884C\u65F6\u76EE\u5F55: ${DIRS.runtime}`);
3829
- console.log(" - config.yaml (\u542F\u52A8\u65F6\u751F\u6210\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
3830
- console.log(" - pid (PID \u6587\u4EF6\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
3831
- console.log(` \u65E5\u5FD7\u6587\u4EF6: ${PATHS.logFile}`);
3832
- console.log(` mihomo \u6570\u636E: ${DIRS.data}`);
3833
- console.log(" - cache.db, Geo*.dat \u7B49 (mihomo \u81EA\u884C\u7BA1\u7406)");
3834
- console.log("");
3835
- console.log("\u6253\u5F00\u76EE\u5F55:");
3836
- console.log(" mihomo dir open \u6253\u5F00\u6839\u76EE\u5F55");
3837
- console.log(" mihomo dir open subs \u6253\u5F00\u8BA2\u9605\u76EE\u5F55");
3838
- console.log(" mihomo dir open logs \u6253\u5F00\u65E5\u5FD7\u76EE\u5F55");
3839
- console.log(" mihomo dir open runtime \u6253\u5F00\u8FD0\u884C\u65F6\u76EE\u5F55");
3840
- console.log(" mihomo dir open kernel \u6253\u5F00\u5185\u6838\u76EE\u5F55");
3841
- console.log("");
3842
- console.log("\u73AF\u5883\u53D8\u91CF:");
3843
- console.log(" MIHOMO_CLI_DIR: \u81EA\u5B9A\u4E49\u6839\u76EE\u5F55\u4F4D\u7F6E");
3844
- console.log("");
4248
+ return info;
3845
4249
  }
3846
-
3847
- // src/commands/help.ts
3848
- function printShortHelp() {
3849
- console.log(`
3850
- ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))} (mihomo help \u67E5\u770B\u5B8C\u6574\u5E2E\u52A9)
3851
- `);
3852
- console.log(
3853
- `\u5E38\u7528\u547D\u4EE4:
3854
- ${colors.bold("start")} [tun|mixed] \u542F\u52A8/\u5207\u6362\u4EE3\u7406
3855
- ${colors.bold("sub")} [use|update] \u8BA2\u9605\u7BA1\u7406
3856
- ${colors.bold("ow")} [on|off] \u8986\u5199\u914D\u7F6E
3857
- ${colors.bold("ui")} [zash|dash|yacd] \u6253\u5F00 Web UI
3858
- `
3859
- );
4250
+ function parseUsernameFromContentDisposition(header) {
4251
+ if (!header) return null;
4252
+ const match = header.match(/filename\s*=\s*["']?([^"';\s]+)["']?/i);
4253
+ if (!match) return null;
4254
+ const filename = match[1];
4255
+ const parts = filename.split("/");
4256
+ return parts[parts.length - 1] || null;
3860
4257
  }
3861
- function printHelp() {
3862
- console.log(
3863
- `
3864
- ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))}
3865
-
3866
- \u547D\u4EE4\u522B\u540D: mihomo, mhm, mh
3867
-
3868
- \u7528\u6CD5:
3869
- mihomo <\u547D\u4EE4> [\u9009\u9879]
3870
-
3871
- ${colors.cyan("\u63A7\u5236:")}
3872
- ${colors.bold("start")} [tun|mixed] \u542F\u52A8/\u5207\u6362\u4EE3\u7406 (\u9ED8\u8BA4 mixed)
3873
- ${colors.bold("stop")} \u505C\u6B62\u4EE3\u7406
3874
- ${colors.bold("status")} \u67E5\u770B\u72B6\u6001
3875
-
3876
- ${colors.cyan("\u754C\u9762:")}
3877
- ${colors.bold("ui")} [zash|dash|yacd] \u6253\u5F00 Web UI (\u9ED8\u8BA4 zash)
3878
- ${colors.bold("log")} [-o] \u5B9E\u65F6\u65E5\u5FD7\uFF08-o \u6253\u5F00\u6587\u4EF6\uFF09
3879
- ${colors.bold("logs")} [\u7F16\u53F7] [-n N] [-o] \u65E5\u5FD7\u5217\u8868\uFF080=\u5F53\u524D\uFF0C1+=\u5F52\u6863\uFF09
3880
-
3881
- ${colors.cyan("\u8BA2\u9605:")}
3882
- ${colors.bold("subscription")} \u5217\u51FA\u6240\u6709\u8BA2\u9605\uFF08\u522B\u540D sub\uFF09
3883
- ${colors.bold("subscription")} use <name> \u5207\u6362\u5F53\u524D\u8BA2\u9605
3884
- ${colors.bold("subscription")} add <url> [name] \u6DFB\u52A0\u8BA2\u9605
3885
- ${colors.bold("subscription")} update [name] \u66F4\u65B0\u8BA2\u9605\uFF08\u65E0\u53C2\u66F4\u65B0\u6240\u6709\uFF09
3886
- ${colors.bold("subscription")} remove <name> \u5220\u9664\u8BA2\u9605
3887
- ${colors.bold("subscription")} web [name] \u6253\u5F00\u8BA2\u9605\u9875\u9762
3888
- ${colors.bold("subscription")} test [name] \u6D4B\u8BD5\u8282\u70B9\u8FDE\u901A\u6027
3889
- ${colors.bold("subscription")} clean [name] \u6D4B\u901F\u5E76\u6E05\u7406\u5931\u8D25\u8282\u70B9
3890
-
3891
- ${colors.cyan("\u914D\u7F6E:")}
3892
- ${colors.bold("overwrite")} \u67E5\u770B\u8986\u5199\u72B6\u6001\uFF08\u522B\u540D ow\uFF09
3893
- ${colors.bold("overwrite")} on|off \u542F\u7528/\u7981\u7528\u8986\u5199\u914D\u7F6E
3894
- ${colors.bold("directory")} \u663E\u793A\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E\uFF08\u522B\u540D dir\uFF09
3895
- ${colors.bold("directory")} open [target] \u6253\u5F00\u76EE\u5F55: root|subs|logs|runtime|...
3896
-
3897
- ${colors.cyan("\u7CFB\u7EDF:")}
3898
- ${colors.bold("kernel")} [--mirror [\u955C\u50CF]] \u66F4\u65B0\u5185\u6838\uFF08\u9ED8\u8BA4\u76F4\u8FDE\uFF0C--mirror \u4F7F\u7528 v6\uFF09
3899
- ${colors.bold("update")} \u66F4\u65B0 mihomo-cli (npm install -g)
3900
- ${colors.bold("reset")} [\u76EE\u6807...] [--full] \u91CD\u7F6E: \u7559\u7A7A\u4FDD\u7559\u8BBE\u7F6E/\u5185\u6838/\u8986\u5199, \u6307\u5B9A\u76EE\u6807\u5220\u5BF9\u5E94\u9879, --full \u5220\u5168\u90E8
3901
- ${colors.bold("help")}, -h \u663E\u793A\u5E2E\u52A9
3902
- ${colors.bold("version")}, -v \u663E\u793A\u7248\u672C
3903
-
3904
- ${colors.cyan("\u793A\u4F8B:")}
3905
- mihomo start # \u542F\u52A8/\u91CD\u542F Mixed \u6A21\u5F0F
3906
- mihomo start tun # \u5207\u6362\u5230 TUN \u900F\u660E\u4EE3\u7406\u6A21\u5F0F
3907
- mihomo sub add <url> # \u6DFB\u52A0\u8BA2\u9605 (sub \u662F subscription \u522B\u540D)
3908
- mihomo ui # \u6253\u5F00 Web UI
3909
-
3910
- ${colors.cyan("\u6A21\u5F0F\u8BF4\u660E:")}
3911
- mixed HTTP + SOCKS5 \u6DF7\u5408\u7AEF\u53E3 (\u9ED8\u8BA4)
3912
- tun \u900F\u660E\u4EE3\u7406\uFF0C\u5168\u5C40\u81EA\u52A8\u8DEF\u7531\uFF0C\u9700\u8981 sudo
3913
-
3914
- ${colors.cyan("\u6570\u636E\u76EE\u5F55:")}
3915
- \u73AF\u5883\u53D8\u91CF MIHOMO_CLI_DIR \u53EF\u81EA\u5B9A\u4E49\u4F4D\u7F6E
3916
- \u9ED8\u8BA4: ${USER_DATA_DIR}
3917
- `
3918
- );
4258
+ function formatProxySummary(info) {
4259
+ const parts = [];
4260
+ if (info.proxyGroups && info.proxyGroups > 0) parts.push(`${info.proxyGroups} \u7EC4`);
4261
+ parts.push(`${info.proxies || 0} \u8282\u70B9`);
4262
+ return parts.join(", ");
3919
4263
  }
3920
- function printVersion() {
3921
- const kv = getKernelVersion() || "\u672A\u5B89\u88C5";
3922
- console.log(colors.cyan(colors.bold(`mihomo-cli v${VERSION}`)));
3923
- console.log(`${colors.gray("\u5185\u6838: ")}${kv}`);
3924
- console.log(`${colors.gray("\u6570\u636E\u76EE\u5F55: ")}${USER_DATA_DIR}`);
4264
+ function getActiveSubscription() {
4265
+ const subs = getSubscriptions();
4266
+ if (subs.length === 0) return null;
4267
+ const settings = readSettings();
4268
+ const activeName = settings.active_subscription;
4269
+ if (activeName) {
4270
+ const found = subs.find((s) => s.name === activeName);
4271
+ if (found) return found;
4272
+ }
4273
+ return subs[0];
3925
4274
  }
3926
-
3927
- // src/kernel.ts
3928
- import { execSync as execSync4, spawnSync } from "child_process";
3929
- import fs6 from "fs";
3930
- import path4 from "path";
3931
-
3932
- // node_modules/compare-versions/lib/esm/utils.js
3933
- var semver = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
3934
- var validateAndParse = (version) => {
3935
- if (typeof version !== "string") {
3936
- throw new TypeError("Invalid argument expected string");
4275
+ function findSubscriptionFuzzy(subs, pattern) {
4276
+ const lowerPattern = pattern.toLowerCase();
4277
+ const exact = [];
4278
+ const prefix = [];
4279
+ const includes = [];
4280
+ for (const s of subs) {
4281
+ const name = s.name.toLowerCase();
4282
+ if (name === lowerPattern) {
4283
+ exact.push(s);
4284
+ } else if (name.startsWith(lowerPattern)) {
4285
+ prefix.push(s);
4286
+ } else if (name.includes(lowerPattern)) {
4287
+ includes.push(s);
4288
+ }
3937
4289
  }
3938
- const match = version.match(semver);
3939
- if (!match) {
3940
- throw new Error(`Invalid argument not valid semver ('${version}' received)`);
4290
+ if (exact.length > 0) return exact;
4291
+ if (prefix.length > 0) return prefix;
4292
+ return includes;
4293
+ }
4294
+ function pickSingleSubscription(subs, pattern) {
4295
+ if (subs.length === 0) {
4296
+ console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u5339\u914D "${pattern}" \u7684\u8BA2\u9605`);
4297
+ process.exit(1);
3941
4298
  }
3942
- match.shift();
3943
- return match;
3944
- };
3945
- var isWildcard = (s) => s === "*" || s === "x" || s === "X";
3946
- var tryParse = (v) => {
3947
- const n = parseInt(v, 10);
3948
- return isNaN(n) ? v : n;
3949
- };
3950
- var forceType = (a, b) => typeof a !== typeof b ? [String(a), String(b)] : [a, b];
3951
- var compareStrings = (a, b) => {
3952
- if (isWildcard(a) || isWildcard(b))
3953
- return 0;
3954
- const [ap, bp] = forceType(tryParse(a), tryParse(b));
3955
- if (ap > bp)
3956
- return 1;
3957
- if (ap < bp)
3958
- return -1;
3959
- return 0;
3960
- };
3961
- var compareSegments = (a, b) => {
3962
- for (let i = 0; i < Math.max(a.length, b.length); i++) {
3963
- const r = compareStrings(a[i] || "0", b[i] || "0");
3964
- if (r !== 0)
3965
- return r;
4299
+ if (subs.length === 1) return subs[0];
4300
+ console.error("\u9519\u8BEF: \u5339\u914D\u5230\u591A\u4E2A\u8BA2\u9605\uFF0C\u8BF7\u66F4\u7CBE\u786E\u6307\u5B9A");
4301
+ console.log("\n\u5339\u914D\u7684\u8BA2\u9605:");
4302
+ for (const s of subs) console.log(` ${s.name}`);
4303
+ process.exit(1);
4304
+ }
4305
+ async function downloadSubscription(url, subName = "default") {
4306
+ let response;
4307
+ try {
4308
+ response = await HTTP_CLIENT.get(url, { responseType: "text" });
4309
+ } catch (e) {
4310
+ const maskedUrl = maskUrl(url);
4311
+ let errorMsg = `\u83B7\u53D6\u8BA2\u9605\u5931\u8D25: ${e.message}`;
4312
+ const err = e;
4313
+ if (err.response) {
4314
+ errorMsg += ` (HTTP ${err.response.status})`;
4315
+ }
4316
+ errorMsg += `
4317
+ URL: ${maskedUrl}`;
4318
+ throw new Error(errorMsg);
3966
4319
  }
3967
- return 0;
3968
- };
3969
-
3970
- // node_modules/compare-versions/lib/esm/compareVersions.js
3971
- var compareVersions = (v1, v2) => {
3972
- const n1 = validateAndParse(v1);
3973
- const n2 = validateAndParse(v2);
3974
- const p1 = n1.pop();
3975
- const p2 = n2.pop();
3976
- const r = compareSegments(n1, n2);
3977
- if (r !== 0)
3978
- return r;
3979
- if (p1 && p2) {
3980
- return compareSegments(p1.split("."), p2.split("."));
3981
- } else if (p1 || p2) {
3982
- return p1 ? -1 : 1;
4320
+ const content = response.data;
4321
+ if (!content?.trim()) {
4322
+ throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
3983
4323
  }
3984
- return 0;
3985
- };
3986
-
3987
- // src/kernel.ts
3988
- var GITHUB_REPO = "MetaCubeX/mihomo";
3989
- var KERNEL_HTTP_TIMEOUT = 12e4;
3990
- var KERNEL_DOWNLOAD_TIMEOUT = 18e4;
3991
- var HTTP_CLIENT = createHttpClient({ timeout: KERNEL_HTTP_TIMEOUT });
3992
- function withMirror(url, mirror) {
3993
- if (mirror && (url.startsWith("https://github.com/") || url.startsWith("https://api.github.com/"))) {
3994
- return mirror + url;
4324
+ const parsed = parseYamlOrJson(content, "\u8BA2\u9605\u5185\u5BB9");
4325
+ if (!parsed) throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
4326
+ saveSubscriptionRawConfig(subName, content);
4327
+ const headers = response.headers;
4328
+ const userInfo = parseUserInfo(headers.get("subscription-userinfo"));
4329
+ const updateIntervalHeader = headers.get("profile-update-interval");
4330
+ const updateInterval = updateIntervalHeader ? parseInt(updateIntervalHeader, 10) : null;
4331
+ const webPageUrl = headers.get("profile-web-page-url") || null;
4332
+ const username = parseUsernameFromContentDisposition(headers.get("content-disposition"));
4333
+ const cacheData = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
4334
+ if (userInfo) {
4335
+ cacheData.upload = userInfo.upload;
4336
+ cacheData.download = userInfo.download;
4337
+ cacheData.total = userInfo.total;
4338
+ cacheData.expire = userInfo.expire;
3995
4339
  }
3996
- return url;
3997
- }
3998
- function getArch() {
3999
- const arch = process.arch;
4000
- if (arch === "arm64") return "arm64";
4001
- if (arch === "x64") return "amd64";
4002
- return arch;
4340
+ if (updateInterval) cacheData.update_interval = updateInterval;
4341
+ if (webPageUrl) cacheData.web_page_url = webPageUrl;
4342
+ if (username) cacheData.username = username;
4343
+ saveSubscriptionCache(subName, cacheData);
4344
+ const proxies = parsed.proxies;
4345
+ const proxyGroups = parsed["proxy-groups"];
4346
+ return {
4347
+ proxies: proxies ? proxies.length : 0,
4348
+ proxyGroups: proxyGroups ? proxyGroups.length : 0,
4349
+ userInfo,
4350
+ updateInterval,
4351
+ webPageUrl,
4352
+ username
4353
+ };
4003
4354
  }
4004
- function findMatchingAsset(assets, platform, arch) {
4005
- const prefix = `mihomo-${platform}-${arch}`;
4006
- const matchingAssets = assets.filter(
4007
- (a) => a.name.startsWith(prefix) && a.name.endsWith(".gz") || a.name.startsWith(`${prefix}-`) && a.name.endsWith(".gz")
4355
+ async function downloadMergedSubscription(urls, subName) {
4356
+ const responses = await Promise.all(
4357
+ urls.map(async (url, index) => {
4358
+ try {
4359
+ const response = await HTTP_CLIENT.get(url, { responseType: "text" });
4360
+ return { url, index, response, error: null };
4361
+ } catch (e) {
4362
+ return { url, index, response: null, error: e };
4363
+ }
4364
+ })
4008
4365
  );
4009
- if (matchingAssets.length === 0) return null;
4010
- if (matchingAssets.length === 1) return matchingAssets[0];
4011
- const standardAsset = matchingAssets.find((a) => {
4012
- const nameWithoutGz = a.name.slice(0, -3);
4013
- const parts = nameWithoutGz.split("-");
4014
- const lastPart = parts[parts.length - 1];
4015
- return /^v?\d+\.\d+\.\d+/.test(lastPart) && !nameWithoutGz.includes("-go");
4366
+ for (const r of responses) {
4367
+ if (r.error) {
4368
+ const maskedUrl = maskUrl(r.url);
4369
+ throw new Error(`\u5408\u5E76\u8BA2\u9605\u7B2C ${r.index + 1} \u4E2A URL \u83B7\u53D6\u5931\u8D25: ${r.error.message}
4370
+ URL: ${maskedUrl}`);
4371
+ }
4372
+ }
4373
+ const parsed = responses.map((r, i) => {
4374
+ const content = r.response?.data;
4375
+ if (!content?.trim()) throw new Error(`\u5408\u5E76\u8BA2\u9605\u7B2C ${i + 1} \u4E2A URL \u5185\u5BB9\u4E3A\u7A7A`);
4376
+ return parseYamlOrJson(content, `\u5408\u5E76\u8BA2\u9605\u7B2C ${i + 1} \u4E2A`);
4016
4377
  });
4017
- return standardAsset || matchingAssets[0];
4018
- }
4019
- async function getLatestRelease(repo, mirror) {
4020
- const url = withMirror(`https://api.github.com/repos/${repo}/releases`, mirror);
4021
- const response = await HTTP_CLIENT.get(url, { responseType: "json" });
4022
- const releases = response.data;
4023
- if (!Array.isArray(releases) || releases.length === 0) {
4024
- throw new Error("\u65E0\u6CD5\u83B7\u53D6\u7248\u672C\u4FE1\u606F");
4378
+ const base = parsed[0];
4379
+ const baseProxies = base.proxies || [];
4380
+ const seenNames = new Set(baseProxies.map((p) => p.name));
4381
+ for (let i = 1; i < parsed.length; i++) {
4382
+ const extraProxies = parsed[i].proxies || [];
4383
+ for (const proxy of extraProxies) {
4384
+ if (!seenNames.has(proxy.name)) {
4385
+ baseProxies.push(proxy);
4386
+ seenNames.add(proxy.name);
4387
+ }
4388
+ }
4025
4389
  }
4026
- const stableReleases = releases.filter(
4027
- (r) => !r.prerelease && !r.tag_name.toLowerCase().includes("alpha") && !r.tag_name.toLowerCase().includes("beta") && !r.tag_name.toLowerCase().includes("prerelease")
4028
- );
4029
- return stableReleases.length > 0 ? stableReleases[0] : releases[0];
4390
+ base.proxies = baseProxies;
4391
+ const mergedContent = jsYaml.dump(base, YAML_DUMP_OPTS);
4392
+ saveSubscriptionRawConfig(subName, mergedContent);
4393
+ const firstHeaders = responses[0].response?.headers;
4394
+ const userInfo = parseUserInfo(firstHeaders?.get("subscription-userinfo") ?? null);
4395
+ const updateIntervalHeader = firstHeaders?.get("profile-update-interval");
4396
+ const updateInterval = updateIntervalHeader ? parseInt(updateIntervalHeader, 10) : null;
4397
+ const webPageUrl = firstHeaders?.get("profile-web-page-url") || null;
4398
+ const username = parseUsernameFromContentDisposition(firstHeaders?.get("content-disposition") ?? null);
4399
+ const cacheData = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
4400
+ if (userInfo) {
4401
+ cacheData.upload = userInfo.upload;
4402
+ cacheData.download = userInfo.download;
4403
+ cacheData.total = userInfo.total;
4404
+ cacheData.expire = userInfo.expire;
4405
+ }
4406
+ if (updateInterval) cacheData.update_interval = updateInterval;
4407
+ if (webPageUrl) cacheData.web_page_url = webPageUrl;
4408
+ if (username) cacheData.username = username;
4409
+ saveSubscriptionCache(subName, cacheData);
4410
+ const proxyGroups = base["proxy-groups"];
4411
+ return {
4412
+ proxies: baseProxies.length,
4413
+ proxyGroups: proxyGroups ? proxyGroups.length : 0,
4414
+ userInfo,
4415
+ updateInterval,
4416
+ webPageUrl,
4417
+ username
4418
+ };
4030
4419
  }
4031
- async function checkUpdate(mirror) {
4032
- const currentVersion = getKernelVersion();
4033
- const latest = await getLatestRelease(GITHUB_REPO, mirror);
4034
- const latestVersion = latest.tag_name;
4035
- let needsUpdate = false;
4036
- const currentDisplay = currentVersion || "\u672A\u5B89\u88C5";
4037
- if (!currentVersion) {
4038
- needsUpdate = true;
4039
- } else {
4040
- try {
4041
- needsUpdate = compareVersions(latestVersion.replace(/^v/, ""), currentVersion.replace(/^v/, "")) > 0;
4042
- } catch {
4043
- needsUpdate = latestVersion !== currentVersion;
4044
- }
4420
+ function prepareConfigForStart(mode, subName = "default") {
4421
+ const rawContent = readSubscriptionRawConfig(subName);
4422
+ if (!rawContent) {
4423
+ throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605`);
4045
4424
  }
4425
+ const buildResult = buildConfig(rawContent, mode);
4426
+ writeMihomoConfig(buildResult.config);
4427
+ writeDebugConfig(buildResult);
4428
+ const proxies = buildResult.config.proxies;
4429
+ const proxyGroups = buildResult.config["proxy-groups"];
4046
4430
  return {
4047
- current: currentDisplay,
4048
- latest: latestVersion,
4049
- needsUpdate,
4050
- assets: latest.assets,
4051
- release: latest
4431
+ proxies: proxies ? proxies.length : 0,
4432
+ proxyGroups: proxyGroups ? proxyGroups.length : 0
4052
4433
  };
4053
4434
  }
4054
- function findBinaryInDir(dir) {
4055
- const files = fs6.readdirSync(dir);
4056
- for (const f of files) {
4057
- const fullPath = path4.join(dir, f);
4058
- const stat = fs6.statSync(fullPath);
4059
- if (stat.isDirectory()) {
4060
- const found = findBinaryInDir(fullPath);
4061
- if (found) return found;
4062
- continue;
4435
+ function needsAutoUpdate(sub) {
4436
+ if (!sub.updated_at) return true;
4437
+ const lastUpdate = new Date(sub.updated_at).getTime();
4438
+ if (Number.isNaN(lastUpdate)) return true;
4439
+ const intervalHours = sub.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
4440
+ const intervalMs = intervalHours * 60 * 60 * 1e3;
4441
+ return Date.now() - lastUpdate > intervalMs;
4442
+ }
4443
+ async function tryUpdateOne(sub) {
4444
+ try {
4445
+ let info;
4446
+ if (isMultiUrl(sub.url)) {
4447
+ info = await downloadMergedSubscription(splitUrls(sub.url), sub.name);
4448
+ } else {
4449
+ info = await downloadSubscription(sub.url, sub.name);
4063
4450
  }
4064
- if (f === "mihomo") return fullPath;
4065
- if (f.includes("mihomo") && !f.endsWith(".gz")) return fullPath;
4451
+ return { name: sub.name, success: true, proxies: info.proxies, proxyGroups: info.proxyGroups };
4452
+ } catch (e) {
4453
+ return { name: sub.name, success: false, error: e.message };
4066
4454
  }
4067
- return null;
4068
4455
  }
4069
- async function downloadKernel(progressCallback, mirror, releaseInfo) {
4070
- ensureDirs();
4071
- const latest = releaseInfo || await getLatestRelease(GITHUB_REPO, mirror);
4072
- const arch = getArch();
4073
- const platform = process.platform;
4074
- const asset = findMatchingAsset(latest.assets, platform, arch);
4075
- if (!asset) {
4076
- const available = latest.assets.map((a) => a.name).join(", ");
4077
- let hint = "";
4078
- if (available) hint = `
4079
- \u53EF\u7528\u7248\u672C: ${available}`;
4080
- throw new Error(`\u672A\u627E\u5230\u5339\u914D\u7684\u5185\u6838\u6587\u4EF6
4081
- \u5E73\u53F0: ${platform}, \u67B6\u6784: ${arch}${hint}`);
4082
- }
4083
- const downloadUrl = withMirror(asset.browser_download_url, mirror);
4084
- const tempPath = path4.join(DIRS.kernel, asset.name);
4085
- const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
4086
- if (progressCallback) {
4087
- progressCallback(`\u4E0B\u8F7D\u5185\u6838: ${asset.name} (${sizeMB} MB)`);
4088
- }
4089
- const curlResult = spawnSync(
4090
- "curl",
4091
- ["-L", "--progress-bar", "--connect-timeout", "30", "--max-time", String(Math.floor(KERNEL_DOWNLOAD_TIMEOUT / 1e3)), "-o", tempPath, downloadUrl],
4092
- { stdio: "inherit" }
4093
- );
4094
- if (curlResult.status !== 0) {
4095
- try {
4096
- fs6.unlinkSync(tempPath);
4097
- } catch {
4098
- }
4099
- throw new Error(`\u4E0B\u8F7D\u5931\u8D25 (curl \u9000\u51FA\u7801 ${curlResult.status})`);
4456
+ async function autoUpdateStaleSubscription() {
4457
+ const allSubs = getSubscriptionsWithCache();
4458
+ const staleSubs = allSubs.filter(needsAutoUpdate);
4459
+ if (staleSubs.length === 0) {
4460
+ return { total: 0, updated: 0, failed: 0 };
4100
4461
  }
4101
- if (!fs6.existsSync(tempPath)) {
4102
- throw new Error("\u4E0B\u8F7D\u5931\u8D25: \u6587\u4EF6\u672A\u751F\u6210");
4462
+ if (staleSubs.length === 1) {
4463
+ const sub = staleSubs[0];
4464
+ const interval = sub.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
4465
+ console.log(`\u8BA2\u9605 "${sub.name}" \u8D85\u8FC7 ${interval} \u5C0F\u65F6\u672A\u66F4\u65B0\uFF0C\u6B63\u5728\u66F4\u65B0...`);
4466
+ } else {
4467
+ console.log(`\u68C0\u67E5\u5230 ${staleSubs.length} \u4E2A\u8BA2\u9605\u9700\u8981\u66F4\u65B0\uFF0C\u6B63\u5728\u5E76\u884C\u66F4\u65B0...`);
4103
4468
  }
4104
- if (progressCallback) {
4105
- progressCallback("\u89E3\u538B\u5185\u6838...");
4469
+ const results = await Promise.all(staleSubs.map(tryUpdateOne));
4470
+ let updatedCount = 0;
4471
+ for (const r of results) {
4472
+ if (r.success) {
4473
+ updatedCount++;
4474
+ console.log(`${colors.green("\u2713")} ${r.name}: ${colors.green("\u5DF2\u66F4\u65B0")} (${formatProxySummary(r)})`);
4475
+ } else {
4476
+ console.log(`${colors.red("\u2717")} ${r.name}: ${colors.red("\u5931\u8D25")} (${(r.error || "").split("\n")[0]})`);
4477
+ }
4106
4478
  }
4107
- const extractPath = DIRS.kernel;
4108
- let extractedBinary = null;
4479
+ return { total: staleSubs.length, updated: updatedCount, failed: staleSubs.length - updatedCount };
4480
+ }
4481
+ var API_BASE = `http://${BASE_CONFIG["external-controller"]}`;
4482
+ var DEFAULT_TEST_URL = "http://www.gstatic.com/generate_204";
4483
+ async function testProxyDelay(proxyName, timeout, testUrl, client) {
4484
+ const encodedName = encodeURIComponent(proxyName);
4485
+ const url = `${API_BASE}/proxies/${encodedName}/delay?timeout=${timeout}&url=${encodeURIComponent(testUrl)}`;
4109
4486
  try {
4110
- if (tempPath.endsWith(".tar.gz") || tempPath.endsWith(".tgz")) {
4111
- execSync4(`tar -xzf "${tempPath}" -C "${extractPath}"`, { stdio: ["pipe", "pipe", "inherit"] });
4112
- } else if (tempPath.endsWith(".gz")) {
4113
- const baseName = path4.basename(tempPath, ".gz");
4114
- const outputPath = path4.join(extractPath, baseName);
4115
- execSync4(`gzip -dc "${tempPath}" > "${outputPath}"`, { stdio: ["pipe", "pipe", "inherit"] });
4116
- extractedBinary = outputPath;
4487
+ const response = await client.get(url);
4488
+ const data = JSON.parse(response.data);
4489
+ if (data.delay && data.delay > 0) {
4490
+ return { name: proxyName, delay: data.delay };
4117
4491
  }
4492
+ return { name: proxyName, delay: null, error: data.message || "no delay" };
4118
4493
  } catch (e) {
4119
- try {
4120
- fs6.unlinkSync(tempPath);
4121
- } catch {
4494
+ const err = e;
4495
+ let errorMsg = "timeout";
4496
+ if (err.response?.data?.message) {
4497
+ errorMsg = String(err.response.data.message);
4498
+ } else if (err.message) {
4499
+ errorMsg = err.message;
4122
4500
  }
4123
- throw new Error(`\u89E3\u538B\u5931\u8D25: ${e.message}`);
4501
+ return { name: proxyName, delay: null, error: errorMsg };
4124
4502
  }
4125
- const foundBinary = extractedBinary || findBinaryInDir(extractPath);
4126
- if (!foundBinary) {
4127
- try {
4128
- fs6.unlinkSync(tempPath);
4129
- } catch {
4130
- }
4131
- throw new Error("\u89E3\u538B\u540E\u672A\u627E\u5230\u53EF\u6267\u884C\u6587\u4EF6");
4503
+ }
4504
+ async function testSubscriptionProxies(subName, options = {}) {
4505
+ const { timeout = 2e3, concurrency = 100, testUrl = DEFAULT_TEST_URL, onResult } = options;
4506
+ const { proxies } = options.parsed || loadSubscriptionConfig(subName);
4507
+ if (proxies.length === 0) {
4508
+ return { total: 0, alive: 0, dead: 0, results: [] };
4132
4509
  }
4133
- const targetPath = PATHS.mihomoBinary;
4134
- if (foundBinary !== targetPath) {
4135
- if (fs6.existsSync(targetPath)) {
4136
- fs6.chmodSync(targetPath, 493);
4137
- try {
4138
- fs6.unlinkSync(targetPath);
4139
- } catch {
4140
- }
4510
+ const client = createHttpClient({ timeout: timeout + 3e3 });
4511
+ const results = [];
4512
+ let completedCount = 0;
4513
+ for (let i = 0; i < proxies.length; i += concurrency) {
4514
+ const batch = proxies.slice(i, i + concurrency);
4515
+ const batchResults = await Promise.all(batch.map((proxy) => testProxyDelay(proxy.name, timeout, testUrl, client)));
4516
+ for (const result of batchResults) {
4517
+ results.push(result);
4518
+ onResult?.(result, completedCount, proxies.length);
4519
+ completedCount++;
4141
4520
  }
4142
- fs6.renameSync(foundBinary, targetPath);
4143
- }
4144
- fs6.chmodSync(targetPath, 493);
4145
- try {
4146
- fs6.unlinkSync(tempPath);
4147
- } catch {
4148
4521
  }
4149
- clearKernelVersionCache();
4150
- return { version: latest.tag_name, path: targetPath };
4522
+ const alive = results.filter((r) => r.delay !== null).length;
4523
+ return { total: results.length, alive, dead: results.length - alive, results };
4151
4524
  }
4152
-
4153
- // src/commands/kernel.ts
4154
- async function cmdKernel(args) {
4155
- const mirrorInfo = parseMirrorArg(args);
4156
- const effectiveMirror = mirrorInfo.mirror;
4157
- if (effectiveMirror) {
4158
- const mirrorDesc = mirrorInfo.type === "all" ? " (API\u548C\u4E0B\u8F7D\u5747\u4F7F\u7528\u955C\u50CF)" : " (\u4E0B\u8F7D\u65F6\u4F7F\u7528\u955C\u50CF)";
4159
- console.log(`\u955C\u50CF: ${effectiveMirror}${mirrorDesc}`);
4525
+ function normalizeProxyNamesBeforeSave(parsed) {
4526
+ const { proxies, proxyGroups } = parsed;
4527
+ const renameMap = /* @__PURE__ */ new Map();
4528
+ const usedNames = /* @__PURE__ */ new Set();
4529
+ for (const proxy of proxies) {
4530
+ const shortened = proxy.name.replace(/_github\.com\/[^_]+/, "");
4531
+ if (shortened !== proxy.name && !usedNames.has(shortened)) {
4532
+ renameMap.set(proxy.name, shortened);
4533
+ usedNames.add(shortened);
4534
+ } else {
4535
+ usedNames.add(proxy.name);
4536
+ }
4160
4537
  }
4161
- console.log("\n\u63D0\u793A: \u5982\u679C\u4E0B\u8F7D\u901F\u5EA6\u8FC7\u6162\u6216\u76F4\u8FDE\u5931\u8D25\uFF0C\u53EF\u4F7F\u7528 --mirror \u53C2\u6570\u901A\u8FC7\u955C\u50CF\u4E0B\u8F7D");
4162
- console.log("\n\u7528\u6CD5:");
4163
- console.log(" mihomo kernel # \u76F4\u8FDE");
4164
- console.log(" mihomo kernel --mirror # \u4E0B\u8F7D\u4F7F\u7528\u9ED8\u8BA4\u955C\u50CF (v6.gh-proxy.org)");
4165
- console.log(" mihomo kernel --mirror hk.gh-proxy.org # \u4E0B\u8F7D\u4F7F\u7528\u6307\u5B9A\u955C\u50CF");
4166
- console.log(" mihomo kernel --mirror-all # API\u8BF7\u6C42\u548C\u4E0B\u8F7D\u90FD\u4F7F\u7528\u9ED8\u8BA4\u955C\u50CF");
4167
- console.log(" mihomo kernel --mirror-all hk.gh-proxy.org # API\u548C\u4E0B\u8F7D\u90FD\u4F7F\u7528\u6307\u5B9A\u955C\u50CF");
4168
- console.log("\n\u53EF\u7528\u955C\u50CF:");
4169
- for (const m of AVAILABLE_MIRRORS) {
4170
- const isCurrent = effectiveMirror && (effectiveMirror.includes(`//${m}/`) || effectiveMirror.includes(`//${m}:`) || effectiveMirror.endsWith(`//${m}`));
4171
- console.log(` ${m}${isCurrent ? " (\u5F53\u524D)" : ""}`);
4538
+ if (renameMap.size === 0) return 0;
4539
+ for (const proxy of proxies) {
4540
+ const newName = renameMap.get(proxy.name);
4541
+ if (newName) proxy.name = newName;
4172
4542
  }
4173
- console.log("");
4174
- console.log("\u68C0\u67E5\u5185\u6838\u66F4\u65B0...");
4175
- try {
4176
- const apiMirror = mirrorInfo.type === "all" ? effectiveMirror : null;
4177
- const info = await checkUpdate(apiMirror);
4178
- console.log(`\u5F53\u524D: ${info.current}`);
4179
- console.log(`\u6700\u65B0: ${info.latest}`);
4180
- if (!info.needsUpdate) {
4181
- console.log("\u5DF2\u662F\u6700\u65B0\u7248\u672C");
4182
- } else {
4183
- console.log("\n\u6B63\u5728\u4E0B\u8F7D...");
4184
- const result = await downloadKernel((msg) => console.log(msg), mirrorInfo.mirror, info.release);
4185
- console.log(`
4186
- \u5DF2\u66F4\u65B0\u5230 ${result.version}`);
4543
+ for (const group of proxyGroups) {
4544
+ if (Array.isArray(group.proxies)) {
4545
+ group.proxies = group.proxies.map((name) => renameMap.get(name) || name);
4187
4546
  }
4188
- } catch (e) {
4189
- console.error(`
4190
- \u66F4\u65B0\u5931\u8D25: ${e.message}`);
4191
- const err = e;
4192
- if (err.response?.data) {
4193
- if (err.response.data.message) {
4194
- console.error(`\u539F\u56E0: ${err.response.data.message}`);
4547
+ }
4548
+ return renameMap.size;
4549
+ }
4550
+ function cleanDeadProxies(parsed, deadNames) {
4551
+ const { proxies, proxyGroups } = parsed;
4552
+ const originalCount = proxies.length;
4553
+ parsed.proxies = proxies.filter((p) => !deadNames.has(p.name));
4554
+ const removedProxies = originalCount - parsed.proxies.length;
4555
+ let updatedGroups = 0;
4556
+ const removedGroupNames = /* @__PURE__ */ new Set();
4557
+ for (const group of proxyGroups) {
4558
+ if (Array.isArray(group.proxies)) {
4559
+ const before = group.proxies.length;
4560
+ group.proxies = group.proxies.filter((name) => !deadNames.has(name));
4561
+ if (group.proxies.length < before) {
4562
+ updatedGroups++;
4195
4563
  }
4196
- if (err.response.data.documentation_url) {
4197
- console.error(`\u6587\u6863: ${err.response.data.documentation_url}`);
4564
+ if (group.proxies.length === 0) {
4565
+ removedGroupNames.add(group.name);
4198
4566
  }
4199
4567
  }
4200
- process.exit(1);
4201
4568
  }
4202
- }
4203
-
4204
- // src/commands/log.ts
4205
- function cmdLog(args) {
4206
- const logPath = getLogPath();
4207
- if (hasFlag(args, "-o", "--open")) {
4208
- openLogFile(logPath);
4209
- return;
4569
+ if (removedGroupNames.size > 0) {
4570
+ parsed.proxyGroups = proxyGroups.filter((g) => !removedGroupNames.has(g.name));
4571
+ for (const group of parsed.proxyGroups) {
4572
+ if (Array.isArray(group.proxies)) {
4573
+ group.proxies = group.proxies.filter((name) => !removedGroupNames.has(name));
4574
+ }
4575
+ }
4210
4576
  }
4211
- viewLogWithTail(logPath, { follow: true, lines: 50 });
4577
+ return { removedProxies, updatedGroups, removedGroups: removedGroupNames.size };
4212
4578
  }
4213
- function cmdLogs(args) {
4214
- const targetName = getNonFlagArg(args, 1);
4215
- const lines = parseIntArg(args, "-n", "--lines", 100);
4216
- const openInViewer = hasFlag(args, "-o", "--open");
4217
- if (targetName) {
4218
- let logPath;
4219
- if (targetName === "current" || targetName === "0") {
4220
- logPath = getLogPath();
4579
+ async function autoCleanSubscription(subName, options = {}) {
4580
+ const parsed = loadSubscriptionConfig(subName);
4581
+ const summary = await testSubscriptionProxies(subName, { ...options, parsed });
4582
+ let removedProxies = 0;
4583
+ let updatedGroups = 0;
4584
+ let removedGroups = 0;
4585
+ let skipped = false;
4586
+ if (summary.dead > 0) {
4587
+ if (summary.alive === 0 || summary.alive / summary.total < 0.01) {
4588
+ skipped = true;
4221
4589
  } else {
4222
- const parsedIdx = parseInt(targetName, 10);
4223
- if (!Number.isNaN(parsedIdx) && parsedIdx > 0 && String(parsedIdx) === targetName) {
4224
- const archiveLogs = listLogs();
4225
- const archive = archiveLogs.archives[parsedIdx - 1];
4226
- if (!archive) {
4227
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`);
4228
- console.log('\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868');
4229
- process.exit(1);
4230
- }
4231
- logPath = archive.path;
4232
- } else {
4233
- logPath = getLogPathByName(targetName);
4234
- }
4235
- }
4236
- if (!logPath) {
4237
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`);
4238
- console.log('\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868');
4239
- process.exit(1);
4240
- }
4241
- if (openInViewer) {
4242
- openLogFile(logPath);
4243
- return;
4590
+ const deadNames = new Set(summary.results.filter((r) => r.delay === null).map((r) => r.name));
4591
+ const cleanResult = cleanDeadProxies(parsed, deadNames);
4592
+ removedProxies = cleanResult.removedProxies;
4593
+ updatedGroups = cleanResult.updatedGroups;
4594
+ removedGroups = cleanResult.removedGroups;
4244
4595
  }
4245
- viewLogWithTail(logPath, { follow: false, lines });
4246
- return;
4247
4596
  }
4248
- const logs = listLogs();
4249
- const all = [];
4250
- if (logs.current) all.push(logs.current);
4251
- all.push(...logs.archives);
4252
- if (all.length === 0) {
4253
- console.log("\u6682\u65E0\u65E5\u5FD7");
4254
- return;
4597
+ if (!skipped) {
4598
+ saveSubscriptionConfig(subName, parsed);
4255
4599
  }
4600
+ return { summary, removedProxies, updatedGroups, removedGroups, skipped };
4601
+ }
4602
+
4603
+ // src/commands/status.ts
4604
+ function printStatus() {
4605
+ const status = getStatus();
4606
+ const info = getConfigInfo();
4607
+ const overwriteEnabled = isOverwriteEnabled();
4608
+ const overwriteFiles = listOverwriteFile().files;
4609
+ const activeSub = getActiveSubscription();
4256
4610
  console.log("");
4257
- console.log("\u65E5\u5FD7\u5217\u8868:");
4258
- console.log("");
4259
- let archiveCounter = 0;
4260
- for (const log of all) {
4261
- let num;
4262
- if (log.isCurrent) {
4263
- num = " 0";
4611
+ let modeLabel = "";
4612
+ if (info && status.running) {
4613
+ modeLabel = colors.cyan(info.tun ? " (TUN)" : " (Mixed)");
4614
+ }
4615
+ const statusText = status.running ? colors.green("\u25CF \u8FD0\u884C\u4E2D") : colors.yellow("\u4E0D\u5728\u8FD0\u884C");
4616
+ console.log(`${colors.gray("\u72B6\u6001: ")}${statusText}${modeLabel}`);
4617
+ console.log(`${colors.gray("\u5185\u6838: ")}${status.kernelVersion || "\u672A\u5B89\u88C5"}`);
4618
+ if (status.pid) {
4619
+ console.log(`${colors.gray("PID: ")}${status.pid}`);
4620
+ if (status.processInfo) {
4621
+ console.log(`${colors.gray("\u5185\u5B58: ")}${status.processInfo.memory}`);
4622
+ }
4623
+ }
4624
+ if (info) {
4625
+ if (info.mixedPort) {
4626
+ console.log(`${colors.gray("\u7AEF\u53E3: ")}${info.mixedPort}`);
4264
4627
  } else {
4265
- archiveCounter++;
4266
- num = archiveCounter < 10 ? ` ${archiveCounter}` : `${archiveCounter}`;
4628
+ const ports = [];
4629
+ if (info.httpPort) ports.push(`HTTP:${info.httpPort}`);
4630
+ if (info.socksPort) ports.push(`SOCKS:${info.socksPort}`);
4631
+ console.log(`${colors.gray("\u7AEF\u53E3: ")}${ports.length > 0 ? ports.join(", ") : "\u672A\u77E5"}`);
4267
4632
  }
4268
- const time = formatDate(log.mtime);
4269
- const size = formatBytes(log.size);
4270
- const name = log.isCurrent ? "mihomo.log (\u5F53\u524D\u8FD0\u884C\u4E2D)" : log.name;
4271
- console.log(` ${num}. ${name}`);
4272
- console.log(` \u65F6\u95F4: ${time} \u5927\u5C0F: ${size}`);
4273
- if (!log.isCurrent) {
4274
- console.log(` \u67E5\u770B: mihomo logs ${archiveCounter} \u6216 mihomo logs ${archiveCounter} -o`);
4633
+ }
4634
+ if (activeSub) {
4635
+ let subLine = `${colors.gray("\u8BA2\u9605: ")}${activeSub.name}`;
4636
+ if (info) {
4637
+ subLine += ` (${formatProxySummary(info)})`;
4275
4638
  }
4276
- console.log("");
4639
+ console.log(subLine);
4640
+ } else {
4641
+ console.log(`${colors.gray("\u8BA2\u9605: ")}\u672A\u914D\u7F6E`);
4642
+ }
4643
+ if (overwriteEnabled && overwriteFiles.length > 0) {
4644
+ const names = overwriteFiles.map((f) => f.name.replace(/^overwrite\.?/, "").replace(/\.ya?ml$/, "") || "\u4E3B\u6587\u4EF6").join(", ");
4645
+ console.log(`${colors.gray("\u8986\u5199: ")}${colors.green("\u5DF2\u542F\u7528")} (${names})`);
4646
+ } else if (overwriteEnabled) {
4647
+ console.log(`${colors.gray("\u8986\u5199: ")}${colors.green("\u5DF2\u542F\u7528")} (\u65E0\u6587\u4EF6)`);
4648
+ } else {
4649
+ console.log(`${colors.gray("\u8986\u5199: ")}${colors.yellow("\u5DF2\u7981\u7528")}`);
4277
4650
  }
4278
- console.log("\u7528\u6CD5:");
4279
- console.log(" mihomo logs 0 # \u67E5\u770B\u5F53\u524D\u65E5\u5FD7 (\u6700\u540E 100 \u884C)");
4280
- console.log(" mihomo logs 1 # \u67E5\u770B\u7B2C 1 \u4E2A\u5F52\u6863\u65E5\u5FD7\uFF08\u6700\u65B0\uFF09");
4281
- console.log(" mihomo logs 1 -n 200 # \u67E5\u770B 200 \u884C");
4282
- console.log(" mihomo logs 1 -o # \u7528\u7CFB\u7EDF\u9ED8\u8BA4\u7A0B\u5E8F\u6253\u5F00");
4283
4651
  console.log("");
4284
4652
  }
4285
4653
 
4286
- // src/commands/overwrite.ts
4287
- import path5 from "path";
4288
-
4289
- // src/subscription.ts
4290
- var DEFAULT_UPDATE_INTERVAL_HOURS = 12;
4291
- var YAML_DUMP_OPTS = { indent: 2, lineWidth: -1, noCompatMode: true };
4292
- var HTTP_CLIENT2 = createHttpClient({ timeout: 6e4 });
4293
- function loadSubscriptionConfig(subName) {
4294
- const rawContent = readSubscriptionRawConfig(subName);
4295
- if (!rawContent) {
4296
- throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
4654
+ // src/commands/start.ts
4655
+ var AUTO_CLEAN_THRESHOLD = 100;
4656
+ var AUTO_CLEAN_THRESHOLD_FREE = 50;
4657
+ function handleStopResult(result) {
4658
+ if (result.remaining && result.remaining.length > 0) {
4659
+ console.error(`${colors.red("\u90E8\u5206\u8FDB\u7A0B\u672A\u7EC8\u6B62:")} ${result.remaining.join(", ")}`);
4660
+ console.error("\u8BF7\u624B\u52A8\u8FD0\u884C: sudo pkill -9 mihomo");
4661
+ process.exit(1);
4297
4662
  }
4298
- const raw = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
4299
- return {
4300
- raw,
4301
- proxies: raw.proxies || [],
4302
- proxyGroups: raw["proxy-groups"] || []
4303
- };
4304
- }
4305
- function saveSubscriptionConfig(subName, parsed) {
4306
- normalizeProxyNamesBeforeSave(parsed);
4307
- parsed.raw.proxies = parsed.proxies;
4308
- parsed.raw["proxy-groups"] = parsed.proxyGroups;
4309
- saveSubscriptionRawConfig(subName, jsYaml.dump(parsed.raw, YAML_DUMP_OPTS));
4310
4663
  }
4311
- function parseUserInfo(header) {
4312
- if (!header) return null;
4313
- const info = {};
4314
- const parts = header.split(";").map((p) => p.trim());
4315
- for (const part of parts) {
4316
- const [key, val] = part.split("=").map((s) => s.trim());
4317
- if (key && val !== void 0) {
4318
- const numVal = parseFloat(val);
4319
- info[key] = Number.isNaN(numVal) ? 0 : numVal;
4664
+ async function cmdStart(args) {
4665
+ if (!hasKernel()) {
4666
+ console.error('\u9519\u8BEF: \u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
4667
+ process.exit(1);
4668
+ }
4669
+ const targetMode = args[1] === "tun" ? "tun" : "mixed";
4670
+ const sub = getActiveSubscription();
4671
+ if (!sub) {
4672
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
4673
+ process.exit(1);
4674
+ }
4675
+ await autoUpdateStaleSubscription();
4676
+ const status = getStatus();
4677
+ const hasProcess = status.running || status.allProcesses.length > 0;
4678
+ if (hasProcess) {
4679
+ const count = status.allProcesses.length > 0 ? status.allProcesses.length : 1;
4680
+ console.log(`\u505C\u6B62 ${count} \u4E2A\u8FDB\u7A0B...`);
4681
+ }
4682
+ handleStopResult(stop());
4683
+ if (hasProcess) {
4684
+ console.log(`${colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B")}
4685
+ `);
4686
+ }
4687
+ let configInfo;
4688
+ try {
4689
+ configInfo = prepareConfigForStart(targetMode, sub.name);
4690
+ } catch (e) {
4691
+ console.error(`${colors.red("\u914D\u7F6E\u9519\u8BEF:")} ${e.message}`);
4692
+ process.exit(1);
4693
+ }
4694
+ const modeLabel = targetMode === "tun" ? "TUN" : "Mixed";
4695
+ console.log([colors.cyan(modeLabel), sub.name, formatProxySummary(configInfo)].join(" \xB7 "));
4696
+ try {
4697
+ const result = await start(targetMode);
4698
+ console.log(`${colors.green("\u5DF2\u542F\u52A8")} (PID ${result.pid})`);
4699
+ } catch (e) {
4700
+ const msg = e.message;
4701
+ const lines = msg.split("\n");
4702
+ console.error(`${colors.red("\u542F\u52A8\u5931\u8D25:")} ${lines[0]}`);
4703
+ if (lines.length > 1) {
4704
+ for (const line of lines.slice(1)) console.error(line);
4320
4705
  }
4706
+ process.exit(1);
4321
4707
  }
4322
- return info;
4708
+ const cleanThreshold = sub.name.startsWith("free") ? AUTO_CLEAN_THRESHOLD_FREE : AUTO_CLEAN_THRESHOLD;
4709
+ if (configInfo.proxies > cleanThreshold) {
4710
+ console.log("");
4711
+ console.log(`\u8282\u70B9\u6570 ${configInfo.proxies} \u8D85\u8FC7 ${cleanThreshold}\uFF0C\u81EA\u52A8\u6E05\u7406...`);
4712
+ console.log("");
4713
+ await sleep(1e3);
4714
+ const cleanResult = await autoCleanSubscription(sub.name, { onResult: printTestResult });
4715
+ console.log("");
4716
+ console.log(formatTestSummary(cleanResult.summary));
4717
+ if (cleanResult.skipped) {
4718
+ console.log(colors.yellow("\u5B58\u6D3B\u8282\u70B9\u4E0D\u8DB3 1%\uFF0C\u8DF3\u8FC7\u6E05\u7406\u3002\u8BF7\u68C0\u67E5\u539F\u59CB\u8BA2\u9605\u662F\u5426\u6709\u6548"));
4719
+ } else if (cleanResult.removedProxies > 0) {
4720
+ console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(cleanResult)}`);
4721
+ console.log("");
4722
+ console.log("\u91CD\u65B0\u52A0\u8F7D\u914D\u7F6E...");
4723
+ handleStopResult(stop());
4724
+ try {
4725
+ configInfo = prepareConfigForStart(targetMode, sub.name);
4726
+ const result = await start(targetMode);
4727
+ console.log(`${colors.green("\u5DF2\u91CD\u542F")} (PID ${result.pid}) \xB7 ${formatProxySummary(configInfo)}`);
4728
+ } catch (e) {
4729
+ console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
4730
+ process.exit(1);
4731
+ }
4732
+ }
4733
+ }
4734
+ printStatus();
4323
4735
  }
4324
- function parseUsernameFromContentDisposition(header) {
4325
- if (!header) return null;
4326
- const match = header.match(/filename\s*=\s*["']?([^"';\s]+)["']?/i);
4327
- if (!match) return null;
4328
- const filename = match[1];
4329
- const parts = filename.split("/");
4330
- return parts[parts.length - 1] || null;
4736
+
4737
+ // src/commands/subscription.ts
4738
+ function printTestResult(result, index, total) {
4739
+ const prefix = `[${index + 1}/${total}]`;
4740
+ if (result.delay !== null) {
4741
+ const delayColor = result.delay < 300 ? colors.green : result.delay < 800 ? colors.yellow : colors.red;
4742
+ console.log(` ${prefix} ${colors.green("\u2713")} ${result.name} ${delayColor(`${result.delay}ms`)}`);
4743
+ } else {
4744
+ console.log(` ${prefix} ${colors.red("\u2717")} ${result.name} ${colors.gray(result.error || "timeout")}`);
4745
+ }
4331
4746
  }
4332
- function formatProxySummary(info) {
4333
- const parts = [];
4334
- if (info.proxyGroups && info.proxyGroups > 0) parts.push(`${info.proxyGroups} \u7EC4`);
4335
- parts.push(`${info.proxies || 0} \u8282\u70B9`);
4747
+ function formatCleanSummary(result) {
4748
+ const parts = [`\u79FB\u9664 ${result.removedProxies} \u4E2A\u8282\u70B9`];
4749
+ if (result.removedGroups > 0) parts.push(`\u5220\u9664 ${result.removedGroups} \u4E2A\u7A7A\u5206\u7EC4`);
4750
+ if (result.updatedGroups > 0) parts.push(`\u66F4\u65B0 ${result.updatedGroups} \u4E2A\u5206\u7EC4`);
4336
4751
  return parts.join(", ");
4337
4752
  }
4338
- function getActiveSubscription() {
4753
+ function formatTestSummary(summary) {
4754
+ return `\u7ED3\u679C: ${colors.green(`${summary.alive} \u5B58\u6D3B`)} / ${colors.red(`${summary.dead} \u5931\u8D25`)} / ${summary.total} \u603B\u8BA1`;
4755
+ }
4756
+ function githubRepoUrl(rawUrl) {
4757
+ const match = rawUrl.match(/raw\.githubusercontent\.com\/([^/]+\/[^/]+)/);
4758
+ if (match) return `https://github.com/${match[1]}`;
4759
+ return null;
4760
+ }
4761
+ function resolveActiveTestTarget(args) {
4339
4762
  const subs = getSubscriptions();
4340
- if (subs.length === 0) return null;
4341
- const settings = readSettings();
4342
- const activeName = settings.active_subscription;
4343
- if (activeName) {
4344
- const found = subs.find((s) => s.name === activeName);
4345
- if (found) return found;
4763
+ if (subs.length === 0) {
4764
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
4765
+ process.exit(1);
4346
4766
  }
4347
- return subs[0];
4767
+ const nameArg = getNonFlagArg(args, 2);
4768
+ const timeout = parseIntArg(args, "-t", "--timeout", 2e3);
4769
+ const concurrency = parseIntArg(args, "-j", "--concurrency", 100);
4770
+ const activeSub = getActiveSubscription();
4771
+ let target;
4772
+ if (nameArg) {
4773
+ const matches = findSubscriptionFuzzy(subs, nameArg);
4774
+ target = pickSingleSubscription(matches, nameArg);
4775
+ } else {
4776
+ if (!activeSub) {
4777
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
4778
+ process.exit(1);
4779
+ }
4780
+ target = activeSub;
4781
+ }
4782
+ const status = getStatus();
4783
+ if (!status.running) {
4784
+ console.error("\u9519\u8BEF: mihomo \u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 (mihomo start)");
4785
+ process.exit(1);
4786
+ }
4787
+ if (!activeSub || activeSub.name !== target.name) {
4788
+ console.error(`\u9519\u8BEF: \u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605\u662F "${activeSub?.name}"\uFF0C\u4E0D\u662F "${target.name}"`);
4789
+ console.log(`\u8BF7\u5148\u5207\u6362: mihomo sub use ${target.name}`);
4790
+ process.exit(1);
4791
+ }
4792
+ return { target, timeout, concurrency };
4348
4793
  }
4349
- function findSubscriptionFuzzy(subs, pattern) {
4350
- const lowerPattern = pattern.toLowerCase();
4351
- const exact = [];
4352
- const prefix = [];
4353
- const includes = [];
4354
- for (const s of subs) {
4355
- const name = s.name.toLowerCase();
4356
- if (name === lowerPattern) {
4357
- exact.push(s);
4358
- } else if (name.startsWith(lowerPattern)) {
4359
- prefix.push(s);
4360
- } else if (name.includes(lowerPattern)) {
4361
- includes.push(s);
4794
+ async function printSubscriptionList(options) {
4795
+ if (options?.autoUpdate !== false) {
4796
+ const updateResult = await autoUpdateStaleSubscription();
4797
+ if (updateResult.total > 0) console.log("");
4798
+ }
4799
+ const subs = getSubscriptionsWithCache();
4800
+ if (subs.length === 0) {
4801
+ console.log("\u6CA1\u6709\u8BA2\u9605");
4802
+ console.log("");
4803
+ console.log("\u6DFB\u52A0\u8BA2\u9605: mihomo sub add <url> [name]");
4804
+ console.log("");
4805
+ return;
4806
+ }
4807
+ const activeSub = getActiveSubscription();
4808
+ console.log(colors.cyan("\u8BA2\u9605\u5217\u8868:"));
4809
+ subs.forEach((s, i) => {
4810
+ const time = formatDate(s.updated_at);
4811
+ const defaultMark = activeSub && s.name === activeSub.name ? colors.green(" [\u4F7F\u7528\u4E2D]") : "";
4812
+ const mergeBadge = isMultiUrl(s.url) ? colors.cyan(` [\u5408\u5E76 ${splitUrls(s.url).length} \u6E90]`) : "";
4813
+ const interval = s.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
4814
+ console.log(` ${i + 1}. ${s.name}${defaultMark}${mergeBadge}`);
4815
+ console.log(` ${colors.gray("\u66F4\u65B0: ")}${time} (\u95F4\u9694: ${interval}h)`);
4816
+ if (s.username) {
4817
+ console.log(` ${colors.gray("\u7528\u6237: ")}${s.username}`);
4818
+ }
4819
+ if (s.download !== void 0 || s.total !== void 0) {
4820
+ const used = (s.upload || 0) + (s.download || 0);
4821
+ const usedStr = formatBytes(used);
4822
+ const totalStr = formatBytes(s.total);
4823
+ let percentStr = "";
4824
+ if (s.total && s.total > 0) {
4825
+ const percent = Math.min(used / s.total * 100, 100);
4826
+ percentStr = ` (${percent.toFixed(1)}%)`;
4827
+ }
4828
+ console.log(` ${colors.gray("\u6D41\u91CF: ")}${usedStr} / ${totalStr}${percentStr}`);
4829
+ }
4830
+ if (s.expire !== void 0) {
4831
+ console.log(` ${colors.gray("\u5230\u671F: ")}${formatTimestamp(s.expire)}`);
4832
+ }
4833
+ if (s.web_page_url) {
4834
+ console.log(` ${colors.gray("\u9875\u9762: ")}${s.web_page_url}`);
4835
+ }
4836
+ });
4837
+ console.log("");
4838
+ console.log("\u5207\u6362\u8BA2\u9605: mihomo sub use <name>");
4839
+ console.log("\u65B0\u589E\u8BA2\u9605: mihomo sub add <url> [name]");
4840
+ console.log("\u66F4\u65B0\u8BA2\u9605: mihomo sub update [name]");
4841
+ console.log("\u5220\u9664\u8BA2\u9605: mihomo sub remove <name>");
4842
+ console.log("\u6D4B\u8BD5\u8282\u70B9: mihomo sub test [name]");
4843
+ console.log("\u6E05\u7406\u8282\u70B9: mihomo sub clean [name]");
4844
+ console.log("\u6253\u5F00\u9875\u9762: mihomo sub web [name]");
4845
+ console.log("");
4846
+ }
4847
+ function printFreeSourceList() {
4848
+ const freeSources = getFreeSubscriptionSources();
4849
+ console.log(` 00 \u5408\u5E76 #1 + #2 (\u8282\u70B9\u66F4\u591A)`);
4850
+ for (let i = 0; i < freeSources.length; i++) {
4851
+ console.log(` ${String(i + 1).padStart(2, "0")} ${freeSources[i].name}`);
4852
+ }
4853
+ }
4854
+ async function addFreeSubscription(freeId) {
4855
+ const freeSources = getFreeSubscriptionSources();
4856
+ if (freeId === 0) {
4857
+ const sources = [freeSources[0], freeSources[1]];
4858
+ const urls = sources.map((s) => s.url);
4859
+ const mergedUrl = urls.join(",");
4860
+ const name2 = "free0";
4861
+ console.log(`\u6DFB\u52A0\u5408\u5E76\u514D\u8D39\u8BA2\u9605: ${name2} (${sources.map((s) => s.name).join(" + ")})`);
4862
+ try {
4863
+ addSubscription(mergedUrl, name2);
4864
+ setDefaultSubscription(name2);
4865
+ const info = await downloadMergedSubscription(urls, name2);
4866
+ const repoUrls = sources.map((s) => githubRepoUrl(s.url)).filter(Boolean);
4867
+ if (repoUrls.length > 0) saveSubscriptionCache(name2, { web_page_url: repoUrls.join(", ") });
4868
+ console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name2}" (${formatProxySummary(info)}, \u5408\u5E76 ${sources.length} \u6E90)`);
4869
+ } catch (e) {
4870
+ console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
4871
+ process.exit(1);
4362
4872
  }
4873
+ console.log("");
4874
+ await printSubscriptionList();
4875
+ return;
4363
4876
  }
4364
- if (exact.length > 0) return exact;
4365
- if (prefix.length > 0) return prefix;
4366
- return includes;
4367
- }
4368
- function pickSingleSubscription(subs, pattern) {
4369
- if (subs.length === 0) {
4370
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u5339\u914D "${pattern}" \u7684\u8BA2\u9605`);
4877
+ if (freeId < 1 || freeId > freeSources.length) {
4878
+ console.error(`\u9519\u8BEF: \u514D\u8D39\u8BA2\u9605 ID \u8303\u56F4 0-${freeSources.length}`);
4879
+ console.log("\n\u53EF\u7528\u6E90:");
4880
+ printFreeSourceList();
4371
4881
  process.exit(1);
4372
4882
  }
4373
- if (subs.length === 1) return subs[0];
4374
- console.error("\u9519\u8BEF: \u5339\u914D\u5230\u591A\u4E2A\u8BA2\u9605\uFF0C\u8BF7\u66F4\u7CBE\u786E\u6307\u5B9A");
4375
- console.log("\n\u5339\u914D\u7684\u8BA2\u9605:");
4376
- for (const s of subs) console.log(` ${s.name}`);
4377
- process.exit(1);
4378
- }
4379
- async function downloadSubscription(url, subName = "default") {
4380
- let response;
4883
+ const source = freeSources[freeId - 1];
4884
+ const name = `free${freeId}`;
4885
+ console.log(`\u6DFB\u52A0\u514D\u8D39\u8BA2\u9605: ${name}`);
4381
4886
  try {
4382
- response = await HTTP_CLIENT2.get(url, { responseType: "text" });
4887
+ addSubscription(source.url, name);
4888
+ setDefaultSubscription(name);
4889
+ const info = await downloadSubscription(source.url, name);
4890
+ const repoUrl = githubRepoUrl(source.url);
4891
+ if (repoUrl) saveSubscriptionCache(name, { web_page_url: repoUrl });
4892
+ console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
4383
4893
  } catch (e) {
4384
- const maskedUrl = maskUrl(url);
4385
- let errorMsg = `\u83B7\u53D6\u8BA2\u9605\u5931\u8D25: ${e.message}`;
4386
- const err = e;
4387
- if (err.response) {
4388
- errorMsg += ` (HTTP ${err.response.status})`;
4389
- }
4390
- errorMsg += `
4391
- URL: ${maskedUrl}`;
4392
- throw new Error(errorMsg);
4393
- }
4394
- const content = response.data;
4395
- if (!content?.trim()) {
4396
- throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
4397
- }
4398
- const parsed = parseYamlOrJson(content, "\u8BA2\u9605\u5185\u5BB9");
4399
- if (!parsed) throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
4400
- saveSubscriptionRawConfig(subName, content);
4401
- const headers = response.headers;
4402
- const userInfo = parseUserInfo(headers.get("subscription-userinfo"));
4403
- const updateIntervalHeader = headers.get("profile-update-interval");
4404
- const updateInterval = updateIntervalHeader ? parseInt(updateIntervalHeader, 10) : null;
4405
- const webPageUrl = headers.get("profile-web-page-url") || null;
4406
- const username = parseUsernameFromContentDisposition(headers.get("content-disposition"));
4407
- const cacheData = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
4408
- if (userInfo) {
4409
- cacheData.upload = userInfo.upload;
4410
- cacheData.download = userInfo.download;
4411
- cacheData.total = userInfo.total;
4412
- cacheData.expire = userInfo.expire;
4894
+ console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
4895
+ process.exit(1);
4413
4896
  }
4414
- if (updateInterval) cacheData.update_interval = updateInterval;
4415
- if (webPageUrl) cacheData.web_page_url = webPageUrl;
4416
- if (username) cacheData.username = username;
4417
- saveSubscriptionCache(subName, cacheData);
4418
- const proxies = parsed.proxies;
4419
- const proxyGroups = parsed["proxy-groups"];
4420
- return {
4421
- proxies: proxies ? proxies.length : 0,
4422
- proxyGroups: proxyGroups ? proxyGroups.length : 0,
4423
- userInfo,
4424
- updateInterval,
4425
- webPageUrl,
4426
- username
4427
- };
4897
+ console.log("");
4898
+ await printSubscriptionList();
4428
4899
  }
4429
- function prepareConfigForStart(mode, subName = "default") {
4430
- const rawContent = readSubscriptionRawConfig(subName);
4431
- if (!rawContent) {
4432
- throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605`);
4900
+ async function cmdSubscription(args) {
4901
+ const action = args[1];
4902
+ if (!action || action === "list") {
4903
+ await printSubscriptionList();
4904
+ return;
4433
4905
  }
4434
- const buildResult = buildConfig(rawContent, mode);
4435
- writeMihomoConfig(buildResult.config);
4436
- writeDebugConfig(buildResult);
4437
- const proxies = buildResult.config.proxies;
4438
- const proxyGroups = buildResult.config["proxy-groups"];
4439
- return {
4440
- proxies: proxies ? proxies.length : 0,
4441
- proxyGroups: proxyGroups ? proxyGroups.length : 0
4442
- };
4443
- }
4444
- function needsAutoUpdate(sub) {
4445
- if (!sub.updated_at) return true;
4446
- const lastUpdate = new Date(sub.updated_at).getTime();
4447
- if (Number.isNaN(lastUpdate)) return true;
4448
- const intervalHours = sub.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
4449
- const intervalMs = intervalHours * 60 * 60 * 1e3;
4450
- return Date.now() - lastUpdate > intervalMs;
4451
- }
4452
- async function tryUpdateOne(sub) {
4453
- try {
4454
- const info = await downloadSubscription(sub.url, sub.name);
4455
- return { name: sub.name, success: true, proxies: info.proxies, proxyGroups: info.proxyGroups };
4456
- } catch (e) {
4457
- return { name: sub.name, success: false, error: e.message };
4906
+ if (action === "free") {
4907
+ const id = parseInt(args[2], 10);
4908
+ if (Number.isNaN(id)) {
4909
+ console.log("\u7528\u6CD5: mihomo sub free <id>\n");
4910
+ console.log("\u53EF\u7528\u6E90:");
4911
+ printFreeSourceList();
4912
+ process.exit(1);
4913
+ }
4914
+ await addFreeSubscription(id);
4915
+ return;
4458
4916
  }
4459
- }
4460
- async function autoUpdateStaleSubscription() {
4461
- const allSubs = getSubscriptionsWithCache();
4462
- const staleSubs = allSubs.filter(needsAutoUpdate);
4463
- if (staleSubs.length === 0) {
4464
- return { total: 0, updated: 0, failed: 0 };
4917
+ if (action === "add") {
4918
+ const freeId = parseIntArg(args, "--free", "--free", -1);
4919
+ if (freeId > 0) {
4920
+ await addFreeSubscription(freeId);
4921
+ return;
4922
+ }
4923
+ const url = args[2];
4924
+ const name = args[3] || "default";
4925
+ if (!url) {
4926
+ console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
4927
+ process.exit(1);
4928
+ }
4929
+ if (isMultiUrl(url)) {
4930
+ const urls = splitUrls(url);
4931
+ for (const u of urls) {
4932
+ if (!u.startsWith("http")) {
4933
+ console.error(`\u9519\u8BEF: \u65E0\u6548\u7684 URL: ${u}`);
4934
+ process.exit(1);
4935
+ }
4936
+ }
4937
+ console.log(`\u6DFB\u52A0\u5408\u5E76\u8BA2\u9605: ${name} (${urls.length} \u4E2A\u6E90)`);
4938
+ try {
4939
+ addSubscription(url, name);
4940
+ setDefaultSubscription(name);
4941
+ const info = await downloadMergedSubscription(urls, name);
4942
+ console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)}, \u5408\u5E76 ${urls.length} \u6E90)`);
4943
+ } catch (e) {
4944
+ console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
4945
+ process.exit(1);
4946
+ }
4947
+ } else {
4948
+ if (!url.startsWith("http")) {
4949
+ console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
4950
+ process.exit(1);
4951
+ }
4952
+ console.log(`\u6DFB\u52A0\u8BA2\u9605: ${name}`);
4953
+ try {
4954
+ addSubscription(url, name);
4955
+ setDefaultSubscription(name);
4956
+ const info = await downloadSubscription(url, name);
4957
+ const repoUrl = githubRepoUrl(url);
4958
+ if (repoUrl) saveSubscriptionCache(name, { web_page_url: repoUrl });
4959
+ console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
4960
+ } catch (e) {
4961
+ console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
4962
+ process.exit(1);
4963
+ }
4964
+ }
4965
+ console.log("");
4966
+ await printSubscriptionList();
4967
+ return;
4465
4968
  }
4466
- if (staleSubs.length === 1) {
4467
- const sub = staleSubs[0];
4468
- const interval = sub.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
4469
- console.log(`\u8BA2\u9605 "${sub.name}" \u8D85\u8FC7 ${interval} \u5C0F\u65F6\u672A\u66F4\u65B0\uFF0C\u6B63\u5728\u66F4\u65B0...`);
4470
- } else {
4471
- console.log(`\u68C0\u67E5\u5230 ${staleSubs.length} \u4E2A\u8BA2\u9605\u9700\u8981\u66F4\u65B0\uFF0C\u6B63\u5728\u5E76\u884C\u66F4\u65B0...`);
4969
+ if (action === "update") {
4970
+ const name = args[2];
4971
+ const subs = getSubscriptions();
4972
+ if (subs.length === 0) {
4973
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
4974
+ process.exit(1);
4975
+ }
4976
+ if (!name) {
4977
+ console.log(`\u66F4\u65B0\u6240\u6709 ${subs.length} \u4E2A\u8BA2\u9605...`);
4978
+ const results = await Promise.all(subs.map(tryUpdateOne));
4979
+ let ok = 0;
4980
+ for (const r of results) {
4981
+ if (r.success) {
4982
+ ok++;
4983
+ console.log(`${colors.green("\u2713")} ${r.name}: ${colors.green("\u5DF2\u66F4\u65B0")} (${formatProxySummary(r)})`);
4984
+ } else {
4985
+ console.log(`${colors.red("\u2717")} ${r.name}: ${colors.red("\u5931\u8D25")} (${(r.error || "").split("\n")[0]})`);
4986
+ }
4987
+ }
4988
+ if (ok === 0) process.exit(1);
4989
+ console.log("");
4990
+ await printSubscriptionList();
4991
+ return;
4992
+ }
4993
+ const matches = findSubscriptionFuzzy(subs, name);
4994
+ const target = pickSingleSubscription(matches, name);
4995
+ console.log(`\u66F4\u65B0\u8BA2\u9605: ${target.name}`);
4996
+ try {
4997
+ let info;
4998
+ if (isMultiUrl(target.url)) {
4999
+ const urls = splitUrls(target.url);
5000
+ info = await downloadMergedSubscription(urls, target.name);
5001
+ } else {
5002
+ info = await downloadSubscription(target.url, target.name);
5003
+ }
5004
+ console.log(`\u5DF2\u66F4\u65B0 (${formatProxySummary(info)})`);
5005
+ } catch (e) {
5006
+ console.error(`\u66F4\u65B0\u5931\u8D25: ${e.message}`);
5007
+ process.exit(1);
5008
+ }
5009
+ console.log("");
5010
+ await printSubscriptionList();
5011
+ return;
4472
5012
  }
4473
- const results = await Promise.all(staleSubs.map(tryUpdateOne));
4474
- let updatedCount = 0;
4475
- for (const r of results) {
4476
- if (r.success) {
4477
- updatedCount++;
4478
- console.log(`${colors.green("\u2713")} ${r.name}: ${colors.green("\u5DF2\u66F4\u65B0")} (${formatProxySummary(r)})`);
5013
+ if (action === "use") {
5014
+ const name = args[2];
5015
+ const subs = getSubscriptions();
5016
+ if (!name) {
5017
+ console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
5018
+ if (subs.length > 0) {
5019
+ console.log("\n\u53EF\u7528\u8BA2\u9605:");
5020
+ for (const s of subs) console.log(` ${s.name}`);
5021
+ }
5022
+ process.exit(1);
5023
+ }
5024
+ const matches = findSubscriptionFuzzy(subs, name);
5025
+ const target = pickSingleSubscription(matches, name);
5026
+ const currentDefault = getActiveSubscription();
5027
+ const isAlreadyDefault = currentDefault && currentDefault.name === target.name;
5028
+ if (isAlreadyDefault) {
5029
+ console.log(`"${target.name}" \u5DF2\u662F\u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605`);
5030
+ console.log("");
5031
+ await printSubscriptionList();
5032
+ return;
5033
+ }
5034
+ const status = getStatus();
5035
+ const configInfo = getConfigInfo();
5036
+ const currentMode = configInfo?.tun ? "tun" : "mixed";
5037
+ const success = setDefaultSubscription(target.name);
5038
+ if (success) {
5039
+ console.log(`\u5DF2\u5207\u6362\u5230 "${target.name}"`);
4479
5040
  } else {
4480
- console.log(`${colors.red("\u2717")} ${r.name}: ${colors.red("\u5931\u8D25")} (${(r.error || "").split("\n")[0]})`);
5041
+ console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u8BA2\u9605 "${name}"`);
5042
+ process.exit(1);
5043
+ }
5044
+ if (status.running) {
5045
+ console.log("");
5046
+ await cmdStart(["start", currentMode]);
5047
+ return;
5048
+ }
5049
+ console.log("");
5050
+ await printSubscriptionList();
5051
+ return;
5052
+ }
5053
+ if (action === "web" || action === "open") {
5054
+ const name = args[2];
5055
+ const subs = getSubscriptionsWithCache();
5056
+ if (subs.length === 0) {
5057
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
5058
+ process.exit(1);
5059
+ }
5060
+ let target;
5061
+ if (name) {
5062
+ const matches = findSubscriptionFuzzy(subs, name);
5063
+ target = pickSingleSubscription(matches, name);
5064
+ } else {
5065
+ target = subs[0];
5066
+ }
5067
+ const cached = getSubscriptionsWithCache().find((s) => s.name === target.name);
5068
+ let webPageUrl = cached?.web_page_url;
5069
+ if (!webPageUrl) {
5070
+ console.log("\u8BA2\u9605\u4FE1\u606F\u4E2D\u7F3A\u5C11\u9875\u9762\u5730\u5740\uFF0C\u6B63\u5728\u66F4\u65B0\u8BA2\u9605...");
5071
+ try {
5072
+ await downloadSubscription(target.url, target.name);
5073
+ const cache = readSubscriptionCache();
5074
+ if (cache[target.name]?.web_page_url) {
5075
+ webPageUrl = cache[target.name].web_page_url;
5076
+ } else {
5077
+ console.error("\u9519\u8BEF: \u8BE5\u8BA2\u9605\u6CA1\u6709\u63D0\u4F9B\u9875\u9762\u5730\u5740");
5078
+ process.exit(1);
5079
+ }
5080
+ } catch (e) {
5081
+ console.error(`\u66F4\u65B0\u5931\u8D25: ${e.message}`);
5082
+ process.exit(1);
5083
+ }
5084
+ }
5085
+ console.log(`\u6253\u5F00\u8BA2\u9605\u9875\u9762: ${webPageUrl}`);
5086
+ const opened = openUrl(webPageUrl);
5087
+ if (!opened) {
5088
+ console.log("\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740");
4481
5089
  }
5090
+ return;
4482
5091
  }
4483
- return { total: staleSubs.length, updated: updatedCount, failed: staleSubs.length - updatedCount };
4484
- }
4485
- var API_BASE = `http://${BASE_CONFIG["external-controller"]}`;
4486
- var DEFAULT_TEST_URL = "http://www.gstatic.com/generate_204";
4487
- async function testProxyDelay(proxyName, timeout, testUrl, client) {
4488
- const encodedName = encodeURIComponent(proxyName);
4489
- const url = `${API_BASE}/proxies/${encodedName}/delay?timeout=${timeout}&url=${encodeURIComponent(testUrl)}`;
4490
- try {
4491
- const response = await client.get(url);
4492
- const data = JSON.parse(response.data);
4493
- if (data.delay && data.delay > 0) {
4494
- return { name: proxyName, delay: data.delay };
5092
+ if (action === "remove" || action === "rm" || action === "delete") {
5093
+ const name = args[2];
5094
+ const subs = getSubscriptions();
5095
+ if (!name) {
5096
+ console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8981\u5220\u9664\u7684\u8BA2\u9605\u540D\u79F0");
5097
+ if (subs.length > 0) {
5098
+ console.log("\n\u53EF\u7528\u8BA2\u9605:");
5099
+ for (const s of subs) console.log(` ${s.name}`);
5100
+ }
5101
+ process.exit(1);
4495
5102
  }
4496
- return { name: proxyName, delay: null, error: data.message || "no delay" };
4497
- } catch (e) {
4498
- const err = e;
4499
- let errorMsg = "timeout";
4500
- if (err.response?.data?.message) {
4501
- errorMsg = String(err.response.data.message);
4502
- } else if (err.message) {
4503
- errorMsg = err.message;
5103
+ const matches = findSubscriptionFuzzy(subs, name);
5104
+ const target = pickSingleSubscription(matches, name);
5105
+ const switchedTo = removeSubscription(target.name);
5106
+ console.log(`\u5DF2\u5220\u9664\u8BA2\u9605 "${target.name}"`);
5107
+ if (switchedTo) {
5108
+ console.log(`\u5DF2\u81EA\u52A8\u5207\u6362\u5230 "${switchedTo}"`);
4504
5109
  }
4505
- return { name: proxyName, delay: null, error: errorMsg };
5110
+ console.log("");
5111
+ await printSubscriptionList({ autoUpdate: false });
5112
+ return;
5113
+ }
5114
+ if (action === "clean") {
5115
+ const { target, timeout, concurrency } = resolveActiveTestTarget(args);
5116
+ console.log(`\u6E05\u7406\u8BA2\u9605 "${target.name}"...`);
5117
+ console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
5118
+ console.log("");
5119
+ const result = await autoCleanSubscription(target.name, {
5120
+ timeout,
5121
+ concurrency,
5122
+ onResult: printTestResult
5123
+ });
5124
+ console.log("");
5125
+ console.log(formatTestSummary(result.summary));
5126
+ if (result.skipped) {
5127
+ console.log("");
5128
+ console.log(colors.yellow("\u5B58\u6D3B\u8282\u70B9\u4E0D\u8DB3 1%\uFF0C\u8DF3\u8FC7\u6E05\u7406\u3002\u8BF7\u68C0\u67E5\u539F\u59CB\u8BA2\u9605\u662F\u5426\u6709\u6548"));
5129
+ } else if (result.removedProxies > 0) {
5130
+ console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
5131
+ console.log("");
5132
+ console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
5133
+ }
5134
+ return;
5135
+ }
5136
+ if (action === "test") {
5137
+ const { target, timeout, concurrency } = resolveActiveTestTarget(args);
5138
+ console.log(`\u6D4B\u8BD5\u8BA2\u9605 "${target.name}" \u7684\u8282\u70B9\u8FDE\u901A\u6027...`);
5139
+ console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
5140
+ console.log("");
5141
+ const summary = await testSubscriptionProxies(target.name, {
5142
+ timeout,
5143
+ concurrency,
5144
+ onResult: printTestResult
5145
+ });
5146
+ console.log("");
5147
+ console.log(formatTestSummary(summary));
5148
+ return;
4506
5149
  }
5150
+ console.error("\u9519\u8BEF: \u672A\u77E5\u7684\u8BA2\u9605\u547D\u4EE4");
5151
+ console.log("\u7528\u6CD5: mihomo sub [list|use|add|update|remove|web|test|clean]");
5152
+ process.exit(1);
4507
5153
  }
4508
- async function testSubscriptionProxies(subName, options = {}) {
4509
- const { timeout = 2e3, concurrency = 100, testUrl = DEFAULT_TEST_URL, onResult } = options;
4510
- const { proxies } = options.parsed || loadSubscriptionConfig(subName);
4511
- if (proxies.length === 0) {
4512
- return { total: 0, alive: 0, dead: 0, results: [] };
5154
+
5155
+ // src/commands/bench.ts
5156
+ function printRanking(results, sourceOrder) {
5157
+ const valid = results.filter((r) => r.downloadOk && r.alive > 0).sort((a, b) => {
5158
+ const rateA = a.alive / a.totalProxies;
5159
+ const rateB = b.alive / b.totalProxies;
5160
+ if (Math.abs(rateA - rateB) > 0.1) return rateB - rateA;
5161
+ return a.medianDelay - b.medianDelay;
5162
+ });
5163
+ if (valid.length === 0) {
5164
+ console.log(colors.yellow("\u6CA1\u6709\u53EF\u7528\u7684\u8BA2\u9605\u6E90"));
5165
+ return;
4513
5166
  }
4514
- const client = createHttpClient({ timeout: timeout + 3e3 });
4515
- const results = [];
4516
- let completedCount = 0;
4517
- for (let i = 0; i < proxies.length; i += concurrency) {
4518
- const batch = proxies.slice(i, i + concurrency);
4519
- const batchResults = await Promise.all(batch.map((proxy) => testProxyDelay(proxy.name, timeout, testUrl, client)));
4520
- for (const result of batchResults) {
4521
- results.push(result);
4522
- onResult?.(result, completedCount, proxies.length);
4523
- completedCount++;
4524
- }
5167
+ console.log(colors.cyan("\u6392\u540D:"));
5168
+ console.log("");
5169
+ const namedResults = valid.map((r) => {
5170
+ const idx = sourceOrder.get(r.name) ?? 0;
5171
+ return { ...r, displayName: `${String(idx + 1).padStart(2, "0")}-${r.name}` };
5172
+ });
5173
+ const nameWidth = Math.max(12, ...namedResults.map((r) => r.displayName.length));
5174
+ const h = (s, w) => s + " ".repeat(Math.max(0, w - displayWidth(s)));
5175
+ console.log(
5176
+ ` ${"#".padStart(3)} ${h("\u540D\u79F0", nameWidth)} ${h("\u5B58\u6D3B\u7387", 8)} ${h("\u5B58\u6D3B", 6)} ${h("\u603B\u6570", 6)} ${h("\u5206\u7EC4", 6)} ${h("\u4E2D\u4F4D", 7)} ${h("\u5E73\u5747", 7)}`
5177
+ );
5178
+ console.log(
5179
+ ` ${"\u2500".repeat(3)} ${"\u2500".repeat(nameWidth)} ${"\u2500".repeat(8)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(7)} ${"\u2500".repeat(7)}`
5180
+ );
5181
+ for (let i = 0; i < namedResults.length; i++) {
5182
+ const r = namedResults[i];
5183
+ const rate = (r.alive / r.totalProxies * 100).toFixed(1);
5184
+ const rateColor = r.alive / r.totalProxies > 0.3 ? colors.green : colors.yellow;
5185
+ const groups = r.proxyGroups > 0 ? String(r.proxyGroups) : "-";
5186
+ console.log(
5187
+ ` ${String(i + 1).padStart(3)} ${r.displayName.padEnd(nameWidth)} ${rateColor(h(`${rate}%`, 8))} ${String(r.alive).padEnd(6)} ${String(r.totalProxies).padEnd(6)} ${groups.padEnd(6)} ${h(`${r.medianDelay}ms`, 7)} ${h(`${r.avgDelay}ms`, 7)}`
5188
+ );
5189
+ }
5190
+ console.log("");
5191
+ const failed = results.filter((r) => !r.downloadOk);
5192
+ const noAlive = results.filter((r) => r.downloadOk && r.alive === 0);
5193
+ if (failed.length > 0) {
5194
+ const names = failed.map((r) => `${String((sourceOrder.get(r.name) ?? 0) + 1).padStart(2, "0")}-${r.name}`);
5195
+ console.log(colors.gray(`\u4E0B\u8F7D\u5931\u8D25: ${names.join(", ")}`));
5196
+ }
5197
+ if (noAlive.length > 0) {
5198
+ const names = noAlive.map((r) => `${String((sourceOrder.get(r.name) ?? 0) + 1).padStart(2, "0")}-${r.name}`);
5199
+ console.log(colors.gray(`\u65E0\u5B58\u6D3B\u8282\u70B9: ${names.join(", ")}`));
4525
5200
  }
4526
- const alive = results.filter((r) => r.delay !== null).length;
4527
- return { total: results.length, alive, dead: results.length - alive, results };
4528
5201
  }
4529
- function normalizeProxyNamesBeforeSave(parsed) {
4530
- const { proxies, proxyGroups } = parsed;
4531
- const renameMap = /* @__PURE__ */ new Map();
4532
- const usedNames = /* @__PURE__ */ new Set();
4533
- for (const proxy of proxies) {
4534
- const shortened = proxy.name.replace(/_github\.com\/[^_]+/, "");
4535
- if (shortened !== proxy.name && !usedNames.has(shortened)) {
4536
- renameMap.set(proxy.name, shortened);
4537
- usedNames.add(shortened);
4538
- } else {
4539
- usedNames.add(proxy.name);
4540
- }
5202
+ async function cmdBench(args) {
5203
+ if (!hasKernel()) {
5204
+ console.error('\u9519\u8BEF: \u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
5205
+ process.exit(1);
4541
5206
  }
4542
- if (renameMap.size === 0) return 0;
4543
- for (const proxy of proxies) {
4544
- const newName = renameMap.get(proxy.name);
4545
- if (newName) proxy.name = newName;
5207
+ const timeout = parseIntArg(args, "-t", "--timeout", 1500);
5208
+ const concurrency = parseIntArg(args, "-j", "--concurrency", 100);
5209
+ const nameFilter = getNonFlagArg(args, 1);
5210
+ const allSources = getFreeSubscriptionSources();
5211
+ const sourceOrder = new Map(allSources.map((s, i) => [s.name, i]));
5212
+ let sources = allSources;
5213
+ if (nameFilter) {
5214
+ const lower = nameFilter.toLowerCase();
5215
+ sources = sources.filter((s) => s.name.toLowerCase().includes(lower));
5216
+ if (sources.length === 0) {
5217
+ console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u5339\u914D "${nameFilter}" \u7684\u8BA2\u9605\u6E90`);
5218
+ console.log("\n\u53EF\u7528\u6E90:");
5219
+ for (const s of allSources) console.log(` ${s.name}`);
5220
+ process.exit(1);
5221
+ }
4546
5222
  }
4547
- for (const group of proxyGroups) {
4548
- if (Array.isArray(group.proxies)) {
4549
- group.proxies = group.proxies.map((name) => renameMap.get(name) || name);
5223
+ console.log(colors.cyan(`\u57FA\u51C6\u6D4B\u8BD5 ${sources.length} \u4E2A\u514D\u8D39\u8BA2\u9605\u6E90`));
5224
+ console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
5225
+ console.log("");
5226
+ try {
5227
+ console.log(colors.cyan("\u4E0B\u8F7D\u8BA2\u9605..."));
5228
+ const downloaded = await downloadAllSources(sources, (name, ok, count, groups, error) => {
5229
+ const idx = String((sourceOrder.get(name) ?? 0) + 1).padStart(2, "0");
5230
+ if (ok) {
5231
+ const groupsInfo = groups > 0 ? ` ${groups}\u7EC4` : "";
5232
+ console.log(` ${colors.green("\u2713")} ${idx}-${name}: ${count} \u4E2A\u8282\u70B9${groupsInfo}`);
5233
+ } else {
5234
+ console.log(` ${colors.red("\u2717")} ${idx}-${name}: ${colors.gray(error || "\u5931\u8D25")}`);
5235
+ }
5236
+ });
5237
+ const allProxies = downloaded.flatMap((d) => d.proxies);
5238
+ const successSources = downloaded.filter((d) => d.proxies.length > 0);
5239
+ if (allProxies.length === 0) {
5240
+ console.log("");
5241
+ console.log(colors.red("\u6240\u6709\u8BA2\u9605\u6E90\u4E0B\u8F7D\u5931\u8D25\u6216\u65E0\u8282\u70B9"));
5242
+ return;
4550
5243
  }
5244
+ console.log("");
5245
+ console.log(`\u5171 ${allProxies.length} \u4E2A\u8282\u70B9\uFF0C\u6765\u81EA ${successSources.length} \u4E2A\u6E90`);
5246
+ console.log("");
5247
+ const filtered = buildMergedBenchConfig(allProxies);
5248
+ if (filtered > 0) {
5249
+ console.log(colors.gray(`\u8FC7\u6EE4 ${filtered} \u4E2A\u65E0\u6548\u8282\u70B9\uFF0C\u5269\u4F59 ${allProxies.length} \u4E2A`));
5250
+ }
5251
+ const survivingSet = new Set(allProxies);
5252
+ for (const d of downloaded) {
5253
+ d.proxies = d.proxies.filter((p) => survivingSet.has(p));
5254
+ }
5255
+ const benchPort = BENCH_CONFIG.port;
5256
+ const benchApi = BENCH_CONFIG["external-controller"];
5257
+ console.log(colors.cyan("\u542F\u52A8\u6D4B\u8BD5\u5B9E\u4F8B..."));
5258
+ await startBenchInstance();
5259
+ console.log(`${colors.green("\u5DF2\u542F\u52A8")} (\u7AEF\u53E3 ${benchPort}/${benchApi.split(":")[1]})`);
5260
+ console.log("");
5261
+ console.log(colors.cyan("\u6D4B\u8BD5\u8282\u70B9\u5EF6\u8FDF..."));
5262
+ const allNames = allProxies.map((p) => p.name);
5263
+ const testResults = await testBenchProxies(allNames, {
5264
+ timeout,
5265
+ concurrency,
5266
+ onBatch: (batch, total, alive, tested, median) => {
5267
+ const pct = (tested / allNames.length * 100).toFixed(0);
5268
+ process.stdout.write(`\r \u6279\u6B21 ${batch}/${total} \u8FDB\u5EA6 ${pct}% \u5DF2\u6D4B ${tested} \u5B58\u6D3B ${colors.green(String(alive))} \u4E2D\u4F4D\u5EF6\u8FDF ${median}ms`);
5269
+ }
5270
+ });
5271
+ process.stdout.write(`\r${" ".repeat(80)}\r`);
5272
+ const totalAlive = testResults.filter((r) => r.delay !== null).length;
5273
+ const summary = { alive: totalAlive, dead: testResults.length - totalAlive, total: testResults.length };
5274
+ console.log(formatTestSummary(summary));
5275
+ console.log("");
5276
+ const resultsByName = new Map(testResults.map((r) => [r.name, r]));
5277
+ const results = downloaded.map((source) => computeSourceResult(source, resultsByName));
5278
+ printRanking(results, sourceOrder);
5279
+ } finally {
5280
+ stopBenchInstance();
5281
+ cleanupBenchDir();
4551
5282
  }
4552
- return renameMap.size;
4553
5283
  }
4554
- function cleanDeadProxies(parsed, deadNames) {
4555
- const { proxies, proxyGroups } = parsed;
4556
- const originalCount = proxies.length;
4557
- parsed.proxies = proxies.filter((p) => !deadNames.has(p.name));
4558
- const removedProxies = originalCount - parsed.proxies.length;
4559
- let updatedGroups = 0;
4560
- const removedGroupNames = /* @__PURE__ */ new Set();
4561
- for (const group of proxyGroups) {
4562
- if (Array.isArray(group.proxies)) {
4563
- const before = group.proxies.length;
4564
- group.proxies = group.proxies.filter((name) => !deadNames.has(name));
4565
- if (group.proxies.length < before) {
4566
- updatedGroups++;
5284
+
5285
+ // src/commands/directory.ts
5286
+ function cmdDirectory(args) {
5287
+ const action = args?.[1];
5288
+ if (action === "open") {
5289
+ const target = args[2];
5290
+ if (!target || target === "root") {
5291
+ console.log("\u6B63\u5728\u6253\u5F00: \u6839\u76EE\u5F55");
5292
+ const success = openUrl(USER_DATA_DIR);
5293
+ if (!success) {
5294
+ console.log(`\u8BF7\u624B\u52A8\u6253\u5F00: ${USER_DATA_DIR}`);
4567
5295
  }
4568
- if (group.proxies.length === 0) {
4569
- removedGroupNames.add(group.name);
5296
+ return;
5297
+ }
5298
+ const targetInfo = DIRECTORY_TARGETS[target.toLowerCase()];
5299
+ if (targetInfo) {
5300
+ const targetPath = targetInfo.path || USER_DATA_DIR;
5301
+ console.log(`\u6B63\u5728\u6253\u5F00: ${targetInfo.label}`);
5302
+ const success = openUrl(targetPath);
5303
+ if (!success) {
5304
+ console.log(`\u8BF7\u624B\u52A8\u6253\u5F00: ${targetPath}`);
4570
5305
  }
5306
+ return;
4571
5307
  }
4572
- }
4573
- if (removedGroupNames.size > 0) {
4574
- parsed.proxyGroups = proxyGroups.filter((g) => !removedGroupNames.has(g.name));
4575
- for (const group of parsed.proxyGroups) {
4576
- if (Array.isArray(group.proxies)) {
4577
- group.proxies = group.proxies.filter((name) => !removedGroupNames.has(name));
5308
+ console.error(`\u9519\u8BEF: \u672A\u77E5\u7684\u76EE\u5F55\u76EE\u6807 "${target}"`);
5309
+ console.log("");
5310
+ console.log("\u53EF\u7528\u76EE\u6807:");
5311
+ console.log(" root (\u9ED8\u8BA4) \u6839\u76EE\u5F55");
5312
+ for (const [key, val] of Object.entries(DIRECTORY_TARGETS)) {
5313
+ if (key !== "root") {
5314
+ console.log(` ${key.padEnd(14)}${val.label}`);
4578
5315
  }
4579
5316
  }
5317
+ console.log("");
5318
+ process.exit(1);
4580
5319
  }
4581
- return { removedProxies, updatedGroups, removedGroups: removedGroupNames.size };
5320
+ console.log("");
5321
+ console.log("\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E:");
5322
+ console.log(` \u6839\u76EE\u5F55: ${USER_DATA_DIR}`);
5323
+ console.log(` \u5168\u5C40\u8BBE\u7F6E: ${PATHS.settingsFile}`);
5324
+ console.log(` \u5185\u6838\u76EE\u5F55: ${DIRS.kernel}`);
5325
+ console.log(` \u5185\u6838\u6587\u4EF6: ${PATHS.mihomoBinary}`);
5326
+ console.log(` \u8BA2\u9605\u76EE\u5F55: ${DIRS.subscriptions}`);
5327
+ console.log(" - cache.json (\u8BA2\u9605\u7F13\u5B58\uFF1A\u66F4\u65B0\u65F6\u95F4\u3001\u6D41\u91CF\u7B49)");
5328
+ console.log(" - xxx.yaml (\u8BA2\u9605\u539F\u59CB\u914D\u7F6E)");
5329
+ console.log(` \u8FD0\u884C\u65F6\u76EE\u5F55: ${DIRS.runtime}`);
5330
+ console.log(" - config.yaml (\u542F\u52A8\u65F6\u751F\u6210\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
5331
+ console.log(" - pid (PID \u6587\u4EF6\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
5332
+ console.log(` \u65E5\u5FD7\u6587\u4EF6: ${PATHS.logFile}`);
5333
+ console.log(` mihomo \u6570\u636E: ${DIRS.data}`);
5334
+ console.log(" - cache.db, Geo*.dat \u7B49 (mihomo \u81EA\u884C\u7BA1\u7406)");
5335
+ console.log("");
5336
+ console.log("\u6253\u5F00\u76EE\u5F55:");
5337
+ console.log(" mihomo dir open \u6253\u5F00\u6839\u76EE\u5F55");
5338
+ console.log(" mihomo dir open subs \u6253\u5F00\u8BA2\u9605\u76EE\u5F55");
5339
+ console.log(" mihomo dir open logs \u6253\u5F00\u65E5\u5FD7\u76EE\u5F55");
5340
+ console.log(" mihomo dir open runtime \u6253\u5F00\u8FD0\u884C\u65F6\u76EE\u5F55");
5341
+ console.log(" mihomo dir open kernel \u6253\u5F00\u5185\u6838\u76EE\u5F55");
5342
+ console.log("");
5343
+ console.log("\u73AF\u5883\u53D8\u91CF:");
5344
+ console.log(" MIHOMO_CLI_DIR: \u81EA\u5B9A\u4E49\u6839\u76EE\u5F55\u4F4D\u7F6E");
5345
+ console.log("");
5346
+ }
5347
+
5348
+ // src/commands/help.ts
5349
+ function printShortHelp() {
5350
+ console.log(`
5351
+ ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))} (mihomo help \u67E5\u770B\u5B8C\u6574\u5E2E\u52A9)
5352
+ `);
5353
+ console.log(
5354
+ `\u5E38\u7528\u547D\u4EE4:
5355
+ ${colors.bold("start")} [tun|mixed] \u542F\u52A8/\u5207\u6362\u4EE3\u7406
5356
+ ${colors.bold("sub")} [use|update] \u8BA2\u9605\u7BA1\u7406
5357
+ ${colors.bold("ow")} [on|off] \u8986\u5199\u914D\u7F6E
5358
+ ${colors.bold("ui")} [zash|dash|yacd] \u6253\u5F00 Web UI
5359
+ `
5360
+ );
4582
5361
  }
4583
- async function autoCleanSubscription(subName, options = {}) {
4584
- const parsed = loadSubscriptionConfig(subName);
4585
- const summary = await testSubscriptionProxies(subName, { ...options, parsed });
4586
- let removedProxies = 0;
4587
- let updatedGroups = 0;
4588
- let removedGroups = 0;
4589
- let skipped = false;
4590
- if (summary.dead > 0) {
4591
- if (summary.alive === 0 || summary.alive / summary.total < 0.01) {
4592
- skipped = true;
4593
- } else {
4594
- const deadNames = new Set(summary.results.filter((r) => r.delay === null).map((r) => r.name));
4595
- const cleanResult = cleanDeadProxies(parsed, deadNames);
4596
- removedProxies = cleanResult.removedProxies;
4597
- updatedGroups = cleanResult.updatedGroups;
4598
- removedGroups = cleanResult.removedGroups;
4599
- }
4600
- }
4601
- if (!skipped) {
4602
- saveSubscriptionConfig(subName, parsed);
4603
- }
4604
- return { summary, removedProxies, updatedGroups, removedGroups, skipped };
5362
+ function printHelp() {
5363
+ console.log(
5364
+ `
5365
+ ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))}
5366
+
5367
+ \u547D\u4EE4\u522B\u540D: mihomo, mhm, mh
5368
+
5369
+ \u7528\u6CD5:
5370
+ mihomo <\u547D\u4EE4> [\u9009\u9879]
5371
+
5372
+ ${colors.cyan("\u63A7\u5236:")}
5373
+ ${colors.bold("start")} [tun|mixed] \u542F\u52A8/\u5207\u6362\u4EE3\u7406 (\u9ED8\u8BA4 mixed)
5374
+ ${colors.bold("stop")} \u505C\u6B62\u4EE3\u7406
5375
+ ${colors.bold("status")} \u67E5\u770B\u72B6\u6001
5376
+
5377
+ ${colors.cyan("\u754C\u9762:")}
5378
+ ${colors.bold("ui")} [zash|dash|yacd] \u6253\u5F00 Web UI (\u9ED8\u8BA4 zash)
5379
+ ${colors.bold("log")} [-o] \u5B9E\u65F6\u65E5\u5FD7\uFF08-o \u6253\u5F00\u6587\u4EF6\uFF09
5380
+ ${colors.bold("logs")} [\u7F16\u53F7] [-n N] [-o] \u65E5\u5FD7\u5217\u8868\uFF080=\u5F53\u524D\uFF0C1+=\u5F52\u6863\uFF09
5381
+
5382
+ ${colors.cyan("\u8BA2\u9605:")}
5383
+ ${colors.bold("subscription")} \u5217\u51FA\u6240\u6709\u8BA2\u9605\uFF08\u522B\u540D sub\uFF09
5384
+ ${colors.bold("subscription")} use <name> \u5207\u6362\u5F53\u524D\u8BA2\u9605
5385
+ ${colors.bold("subscription")} add <url> [name] \u6DFB\u52A0\u8BA2\u9605
5386
+ ${colors.bold("subscription")} update [name] \u66F4\u65B0\u8BA2\u9605\uFF08\u65E0\u53C2\u66F4\u65B0\u6240\u6709\uFF09
5387
+ ${colors.bold("subscription")} remove <name> \u5220\u9664\u8BA2\u9605
5388
+ ${colors.bold("subscription")} web [name] \u6253\u5F00\u8BA2\u9605\u9875\u9762
5389
+ ${colors.bold("subscription")} test [name] \u6D4B\u8BD5\u8282\u70B9\u8FDE\u901A\u6027
5390
+ ${colors.bold("subscription")} clean [name] \u6D4B\u901F\u5E76\u6E05\u7406\u5931\u8D25\u8282\u70B9
5391
+ ${colors.bold("bench")} [name] [-t ms] [-j N] \u6D4B\u8BD5\u514D\u8D39\u8BA2\u9605\u6E90\u8D28\u91CF\u6392\u540D
5392
+
5393
+ ${colors.cyan("\u914D\u7F6E:")}
5394
+ ${colors.bold("overwrite")} \u67E5\u770B\u8986\u5199\u72B6\u6001\uFF08\u522B\u540D ow\uFF09
5395
+ ${colors.bold("overwrite")} on|off \u542F\u7528/\u7981\u7528\u8986\u5199\u914D\u7F6E
5396
+ ${colors.bold("directory")} \u663E\u793A\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E\uFF08\u522B\u540D dir\uFF09
5397
+ ${colors.bold("directory")} open [target] \u6253\u5F00\u76EE\u5F55: root|subs|logs|runtime|...
5398
+
5399
+ ${colors.cyan("\u7CFB\u7EDF:")}
5400
+ ${colors.bold("kernel")} [--mirror [\u955C\u50CF]] \u66F4\u65B0\u5185\u6838\uFF08\u9ED8\u8BA4\u76F4\u8FDE\uFF0C--mirror \u4F7F\u7528 v6\uFF09
5401
+ ${colors.bold("update")} \u66F4\u65B0 mihomo-cli (npm install -g)
5402
+ ${colors.bold("reset")} [\u76EE\u6807...] [--full] \u91CD\u7F6E: \u7559\u7A7A\u4FDD\u7559\u8BBE\u7F6E/\u5185\u6838/\u8986\u5199, \u6307\u5B9A\u76EE\u6807\u5220\u5BF9\u5E94\u9879, --full \u5220\u5168\u90E8
5403
+ ${colors.bold("help")}, -h \u663E\u793A\u5E2E\u52A9
5404
+ ${colors.bold("version")}, -v \u663E\u793A\u7248\u672C
5405
+
5406
+ ${colors.cyan("\u793A\u4F8B:")}
5407
+ mihomo start # \u542F\u52A8/\u91CD\u542F Mixed \u6A21\u5F0F
5408
+ mihomo start tun # \u5207\u6362\u5230 TUN \u900F\u660E\u4EE3\u7406\u6A21\u5F0F
5409
+ mihomo sub add <url> # \u6DFB\u52A0\u8BA2\u9605 (sub \u662F subscription \u522B\u540D)
5410
+ mihomo bench # \u6D4B\u8BD5\u6240\u6709\u514D\u8D39\u8BA2\u9605\u6E90
5411
+ mihomo ui # \u6253\u5F00 Web UI
5412
+
5413
+ ${colors.cyan("\u6A21\u5F0F\u8BF4\u660E:")}
5414
+ mixed HTTP + SOCKS5 \u6DF7\u5408\u7AEF\u53E3 (\u9ED8\u8BA4)
5415
+ tun \u900F\u660E\u4EE3\u7406\uFF0C\u5168\u5C40\u81EA\u52A8\u8DEF\u7531\uFF0C\u9700\u8981 sudo
5416
+
5417
+ ${colors.cyan("\u6570\u636E\u76EE\u5F55:")}
5418
+ \u73AF\u5883\u53D8\u91CF MIHOMO_CLI_DIR \u53EF\u81EA\u5B9A\u4E49\u4F4D\u7F6E
5419
+ \u9ED8\u8BA4: ${USER_DATA_DIR}
5420
+ `
5421
+ );
5422
+ }
5423
+ function printVersion() {
5424
+ const kv = getKernelVersion() || "\u672A\u5B89\u88C5";
5425
+ console.log(colors.cyan(colors.bold(`mihomo-cli v${VERSION}`)));
5426
+ console.log(`${colors.gray("\u5185\u6838: ")}${kv}`);
5427
+ console.log(`${colors.gray("\u6570\u636E\u76EE\u5F55: ")}${USER_DATA_DIR}`);
4605
5428
  }
4606
5429
 
4607
- // src/commands/status.ts
4608
- function printStatus() {
4609
- const status = getStatus();
4610
- const info = getConfigInfo();
4611
- const overwriteEnabled = isOverwriteEnabled();
4612
- const overwriteFiles = listOverwriteFile().files;
4613
- const activeSub = getActiveSubscription();
4614
- console.log("");
4615
- let modeLabel = "";
4616
- if (info && status.running) {
4617
- modeLabel = colors.cyan(info.tun ? " (TUN)" : " (Mixed)");
4618
- }
4619
- const statusText = status.running ? colors.green("\u25CF \u8FD0\u884C\u4E2D") : colors.yellow("\u4E0D\u5728\u8FD0\u884C");
4620
- console.log(`${colors.gray("\u72B6\u6001: ")}${statusText}${modeLabel}`);
4621
- console.log(`${colors.gray("\u5185\u6838: ")}${status.kernelVersion || "\u672A\u5B89\u88C5"}`);
4622
- if (status.pid) {
4623
- console.log(`${colors.gray("PID: ")}${status.pid}`);
4624
- if (status.processInfo) {
4625
- console.log(`${colors.gray("\u5185\u5B58: ")}${status.processInfo.memory}`);
4626
- }
5430
+ // src/kernel.ts
5431
+ import { execSync as execSync4, spawnSync } from "child_process";
5432
+ import fs7 from "fs";
5433
+ import path5 from "path";
5434
+
5435
+ // node_modules/compare-versions/lib/esm/utils.js
5436
+ var semver = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
5437
+ var validateAndParse = (version) => {
5438
+ if (typeof version !== "string") {
5439
+ throw new TypeError("Invalid argument expected string");
4627
5440
  }
4628
- if (info) {
4629
- if (info.mixedPort) {
4630
- console.log(`${colors.gray("\u7AEF\u53E3: ")}${info.mixedPort}`);
4631
- } else {
4632
- const ports = [];
4633
- if (info.httpPort) ports.push(`HTTP:${info.httpPort}`);
4634
- if (info.socksPort) ports.push(`SOCKS:${info.socksPort}`);
4635
- console.log(`${colors.gray("\u7AEF\u53E3: ")}${ports.length > 0 ? ports.join(", ") : "\u672A\u77E5"}`);
4636
- }
5441
+ const match = version.match(semver);
5442
+ if (!match) {
5443
+ throw new Error(`Invalid argument not valid semver ('${version}' received)`);
4637
5444
  }
4638
- if (activeSub) {
4639
- let subLine = `${colors.gray("\u8BA2\u9605: ")}${activeSub.name}`;
4640
- if (info) {
4641
- subLine += ` (${formatProxySummary(info)})`;
4642
- }
4643
- console.log(subLine);
4644
- } else {
4645
- console.log(`${colors.gray("\u8BA2\u9605: ")}\u672A\u914D\u7F6E`);
5445
+ match.shift();
5446
+ return match;
5447
+ };
5448
+ var isWildcard = (s) => s === "*" || s === "x" || s === "X";
5449
+ var tryParse = (v) => {
5450
+ const n = parseInt(v, 10);
5451
+ return isNaN(n) ? v : n;
5452
+ };
5453
+ var forceType = (a, b) => typeof a !== typeof b ? [String(a), String(b)] : [a, b];
5454
+ var compareStrings = (a, b) => {
5455
+ if (isWildcard(a) || isWildcard(b))
5456
+ return 0;
5457
+ const [ap, bp] = forceType(tryParse(a), tryParse(b));
5458
+ if (ap > bp)
5459
+ return 1;
5460
+ if (ap < bp)
5461
+ return -1;
5462
+ return 0;
5463
+ };
5464
+ var compareSegments = (a, b) => {
5465
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
5466
+ const r = compareStrings(a[i] || "0", b[i] || "0");
5467
+ if (r !== 0)
5468
+ return r;
4646
5469
  }
4647
- if (overwriteEnabled && overwriteFiles.length > 0) {
4648
- const names = overwriteFiles.map((f) => f.name.replace(/^overwrite\.?/, "").replace(/\.ya?ml$/, "") || "\u4E3B\u6587\u4EF6").join(", ");
4649
- console.log(`${colors.gray("\u8986\u5199: ")}${colors.green("\u5DF2\u542F\u7528")} (${names})`);
4650
- } else if (overwriteEnabled) {
4651
- console.log(`${colors.gray("\u8986\u5199: ")}${colors.green("\u5DF2\u542F\u7528")} (\u65E0\u6587\u4EF6)`);
4652
- } else {
4653
- console.log(`${colors.gray("\u8986\u5199: ")}${colors.yellow("\u5DF2\u7981\u7528")}`);
5470
+ return 0;
5471
+ };
5472
+
5473
+ // node_modules/compare-versions/lib/esm/compareVersions.js
5474
+ var compareVersions = (v1, v2) => {
5475
+ const n1 = validateAndParse(v1);
5476
+ const n2 = validateAndParse(v2);
5477
+ const p1 = n1.pop();
5478
+ const p2 = n2.pop();
5479
+ const r = compareSegments(n1, n2);
5480
+ if (r !== 0)
5481
+ return r;
5482
+ if (p1 && p2) {
5483
+ return compareSegments(p1.split("."), p2.split("."));
5484
+ } else if (p1 || p2) {
5485
+ return p1 ? -1 : 1;
4654
5486
  }
4655
- console.log("");
4656
- }
5487
+ return 0;
5488
+ };
4657
5489
 
4658
- // src/commands/subscription.ts
4659
- function printTestResult(result, index, total) {
4660
- const prefix = `[${index + 1}/${total}]`;
4661
- if (result.delay !== null) {
4662
- const delayColor = result.delay < 300 ? colors.green : result.delay < 800 ? colors.yellow : colors.red;
4663
- console.log(` ${prefix} ${colors.green("\u2713")} ${result.name} ${delayColor(`${result.delay}ms`)}`);
4664
- } else {
4665
- console.log(` ${prefix} ${colors.red("\u2717")} ${result.name} ${colors.gray(result.error || "timeout")}`);
5490
+ // src/kernel.ts
5491
+ var GITHUB_REPO = "MetaCubeX/mihomo";
5492
+ var KERNEL_HTTP_TIMEOUT = 12e4;
5493
+ var KERNEL_DOWNLOAD_TIMEOUT = 18e4;
5494
+ var HTTP_CLIENT2 = createHttpClient({ timeout: KERNEL_HTTP_TIMEOUT });
5495
+ function withMirror(url, mirror) {
5496
+ if (mirror && (url.startsWith("https://github.com/") || url.startsWith("https://api.github.com/"))) {
5497
+ return mirror + url;
4666
5498
  }
5499
+ return url;
4667
5500
  }
4668
- function formatCleanSummary(result) {
4669
- const parts = [`\u79FB\u9664 ${result.removedProxies} \u4E2A\u8282\u70B9`];
4670
- if (result.removedGroups > 0) parts.push(`\u5220\u9664 ${result.removedGroups} \u4E2A\u7A7A\u5206\u7EC4`);
4671
- if (result.updatedGroups > 0) parts.push(`\u66F4\u65B0 ${result.updatedGroups} \u4E2A\u5206\u7EC4`);
4672
- return parts.join(", ");
5501
+ function getArch() {
5502
+ const arch = process.arch;
5503
+ if (arch === "arm64") return "arm64";
5504
+ if (arch === "x64") return "amd64";
5505
+ return arch;
4673
5506
  }
4674
- function formatTestSummary(summary) {
4675
- return `\u7ED3\u679C: ${colors.green(`${summary.alive} \u5B58\u6D3B`)} / ${colors.red(`${summary.dead} \u5931\u8D25`)} / ${summary.total} \u603B\u8BA1`;
5507
+ function findMatchingAsset(assets, platform, arch) {
5508
+ const prefix = `mihomo-${platform}-${arch}`;
5509
+ const matchingAssets = assets.filter(
5510
+ (a) => a.name.startsWith(prefix) && a.name.endsWith(".gz") || a.name.startsWith(`${prefix}-`) && a.name.endsWith(".gz")
5511
+ );
5512
+ if (matchingAssets.length === 0) return null;
5513
+ if (matchingAssets.length === 1) return matchingAssets[0];
5514
+ const standardAsset = matchingAssets.find((a) => {
5515
+ const nameWithoutGz = a.name.slice(0, -3);
5516
+ const parts = nameWithoutGz.split("-");
5517
+ const lastPart = parts[parts.length - 1];
5518
+ return /^v?\d+\.\d+\.\d+/.test(lastPart) && !nameWithoutGz.includes("-go");
5519
+ });
5520
+ return standardAsset || matchingAssets[0];
4676
5521
  }
4677
- function resolveActiveTestTarget(args) {
4678
- const subs = getSubscriptions();
4679
- if (subs.length === 0) {
4680
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
4681
- process.exit(1);
5522
+ async function getLatestRelease(repo, mirror) {
5523
+ const url = withMirror(`https://api.github.com/repos/${repo}/releases`, mirror);
5524
+ const response = await HTTP_CLIENT2.get(url, { responseType: "json" });
5525
+ const releases = response.data;
5526
+ if (!Array.isArray(releases) || releases.length === 0) {
5527
+ throw new Error("\u65E0\u6CD5\u83B7\u53D6\u7248\u672C\u4FE1\u606F");
4682
5528
  }
4683
- const nameArg = getNonFlagArg(args, 2);
4684
- const timeout = parseIntArg(args, "-t", "--timeout", 2e3);
4685
- const concurrency = parseIntArg(args, "-j", "--concurrency", 100);
4686
- const activeSub = getActiveSubscription();
4687
- let target;
4688
- if (nameArg) {
4689
- const matches = findSubscriptionFuzzy(subs, nameArg);
4690
- target = pickSingleSubscription(matches, nameArg);
5529
+ const stableReleases = releases.filter(
5530
+ (r) => !r.prerelease && !r.tag_name.toLowerCase().includes("alpha") && !r.tag_name.toLowerCase().includes("beta") && !r.tag_name.toLowerCase().includes("prerelease")
5531
+ );
5532
+ return stableReleases.length > 0 ? stableReleases[0] : releases[0];
5533
+ }
5534
+ async function checkUpdate(mirror) {
5535
+ const currentVersion = getKernelVersion();
5536
+ const latest = await getLatestRelease(GITHUB_REPO, mirror);
5537
+ const latestVersion = latest.tag_name;
5538
+ let needsUpdate = false;
5539
+ const currentDisplay = currentVersion || "\u672A\u5B89\u88C5";
5540
+ if (!currentVersion) {
5541
+ needsUpdate = true;
4691
5542
  } else {
4692
- if (!activeSub) {
4693
- console.error("\u9519\u8BEF: \u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
4694
- process.exit(1);
5543
+ try {
5544
+ needsUpdate = compareVersions(latestVersion.replace(/^v/, ""), currentVersion.replace(/^v/, "")) > 0;
5545
+ } catch {
5546
+ needsUpdate = latestVersion !== currentVersion;
4695
5547
  }
4696
- target = activeSub;
4697
- }
4698
- const status = getStatus();
4699
- if (!status.running) {
4700
- console.error("\u9519\u8BEF: mihomo \u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 (mihomo start)");
4701
- process.exit(1);
4702
- }
4703
- if (!activeSub || activeSub.name !== target.name) {
4704
- console.error(`\u9519\u8BEF: \u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605\u662F "${activeSub?.name}"\uFF0C\u4E0D\u662F "${target.name}"`);
4705
- console.log(`\u8BF7\u5148\u5207\u6362: mihomo sub use ${target.name}`);
4706
- process.exit(1);
4707
5548
  }
4708
- return { target, timeout, concurrency };
5549
+ return {
5550
+ current: currentDisplay,
5551
+ latest: latestVersion,
5552
+ needsUpdate,
5553
+ assets: latest.assets,
5554
+ release: latest
5555
+ };
4709
5556
  }
4710
- async function printSubscriptionList(options) {
4711
- if (options?.autoUpdate !== false) {
4712
- const updateResult = await autoUpdateStaleSubscription();
4713
- if (updateResult.total > 0) console.log("");
4714
- }
4715
- const subs = getSubscriptionsWithCache();
4716
- if (subs.length === 0) {
4717
- console.log("\u6CA1\u6709\u8BA2\u9605");
4718
- console.log("");
4719
- console.log("\u6DFB\u52A0\u8BA2\u9605: mihomo sub add <url> [name]");
4720
- console.log("");
4721
- return;
4722
- }
4723
- const activeSub = getActiveSubscription();
4724
- console.log(colors.cyan("\u8BA2\u9605\u5217\u8868:"));
4725
- subs.forEach((s, i) => {
4726
- const time = formatDate(s.updated_at);
4727
- const defaultMark = activeSub && s.name === activeSub.name ? colors.green(" [\u4F7F\u7528\u4E2D]") : "";
4728
- const interval = s.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
4729
- console.log(` ${i + 1}. ${s.name}${defaultMark}`);
4730
- console.log(` ${colors.gray("\u66F4\u65B0: ")}${time} (\u95F4\u9694: ${interval}h)`);
4731
- if (s.username) {
4732
- console.log(` ${colors.gray("\u7528\u6237: ")}${s.username}`);
4733
- }
4734
- if (s.download !== void 0 || s.total !== void 0) {
4735
- const used = (s.upload || 0) + (s.download || 0);
4736
- const usedStr = formatBytes(used);
4737
- const totalStr = formatBytes(s.total);
4738
- let percentStr = "";
4739
- if (s.total && s.total > 0) {
4740
- const percent = Math.min(used / s.total * 100, 100);
4741
- percentStr = ` (${percent.toFixed(1)}%)`;
4742
- }
4743
- console.log(` ${colors.gray("\u6D41\u91CF: ")}${usedStr} / ${totalStr}${percentStr}`);
4744
- }
4745
- if (s.expire !== void 0) {
4746
- console.log(` ${colors.gray("\u5230\u671F: ")}${formatTimestamp(s.expire)}`);
4747
- }
4748
- if (s.web_page_url) {
4749
- console.log(` ${colors.gray("\u9875\u9762: ")}${s.web_page_url}`);
5557
+ function findBinaryInDir(dir) {
5558
+ const files = fs7.readdirSync(dir);
5559
+ for (const f of files) {
5560
+ const fullPath = path5.join(dir, f);
5561
+ const stat = fs7.statSync(fullPath);
5562
+ if (stat.isDirectory()) {
5563
+ const found = findBinaryInDir(fullPath);
5564
+ if (found) return found;
5565
+ continue;
4750
5566
  }
4751
- });
4752
- console.log("");
4753
- console.log("\u5207\u6362\u8BA2\u9605: mihomo sub use <name>");
4754
- console.log("\u65B0\u589E\u8BA2\u9605: mihomo sub add <url> [name]");
4755
- console.log("\u66F4\u65B0\u8BA2\u9605: mihomo sub update [name]");
4756
- console.log("\u5220\u9664\u8BA2\u9605: mihomo sub remove <name>");
4757
- console.log("\u6D4B\u8BD5\u8282\u70B9: mihomo sub test [name]");
4758
- console.log("\u6E05\u7406\u8282\u70B9: mihomo sub clean [name]");
4759
- console.log("\u6253\u5F00\u9875\u9762: mihomo sub web [name]");
4760
- console.log("");
5567
+ if (f === "mihomo") return fullPath;
5568
+ if (f.includes("mihomo") && !f.endsWith(".gz")) return fullPath;
5569
+ }
5570
+ return null;
4761
5571
  }
4762
- async function cmdSubscription(args) {
4763
- const action = args[1];
4764
- if (!action || action === "list") {
4765
- await printSubscriptionList();
4766
- return;
5572
+ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5573
+ ensureDirs();
5574
+ const latest = releaseInfo || await getLatestRelease(GITHUB_REPO, mirror);
5575
+ const arch = getArch();
5576
+ const platform = process.platform;
5577
+ const asset = findMatchingAsset(latest.assets, platform, arch);
5578
+ if (!asset) {
5579
+ const available = latest.assets.map((a) => a.name).join(", ");
5580
+ let hint = "";
5581
+ if (available) hint = `
5582
+ \u53EF\u7528\u7248\u672C: ${available}`;
5583
+ throw new Error(`\u672A\u627E\u5230\u5339\u914D\u7684\u5185\u6838\u6587\u4EF6
5584
+ \u5E73\u53F0: ${platform}, \u67B6\u6784: ${arch}${hint}`);
4767
5585
  }
4768
- if (action === "add") {
4769
- const url = args[2];
4770
- const name = args[3] || "default";
4771
- if (!url?.startsWith("http")) {
4772
- console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
4773
- process.exit(1);
4774
- }
4775
- console.log(`\u6DFB\u52A0\u8BA2\u9605: ${name}`);
4776
- try {
4777
- addSubscription(url, name);
4778
- setDefaultSubscription(name);
4779
- const info = await downloadSubscription(url, name);
4780
- console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
4781
- } catch (e) {
4782
- console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
4783
- process.exit(1);
4784
- }
4785
- console.log("");
4786
- await printSubscriptionList();
4787
- return;
5586
+ const downloadUrl = withMirror(asset.browser_download_url, mirror);
5587
+ const tempPath = path5.join(DIRS.kernel, asset.name);
5588
+ const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
5589
+ if (progressCallback) {
5590
+ progressCallback(`\u4E0B\u8F7D\u5185\u6838: ${asset.name} (${sizeMB} MB)`);
4788
5591
  }
4789
- if (action === "update") {
4790
- const name = args[2];
4791
- const subs = getSubscriptions();
4792
- if (subs.length === 0) {
4793
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
4794
- process.exit(1);
4795
- }
4796
- if (!name) {
4797
- console.log(`\u66F4\u65B0\u6240\u6709 ${subs.length} \u4E2A\u8BA2\u9605...`);
4798
- const results = await Promise.all(subs.map(tryUpdateOne));
4799
- let ok = 0;
4800
- for (const r of results) {
4801
- if (r.success) {
4802
- ok++;
4803
- console.log(`${colors.green("\u2713")} ${r.name}: ${colors.green("\u5DF2\u66F4\u65B0")} (${formatProxySummary(r)})`);
4804
- } else {
4805
- console.log(`${colors.red("\u2717")} ${r.name}: ${colors.red("\u5931\u8D25")} (${(r.error || "").split("\n")[0]})`);
4806
- }
4807
- }
4808
- if (ok === 0) process.exit(1);
4809
- console.log("");
4810
- await printSubscriptionList();
4811
- return;
4812
- }
4813
- const matches = findSubscriptionFuzzy(subs, name);
4814
- const target = pickSingleSubscription(matches, name);
4815
- console.log(`\u66F4\u65B0\u8BA2\u9605: ${target.name}`);
5592
+ const curlResult = spawnSync(
5593
+ "curl",
5594
+ ["-L", "--progress-bar", "--connect-timeout", "30", "--max-time", String(Math.floor(KERNEL_DOWNLOAD_TIMEOUT / 1e3)), "-o", tempPath, downloadUrl],
5595
+ { stdio: "inherit" }
5596
+ );
5597
+ if (curlResult.status !== 0) {
4816
5598
  try {
4817
- const info = await downloadSubscription(target.url, target.name);
4818
- console.log(`\u5DF2\u66F4\u65B0 (${formatProxySummary(info)})`);
4819
- } catch (e) {
4820
- console.error(`\u66F4\u65B0\u5931\u8D25: ${e.message}`);
4821
- process.exit(1);
5599
+ fs7.unlinkSync(tempPath);
5600
+ } catch {
4822
5601
  }
4823
- console.log("");
4824
- await printSubscriptionList();
4825
- return;
5602
+ throw new Error(`\u4E0B\u8F7D\u5931\u8D25 (curl \u9000\u51FA\u7801 ${curlResult.status})`);
4826
5603
  }
4827
- if (action === "use") {
4828
- const name = args[2];
4829
- const subs = getSubscriptions();
4830
- if (!name) {
4831
- console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
4832
- if (subs.length > 0) {
4833
- console.log("\n\u53EF\u7528\u8BA2\u9605:");
4834
- for (const s of subs) console.log(` ${s.name}`);
4835
- }
4836
- process.exit(1);
4837
- }
4838
- const matches = findSubscriptionFuzzy(subs, name);
4839
- const target = pickSingleSubscription(matches, name);
4840
- const currentDefault = getActiveSubscription();
4841
- const isAlreadyDefault = currentDefault && currentDefault.name === target.name;
4842
- if (isAlreadyDefault) {
4843
- console.log(`"${target.name}" \u5DF2\u662F\u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605`);
4844
- console.log("");
4845
- await printSubscriptionList();
4846
- return;
4847
- }
4848
- const status = getStatus();
4849
- const configInfo = getConfigInfo();
4850
- const currentMode = configInfo?.tun ? "tun" : "mixed";
4851
- const success = setDefaultSubscription(target.name);
4852
- if (success) {
4853
- console.log(`\u5DF2\u5207\u6362\u5230 "${target.name}"`);
4854
- } else {
4855
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u8BA2\u9605 "${name}"`);
4856
- process.exit(1);
4857
- }
4858
- if (status.running) {
4859
- console.log("");
4860
- await cmdStart(["start", currentMode]);
4861
- return;
4862
- }
4863
- console.log("");
4864
- await printSubscriptionList();
4865
- return;
5604
+ if (!fs7.existsSync(tempPath)) {
5605
+ throw new Error("\u4E0B\u8F7D\u5931\u8D25: \u6587\u4EF6\u672A\u751F\u6210");
4866
5606
  }
4867
- if (action === "web" || action === "open") {
4868
- const name = args[2];
4869
- const subs = getSubscriptionsWithCache();
4870
- if (subs.length === 0) {
4871
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
4872
- process.exit(1);
4873
- }
4874
- let target;
4875
- if (name) {
4876
- const matches = findSubscriptionFuzzy(subs, name);
4877
- target = pickSingleSubscription(matches, name);
4878
- } else {
4879
- target = subs[0];
4880
- }
4881
- const cached = getSubscriptionsWithCache().find((s) => s.name === target.name);
4882
- let webPageUrl = cached?.web_page_url;
4883
- if (!webPageUrl) {
4884
- console.log("\u8BA2\u9605\u4FE1\u606F\u4E2D\u7F3A\u5C11\u9875\u9762\u5730\u5740\uFF0C\u6B63\u5728\u66F4\u65B0\u8BA2\u9605...");
4885
- try {
4886
- await downloadSubscription(target.url, target.name);
4887
- const cache = readSubscriptionCache();
4888
- if (cache[target.name]?.web_page_url) {
4889
- webPageUrl = cache[target.name].web_page_url;
4890
- } else {
4891
- console.error("\u9519\u8BEF: \u8BE5\u8BA2\u9605\u6CA1\u6709\u63D0\u4F9B\u9875\u9762\u5730\u5740");
4892
- process.exit(1);
4893
- }
4894
- } catch (e) {
4895
- console.error(`\u66F4\u65B0\u5931\u8D25: ${e.message}`);
4896
- process.exit(1);
4897
- }
5607
+ if (progressCallback) {
5608
+ progressCallback("\u89E3\u538B\u5185\u6838...");
5609
+ }
5610
+ const extractPath = DIRS.kernel;
5611
+ let extractedBinary = null;
5612
+ try {
5613
+ if (tempPath.endsWith(".tar.gz") || tempPath.endsWith(".tgz")) {
5614
+ execSync4(`tar -xzf "${tempPath}" -C "${extractPath}"`, { stdio: ["pipe", "pipe", "inherit"] });
5615
+ } else if (tempPath.endsWith(".gz")) {
5616
+ const baseName = path5.basename(tempPath, ".gz");
5617
+ const outputPath = path5.join(extractPath, baseName);
5618
+ execSync4(`gzip -dc "${tempPath}" > "${outputPath}"`, { stdio: ["pipe", "pipe", "inherit"] });
5619
+ extractedBinary = outputPath;
4898
5620
  }
4899
- console.log(`\u6253\u5F00\u8BA2\u9605\u9875\u9762: ${webPageUrl}`);
4900
- const opened = openUrl(webPageUrl);
4901
- if (!opened) {
4902
- console.log("\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740");
5621
+ } catch (e) {
5622
+ try {
5623
+ fs7.unlinkSync(tempPath);
5624
+ } catch {
4903
5625
  }
4904
- return;
5626
+ throw new Error(`\u89E3\u538B\u5931\u8D25: ${e.message}`);
4905
5627
  }
4906
- if (action === "remove" || action === "rm" || action === "delete") {
4907
- const name = args[2];
4908
- const subs = getSubscriptions();
4909
- if (!name) {
4910
- console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8981\u5220\u9664\u7684\u8BA2\u9605\u540D\u79F0");
4911
- if (subs.length > 0) {
4912
- console.log("\n\u53EF\u7528\u8BA2\u9605:");
4913
- for (const s of subs) console.log(` ${s.name}`);
4914
- }
4915
- process.exit(1);
4916
- }
4917
- const matches = findSubscriptionFuzzy(subs, name);
4918
- const target = pickSingleSubscription(matches, name);
4919
- const switchedTo = removeSubscription(target.name);
4920
- console.log(`\u5DF2\u5220\u9664\u8BA2\u9605 "${target.name}"`);
4921
- if (switchedTo) {
4922
- console.log(`\u5DF2\u81EA\u52A8\u5207\u6362\u5230 "${switchedTo}"`);
5628
+ const foundBinary = extractedBinary || findBinaryInDir(extractPath);
5629
+ if (!foundBinary) {
5630
+ try {
5631
+ fs7.unlinkSync(tempPath);
5632
+ } catch {
4923
5633
  }
4924
- console.log("");
4925
- await printSubscriptionList({ autoUpdate: false });
4926
- return;
5634
+ throw new Error("\u89E3\u538B\u540E\u672A\u627E\u5230\u53EF\u6267\u884C\u6587\u4EF6");
4927
5635
  }
4928
- if (action === "clean") {
4929
- const { target, timeout, concurrency } = resolveActiveTestTarget(args);
4930
- console.log(`\u6E05\u7406\u8BA2\u9605 "${target.name}"...`);
4931
- console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
4932
- console.log("");
4933
- const result = await autoCleanSubscription(target.name, {
4934
- timeout,
4935
- concurrency,
4936
- onResult: printTestResult
4937
- });
4938
- console.log("");
4939
- console.log(formatTestSummary(result.summary));
4940
- if (result.skipped) {
4941
- console.log("");
4942
- console.log(colors.yellow("\u5B58\u6D3B\u8282\u70B9\u4E0D\u8DB3 1%\uFF0C\u8DF3\u8FC7\u6E05\u7406\u3002\u8BF7\u68C0\u67E5\u539F\u59CB\u8BA2\u9605\u662F\u5426\u6709\u6548"));
4943
- } else if (result.removedProxies > 0) {
4944
- console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
4945
- console.log("");
4946
- console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
5636
+ const targetPath = PATHS.mihomoBinary;
5637
+ if (foundBinary !== targetPath) {
5638
+ if (fs7.existsSync(targetPath)) {
5639
+ fs7.chmodSync(targetPath, 493);
5640
+ try {
5641
+ fs7.unlinkSync(targetPath);
5642
+ } catch {
5643
+ }
4947
5644
  }
4948
- return;
5645
+ fs7.renameSync(foundBinary, targetPath);
4949
5646
  }
4950
- if (action === "test") {
4951
- const { target, timeout, concurrency } = resolveActiveTestTarget(args);
4952
- console.log(`\u6D4B\u8BD5\u8BA2\u9605 "${target.name}" \u7684\u8282\u70B9\u8FDE\u901A\u6027...`);
4953
- console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
4954
- console.log("");
4955
- const summary = await testSubscriptionProxies(target.name, {
4956
- timeout,
4957
- concurrency,
4958
- onResult: printTestResult
4959
- });
4960
- console.log("");
4961
- console.log(formatTestSummary(summary));
4962
- return;
5647
+ fs7.chmodSync(targetPath, 493);
5648
+ try {
5649
+ fs7.unlinkSync(tempPath);
5650
+ } catch {
4963
5651
  }
4964
- console.error("\u9519\u8BEF: \u672A\u77E5\u7684\u8BA2\u9605\u547D\u4EE4");
4965
- console.log("\u7528\u6CD5: mihomo sub [list|use|add|update|remove|web|test|clean]");
4966
- process.exit(1);
5652
+ clearKernelVersionCache();
5653
+ return { version: latest.tag_name, path: targetPath };
4967
5654
  }
4968
5655
 
4969
- // src/commands/start.ts
4970
- var AUTO_CLEAN_THRESHOLD = 100;
4971
- function handleStopResult(result) {
4972
- if (result.remaining && result.remaining.length > 0) {
4973
- console.error(`${colors.red("\u90E8\u5206\u8FDB\u7A0B\u672A\u7EC8\u6B62:")} ${result.remaining.join(", ")}`);
4974
- console.error("\u8BF7\u624B\u52A8\u8FD0\u884C: sudo pkill -9 mihomo");
4975
- process.exit(1);
4976
- }
4977
- }
4978
- async function cmdStart(args) {
4979
- if (!hasKernel()) {
4980
- console.error('\u9519\u8BEF: \u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
4981
- process.exit(1);
4982
- }
4983
- const targetMode = args[1] === "tun" ? "tun" : "mixed";
4984
- const sub = getActiveSubscription();
4985
- if (!sub) {
4986
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
4987
- process.exit(1);
4988
- }
4989
- await autoUpdateStaleSubscription();
4990
- const status = getStatus();
4991
- const hasProcess = status.running || status.allProcesses.length > 0;
4992
- if (hasProcess) {
4993
- const count = status.allProcesses.length > 0 ? status.allProcesses.length : 1;
4994
- console.log(`\u505C\u6B62 ${count} \u4E2A\u8FDB\u7A0B...`);
5656
+ // src/commands/kernel.ts
5657
+ async function cmdKernel(args) {
5658
+ const mirrorInfo = parseMirrorArg(args);
5659
+ const effectiveMirror = mirrorInfo.mirror;
5660
+ if (effectiveMirror) {
5661
+ const mirrorDesc = mirrorInfo.type === "all" ? " (API\u548C\u4E0B\u8F7D\u5747\u4F7F\u7528\u955C\u50CF)" : " (\u4E0B\u8F7D\u65F6\u4F7F\u7528\u955C\u50CF)";
5662
+ console.log(`\u955C\u50CF: ${effectiveMirror}${mirrorDesc}`);
4995
5663
  }
4996
- handleStopResult(stop());
4997
- if (hasProcess) {
4998
- console.log(`${colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B")}
4999
- `);
5664
+ console.log("\n\u63D0\u793A: \u5982\u679C\u4E0B\u8F7D\u901F\u5EA6\u8FC7\u6162\u6216\u76F4\u8FDE\u5931\u8D25\uFF0C\u53EF\u4F7F\u7528 --mirror \u53C2\u6570\u901A\u8FC7\u955C\u50CF\u4E0B\u8F7D");
5665
+ console.log("\n\u7528\u6CD5:");
5666
+ console.log(" mihomo kernel # \u76F4\u8FDE");
5667
+ console.log(" mihomo kernel --mirror # \u4E0B\u8F7D\u4F7F\u7528\u9ED8\u8BA4\u955C\u50CF (v6.gh-proxy.org)");
5668
+ console.log(" mihomo kernel --mirror hk.gh-proxy.org # \u4E0B\u8F7D\u4F7F\u7528\u6307\u5B9A\u955C\u50CF");
5669
+ console.log(" mihomo kernel --mirror-all # API\u8BF7\u6C42\u548C\u4E0B\u8F7D\u90FD\u4F7F\u7528\u9ED8\u8BA4\u955C\u50CF");
5670
+ console.log(" mihomo kernel --mirror-all hk.gh-proxy.org # API\u548C\u4E0B\u8F7D\u90FD\u4F7F\u7528\u6307\u5B9A\u955C\u50CF");
5671
+ console.log("\n\u53EF\u7528\u955C\u50CF:");
5672
+ for (const m of AVAILABLE_MIRRORS) {
5673
+ const isCurrent = effectiveMirror && (effectiveMirror.includes(`//${m}/`) || effectiveMirror.includes(`//${m}:`) || effectiveMirror.endsWith(`//${m}`));
5674
+ console.log(` ${m}${isCurrent ? " (\u5F53\u524D)" : ""}`);
5000
5675
  }
5001
- let configInfo;
5676
+ console.log("");
5677
+ console.log("\u68C0\u67E5\u5185\u6838\u66F4\u65B0...");
5002
5678
  try {
5003
- configInfo = prepareConfigForStart(targetMode, sub.name);
5679
+ const apiMirror = mirrorInfo.type === "all" ? effectiveMirror : null;
5680
+ const info = await checkUpdate(apiMirror);
5681
+ console.log(`\u5F53\u524D: ${info.current}`);
5682
+ console.log(`\u6700\u65B0: ${info.latest}`);
5683
+ if (!info.needsUpdate) {
5684
+ console.log("\u5DF2\u662F\u6700\u65B0\u7248\u672C");
5685
+ } else {
5686
+ console.log("\n\u6B63\u5728\u4E0B\u8F7D...");
5687
+ const result = await downloadKernel((msg) => console.log(msg), mirrorInfo.mirror, info.release);
5688
+ console.log(`
5689
+ \u5DF2\u66F4\u65B0\u5230 ${result.version}`);
5690
+ }
5004
5691
  } catch (e) {
5005
- console.error(`${colors.red("\u914D\u7F6E\u9519\u8BEF:")} ${e.message}`);
5692
+ console.error(`
5693
+ \u66F4\u65B0\u5931\u8D25: ${e.message}`);
5694
+ const err = e;
5695
+ if (err.response?.data) {
5696
+ if (err.response.data.message) {
5697
+ console.error(`\u539F\u56E0: ${err.response.data.message}`);
5698
+ }
5699
+ if (err.response.data.documentation_url) {
5700
+ console.error(`\u6587\u6863: ${err.response.data.documentation_url}`);
5701
+ }
5702
+ }
5006
5703
  process.exit(1);
5007
5704
  }
5008
- const modeLabel = targetMode === "tun" ? "TUN" : "Mixed";
5009
- console.log([colors.cyan(modeLabel), sub.name, formatProxySummary(configInfo)].join(" \xB7 "));
5010
- try {
5011
- const result = await start(targetMode);
5012
- console.log(`${colors.green("\u5DF2\u542F\u52A8")} (PID ${result.pid})`);
5013
- } catch (e) {
5014
- console.error(`${colors.red("\u542F\u52A8\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
5015
- process.exit(1);
5705
+ }
5706
+
5707
+ // src/commands/log.ts
5708
+ function cmdLog(args) {
5709
+ const logPath = getLogPath();
5710
+ if (hasFlag(args, "-o", "--open")) {
5711
+ openLogFile(logPath);
5712
+ return;
5016
5713
  }
5017
- if (configInfo.proxies > AUTO_CLEAN_THRESHOLD) {
5018
- console.log("");
5019
- console.log(`\u8282\u70B9\u6570 ${configInfo.proxies} \u8D85\u8FC7 ${AUTO_CLEAN_THRESHOLD}\uFF0C\u81EA\u52A8\u6E05\u7406...`);
5020
- console.log("");
5021
- await sleep(1e3);
5022
- const cleanResult = await autoCleanSubscription(sub.name, { onResult: printTestResult });
5023
- console.log("");
5024
- console.log(formatTestSummary(cleanResult.summary));
5025
- if (cleanResult.skipped) {
5026
- console.log(colors.yellow("\u5B58\u6D3B\u8282\u70B9\u4E0D\u8DB3 1%\uFF0C\u8DF3\u8FC7\u6E05\u7406\u3002\u8BF7\u68C0\u67E5\u539F\u59CB\u8BA2\u9605\u662F\u5426\u6709\u6548"));
5027
- } else if (cleanResult.removedProxies > 0) {
5028
- console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(cleanResult)}`);
5029
- console.log("");
5030
- console.log("\u91CD\u65B0\u52A0\u8F7D\u914D\u7F6E...");
5031
- handleStopResult(stop());
5032
- try {
5033
- configInfo = prepareConfigForStart(targetMode, sub.name);
5034
- const result = await start(targetMode);
5035
- console.log(`${colors.green("\u5DF2\u91CD\u542F")} (PID ${result.pid}) \xB7 ${formatProxySummary(configInfo)}`);
5036
- } catch (e) {
5037
- console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
5038
- process.exit(1);
5714
+ viewLogWithTail(logPath, { follow: true, lines: 50 });
5715
+ }
5716
+ function cmdLogs(args) {
5717
+ const targetName = getNonFlagArg(args, 1);
5718
+ const lines = parseIntArg(args, "-n", "--lines", 100);
5719
+ const openInViewer = hasFlag(args, "-o", "--open");
5720
+ if (targetName) {
5721
+ let logPath;
5722
+ if (targetName === "current" || targetName === "0") {
5723
+ logPath = getLogPath();
5724
+ } else {
5725
+ const parsedIdx = parseInt(targetName, 10);
5726
+ if (!Number.isNaN(parsedIdx) && parsedIdx > 0 && String(parsedIdx) === targetName) {
5727
+ const archiveLogs = listLogs();
5728
+ const archive = archiveLogs.archives[parsedIdx - 1];
5729
+ if (!archive) {
5730
+ console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`);
5731
+ console.log('\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868');
5732
+ process.exit(1);
5733
+ }
5734
+ logPath = archive.path;
5735
+ } else {
5736
+ logPath = getLogPathByName(targetName);
5039
5737
  }
5040
5738
  }
5739
+ if (!logPath) {
5740
+ console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`);
5741
+ console.log('\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868');
5742
+ process.exit(1);
5743
+ }
5744
+ if (openInViewer) {
5745
+ openLogFile(logPath);
5746
+ return;
5747
+ }
5748
+ viewLogWithTail(logPath, { follow: false, lines });
5749
+ return;
5041
5750
  }
5042
- printStatus();
5751
+ const logs = listLogs();
5752
+ const all = [];
5753
+ if (logs.current) all.push(logs.current);
5754
+ all.push(...logs.archives);
5755
+ if (all.length === 0) {
5756
+ console.log("\u6682\u65E0\u65E5\u5FD7");
5757
+ return;
5758
+ }
5759
+ console.log("");
5760
+ console.log("\u65E5\u5FD7\u5217\u8868:");
5761
+ console.log("");
5762
+ let archiveCounter = 0;
5763
+ for (const log of all) {
5764
+ let num;
5765
+ if (log.isCurrent) {
5766
+ num = " 0";
5767
+ } else {
5768
+ archiveCounter++;
5769
+ num = archiveCounter < 10 ? ` ${archiveCounter}` : `${archiveCounter}`;
5770
+ }
5771
+ const time = formatDate(log.mtime);
5772
+ const size = formatBytes(log.size);
5773
+ const name = log.isCurrent ? "mihomo.log (\u5F53\u524D\u8FD0\u884C\u4E2D)" : log.name;
5774
+ console.log(` ${num}. ${name}`);
5775
+ console.log(` \u65F6\u95F4: ${time} \u5927\u5C0F: ${size}`);
5776
+ if (!log.isCurrent) {
5777
+ console.log(` \u67E5\u770B: mihomo logs ${archiveCounter} \u6216 mihomo logs ${archiveCounter} -o`);
5778
+ }
5779
+ console.log("");
5780
+ }
5781
+ console.log("\u7528\u6CD5:");
5782
+ console.log(" mihomo logs 0 # \u67E5\u770B\u5F53\u524D\u65E5\u5FD7 (\u6700\u540E 100 \u884C)");
5783
+ console.log(" mihomo logs 1 # \u67E5\u770B\u7B2C 1 \u4E2A\u5F52\u6863\u65E5\u5FD7\uFF08\u6700\u65B0\uFF09");
5784
+ console.log(" mihomo logs 1 -n 200 # \u67E5\u770B 200 \u884C");
5785
+ console.log(" mihomo logs 1 -o # \u7528\u7CFB\u7EDF\u9ED8\u8BA4\u7A0B\u5E8F\u6253\u5F00");
5786
+ console.log("");
5043
5787
  }
5044
5788
 
5045
5789
  // src/commands/overwrite.ts
5790
+ import path6 from "path";
5046
5791
  function printOverwriteList() {
5047
5792
  const info = listOverwriteFile();
5048
5793
  const statusText = info.enabled ? colors.green("\u5DF2\u542F\u7528") : colors.yellow("\u5DF2\u7981\u7528");
@@ -5052,8 +5797,8 @@ function printOverwriteList() {
5052
5797
  if (info.files.length === 0) {
5053
5798
  console.log("\u6682\u65E0\u8986\u5199\u6587\u4EF6");
5054
5799
  console.log("");
5055
- console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path5.join(info.dir, "overwrite.yaml")}`);
5056
- console.log(` \u6216 ${path5.join(info.dir, "overwrite.dns.yaml")}`);
5800
+ console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path6.join(info.dir, "overwrite.yaml")}`);
5801
+ console.log(` \u6216 ${path6.join(info.dir, "overwrite.dns.yaml")}`);
5057
5802
  console.log("");
5058
5803
  } else {
5059
5804
  console.log(`${colors.cyan("\u8986\u5199\u6587\u4EF6")} (${info.files.length} \u4E2A\uFF0C\u6309\u987A\u5E8F\u52A0\u8F7D):`);
@@ -5117,7 +5862,7 @@ async function cmdOverwrite(args) {
5117
5862
  }
5118
5863
 
5119
5864
  // src/commands/reset.ts
5120
- import fs7 from "fs";
5865
+ import fs8 from "fs";
5121
5866
  import readline from "readline";
5122
5867
  var RESET_TARGETS = [
5123
5868
  {
@@ -5172,8 +5917,8 @@ var RESET_TARGETS = [
5172
5917
  label: "\u8986\u5199",
5173
5918
  paths: () => {
5174
5919
  const dir = USER_DATA_DIR;
5175
- if (!fs7.existsSync(dir)) return [];
5176
- return fs7.readdirSync(dir).filter((f) => f === "overwrite.yaml" || /^overwrite\..+\.ya?ml$/.test(f)).map((f) => `${dir}/${f}`);
5920
+ if (!fs8.existsSync(dir)) return [];
5921
+ return fs8.readdirSync(dir).filter((f) => f === "overwrite.yaml" || /^overwrite\..+\.ya?ml$/.test(f)).map((f) => `${dir}/${f}`);
5177
5922
  },
5178
5923
  needsStop: false
5179
5924
  }
@@ -5307,7 +6052,7 @@ function cmdUI(args) {
5307
6052
  }
5308
6053
 
5309
6054
  // src/commands/update.ts
5310
- import { exec, spawn as spawn2 } from "child_process";
6055
+ import { exec, spawn as spawn3 } from "child_process";
5311
6056
  import { promisify } from "util";
5312
6057
  var execAsync = promisify(exec);
5313
6058
  async function cmdUpdate() {
@@ -5316,7 +6061,7 @@ async function cmdUpdate() {
5316
6061
  console.log("\u6B63\u5728\u66F4\u65B0 mihomo-cli...");
5317
6062
  console.log("");
5318
6063
  await new Promise((resolve) => {
5319
- const npm = spawn2("npm", ["install", "-g", "mihomo-cli"], { stdio: "inherit" });
6064
+ const npm = spawn3("npm", ["install", "-g", "mihomo-cli"], { stdio: "inherit" });
5320
6065
  npm.on("close", (code) => {
5321
6066
  if (code === 0) {
5322
6067
  resolve();
@@ -5452,6 +6197,9 @@ async function main() {
5452
6197
  case "overwrite":
5453
6198
  await cmdOverwrite(args);
5454
6199
  break;
6200
+ case "bench":
6201
+ await cmdBench(args);
6202
+ break;
5455
6203
  default:
5456
6204
  console.error(`\u672A\u77E5\u547D\u4EE4: ${cmd}`);
5457
6205
  console.error('\u4F7F\u7528 "mihomo help" \u67E5\u770B\u5E2E\u52A9');