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