mihomo-cli 2.2.4 → 2.3.0

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