mihomo-cli 2.6.3 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,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,1814 +3924,1601 @@ 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 };
4455
- }
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}`);
4463
- }
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);
4479
- }
4480
- }
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}`);
4521
- }
4522
- console.log("");
4523
- }
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;
4540
- }
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
- }
4553
- }
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 };
4578
- }
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
- }
4601
- }
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: [] };
4607
- }
4608
- const client = createHttpClient({ timeout: timeout + 3e3 });
4609
- const results = new Array(proxies.length);
4610
- let completedCount = 0;
4611
- let nextIndex = 0;
4612
- async function runNext() {
4613
- while (nextIndex < proxies.length) {
4614
- const idx = nextIndex++;
4615
- const result = await testProxyDelay(proxies[idx].name, timeout, testUrl, client, apiBase);
4616
- results[idx] = result;
4617
- onResult?.(result, completedCount, proxies.length);
4618
- completedCount++;
4619
- }
4620
- }
4621
- const workers = Array.from({ length: Math.min(concurrency, proxies.length) }, () => runNext());
4622
- await Promise.all(workers);
4623
- const alive = results.filter((r) => r.delay !== null).length;
4624
- return { total: results.length, alive, dead: results.length - alive, results };
4625
- }
4626
- function normalizeProxyNamesBeforeSave(parsed) {
4627
- const { proxies, proxyGroups } = parsed;
4628
- const renameMap = /* @__PURE__ */ new Map();
4629
- const usedNames = /* @__PURE__ */ new Set();
4630
- for (const proxy of proxies) {
4631
- const shortened = proxy.name.replace(/_github\.com\/[^_]+/, "");
4632
- if (shortened !== proxy.name && !usedNames.has(shortened)) {
4633
- renameMap.set(proxy.name, shortened);
4634
- usedNames.add(shortened);
4635
- } else {
4636
- usedNames.add(proxy.name);
4637
- }
4638
- }
4639
- if (renameMap.size === 0) return 0;
4640
- for (const proxy of proxies) {
4641
- const newName = renameMap.get(proxy.name);
4642
- if (newName) proxy.name = newName;
4643
- }
4644
- for (const group of proxyGroups) {
4645
- if (Array.isArray(group.proxies)) {
4646
- group.proxies = group.proxies.map((name) => renameMap.get(name) || name);
4647
- }
4648
- }
4649
- return renameMap.size;
4650
- }
4651
- function cleanDeadProxies(parsed, deadNames) {
4652
- const { proxies, proxyGroups } = parsed;
4653
- const originalCount = proxies.length;
4654
- parsed.proxies = proxies.filter((p) => !deadNames.has(p.name));
4655
- const removedProxies = originalCount - parsed.proxies.length;
4656
- let updatedGroups = 0;
4657
- const removedGroupNames = /* @__PURE__ */ new Set();
4658
- for (const group of proxyGroups) {
4659
- if (Array.isArray(group.proxies)) {
4660
- const before = group.proxies.length;
4661
- group.proxies = group.proxies.filter((name) => !deadNames.has(name));
4662
- if (group.proxies.length < before) {
4663
- updatedGroups++;
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}`);
4664
3937
  }
4665
- if (group.proxies.length === 0) {
4666
- removedGroupNames.add(group.name);
3938
+ return;
3939
+ }
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}`);
4667
3947
  }
3948
+ return;
4668
3949
  }
4669
- }
4670
- if (removedGroupNames.size > 0) {
4671
- parsed.proxyGroups = proxyGroups.filter((g) => !removedGroupNames.has(g.name));
4672
- for (const group of parsed.proxyGroups) {
4673
- if (Array.isArray(group.proxies)) {
4674
- group.proxies = group.proxies.filter((name) => !removedGroupNames.has(name));
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}`);
4675
3957
  }
4676
3958
  }
3959
+ console.log("");
3960
+ process.exit(1);
4677
3961
  }
4678
- return { removedProxies, updatedGroups, removedGroups: removedGroupNames.size };
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("");
4679
3988
  }
4680
- async function autoCleanSubscription(subName, options = {}) {
4681
- const parsed = loadSubscriptionConfig(subName);
4682
- const { onResult, onRetryRound, ...testOptions } = options;
4683
- const wrapOnResult = (round) => onResult ? (r, i, t) => onResult(r, i, t, round) : void 0;
4684
- const summary = await testSubscriptionProxies(subName, {
4685
- ...testOptions,
4686
- parsed,
4687
- onResult: wrapOnResult(1)
4688
- });
4689
- let removedProxies = 0;
4690
- let updatedGroups = 0;
4691
- let removedGroups = 0;
4692
- let skipped = false;
4693
- if (summary.dead > 0) {
4694
- if (summary.alive === 0 || summary.alive / summary.total < 0.01) {
4695
- skipped = true;
4696
- } else {
4697
- const deadNames = new Set(summary.results.filter((r) => r.delay === null).map((r) => r.name));
4698
- const deadProxies = parsed.proxies.filter((p) => deadNames.has(p.name));
4699
- for (let retry = 0; retry < 2; retry++) {
4700
- const round = retry + 2;
4701
- const retryTargets = deadProxies.filter((p) => deadNames.has(p.name));
4702
- if (retryTargets.length === 0) break;
4703
- onRetryRound?.(round, retryTargets.length);
4704
- const retryParsed = { raw: {}, proxies: retryTargets, proxyGroups: [] };
4705
- const retrySummary = await testSubscriptionProxies(subName, {
4706
- ...testOptions,
4707
- parsed: retryParsed,
4708
- onResult: wrapOnResult(round)
4709
- });
4710
- for (const r of retrySummary.results) {
4711
- if (r.delay !== null) {
4712
- deadNames.delete(r.name);
4713
- }
4714
- }
4715
- }
4716
- summary.dead = deadNames.size;
4717
- summary.alive = summary.total - summary.dead;
4718
- if (deadNames.size > 0) {
4719
- const cleanResult = cleanDeadProxies(parsed, deadNames);
4720
- removedProxies = cleanResult.removedProxies;
4721
- updatedGroups = cleanResult.updatedGroups;
4722
- removedGroups = cleanResult.removedGroups;
4723
- }
4724
- }
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
+ );
4003
+ }
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
+ );
4064
+ }
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}`);
4070
+ }
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");
4725
4082
  }
4726
- if (!skipped && removedProxies > 0) {
4727
- saveSubscriptionConfig(subName, parsed);
4083
+ const match = version.match(semver);
4084
+ if (!match) {
4085
+ throw new Error(`Invalid argument not valid semver ('${version}' received)`);
4086
+ }
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;
4728
4111
  }
4729
- return { summary, removedProxies, updatedGroups, removedGroups, skipped };
4730
- }
4731
-
4732
- // src/test-instance.ts
4733
- import { spawn as spawn3 } from "child_process";
4734
- import fs7 from "fs";
4735
- import path5 from "path";
4736
- var TEST_DIR = path5.join(USER_DATA_DIR, "test");
4737
- var TEST_DIRS = {
4738
- data: path5.join(TEST_DIR, "data"),
4739
- runtime: path5.join(TEST_DIR, "runtime")
4112
+ return 0;
4740
4113
  };
4741
- var TEST_PATHS = {
4742
- configFile: path5.join(TEST_DIRS.runtime, "config.yaml"),
4743
- pidFile: path5.join(TEST_DIRS.runtime, "pid"),
4744
- logFile: path5.join(TEST_DIR, "test.log")
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;
4128
+ }
4129
+ return 0;
4745
4130
  };
4746
- var TEST_API = `http://${TEST_CONFIG["external-controller"]}`;
4747
- function ensureTestDirs() {
4748
- for (const dir of Object.values(TEST_DIRS)) {
4749
- fs7.mkdirSync(dir, { recursive: true, mode: 448 });
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;
4750
4140
  }
4141
+ return url;
4751
4142
  }
4752
- function cleanupTestDir() {
4753
- rmrf(TEST_DIR);
4754
- }
4755
- function buildTestConfig(subName) {
4756
- ensureTestDirs();
4757
- const rawContent = readSubscriptionRawConfig(subName);
4758
- if (!rawContent) {
4759
- throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
4760
- }
4761
- const parsed = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
4762
- const proxies = (parsed.proxies || []).filter(isProxyValid);
4763
- if (proxies.length === 0) {
4764
- throw new Error(`\u8BA2\u9605 "${subName}" \u6CA1\u6709\u6709\u6548\u8282\u70B9`);
4765
- }
4766
- const nameCount = /* @__PURE__ */ new Map();
4767
- for (const proxy of proxies) {
4768
- const count = (nameCount.get(proxy.name) || 0) + 1;
4769
- nameCount.set(proxy.name, count);
4770
- if (count > 1) {
4771
- proxy.name = `${proxy.name} #${count}`;
4772
- }
4773
- }
4774
- const config = {
4775
- ...TEST_CONFIG,
4776
- proxies,
4777
- "proxy-groups": [
4778
- {
4779
- name: "PROXY",
4780
- type: "select",
4781
- proxies: proxies.map((p) => p.name)
4782
- }
4783
- ],
4784
- rules: ["MATCH,PROXY"]
4785
- };
4786
- const content = jsYaml.dump(config, { indent: 2, lineWidth: -1, noCompatMode: true });
4787
- fs7.writeFileSync(TEST_PATHS.configFile, content, { mode: 384 });
4143
+ function getArch() {
4144
+ const arch = process.arch;
4145
+ if (arch === "arm64") return "arm64";
4146
+ if (arch === "x64") return "amd64";
4147
+ return arch;
4788
4148
  }
4789
- async function startTestInstance() {
4790
- const binary2 = PATHS.mihomoBinary;
4791
- if (!fs7.existsSync(binary2)) throw new Error("\u672A\u627E\u5230 mihomo \u5185\u6838");
4792
- stopTestInstance();
4793
- const logFd = fs7.openSync(TEST_PATHS.logFile, "a");
4794
- const child = spawn3(binary2, ["-d", TEST_DIRS.data, "-f", TEST_PATHS.configFile], {
4795
- detached: true,
4796
- stdio: ["ignore", logFd, logFd]
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");
4797
4161
  });
4798
- fs7.closeSync(logFd);
4799
- child.unref();
4800
- const pid = child.pid;
4801
- fs7.writeFileSync(TEST_PATHS.pidFile, pid.toString(), { mode: 384 });
4802
- const client = createHttpClient({ timeout: 2e3 });
4803
- let ready = false;
4804
- for (let i = 0; i < 60; i++) {
4805
- if (!isProcessRunning(pid)) break;
4806
- try {
4807
- await client.get(`${TEST_API}/version`);
4808
- ready = true;
4809
- break;
4810
- } catch {
4811
- await sleep(500);
4812
- }
4162
+ return standardAsset || matchingAssets[0];
4163
+ }
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");
4813
4170
  }
4814
- if (!isProcessRunning(pid)) {
4815
- let errorDetail = "";
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];
4175
+ }
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 {
4816
4185
  try {
4817
- errorDetail = fs7.readFileSync(TEST_PATHS.logFile, "utf8").slice(-1e3);
4186
+ needsUpdate = compareVersions(latestVersion.replace(/^v/, ""), currentVersion.replace(/^v/, "")) > 0;
4818
4187
  } catch {
4188
+ needsUpdate = latestVersion !== currentVersion;
4819
4189
  }
4820
- throw new Error(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
4821
- ${errorDetail}` : ""}`);
4822
- }
4823
- if (!ready) {
4824
- throw new Error("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u8D85\u65F6\uFF0CAPI \u672A\u54CD\u5E94");
4825
- }
4826
- }
4827
- function stopTestInstance() {
4828
- let pid;
4829
- try {
4830
- pid = parseInt(fs7.readFileSync(TEST_PATHS.pidFile, "utf8").trim(), 10);
4831
- } catch {
4832
- return;
4833
- }
4834
- if (pid > 0 && isProcessRunning(pid)) {
4835
- process.kill(pid, "SIGKILL");
4836
- for (let i = 0; i < 20; i++) {
4837
- if (!isProcessRunning(pid)) break;
4838
- sleepSync(100);
4839
- }
4840
- }
4841
- try {
4842
- fs7.unlinkSync(TEST_PATHS.pidFile);
4843
- } catch {
4844
- }
4845
- }
4846
- async function withTestInstance(subName, fn) {
4847
- cleanupTestDir();
4848
- buildTestConfig(subName);
4849
- try {
4850
- await startTestInstance();
4851
- return await fn(TEST_API);
4852
- } finally {
4853
- stopTestInstance();
4854
- cleanupTestDir();
4855
4190
  }
4191
+ return {
4192
+ current: currentDisplay,
4193
+ latest: latestVersion,
4194
+ needsUpdate,
4195
+ assets: latest.assets,
4196
+ release: latest
4197
+ };
4856
4198
  }
4857
-
4858
- // src/commands/status.ts
4859
- function printStatus() {
4860
- const status = getStatus();
4861
- const info = getConfigInfo();
4862
- const overwriteEnabled = isOverwriteEnabled();
4863
- const overwriteFiles = listOverwriteFile().files;
4864
- const activeSub = getActiveSubscription();
4865
- console.log("");
4866
- let modeLabel = "";
4867
- if (info && status.running) {
4868
- modeLabel = colors.cyan(info.tun ? " (TUN)" : " (Mixed)");
4869
- }
4870
- const statusText = status.running ? colors.green("\u25CF \u8FD0\u884C\u4E2D") : colors.yellow("\u4E0D\u5728\u8FD0\u884C");
4871
- console.log(`${colors.gray("\u72B6\u6001: ")}${statusText}${modeLabel}`);
4872
- console.log(`${colors.gray("\u5185\u6838: ")}${status.kernelVersion || "\u672A\u5B89\u88C5"}`);
4873
- if (status.pid) {
4874
- console.log(`${colors.gray("PID: ")}${status.pid}`);
4875
- if (status.processInfo) {
4876
- console.log(`${colors.gray("\u5185\u5B58: ")}${status.processInfo.memory}`);
4877
- }
4878
- }
4879
- if (info) {
4880
- if (info.mixedPort) {
4881
- console.log(`${colors.gray("\u7AEF\u53E3: ")}${info.mixedPort}`);
4882
- } else {
4883
- const ports = [];
4884
- if (info.httpPort) ports.push(`HTTP:${info.httpPort}`);
4885
- if (info.socksPort) ports.push(`SOCKS:${info.socksPort}`);
4886
- console.log(`${colors.gray("\u7AEF\u53E3: ")}${ports.length > 0 ? ports.join(", ") : "\u672A\u77E5"}`);
4887
- }
4888
- }
4889
- if (activeSub) {
4890
- let subLine = `${colors.gray("\u8BA2\u9605: ")}${activeSub.name}`;
4891
- if (info) {
4892
- subLine += ` (${formatProxySummary(info)})`;
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;
4893
4208
  }
4894
- console.log(subLine);
4895
- } else {
4896
- console.log(`${colors.gray("\u8BA2\u9605: ")}\u672A\u914D\u7F6E`);
4897
- }
4898
- if (overwriteEnabled && overwriteFiles.length > 0) {
4899
- const names = overwriteFiles.map((f) => f.name.replace(/^overwrite\.?/, "").replace(/\.ya?ml$/, "") || "\u4E3B\u6587\u4EF6").join(", ");
4900
- console.log(`${colors.gray("\u8986\u5199: ")}${colors.green("\u5DF2\u542F\u7528")} (${names})`);
4901
- } else if (overwriteEnabled) {
4902
- console.log(`${colors.gray("\u8986\u5199: ")}${colors.green("\u5DF2\u542F\u7528")} (\u65E0\u6587\u4EF6)`);
4903
- } else {
4904
- console.log(`${colors.gray("\u8986\u5199: ")}${colors.yellow("\u5DF2\u7981\u7528")}`);
4905
- }
4906
- console.log("");
4907
- }
4908
-
4909
- // src/commands/start.ts
4910
- var AUTO_CLEAN_THRESHOLD = 50;
4911
- function handleStopResult(result) {
4912
- if (result.remaining && result.remaining.length > 0) {
4913
- console.error(`${colors.red("\u90E8\u5206\u8FDB\u7A0B\u672A\u7EC8\u6B62:")} ${result.remaining.join(", ")}`);
4914
- console.error("\u8BF7\u624B\u52A8\u8FD0\u884C: sudo pkill -9 mihomo");
4915
- process.exit(1);
4209
+ if (f === "mihomo") return fullPath;
4210
+ if (f.includes("mihomo") && !f.endsWith(".gz")) return fullPath;
4916
4211
  }
4212
+ return null;
4917
4213
  }
4918
- async function cmdStart(args) {
4919
- if (!hasKernel()) {
4920
- console.error('\u9519\u8BEF: \u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
4921
- process.exit(1);
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}`);
4922
4227
  }
4923
- const targetMode = args[1] === "tun" ? "tun" : "mixed";
4924
- const sub = getActiveSubscription();
4925
- if (!sub) {
4926
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
4927
- process.exit(1);
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)`);
4928
4233
  }
4929
- await autoUpdateStaleSubscription();
4930
- const status = getStatus();
4931
- const hasProcess = status.running || status.allProcesses.length > 0;
4932
- if (hasProcess) {
4933
- const count = status.allProcesses.length > 0 ? status.allProcesses.length : 1;
4934
- console.log(`\u505C\u6B62 ${count} \u4E2A\u8FDB\u7A0B...`);
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) {
4240
+ try {
4241
+ fs6.unlinkSync(tempPath);
4242
+ } catch {
4243
+ }
4244
+ throw new Error(`\u4E0B\u8F7D\u5931\u8D25 (curl \u9000\u51FA\u7801 ${curlResult.status})`);
4935
4245
  }
4936
- handleStopResult(stop());
4937
- if (hasProcess) {
4938
- console.log(`${colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B")}
4939
- `);
4246
+ if (!fs6.existsSync(tempPath)) {
4247
+ throw new Error("\u4E0B\u8F7D\u5931\u8D25: \u6587\u4EF6\u672A\u751F\u6210");
4940
4248
  }
4941
- let configInfo;
4942
- try {
4943
- configInfo = prepareConfigForStart(targetMode, sub.name);
4944
- } catch (e) {
4945
- console.error(`${colors.red("\u914D\u7F6E\u9519\u8BEF:")} ${e.message}`);
4946
- process.exit(1);
4249
+ if (progressCallback) {
4250
+ progressCallback("\u89E3\u538B\u5185\u6838...");
4947
4251
  }
4948
- const modeLabel = targetMode === "tun" ? "TUN" : "Mixed";
4949
- console.log([colors.cyan(modeLabel), sub.name, formatProxySummary(configInfo)].join(" \xB7 "));
4252
+ const extractPath = DIRS.kernel;
4253
+ let extractedBinary = null;
4950
4254
  try {
4951
- const result = await start(targetMode);
4952
- console.log(`${colors.green("\u5DF2\u542F\u52A8")} (PID ${result.pid})`);
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
+ }
4953
4263
  } catch (e) {
4954
- const msg = e.message;
4955
- const lines = msg.split("\n");
4956
- console.error(`${colors.red("\u542F\u52A8\u5931\u8D25:")} ${lines[0]}`);
4957
- if (lines.length > 1) {
4958
- for (const line of lines.slice(1)) console.error(line);
4264
+ try {
4265
+ fs6.unlinkSync(tempPath);
4266
+ } catch {
4959
4267
  }
4960
- process.exit(1);
4268
+ throw new Error(`\u89E3\u538B\u5931\u8D25: ${e.message}`);
4961
4269
  }
4962
- if (configInfo.proxies > AUTO_CLEAN_THRESHOLD) {
4963
- console.log("");
4964
- console.log(`\u8282\u70B9\u6570 ${configInfo.proxies} \u8D85\u8FC7 ${AUTO_CLEAN_THRESHOLD}\uFF0C\u81EA\u52A8\u6E05\u7406...`);
4965
- console.log("");
4966
- await sleep(1e3);
4967
- const progress = createProgressPrinter();
4968
- const cleanResult = await autoCleanSubscription(sub.name, {
4969
- onResult: progress.onResult,
4970
- onRetryRound: progress.onRetryRound
4971
- });
4972
- progress.finish();
4973
- console.log(formatTestSummary(cleanResult.summary));
4974
- if (cleanResult.skipped) {
4975
- 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"));
4976
- } else if (cleanResult.removedProxies > 0) {
4977
- console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(cleanResult)}`);
4978
- console.log("");
4979
- console.log("\u91CD\u65B0\u52A0\u8F7D\u914D\u7F6E...");
4980
- handleStopResult(stop());
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");
4277
+ }
4278
+ const targetPath = PATHS.mihomoBinary;
4279
+ if (foundBinary !== targetPath) {
4280
+ if (fs6.existsSync(targetPath)) {
4281
+ fs6.chmodSync(targetPath, 493);
4981
4282
  try {
4982
- configInfo = prepareConfigForStart(targetMode, sub.name);
4983
- const result = await start(targetMode);
4984
- console.log(`${colors.green("\u5DF2\u91CD\u542F")} (PID ${result.pid}) \xB7 ${formatProxySummary(configInfo)}`);
4985
- } catch (e) {
4986
- console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
4987
- process.exit(1);
4283
+ fs6.unlinkSync(targetPath);
4284
+ } catch {
4988
4285
  }
4989
4286
  }
4287
+ fs6.renameSync(foundBinary, targetPath);
4990
4288
  }
4991
- printStatus();
4289
+ fs6.chmodSync(targetPath, 493);
4290
+ try {
4291
+ fs6.unlinkSync(tempPath);
4292
+ } catch {
4293
+ }
4294
+ clearKernelVersionCache();
4295
+ return { version: latest.tag_name, path: targetPath };
4992
4296
  }
4993
4297
 
4994
- // src/commands/subscription.ts
4995
- var IS_TTY = process.stdout.isTTY === true;
4996
- var BAR_WIDTH = 20;
4997
- function createProgressPrinter() {
4998
- let alive = 0;
4999
- let dead = 0;
5000
- let hasRetry = false;
5001
- const resultMap = /* @__PURE__ */ new Map();
5002
- function render(done, total) {
5003
- if (!IS_TTY) return;
5004
- const pct = Math.round(done / total * 100);
5005
- const filled = Math.round(done / total * BAR_WIDTH);
5006
- const bar = "\u2588".repeat(filled) + "\u2591".repeat(BAR_WIDTH - filled);
5007
- process.stdout.write(`\r${bar} ${done}/${total} (${pct}%) | ${colors.green(`\u2713${alive}`)} ${colors.red(`\u2717${dead}`)}`);
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}`);
5008
4305
  }
5009
- return {
5010
- onResult(result, index, total, round = 1) {
5011
- const prev = resultMap.get(result.name);
5012
- if (prev) {
5013
- if (prev.result.delay === null && result.delay !== null) {
5014
- alive++;
5015
- dead--;
5016
- }
5017
- } else {
5018
- if (result.delay !== null) alive++;
5019
- else dead++;
5020
- }
5021
- resultMap.set(result.name, { result, round });
5022
- render(index + 1, total);
5023
- },
5024
- onRetryRound(round, count) {
5025
- if (!hasRetry) {
5026
- hasRetry = true;
5027
- if (IS_TTY) {
5028
- process.stdout.write("\n");
5029
- }
5030
- console.log(`--- \u7B2C 1 \u8F6E\u6D4B\u8BD5 (${resultMap.size} \u4E2A\u8282\u70B9) ---`);
5031
- }
5032
- if (IS_TTY) {
5033
- process.stdout.write("\n");
5034
- }
5035
- console.log(`--- \u7B2C ${round} \u8F6E\u91CD\u8BD5 (${count} \u4E2A\u8282\u70B9) ---`);
5036
- },
5037
- finish() {
5038
- if (IS_TTY) {
5039
- process.stdout.write("\n");
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...");
4320
+ try {
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}`);
5040
4340
  }
5041
- console.log("");
5042
- const entries = [...resultMap.values()];
5043
- entries.sort((a, b) => a.result.name.localeCompare(b.result.name));
5044
- const total = entries.length;
5045
- console.log("\u8282\u70B9\u6700\u7EC8\u72B6\u6001:");
5046
- for (let i = 0; i < entries.length; i++) {
5047
- const { result, round } = entries[i];
5048
- const prefix = `[${i + 1}/${total}]`;
5049
- if (result.delay !== null) {
5050
- const delayColor = result.delay < 300 ? colors.green : result.delay < 800 ? colors.yellow : colors.red;
5051
- const retryNote = round > 1 ? colors.gray(` (\u7B2C${round}\u8F6E\u901A\u8FC7)`) : "";
5052
- console.log(`${prefix} ${colors.green("\u2713")} ${result.name} ${delayColor(`${result.delay}ms`)}${retryNote}`);
5053
- } else {
5054
- console.log(`${prefix} ${colors.red("\u2717")} ${result.name} ${colors.gray(result.error || "timeout")}`);
5055
- }
4341
+ if (err.response.data.documentation_url) {
4342
+ console.error(`\u6587\u6863: ${err.response.data.documentation_url}`);
5056
4343
  }
5057
- console.log("");
5058
4344
  }
5059
- };
5060
- }
5061
- function formatCleanSummary(result) {
5062
- const parts = [`\u79FB\u9664 ${result.removedProxies} \u4E2A\u8282\u70B9`];
5063
- if (result.removedGroups > 0) parts.push(`\u5220\u9664 ${result.removedGroups} \u4E2A\u7A7A\u5206\u7EC4`);
5064
- if (result.updatedGroups > 0) parts.push(`\u66F4\u65B0 ${result.updatedGroups} \u4E2A\u5206\u7EC4`);
5065
- return parts.join(", ");
5066
- }
5067
- function formatTestSummary(summary) {
5068
- return `\u7ED3\u679C: ${colors.green(`${summary.alive} \u5B58\u6D3B`)} / ${colors.red(`${summary.dead} \u5931\u8D25`)} / ${summary.total} \u603B\u8BA1`;
5069
- }
5070
- function githubRepoUrl(rawUrl) {
5071
- const match = rawUrl.match(/raw\.githubusercontent\.com\/([^/]+\/[^/]+)/);
5072
- if (match) return `https://github.com/${match[1]}`;
5073
- return null;
5074
- }
5075
- function resolveTestTarget(args) {
5076
- const subs = getSubscriptions();
5077
- if (subs.length === 0) {
5078
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
5079
4345
  process.exit(1);
5080
4346
  }
5081
- const nameArg = getNonFlagArg(args, 2);
5082
- const timeout = parseIntArg(args, "-t", "--timeout", 2e3);
5083
- const concurrency = parseIntArg(args, "-j", "--concurrency", 100);
5084
- let target;
5085
- if (nameArg) {
5086
- const matches = findSubscriptionFuzzy(subs, nameArg);
5087
- target = pickSingleSubscription(matches, nameArg);
5088
- } else {
5089
- const activeSub = getActiveSubscription();
5090
- if (!activeSub) {
5091
- console.error("\u9519\u8BEF: \u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605\uFF0C\u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
5092
- process.exit(1);
5093
- }
5094
- target = activeSub;
5095
- }
5096
- return { target, timeout, concurrency };
5097
4347
  }
5098
- async function printSubscriptionList(options) {
5099
- if (options?.autoUpdate !== false) {
5100
- const updateResult = await autoUpdateStaleSubscription();
5101
- if (updateResult.total > 0) console.log("");
5102
- }
5103
- const subs = getSubscriptionsWithCache();
5104
- if (subs.length === 0) {
5105
- console.log("\u6CA1\u6709\u8BA2\u9605");
5106
- console.log("");
5107
- console.log("\u6DFB\u52A0\u8BA2\u9605: mihomo sub add <url> [name]");
5108
- console.log("");
4348
+
4349
+ // src/commands/log.ts
4350
+ function cmdLog(args) {
4351
+ const logPath = getLogPath();
4352
+ if (hasFlag(args, "-o", "--open")) {
4353
+ openLogFile(logPath);
5109
4354
  return;
5110
4355
  }
5111
- const activeSub = getActiveSubscription();
5112
- console.log(colors.cyan("\u8BA2\u9605\u5217\u8868:"));
5113
- subs.forEach((s, i) => {
5114
- const time = formatDate(s.updated_at);
5115
- const defaultMark = activeSub && s.name === activeSub.name ? colors.green(" [\u4F7F\u7528\u4E2D]") : "";
5116
- const mergeBadge = isMultiUrl(s.url) ? colors.cyan(` [\u5408\u5E76 ${splitUrls(s.url).length} \u6E90]`) : "";
5117
- const interval = s.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
5118
- console.log(` ${i + 1}. ${s.name}${defaultMark}${mergeBadge}`);
5119
- console.log(` ${colors.gray("\u66F4\u65B0: ")}${time} (\u95F4\u9694: ${interval}h)`);
5120
- if (s.username) {
5121
- console.log(` ${colors.gray("\u7528\u6237: ")}${s.username}`);
5122
- }
5123
- if (s.download !== void 0 || s.total !== void 0) {
5124
- const used = (s.upload || 0) + (s.download || 0);
5125
- const usedStr = formatBytes(used);
5126
- const totalStr = formatBytes(s.total);
5127
- let percentStr = "";
5128
- if (s.total && s.total > 0) {
5129
- const percent = Math.min(used / s.total * 100, 100);
5130
- percentStr = ` (${percent.toFixed(1)}%)`;
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);
5131
4379
  }
5132
- console.log(` ${colors.gray("\u6D41\u91CF: ")}${usedStr} / ${totalStr}${percentStr}`);
5133
4380
  }
5134
- if (s.expire !== void 0) {
5135
- console.log(` ${colors.gray("\u5230\u671F: ")}${formatTimestamp(s.expire)}`);
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);
5136
4385
  }
5137
- if (s.web_page_url) {
5138
- console.log(` ${colors.gray("\u9875\u9762: ")}${s.web_page_url}`);
4386
+ if (openInViewer) {
4387
+ openLogFile(logPath);
4388
+ return;
5139
4389
  }
5140
- });
4390
+ viewLogWithTail(logPath, { follow: false, lines });
4391
+ return;
4392
+ }
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
+ }
5141
4401
  console.log("");
5142
- console.log("\u5207\u6362\u8BA2\u9605: mihomo sub use <name>");
5143
- console.log("\u65B0\u589E\u8BA2\u9605: mihomo sub add <url> [name]");
5144
- console.log("\u66F4\u65B0\u8BA2\u9605: mihomo sub update [name]");
5145
- console.log("\u5220\u9664\u8BA2\u9605: mihomo sub remove <name>");
5146
- console.log("\u6D4B\u8BD5\u8282\u70B9: mihomo sub test [name]");
5147
- console.log("\u6E05\u7406\u8282\u70B9: mihomo sub clean [name]");
5148
- console.log("\u6253\u5F00\u9875\u9762: mihomo sub web [name]");
4402
+ console.log("\u65E5\u5FD7\u5217\u8868:");
5149
4403
  console.log("");
5150
- }
5151
- function printFreeSourceList() {
5152
- const freeSources = getFreeSubscriptionSources();
5153
- console.log(` 00 \u5408\u5E76 #1 + #2 (\u8282\u70B9\u66F4\u591A)`);
5154
- for (let i = 0; i < freeSources.length; i++) {
5155
- console.log(` ${String(i + 1).padStart(2, "0")} ${freeSources[i].name}`);
5156
- }
5157
- }
5158
- async function addFreeSubscription(freeId) {
5159
- const freeSources = getFreeSubscriptionSources();
5160
- if (freeId === 0) {
5161
- const sources = [freeSources[0], freeSources[1]];
5162
- const urls = sources.map((s) => s.url);
5163
- const mergedUrl = urls.join(",");
5164
- const name2 = "free0";
5165
- console.log(`\u6DFB\u52A0\u5408\u5E76\u514D\u8D39\u8BA2\u9605: ${name2} (${sources.map((s) => s.name).join(" + ")})`);
5166
- try {
5167
- addSubscription(mergedUrl, name2);
5168
- const info = await downloadMergedSubscription(urls, name2);
5169
- setDefaultSubscription(name2);
5170
- const repoUrls = sources.map((s) => githubRepoUrl(s.url)).filter(Boolean);
5171
- if (repoUrls.length > 0) saveSubscriptionCache(name2, { web_page_url: repoUrls.join(", ") });
5172
- console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name2}" (${formatProxySummary(info)}, \u5408\u5E76 ${sources.length} \u6E90)`);
5173
- } catch (e) {
5174
- console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
5175
- process.exit(1);
4404
+ let archiveCounter = 0;
4405
+ for (const log of all) {
4406
+ let num;
4407
+ if (log.isCurrent) {
4408
+ num = " 0";
4409
+ } else {
4410
+ archiveCounter++;
4411
+ num = archiveCounter < 10 ? ` ${archiveCounter}` : `${archiveCounter}`;
4412
+ }
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`);
5176
4420
  }
5177
4421
  console.log("");
5178
- await printSubscriptionList();
5179
- return;
5180
- }
5181
- if (freeId < 1 || freeId > freeSources.length) {
5182
- console.error(`\u9519\u8BEF: \u514D\u8D39\u8BA2\u9605 ID \u8303\u56F4 0-${freeSources.length}`);
5183
- console.log("\n\u53EF\u7528\u6E90:");
5184
- printFreeSourceList();
5185
- process.exit(1);
5186
- }
5187
- const source = freeSources[freeId - 1];
5188
- const name = `free${freeId}`;
5189
- console.log(`\u6DFB\u52A0\u514D\u8D39\u8BA2\u9605: ${name}`);
5190
- try {
5191
- addSubscription(source.url, name);
5192
- const info = await downloadSubscription(source.url, name);
5193
- setDefaultSubscription(name);
5194
- const repoUrl = githubRepoUrl(source.url);
5195
- if (repoUrl) saveSubscriptionCache(name, { web_page_url: repoUrl });
5196
- console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
5197
- } catch (e) {
5198
- console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
5199
- process.exit(1);
5200
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");
5201
4428
  console.log("");
5202
- await printSubscriptionList();
5203
4429
  }
5204
- async function cmdSubscription(args) {
5205
- const action = args[1];
5206
- if (!action || action === "list") {
5207
- await printSubscriptionList();
5208
- return;
4430
+
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(",");
4440
+ }
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}"`);
5209
4448
  }
5210
- if (action === "free") {
5211
- const id = parseInt(args[2], 10);
5212
- if (Number.isNaN(id)) {
5213
- console.log("\u7528\u6CD5: mihomo sub free <id>\n");
5214
- console.log("\u53EF\u7528\u6E90:");
5215
- printFreeSourceList();
5216
- process.exit(1);
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;
5217
4471
  }
5218
- await addFreeSubscription(id);
5219
- return;
5220
4472
  }
5221
- if (action === "add") {
5222
- const freeId = parseIntArg(args, "--free", "--free", -1);
5223
- if (freeId > 0) {
5224
- await addFreeSubscription(freeId);
5225
- return;
5226
- }
5227
- const url = args[2];
5228
- const name = args[3] || "default";
5229
- if (!url) {
5230
- console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
5231
- process.exit(1);
4473
+ return info;
4474
+ }
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;
4482
+ }
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`);
4487
+ return parts.join(", ");
4488
+ }
4489
+ function getActiveSubscription() {
4490
+ const subs = getSubscriptions();
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;
4497
+ }
4498
+ return subs[0];
4499
+ }
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);
5232
4513
  }
5233
- if (isMultiUrl(url)) {
5234
- const urls = splitUrls(url);
5235
- for (const u of urls) {
5236
- if (!u.startsWith("http")) {
5237
- console.error(`\u9519\u8BEF: \u65E0\u6548\u7684 URL: ${u}`);
5238
- process.exit(1);
5239
- }
5240
- }
5241
- console.log(`\u6DFB\u52A0\u5408\u5E76\u8BA2\u9605: ${name} (${urls.length} \u4E2A\u6E90)`);
5242
- try {
5243
- addSubscription(url, name);
5244
- setDefaultSubscription(name);
5245
- const info = await downloadMergedSubscription(urls, name);
5246
- console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)}, \u5408\u5E76 ${urls.length} \u6E90)`);
5247
- } catch (e) {
5248
- console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
5249
- process.exit(1);
5250
- }
5251
- } else {
5252
- if (!url.startsWith("http")) {
5253
- console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
5254
- process.exit(1);
5255
- }
5256
- console.log(`\u6DFB\u52A0\u8BA2\u9605: ${name}`);
4514
+ }
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`);
4522
+ process.exit(1);
4523
+ }
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;
4532
+ try {
4533
+ response = await HTTP_CLIENT2.get(url, { responseType: "text" });
4534
+ } catch (e) {
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);
4544
+ }
4545
+ const content = response.data;
4546
+ if (!content?.trim()) {
4547
+ throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
4548
+ }
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;
4564
+ }
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) => {
5257
4583
  try {
5258
- addSubscription(url, name);
5259
- setDefaultSubscription(name);
5260
- const info = await downloadSubscription(url, name);
5261
- const repoUrl = githubRepoUrl(url);
5262
- if (repoUrl) saveSubscriptionCache(name, { web_page_url: repoUrl });
5263
- 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 };
5264
4586
  } catch (e) {
5265
- console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
5266
- process.exit(1);
4587
+ return { url, index, response: null, error: e };
5267
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}`);
5268
4596
  }
5269
- console.log("");
5270
- await printSubscriptionList();
5271
- return;
5272
4597
  }
5273
- if (action === "update") {
5274
- const name = args[2];
5275
- const subs = getSubscriptions();
5276
- if (subs.length === 0) {
5277
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
5278
- process.exit(1);
5279
- }
5280
- if (!name) {
5281
- console.log(`\u66F4\u65B0\u6240\u6709 ${subs.length} \u4E2A\u8BA2\u9605...`);
5282
- const results = await Promise.all(subs.map(tryUpdateOne));
5283
- let ok = 0;
5284
- for (const r of results) {
5285
- if (r.success) {
5286
- ok++;
5287
- console.log(`${colors.green("\u2713")} ${r.name}: ${colors.green("\u5DF2\u66F4\u65B0")} (${formatProxySummary(r)})`);
5288
- } else {
5289
- console.log(`${colors.red("\u2717")} ${r.name}: ${colors.red("\u5931\u8D25")} (${(r.error || "").split("\n")[0]})`);
5290
- }
5291
- }
5292
- if (ok === 0) process.exit(1);
5293
- console.log("");
5294
- await printSubscriptionList();
5295
- return;
5296
- }
5297
- const matches = findSubscriptionFuzzy(subs, name);
5298
- const target = pickSingleSubscription(matches, name);
5299
- console.log(`\u66F4\u65B0\u8BA2\u9605: ${target.name}`);
5300
- try {
5301
- let info;
5302
- if (isMultiUrl(target.url)) {
5303
- const urls = splitUrls(target.url);
5304
- info = await downloadMergedSubscription(urls, target.name);
5305
- } else {
5306
- 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);
5307
4612
  }
5308
- console.log(`\u5DF2\u66F4\u65B0 (${formatProxySummary(info)})`);
5309
- } catch (e) {
5310
- console.error(`\u66F4\u65B0\u5931\u8D25: ${e.message}`);
5311
- process.exit(1);
5312
4613
  }
5313
- console.log("");
5314
- await printSubscriptionList();
5315
- return;
5316
4614
  }
5317
- if (action === "use") {
5318
- const name = args[2];
5319
- const subs = getSubscriptions();
5320
- if (!name) {
5321
- console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
5322
- if (subs.length > 0) {
5323
- console.log("\n\u53EF\u7528\u8BA2\u9605:");
5324
- for (const s of subs) console.log(` ${s.name}`);
5325
- }
5326
- process.exit(1);
5327
- }
5328
- const matches = findSubscriptionFuzzy(subs, name);
5329
- const target = pickSingleSubscription(matches, name);
5330
- const currentDefault = getActiveSubscription();
5331
- const isAlreadyDefault = currentDefault && currentDefault.name === target.name;
5332
- if (isAlreadyDefault) {
5333
- console.log(`"${target.name}" \u5DF2\u662F\u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605`);
5334
- console.log("");
5335
- await printSubscriptionList();
5336
- return;
5337
- }
5338
- const status = getStatus();
5339
- const configInfo = getConfigInfo();
5340
- const currentMode = configInfo?.tun ? "tun" : "mixed";
5341
- const success = setDefaultSubscription(target.name);
5342
- if (success) {
5343
- console.log(`\u5DF2\u5207\u6362\u5230 "${target.name}"`);
5344
- } else {
5345
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u8BA2\u9605 "${name}"`);
5346
- process.exit(1);
5347
- }
5348
- if (status.running) {
5349
- console.log("");
5350
- await cmdStart(["start", currentMode]);
5351
- 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}`);
5352
4654
  }
5353
4655
  console.log("");
5354
- await printSubscriptionList();
5355
- return;
5356
4656
  }
5357
- if (action === "web" || action === "open") {
5358
- const name = args[2];
5359
- const subs = getSubscriptionsWithCache();
5360
- if (subs.length === 0) {
5361
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
5362
- process.exit(1);
5363
- }
5364
- let target;
5365
- if (name) {
5366
- const matches = findSubscriptionFuzzy(subs, name);
5367
- 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);
5368
4679
  } else {
5369
- target = subs[0];
5370
- }
5371
- const cached = getSubscriptionsWithCache().find((s) => s.name === target.name);
5372
- let webPageUrl = cached?.web_page_url;
5373
- if (!webPageUrl) {
5374
- console.log("\u8BA2\u9605\u4FE1\u606F\u4E2D\u7F3A\u5C11\u9875\u9762\u5730\u5740\uFF0C\u6B63\u5728\u66F4\u65B0\u8BA2\u9605...");
5375
- try {
5376
- await downloadSubscription(target.url, target.name);
5377
- const cache = readSubscriptionCache();
5378
- if (cache[target.name]?.web_page_url) {
5379
- webPageUrl = cache[target.name].web_page_url;
5380
- } else {
5381
- console.error("\u9519\u8BEF: \u8BE5\u8BA2\u9605\u6CA1\u6709\u63D0\u4F9B\u9875\u9762\u5730\u5740");
5382
- process.exit(1);
5383
- }
5384
- } catch (e) {
5385
- console.error(`\u66F4\u65B0\u5931\u8D25: ${e.message}`);
5386
- process.exit(1);
5387
- }
4680
+ info = await downloadSubscription(sub.url, sub.name);
5388
4681
  }
5389
- console.log(`\u6253\u5F00\u8BA2\u9605\u9875\u9762: ${webPageUrl}`);
5390
- const opened = openUrl(webPageUrl);
5391
- if (!opened) {
5392
- 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]})`);
5393
4708
  }
5394
- return;
5395
4709
  }
5396
- if (action === "remove" || action === "rm" || action === "delete") {
5397
- const name = args[2];
5398
- const subs = getSubscriptions();
5399
- if (!name) {
5400
- console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8981\u5220\u9664\u7684\u8BA2\u9605\u540D\u79F0");
5401
- if (subs.length > 0) {
5402
- console.log("\n\u53EF\u7528\u8BA2\u9605:");
5403
- for (const s of subs) console.log(` ${s.name}`);
5404
- }
5405
- 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 };
5406
4722
  }
5407
- const matches = findSubscriptionFuzzy(subs, name);
5408
- const target = pickSingleSubscription(matches, name);
5409
- const switchedTo = removeSubscription(target.name);
5410
- console.log(`\u5DF2\u5220\u9664\u8BA2\u9605 "${target.name}"`);
5411
- if (switchedTo) {
5412
- 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;
5413
4731
  }
5414
- console.log("");
5415
- await printSubscriptionList({ autoUpdate: false });
5416
- return;
4732
+ return { name: proxyName, delay: null, error: errorMsg };
5417
4733
  }
5418
- if (action === "clean") {
5419
- const { target, timeout, concurrency } = resolveTestTarget(args);
5420
- console.log(`\u6E05\u7406\u8BA2\u9605 "${target.name}"...`);
5421
- console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
5422
- console.log("");
5423
- const progress = createProgressPrinter();
5424
- const result = await withTestInstance(target.name, async (apiBase) => {
5425
- return autoCleanSubscription(target.name, {
5426
- timeout,
5427
- concurrency,
5428
- apiBase,
5429
- onResult: progress.onResult,
5430
- onRetryRound: progress.onRetryRound
5431
- });
5432
- });
5433
- progress.finish();
5434
- console.log(formatTestSummary(result.summary));
5435
- if (result.skipped) {
5436
- console.log("");
5437
- 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"));
5438
- } else if (result.removedProxies > 0) {
5439
- console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
5440
- const status = getStatus();
5441
- if (status.running) {
5442
- console.log("");
5443
- console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
5444
- }
5445
- }
5446
- 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: [] };
5447
4740
  }
5448
- if (action === "test") {
5449
- const { target, timeout, concurrency } = resolveTestTarget(args);
5450
- console.log(`\u6D4B\u8BD5\u8BA2\u9605 "${target.name}" \u7684\u8282\u70B9\u8FDE\u901A\u6027...`);
5451
- console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
5452
- console.log("");
5453
- const progress = createProgressPrinter();
5454
- const summary = await withTestInstance(target.name, async (apiBase) => {
5455
- return testSubscriptionProxies(target.name, {
5456
- timeout,
5457
- concurrency,
5458
- apiBase,
5459
- onResult: progress.onResult
5460
- });
5461
- });
5462
- progress.finish();
5463
- console.log(formatTestSummary(summary));
5464
- 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
+ }
5465
4753
  }
5466
- console.error("\u9519\u8BEF: \u672A\u77E5\u7684\u8BA2\u9605\u547D\u4EE4");
5467
- console.log("\u7528\u6CD5: mihomo sub [list|use|add|update|remove|web|test|clean]");
5468
- 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 };
5469
4758
  }
5470
-
5471
- // src/commands/bench.ts
5472
- function printRanking(results, sourceOrder) {
5473
- const valid = results.filter((r) => r.downloadOk && r.alive > 0).sort((a, b) => {
5474
- const rateA = a.alive / a.totalProxies;
5475
- const rateB = b.alive / b.totalProxies;
5476
- if (Math.abs(rateA - rateB) > 0.1) return rateB - rateA;
5477
- return a.medianDelay - b.medianDelay;
5478
- });
5479
- if (valid.length === 0) {
5480
- console.log(colors.yellow("\u6CA1\u6709\u53EF\u7528\u7684\u8BA2\u9605\u6E90"));
5481
- return;
5482
- }
5483
- console.log(colors.cyan("\u6392\u540D:"));
5484
- console.log("");
5485
- const namedResults = valid.map((r) => {
5486
- const idx = sourceOrder.get(r.name) ?? 0;
5487
- return { ...r, displayName: `${String(idx + 1).padStart(2, "0")}-${r.name}` };
5488
- });
5489
- const nameWidth = Math.max(12, ...namedResults.map((r) => r.displayName.length));
5490
- const h = (s, w) => s + " ".repeat(Math.max(0, w - displayWidth(s)));
5491
- console.log(
5492
- ` ${"#".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)}`
5493
- );
5494
- console.log(
5495
- ` ${"\u2500".repeat(3)} ${"\u2500".repeat(nameWidth)} ${"\u2500".repeat(8)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(7)} ${"\u2500".repeat(7)}`
5496
- );
5497
- for (let i = 0; i < namedResults.length; i++) {
5498
- const r = namedResults[i];
5499
- const rate = (r.alive / r.totalProxies * 100).toFixed(1);
5500
- const rateColor = r.alive / r.totalProxies > 0.3 ? colors.green : colors.yellow;
5501
- const groups = r.proxyGroups > 0 ? String(r.proxyGroups) : "-";
5502
- console.log(
5503
- ` ${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)}`
5504
- );
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
+ }
5505
4771
  }
5506
- console.log("");
5507
- const failed = results.filter((r) => !r.downloadOk);
5508
- const noAlive = results.filter((r) => r.downloadOk && r.alive === 0);
5509
- if (failed.length > 0) {
5510
- const names = failed.map((r) => `${String((sourceOrder.get(r.name) ?? 0) + 1).padStart(2, "0")}-${r.name}`);
5511
- 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;
5512
4776
  }
5513
- if (noAlive.length > 0) {
5514
- const names = noAlive.map((r) => `${String((sourceOrder.get(r.name) ?? 0) + 1).padStart(2, "0")}-${r.name}`);
5515
- 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
+ }
5516
4781
  }
4782
+ return renameMap.size;
5517
4783
  }
5518
- async function cmdBench(args) {
5519
- if (!hasKernel()) {
5520
- console.error('\u9519\u8BEF: \u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
5521
- process.exit(1);
5522
- }
5523
- const timeout = parseIntArg(args, "-t", "--timeout", 1500);
5524
- const concurrency = parseIntArg(args, "-j", "--concurrency", 100);
5525
- const nameFilter = getNonFlagArg(args, 1);
5526
- const allSources = getFreeSubscriptionSources();
5527
- const sourceOrder = new Map(allSources.map((s, i) => [s.name, i]));
5528
- let sources = allSources;
5529
- if (nameFilter) {
5530
- const lower = nameFilter.toLowerCase();
5531
- sources = sources.filter((s) => s.name.toLowerCase().includes(lower));
5532
- if (sources.length === 0) {
5533
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u5339\u914D "${nameFilter}" \u7684\u8BA2\u9605\u6E90`);
5534
- console.log("\n\u53EF\u7528\u6E90:");
5535
- for (const s of allSources) console.log(` ${s.name}`);
5536
- 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
+ }
5537
4801
  }
5538
4802
  }
5539
- console.log(colors.cyan(`\u57FA\u51C6\u6D4B\u8BD5 ${sources.length} \u4E2A\u514D\u8D39\u8BA2\u9605\u6E90`));
5540
- console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
5541
- console.log("");
5542
- try {
5543
- console.log(colors.cyan("\u4E0B\u8F7D\u8BA2\u9605..."));
5544
- const downloaded = await downloadAllSources(sources, (name, ok, count, groups, error) => {
5545
- const idx = String((sourceOrder.get(name) ?? 0) + 1).padStart(2, "0");
5546
- if (ok) {
5547
- const groupsInfo = groups > 0 ? ` ${groups}\u7EC4` : "";
5548
- console.log(` ${colors.green("\u2713")} ${idx}-${name}: ${count} \u4E2A\u8282\u70B9${groupsInfo}`);
5549
- } else {
5550
- 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));
5551
4808
  }
5552
- });
5553
- const allProxies = downloaded.flatMap((d) => d.proxies);
5554
- const successSources = downloaded.filter((d) => d.proxies.length > 0);
5555
- if (allProxies.length === 0) {
5556
- console.log("");
5557
- console.log(colors.red("\u6240\u6709\u8BA2\u9605\u6E90\u4E0B\u8F7D\u5931\u8D25\u6216\u65E0\u8282\u70B9"));
5558
- return;
5559
4809
  }
5560
- console.log("");
5561
- console.log(`\u5171 ${allProxies.length} \u4E2A\u8282\u70B9\uFF0C\u6765\u81EA ${successSources.length} \u4E2A\u6E90`);
5562
- console.log("");
5563
- const filtered = buildMergedBenchConfig(allProxies);
5564
- if (filtered > 0) {
5565
- console.log(colors.gray(`\u8FC7\u6EE4 ${filtered} \u4E2A\u65E0\u6548\u8282\u70B9\uFF0C\u5269\u4F59 ${allProxies.length} \u4E2A`));
5566
- }
5567
- const survivingSet = new Set(allProxies);
5568
- for (const d of downloaded) {
5569
- d.proxies = d.proxies.filter((p) => survivingSet.has(p));
5570
- }
5571
- const benchPort = BENCH_CONFIG["mixed-port"];
5572
- const benchApi = BENCH_CONFIG["external-controller"];
5573
- console.log(colors.cyan("\u542F\u52A8\u6D4B\u8BD5\u5B9E\u4F8B..."));
5574
- await startBenchInstance();
5575
- console.log(`${colors.green("\u5DF2\u542F\u52A8")} (\u7AEF\u53E3 ${benchPort}/${benchApi.split(":")[1]})`);
5576
- console.log("");
5577
- console.log(colors.cyan("\u6D4B\u8BD5\u8282\u70B9\u5EF6\u8FDF..."));
5578
- const allNames = allProxies.map((p) => p.name);
5579
- const testResults = await testBenchProxies(allNames, {
5580
- timeout,
5581
- concurrency,
5582
- onBatch: (batch, total, alive, tested, median) => {
5583
- const pct = (tested / allNames.length * 100).toFixed(0);
5584
- 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`);
5585
- }
5586
- });
5587
- process.stdout.write(`\r${" ".repeat(80)}\r`);
5588
- const totalAlive = testResults.filter((r) => r.delay !== null).length;
5589
- const summary = { alive: totalAlive, dead: testResults.length - totalAlive, total: testResults.length };
5590
- console.log(formatTestSummary(summary));
5591
- console.log("");
5592
- const resultsByName = new Map(testResults.map((r) => [r.name, r]));
5593
- const results = downloaded.map((source) => computeSourceResult(source, resultsByName));
5594
- printRanking(results, sourceOrder);
5595
- } finally {
5596
- stopBenchInstance();
5597
- cleanupBenchDir();
5598
4810
  }
4811
+ return { removedProxies, updatedGroups, removedGroups: removedGroupNames.size };
5599
4812
  }
5600
-
5601
- // src/commands/directory.ts
5602
- function cmdDirectory(args) {
5603
- const action = args?.[1];
5604
- if (action === "open") {
5605
- const target = args[2];
5606
- if (!target || target === "root") {
5607
- console.log("\u6B63\u5728\u6253\u5F00: \u6839\u76EE\u5F55");
5608
- const success = openUrl(USER_DATA_DIR);
5609
- if (!success) {
5610
- console.log(`\u8BF7\u624B\u52A8\u6253\u5F00: ${USER_DATA_DIR}`);
5611
- }
5612
- return;
5613
- }
5614
- const targetInfo = DIRECTORY_TARGETS[target.toLowerCase()];
5615
- if (targetInfo) {
5616
- const targetPath = targetInfo.path || USER_DATA_DIR;
5617
- console.log(`\u6B63\u5728\u6253\u5F00: ${targetInfo.label}`);
5618
- const success = openUrl(targetPath);
5619
- if (!success) {
5620
- 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
+ }
5621
4848
  }
5622
- return;
5623
- }
5624
- console.error(`\u9519\u8BEF: \u672A\u77E5\u7684\u76EE\u5F55\u76EE\u6807 "${target}"`);
5625
- console.log("");
5626
- console.log("\u53EF\u7528\u76EE\u6807:");
5627
- console.log(" root (\u9ED8\u8BA4) \u6839\u76EE\u5F55");
5628
- for (const [key, val] of Object.entries(DIRECTORY_TARGETS)) {
5629
- if (key !== "root") {
5630
- 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;
5631
4856
  }
5632
4857
  }
5633
- console.log("");
5634
- process.exit(1);
5635
4858
  }
5636
- console.log("");
5637
- console.log("\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E:");
5638
- console.log(` \u6839\u76EE\u5F55: ${USER_DATA_DIR}`);
5639
- console.log(` \u5168\u5C40\u8BBE\u7F6E: ${PATHS.settingsFile}`);
5640
- console.log(` \u5185\u6838\u76EE\u5F55: ${DIRS.kernel}`);
5641
- console.log(` \u5185\u6838\u6587\u4EF6: ${PATHS.mihomoBinary}`);
5642
- console.log(` \u8BA2\u9605\u76EE\u5F55: ${DIRS.subscriptions}`);
5643
- console.log(" - cache.json (\u8BA2\u9605\u7F13\u5B58\uFF1A\u66F4\u65B0\u65F6\u95F4\u3001\u6D41\u91CF\u7B49)");
5644
- console.log(" - xxx.yaml (\u8BA2\u9605\u539F\u59CB\u914D\u7F6E)");
5645
- console.log(` \u8FD0\u884C\u65F6\u76EE\u5F55: ${DIRS.runtime}`);
5646
- console.log(" - config.yaml (\u542F\u52A8\u65F6\u751F\u6210\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
5647
- console.log(" - pid (PID \u6587\u4EF6\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
5648
- console.log(` \u65E5\u5FD7\u6587\u4EF6: ${PATHS.logFile}`);
5649
- console.log(` mihomo \u6570\u636E: ${DIRS.data}`);
5650
- console.log(" - cache.db, Geo*.dat \u7B49 (mihomo \u81EA\u884C\u7BA1\u7406)");
5651
- console.log("");
5652
- console.log("\u6253\u5F00\u76EE\u5F55:");
5653
- console.log(" mihomo dir open \u6253\u5F00\u6839\u76EE\u5F55");
5654
- console.log(" mihomo dir open subs \u6253\u5F00\u8BA2\u9605\u76EE\u5F55");
5655
- console.log(" mihomo dir open logs \u6253\u5F00\u65E5\u5FD7\u76EE\u5F55");
5656
- console.log(" mihomo dir open runtime \u6253\u5F00\u8FD0\u884C\u65F6\u76EE\u5F55");
5657
- console.log(" mihomo dir open kernel \u6253\u5F00\u5185\u6838\u76EE\u5F55");
5658
- console.log("");
5659
- console.log("\u73AF\u5883\u53D8\u91CF:");
5660
- console.log(" MIHOMO_CLI_DIR: \u81EA\u5B9A\u4E49\u6839\u76EE\u5F55\u4F4D\u7F6E");
5661
- console.log("");
5662
- }
5663
-
5664
- // src/commands/help.ts
5665
- function printShortHelp() {
5666
- console.log(`
5667
- ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))} (mihomo help \u67E5\u770B\u5B8C\u6574\u5E2E\u52A9)
5668
- `);
5669
- console.log(
5670
- `\u5E38\u7528\u547D\u4EE4:
5671
- ${colors.bold("start")} [tun|mixed] \u542F\u52A8/\u5207\u6362\u4EE3\u7406
5672
- ${colors.bold("sub")} [use|update] \u8BA2\u9605\u7BA1\u7406
5673
- ${colors.bold("ow")} [on|off] \u8986\u5199\u914D\u7F6E
5674
- ${colors.bold("ui")} [zash|dash|yacd] \u6253\u5F00 Web UI
5675
- `
5676
- );
5677
- }
5678
- function printHelp() {
5679
- console.log(
5680
- `
5681
- ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))}
5682
-
5683
- \u547D\u4EE4\u522B\u540D: mihomo, mhm, mh
5684
-
5685
- \u7528\u6CD5:
5686
- mihomo <\u547D\u4EE4> [\u9009\u9879]
5687
-
5688
- ${colors.cyan("\u63A7\u5236:")}
5689
- ${colors.bold("start")} [tun|mixed] \u542F\u52A8/\u5207\u6362\u4EE3\u7406 (\u9ED8\u8BA4 mixed)
5690
- ${colors.bold("stop")} \u505C\u6B62\u4EE3\u7406
5691
- ${colors.bold("status")} \u67E5\u770B\u72B6\u6001
5692
-
5693
- ${colors.cyan("\u754C\u9762:")}
5694
- ${colors.bold("ui")} [zash|dash|yacd] \u6253\u5F00 Web UI (\u9ED8\u8BA4 zash)
5695
- ${colors.bold("log")} [-o] \u5B9E\u65F6\u65E5\u5FD7\uFF08-o \u6253\u5F00\u6587\u4EF6\uFF09
5696
- ${colors.bold("logs")} [\u7F16\u53F7] [-n N] [-o] \u65E5\u5FD7\u5217\u8868\uFF080=\u5F53\u524D\uFF0C1+=\u5F52\u6863\uFF09
5697
-
5698
- ${colors.cyan("\u8BA2\u9605:")}
5699
- ${colors.bold("subscription")} \u5217\u51FA\u6240\u6709\u8BA2\u9605\uFF08\u522B\u540D sub\uFF09
5700
- ${colors.bold("subscription")} use <name> \u5207\u6362\u5F53\u524D\u8BA2\u9605
5701
- ${colors.bold("subscription")} add <url> [name] \u6DFB\u52A0\u8BA2\u9605
5702
- ${colors.bold("subscription")} update [name] \u66F4\u65B0\u8BA2\u9605\uFF08\u65E0\u53C2\u66F4\u65B0\u6240\u6709\uFF09
5703
- ${colors.bold("subscription")} remove <name> \u5220\u9664\u8BA2\u9605
5704
- ${colors.bold("subscription")} web [name] \u6253\u5F00\u8BA2\u9605\u9875\u9762
5705
- ${colors.bold("subscription")} test [name] \u6D4B\u8BD5\u8282\u70B9\u8FDE\u901A\u6027
5706
- ${colors.bold("subscription")} clean [name] \u6D4B\u901F\u5E76\u6E05\u7406\u5931\u8D25\u8282\u70B9
5707
- ${colors.bold("test")} [-t ms] [-j N] \u5FEB\u901F\u6D4B\u8BD5\u5F53\u524D\u8282\u70B9\u8FDE\u901A\u6027
5708
- ${colors.bold("clean")} [-t ms] [-j N] \u6E05\u7406\u5931\u8D25\u8282\u70B9\u5E76\u81EA\u52A8\u91CD\u542F
5709
- ${colors.bold("bench")} [name] [-t ms] [-j N] \u6D4B\u8BD5\u514D\u8D39\u8BA2\u9605\u6E90\u8D28\u91CF\u6392\u540D
5710
-
5711
- ${colors.cyan("\u914D\u7F6E:")}
5712
- ${colors.bold("overwrite")} \u67E5\u770B\u8986\u5199\u72B6\u6001\uFF08\u522B\u540D ow\uFF09
5713
- ${colors.bold("overwrite")} on|off \u542F\u7528/\u7981\u7528\u8986\u5199\u914D\u7F6E
5714
- ${colors.bold("directory")} \u663E\u793A\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E\uFF08\u522B\u540D dir\uFF09
5715
- ${colors.bold("directory")} open [target] \u6253\u5F00\u76EE\u5F55: root|subs|logs|runtime|...
5716
-
5717
- ${colors.cyan("\u7CFB\u7EDF:")}
5718
- ${colors.bold("kernel")} [--mirror [\u955C\u50CF]] \u66F4\u65B0\u5185\u6838\uFF08\u9ED8\u8BA4\u76F4\u8FDE\uFF0C--mirror \u4F7F\u7528 v6\uFF09
5719
- ${colors.bold("update")} \u66F4\u65B0 mihomo-cli (npm install -g)
5720
- ${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
5721
- ${colors.bold("help")}, -h \u663E\u793A\u5E2E\u52A9
5722
- ${colors.bold("version")}, -v \u663E\u793A\u7248\u672C
5723
-
5724
- ${colors.cyan("\u793A\u4F8B:")}
5725
- mihomo start # \u542F\u52A8/\u91CD\u542F Mixed \u6A21\u5F0F
5726
- mihomo start tun # \u5207\u6362\u5230 TUN \u900F\u660E\u4EE3\u7406\u6A21\u5F0F
5727
- mihomo sub add <url> # \u6DFB\u52A0\u8BA2\u9605 (sub \u662F subscription \u522B\u540D)
5728
- mihomo bench # \u6D4B\u8BD5\u6240\u6709\u514D\u8D39\u8BA2\u9605\u6E90
5729
- mihomo ui # \u6253\u5F00 Web UI
5730
-
5731
- ${colors.cyan("\u6A21\u5F0F\u8BF4\u660E:")}
5732
- mixed HTTP + SOCKS5 \u6DF7\u5408\u7AEF\u53E3 (\u9ED8\u8BA4)
5733
- tun \u900F\u660E\u4EE3\u7406\uFF0C\u5168\u5C40\u81EA\u52A8\u8DEF\u7531\uFF0C\u9700\u8981 sudo
5734
-
5735
- ${colors.cyan("\u6570\u636E\u76EE\u5F55:")}
5736
- \u73AF\u5883\u53D8\u91CF MIHOMO_CLI_DIR \u53EF\u81EA\u5B9A\u4E49\u4F4D\u7F6E
5737
- \u9ED8\u8BA4: ${USER_DATA_DIR}
5738
- `
5739
- );
5740
- }
5741
- function printVersion() {
5742
- const kv = getKernelVersion() || "\u672A\u5B89\u88C5";
5743
- console.log(colors.cyan(colors.bold(`mihomo-cli v${VERSION}`)));
5744
- console.log(`${colors.gray("\u5185\u6838: ")}${kv}`);
5745
- 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 };
5746
4863
  }
5747
4864
 
5748
- // src/kernel.ts
5749
- import { execSync as execSync4, spawnSync } from "child_process";
5750
- import fs8 from "fs";
5751
- import path6 from "path";
5752
-
5753
- // node_modules/compare-versions/lib/esm/utils.js
5754
- var semver = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
5755
- var validateAndParse = (version) => {
5756
- if (typeof version !== "string") {
5757
- 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)");
5758
4876
  }
5759
- const match = version.match(semver);
5760
- if (!match) {
5761
- 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
+ }
5762
4885
  }
5763
- match.shift();
5764
- return match;
5765
- };
5766
- var isWildcard = (s) => s === "*" || s === "x" || s === "X";
5767
- var tryParse = (v) => {
5768
- const n = parseInt(v, 10);
5769
- return isNaN(n) ? v : n;
5770
- };
5771
- var forceType = (a, b) => typeof a !== typeof b ? [String(a), String(b)] : [a, b];
5772
- var compareStrings = (a, b) => {
5773
- if (isWildcard(a) || isWildcard(b))
5774
- return 0;
5775
- const [ap, bp] = forceType(tryParse(a), tryParse(b));
5776
- if (ap > bp)
5777
- return 1;
5778
- if (ap < bp)
5779
- return -1;
5780
- return 0;
5781
- };
5782
- var compareSegments = (a, b) => {
5783
- for (let i = 0; i < Math.max(a.length, b.length); i++) {
5784
- const r = compareStrings(a[i] || "0", b[i] || "0");
5785
- if (r !== 0)
5786
- 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
+ }
5787
4895
  }
5788
- return 0;
5789
- };
5790
-
5791
- // node_modules/compare-versions/lib/esm/compareVersions.js
5792
- var compareVersions = (v1, v2) => {
5793
- const n1 = validateAndParse(v1);
5794
- const n2 = validateAndParse(v2);
5795
- const p1 = n1.pop();
5796
- const p2 = n2.pop();
5797
- const r = compareSegments(n1, n2);
5798
- if (r !== 0)
5799
- return r;
5800
- if (p1 && p2) {
5801
- return compareSegments(p1.split("."), p2.split("."));
5802
- } else if (p1 || p2) {
5803
- return p1 ? -1 : 1;
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`);
5804
4904
  }
5805
- return 0;
5806
- };
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")}`);
4912
+ }
4913
+ console.log("");
4914
+ }
5807
4915
 
5808
- // src/kernel.ts
5809
- var GITHUB_REPO = "MetaCubeX/mihomo";
5810
- var KERNEL_HTTP_TIMEOUT = 12e4;
5811
- var KERNEL_DOWNLOAD_TIMEOUT = 18e4;
5812
- var HTTP_CLIENT2 = createHttpClient({ timeout: KERNEL_HTTP_TIMEOUT });
5813
- function withMirror(url, mirror) {
5814
- if (mirror && (url.startsWith("https://github.com/") || url.startsWith("https://api.github.com/"))) {
5815
- return mirror + url;
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")
4924
+ };
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 });
5816
4934
  }
5817
- return url;
5818
- }
5819
- function getArch() {
5820
- const arch = process.arch;
5821
- if (arch === "arm64") return "arm64";
5822
- if (arch === "x64") return "amd64";
5823
- return arch;
5824
4935
  }
5825
- function findMatchingAsset(assets, platform, arch) {
5826
- const prefix = `mihomo-${platform}-${arch}`;
5827
- const matchingAssets = assets.filter(
5828
- (a) => a.name.startsWith(prefix) && a.name.endsWith(".gz") || a.name.startsWith(`${prefix}-`) && a.name.endsWith(".gz")
5829
- );
5830
- if (matchingAssets.length === 0) return null;
5831
- if (matchingAssets.length === 1) return matchingAssets[0];
5832
- const standardAsset = matchingAssets.find((a) => {
5833
- const nameWithoutGz = a.name.slice(0, -3);
5834
- const parts = nameWithoutGz.split("-");
5835
- const lastPart = parts[parts.length - 1];
5836
- return /^v?\d+\.\d+\.\d+/.test(lastPart) && !nameWithoutGz.includes("-go");
5837
- });
5838
- return standardAsset || matchingAssets[0];
4936
+ function cleanupTestDir() {
4937
+ rmrf(TEST_DIR);
5839
4938
  }
5840
- async function getLatestRelease(repo, mirror) {
5841
- const url = withMirror(`https://api.github.com/repos/${repo}/releases`, mirror);
5842
- const response = await HTTP_CLIENT2.get(url, { responseType: "json" });
5843
- const releases = response.data;
5844
- if (!Array.isArray(releases) || releases.length === 0) {
5845
- 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}"`);
5846
4944
  }
5847
- const stableReleases = releases.filter(
5848
- (r) => !r.prerelease && !r.tag_name.toLowerCase().includes("alpha") && !r.tag_name.toLowerCase().includes("beta") && !r.tag_name.toLowerCase().includes("prerelease")
5849
- );
5850
- 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 });
5851
4972
  }
5852
- async function checkUpdate(mirror) {
5853
- const currentVersion = getKernelVersion();
5854
- const latest = await getLatestRelease(GITHUB_REPO, mirror);
5855
- const latestVersion = latest.tag_name;
5856
- let needsUpdate = false;
5857
- const currentDisplay = currentVersion || "\u672A\u5B89\u88C5";
5858
- if (!currentVersion) {
5859
- needsUpdate = true;
5860
- } 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;
5861
4990
  try {
5862
- needsUpdate = compareVersions(latestVersion.replace(/^v/, ""), currentVersion.replace(/^v/, "")) > 0;
4991
+ await client.get(`${TEST_API}/version`);
4992
+ ready = true;
4993
+ break;
5863
4994
  } catch {
5864
- needsUpdate = latestVersion !== currentVersion;
4995
+ await sleep(500);
5865
4996
  }
5866
4997
  }
5867
- return {
5868
- current: currentDisplay,
5869
- latest: latestVersion,
5870
- needsUpdate,
5871
- assets: latest.assets,
5872
- release: latest
5873
- };
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
+ }
5874
5010
  }
5875
- function findBinaryInDir(dir) {
5876
- const files = fs8.readdirSync(dir);
5877
- for (const f of files) {
5878
- const fullPath = path6.join(dir, f);
5879
- const stat = fs8.statSync(fullPath);
5880
- if (stat.isDirectory()) {
5881
- const found = findBinaryInDir(fullPath);
5882
- if (found) return found;
5883
- 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);
5884
5023
  }
5885
- if (f === "mihomo") return fullPath;
5886
- if (f.includes("mihomo") && !f.endsWith(".gz")) return fullPath;
5887
5024
  }
5888
- return null;
5025
+ try {
5026
+ fs7.unlinkSync(TEST_PATHS.pidFile);
5027
+ } catch {
5028
+ }
5889
5029
  }
5890
- async function downloadKernel(progressCallback, mirror, releaseInfo) {
5891
- ensureDirs();
5892
- const latest = releaseInfo || await getLatestRelease(GITHUB_REPO, mirror);
5893
- const arch = getArch();
5894
- const platform = process.platform;
5895
- const asset = findMatchingAsset(latest.assets, platform, arch);
5896
- if (!asset) {
5897
- const available = latest.assets.map((a) => a.name).join(", ");
5898
- let hint = "";
5899
- if (available) hint = `
5900
- \u53EF\u7528\u7248\u672C: ${available}`;
5901
- throw new Error(`\u672A\u627E\u5230\u5339\u914D\u7684\u5185\u6838\u6587\u4EF6
5902
- \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();
5903
5039
  }
5904
- const downloadUrl = withMirror(asset.browser_download_url, mirror);
5905
- const tempPath = path6.join(DIRS.kernel, asset.name);
5906
- const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
5907
- if (progressCallback) {
5908
- 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
+ const resultMap = /* @__PURE__ */ new Map();
5049
+ function render(done, total) {
5050
+ if (!IS_TTY) return;
5051
+ const pct = Math.round(done / total * 100);
5052
+ const filled = Math.round(done / total * BAR_WIDTH);
5053
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(BAR_WIDTH - filled);
5054
+ process.stdout.write(`\r${bar} ${done}/${total} (${pct}%) | ${colors.green(`\u2713${alive}`)} ${colors.red(`\u2717${dead}`)}`);
5909
5055
  }
5910
- const curlResult = spawnSync(
5911
- "curl",
5912
- ["-L", "--progress-bar", "--connect-timeout", "30", "--max-time", String(Math.floor(KERNEL_DOWNLOAD_TIMEOUT / 1e3)), "-o", tempPath, downloadUrl],
5913
- { stdio: "inherit" }
5914
- );
5915
- if (curlResult.status !== 0) {
5916
- try {
5917
- fs8.unlinkSync(tempPath);
5918
- } catch {
5056
+ return {
5057
+ onResult(result, index, total, round = 1) {
5058
+ if (result.delay !== null) alive++;
5059
+ else dead++;
5060
+ resultMap.set(result.name, { result, round });
5061
+ render(index + 1, total);
5062
+ },
5063
+ onRetryRound(round, count) {
5064
+ if (IS_TTY) {
5065
+ process.stdout.write("\n");
5066
+ }
5067
+ console.log(`--- \u7B2C ${round} \u8F6E\u91CD\u8BD5 (${count} \u4E2A\u8282\u70B9) ---`);
5068
+ alive = 0;
5069
+ dead = 0;
5070
+ },
5071
+ finish() {
5072
+ if (IS_TTY) {
5073
+ process.stdout.write("\n");
5074
+ }
5075
+ console.log("");
5076
+ const entries = [...resultMap.values()];
5077
+ entries.sort((a, b) => a.result.name.localeCompare(b.result.name));
5078
+ const total = entries.length;
5079
+ console.log("\u8282\u70B9\u6700\u7EC8\u72B6\u6001:");
5080
+ for (let i = 0; i < entries.length; i++) {
5081
+ const { result, round } = entries[i];
5082
+ const prefix = `[${i + 1}/${total}]`;
5083
+ if (result.delay !== null) {
5084
+ const delayColor = result.delay < 300 ? colors.green : result.delay < 800 ? colors.yellow : colors.red;
5085
+ const retryNote = round > 1 ? colors.gray(` (\u7B2C${round}\u8F6E\u901A\u8FC7)`) : "";
5086
+ console.log(`${prefix} ${colors.green("\u2713")} ${result.name} ${delayColor(`${result.delay}ms`)}${retryNote}`);
5087
+ } else {
5088
+ console.log(`${prefix} ${colors.red("\u2717")} ${result.name} ${colors.gray(result.error || "timeout")}`);
5089
+ }
5090
+ }
5091
+ console.log("");
5092
+ }
5093
+ };
5094
+ }
5095
+ function formatCleanSummary(result) {
5096
+ const parts = [`\u79FB\u9664 ${result.removedProxies} \u4E2A\u8282\u70B9`];
5097
+ if (result.removedGroups > 0) parts.push(`\u5220\u9664 ${result.removedGroups} \u4E2A\u7A7A\u5206\u7EC4`);
5098
+ if (result.updatedGroups > 0) parts.push(`\u66F4\u65B0 ${result.updatedGroups} \u4E2A\u5206\u7EC4`);
5099
+ return parts.join(", ");
5100
+ }
5101
+ function formatTestSummary(summary) {
5102
+ return `\u7ED3\u679C: ${colors.green(`${summary.alive} \u5B58\u6D3B`)} / ${colors.red(`${summary.dead} \u5931\u8D25`)} / ${summary.total} \u603B\u8BA1`;
5103
+ }
5104
+ function githubRepoUrl(rawUrl) {
5105
+ const match = rawUrl.match(/raw\.githubusercontent\.com\/([^/]+\/[^/]+)/);
5106
+ if (match) return `https://github.com/${match[1]}`;
5107
+ return null;
5108
+ }
5109
+ function resolveTestTarget(args) {
5110
+ const subs = getSubscriptions();
5111
+ if (subs.length === 0) {
5112
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
5113
+ process.exit(1);
5114
+ }
5115
+ const nameArg = getNonFlagArg(args, 2);
5116
+ const timeout = parseIntArg(args, "-t", "--timeout", 2e3);
5117
+ const concurrency = parseIntArg(args, "-j", "--concurrency", 100);
5118
+ let target;
5119
+ if (nameArg) {
5120
+ const matches = findSubscriptionFuzzy(subs, nameArg);
5121
+ target = pickSingleSubscription(matches, nameArg);
5122
+ } else {
5123
+ const activeSub = getActiveSubscription();
5124
+ if (!activeSub) {
5125
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605\uFF0C\u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
5126
+ process.exit(1);
5919
5127
  }
5920
- throw new Error(`\u4E0B\u8F7D\u5931\u8D25 (curl \u9000\u51FA\u7801 ${curlResult.status})`);
5128
+ target = activeSub;
5921
5129
  }
5922
- if (!fs8.existsSync(tempPath)) {
5923
- throw new Error("\u4E0B\u8F7D\u5931\u8D25: \u6587\u4EF6\u672A\u751F\u6210");
5130
+ return { target, timeout, concurrency };
5131
+ }
5132
+ async function printSubscriptionList(options) {
5133
+ if (options?.autoUpdate !== false) {
5134
+ const updateResult = await autoUpdateStaleSubscription();
5135
+ if (updateResult.total > 0) console.log("");
5924
5136
  }
5925
- if (progressCallback) {
5926
- progressCallback("\u89E3\u538B\u5185\u6838...");
5137
+ const subs = getSubscriptionsWithCache();
5138
+ if (subs.length === 0) {
5139
+ console.log("\u6CA1\u6709\u8BA2\u9605");
5140
+ console.log("");
5141
+ console.log("\u6DFB\u52A0\u8BA2\u9605: mihomo sub add <url> [name]");
5142
+ console.log("");
5143
+ return;
5927
5144
  }
5928
- const extractPath = DIRS.kernel;
5929
- let extractedBinary = null;
5930
- try {
5931
- if (tempPath.endsWith(".tar.gz") || tempPath.endsWith(".tgz")) {
5932
- execSync4(`tar -xzf "${tempPath}" -C "${extractPath}"`, { stdio: ["pipe", "pipe", "inherit"] });
5933
- } else if (tempPath.endsWith(".gz")) {
5934
- const baseName = path6.basename(tempPath, ".gz");
5935
- const outputPath = path6.join(extractPath, baseName);
5936
- execSync4(`gzip -dc "${tempPath}" > "${outputPath}"`, { stdio: ["pipe", "pipe", "inherit"] });
5937
- extractedBinary = outputPath;
5145
+ const activeSub = getActiveSubscription();
5146
+ console.log(colors.cyan("\u8BA2\u9605\u5217\u8868:"));
5147
+ subs.forEach((s, i) => {
5148
+ const time = formatDate(s.updated_at);
5149
+ const defaultMark = activeSub && s.name === activeSub.name ? colors.green(" [\u4F7F\u7528\u4E2D]") : "";
5150
+ const mergeBadge = isMultiUrl(s.url) ? colors.cyan(` [\u5408\u5E76 ${splitUrls(s.url).length} \u6E90]`) : "";
5151
+ const interval = s.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
5152
+ console.log(` ${i + 1}. ${s.name}${defaultMark}${mergeBadge}`);
5153
+ console.log(` ${colors.gray("\u66F4\u65B0: ")}${time} (\u95F4\u9694: ${interval}h)`);
5154
+ if (s.username) {
5155
+ console.log(` ${colors.gray("\u7528\u6237: ")}${s.username}`);
5938
5156
  }
5939
- } catch (e) {
5940
- try {
5941
- fs8.unlinkSync(tempPath);
5942
- } catch {
5157
+ if (s.download !== void 0 || s.total !== void 0) {
5158
+ const used = (s.upload || 0) + (s.download || 0);
5159
+ const usedStr = formatBytes(used);
5160
+ const totalStr = formatBytes(s.total);
5161
+ let percentStr = "";
5162
+ if (s.total && s.total > 0) {
5163
+ const percent = Math.min(used / s.total * 100, 100);
5164
+ percentStr = ` (${percent.toFixed(1)}%)`;
5165
+ }
5166
+ console.log(` ${colors.gray("\u6D41\u91CF: ")}${usedStr} / ${totalStr}${percentStr}`);
5943
5167
  }
5944
- throw new Error(`\u89E3\u538B\u5931\u8D25: ${e.message}`);
5945
- }
5946
- const foundBinary = extractedBinary || findBinaryInDir(extractPath);
5947
- if (!foundBinary) {
5948
- try {
5949
- fs8.unlinkSync(tempPath);
5950
- } catch {
5168
+ if (s.expire !== void 0) {
5169
+ console.log(` ${colors.gray("\u5230\u671F: ")}${formatTimestamp(s.expire)}`);
5951
5170
  }
5952
- throw new Error("\u89E3\u538B\u540E\u672A\u627E\u5230\u53EF\u6267\u884C\u6587\u4EF6");
5171
+ if (s.web_page_url) {
5172
+ console.log(` ${colors.gray("\u9875\u9762: ")}${s.web_page_url}`);
5173
+ }
5174
+ });
5175
+ console.log("");
5176
+ console.log("\u5207\u6362\u8BA2\u9605: mihomo sub use <name>");
5177
+ console.log("\u65B0\u589E\u8BA2\u9605: mihomo sub add <url> [name]");
5178
+ console.log("\u66F4\u65B0\u8BA2\u9605: mihomo sub update [name]");
5179
+ console.log("\u5220\u9664\u8BA2\u9605: mihomo sub remove <name>");
5180
+ console.log("\u6D4B\u8BD5\u8282\u70B9: mihomo sub test [name]");
5181
+ console.log("\u6E05\u7406\u8282\u70B9: mihomo sub clean [name]");
5182
+ console.log("\u6253\u5F00\u9875\u9762: mihomo sub web [name]");
5183
+ console.log("");
5184
+ }
5185
+ async function cmdSubscription(args) {
5186
+ const action = args[1];
5187
+ if (!action || action === "list") {
5188
+ await printSubscriptionList();
5189
+ return;
5953
5190
  }
5954
- const targetPath = PATHS.mihomoBinary;
5955
- if (foundBinary !== targetPath) {
5956
- if (fs8.existsSync(targetPath)) {
5957
- fs8.chmodSync(targetPath, 493);
5191
+ if (action === "add") {
5192
+ const url = args[2];
5193
+ const name = args[3] || "default";
5194
+ if (!url) {
5195
+ console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
5196
+ process.exit(1);
5197
+ }
5198
+ if (isMultiUrl(url)) {
5199
+ const urls = splitUrls(url);
5200
+ for (const u of urls) {
5201
+ if (!u.startsWith("http")) {
5202
+ console.error(`\u9519\u8BEF: \u65E0\u6548\u7684 URL: ${u}`);
5203
+ process.exit(1);
5204
+ }
5205
+ }
5206
+ console.log(`\u6DFB\u52A0\u5408\u5E76\u8BA2\u9605: ${name} (${urls.length} \u4E2A\u6E90)`);
5958
5207
  try {
5959
- fs8.unlinkSync(targetPath);
5960
- } catch {
5208
+ addSubscription(url, name);
5209
+ setDefaultSubscription(name);
5210
+ const info = await downloadMergedSubscription(urls, name);
5211
+ console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)}, \u5408\u5E76 ${urls.length} \u6E90)`);
5212
+ } catch (e) {
5213
+ console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
5214
+ process.exit(1);
5215
+ }
5216
+ } else {
5217
+ if (!url.startsWith("http")) {
5218
+ console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
5219
+ process.exit(1);
5220
+ }
5221
+ console.log(`\u6DFB\u52A0\u8BA2\u9605: ${name}`);
5222
+ try {
5223
+ addSubscription(url, name);
5224
+ setDefaultSubscription(name);
5225
+ const info = await downloadSubscription(url, name);
5226
+ const repoUrl = githubRepoUrl(url);
5227
+ if (repoUrl) saveSubscriptionCache(name, { web_page_url: repoUrl });
5228
+ console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
5229
+ } catch (e) {
5230
+ console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
5231
+ process.exit(1);
5961
5232
  }
5962
5233
  }
5963
- fs8.renameSync(foundBinary, targetPath);
5964
- }
5965
- fs8.chmodSync(targetPath, 493);
5966
- try {
5967
- fs8.unlinkSync(tempPath);
5968
- } catch {
5969
- }
5970
- clearKernelVersionCache();
5971
- return { version: latest.tag_name, path: targetPath };
5972
- }
5973
-
5974
- // src/commands/kernel.ts
5975
- async function cmdKernel(args) {
5976
- const mirrorInfo = parseMirrorArg(args);
5977
- const effectiveMirror = mirrorInfo.mirror;
5978
- if (effectiveMirror) {
5979
- const mirrorDesc = mirrorInfo.type === "all" ? " (API\u548C\u4E0B\u8F7D\u5747\u4F7F\u7528\u955C\u50CF)" : " (\u4E0B\u8F7D\u65F6\u4F7F\u7528\u955C\u50CF)";
5980
- console.log(`\u955C\u50CF: ${effectiveMirror}${mirrorDesc}`);
5981
- }
5982
- 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");
5983
- console.log("\n\u7528\u6CD5:");
5984
- console.log(" mihomo kernel # \u76F4\u8FDE");
5985
- console.log(" mihomo kernel --mirror # \u4E0B\u8F7D\u4F7F\u7528\u9ED8\u8BA4\u955C\u50CF (v6.gh-proxy.org)");
5986
- console.log(" mihomo kernel --mirror hk.gh-proxy.org # \u4E0B\u8F7D\u4F7F\u7528\u6307\u5B9A\u955C\u50CF");
5987
- console.log(" mihomo kernel --mirror-all # API\u8BF7\u6C42\u548C\u4E0B\u8F7D\u90FD\u4F7F\u7528\u9ED8\u8BA4\u955C\u50CF");
5988
- console.log(" mihomo kernel --mirror-all hk.gh-proxy.org # API\u548C\u4E0B\u8F7D\u90FD\u4F7F\u7528\u6307\u5B9A\u955C\u50CF");
5989
- console.log("\n\u53EF\u7528\u955C\u50CF:");
5990
- for (const m of AVAILABLE_MIRRORS) {
5991
- const isCurrent = effectiveMirror && (effectiveMirror.includes(`//${m}/`) || effectiveMirror.includes(`//${m}:`) || effectiveMirror.endsWith(`//${m}`));
5992
- console.log(` ${m}${isCurrent ? " (\u5F53\u524D)" : ""}`);
5234
+ console.log("");
5235
+ await printSubscriptionList();
5236
+ return;
5993
5237
  }
5994
- console.log("");
5995
- console.log("\u68C0\u67E5\u5185\u6838\u66F4\u65B0...");
5996
- try {
5997
- const apiMirror = mirrorInfo.type === "all" ? effectiveMirror : null;
5998
- const info = await checkUpdate(apiMirror);
5999
- console.log(`\u5F53\u524D: ${info.current}`);
6000
- console.log(`\u6700\u65B0: ${info.latest}`);
6001
- if (!info.needsUpdate) {
6002
- console.log("\u5DF2\u662F\u6700\u65B0\u7248\u672C");
6003
- } else {
6004
- console.log("\n\u6B63\u5728\u4E0B\u8F7D...");
6005
- const result = await downloadKernel((msg) => console.log(msg), mirrorInfo.mirror, info.release);
6006
- console.log(`
6007
- \u5DF2\u66F4\u65B0\u5230 ${result.version}`);
5238
+ if (action === "update") {
5239
+ const name = args[2];
5240
+ const subs = getSubscriptions();
5241
+ if (subs.length === 0) {
5242
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
5243
+ process.exit(1);
6008
5244
  }
6009
- } catch (e) {
6010
- console.error(`
6011
- \u66F4\u65B0\u5931\u8D25: ${e.message}`);
6012
- const err = e;
6013
- if (err.response?.data) {
6014
- if (err.response.data.message) {
6015
- console.error(`\u539F\u56E0: ${err.response.data.message}`);
5245
+ if (!name) {
5246
+ console.log(`\u66F4\u65B0\u6240\u6709 ${subs.length} \u4E2A\u8BA2\u9605...`);
5247
+ const results = await Promise.all(subs.map(tryUpdateOne));
5248
+ let ok = 0;
5249
+ for (const r of results) {
5250
+ if (r.success) {
5251
+ ok++;
5252
+ console.log(`${colors.green("\u2713")} ${r.name}: ${colors.green("\u5DF2\u66F4\u65B0")} (${formatProxySummary(r)})`);
5253
+ } else {
5254
+ console.log(`${colors.red("\u2717")} ${r.name}: ${colors.red("\u5931\u8D25")} (${(r.error || "").split("\n")[0]})`);
5255
+ }
6016
5256
  }
6017
- if (err.response.data.documentation_url) {
6018
- console.error(`\u6587\u6863: ${err.response.data.documentation_url}`);
5257
+ if (ok === 0) process.exit(1);
5258
+ console.log("");
5259
+ await printSubscriptionList();
5260
+ return;
5261
+ }
5262
+ const matches = findSubscriptionFuzzy(subs, name);
5263
+ const target = pickSingleSubscription(matches, name);
5264
+ console.log(`\u66F4\u65B0\u8BA2\u9605: ${target.name}`);
5265
+ try {
5266
+ let info;
5267
+ if (isMultiUrl(target.url)) {
5268
+ const urls = splitUrls(target.url);
5269
+ info = await downloadMergedSubscription(urls, target.name);
5270
+ } else {
5271
+ info = await downloadSubscription(target.url, target.name);
5272
+ }
5273
+ console.log(`\u5DF2\u66F4\u65B0 (${formatProxySummary(info)})`);
5274
+ } catch (e) {
5275
+ console.error(`\u66F4\u65B0\u5931\u8D25: ${e.message}`);
5276
+ process.exit(1);
5277
+ }
5278
+ console.log("");
5279
+ await printSubscriptionList();
5280
+ return;
5281
+ }
5282
+ if (action === "use") {
5283
+ const name = args[2];
5284
+ const subs = getSubscriptions();
5285
+ if (!name) {
5286
+ console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
5287
+ if (subs.length > 0) {
5288
+ console.log("\n\u53EF\u7528\u8BA2\u9605:");
5289
+ for (const s of subs) console.log(` ${s.name}`);
6019
5290
  }
5291
+ process.exit(1);
5292
+ }
5293
+ const matches = findSubscriptionFuzzy(subs, name);
5294
+ const target = pickSingleSubscription(matches, name);
5295
+ const currentDefault = getActiveSubscription();
5296
+ const isAlreadyDefault = currentDefault && currentDefault.name === target.name;
5297
+ if (isAlreadyDefault) {
5298
+ console.log(`"${target.name}" \u5DF2\u662F\u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605`);
5299
+ console.log("");
5300
+ await printSubscriptionList();
5301
+ return;
5302
+ }
5303
+ const status = getStatus();
5304
+ const configInfo = getConfigInfo();
5305
+ const currentMode = configInfo?.tun ? "tun" : "mixed";
5306
+ const success = setDefaultSubscription(target.name);
5307
+ if (success) {
5308
+ console.log(`\u5DF2\u5207\u6362\u5230 "${target.name}"`);
5309
+ } else {
5310
+ console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u8BA2\u9605 "${name}"`);
5311
+ process.exit(1);
5312
+ }
5313
+ if (status.running) {
5314
+ console.log("");
5315
+ await cmdStart(["start", currentMode]);
5316
+ return;
6020
5317
  }
6021
- process.exit(1);
6022
- }
6023
- }
6024
-
6025
- // src/commands/log.ts
6026
- function cmdLog(args) {
6027
- const logPath = getLogPath();
6028
- if (hasFlag(args, "-o", "--open")) {
6029
- openLogFile(logPath);
5318
+ console.log("");
5319
+ await printSubscriptionList();
6030
5320
  return;
6031
5321
  }
6032
- viewLogWithTail(logPath, { follow: true, lines: 50 });
6033
- }
6034
- function cmdLogs(args) {
6035
- const targetName = getNonFlagArg(args, 1);
6036
- const lines = parseIntArg(args, "-n", "--lines", 100);
6037
- const openInViewer = hasFlag(args, "-o", "--open");
6038
- if (targetName) {
6039
- let logPath;
6040
- if (targetName === "current" || targetName === "0") {
6041
- logPath = getLogPath();
5322
+ if (action === "web" || action === "open") {
5323
+ const name = args[2];
5324
+ const subs = getSubscriptionsWithCache();
5325
+ if (subs.length === 0) {
5326
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
5327
+ process.exit(1);
5328
+ }
5329
+ let target;
5330
+ if (name) {
5331
+ const matches = findSubscriptionFuzzy(subs, name);
5332
+ target = pickSingleSubscription(matches, name);
6042
5333
  } else {
6043
- const parsedIdx = parseInt(targetName, 10);
6044
- if (!Number.isNaN(parsedIdx) && parsedIdx > 0 && String(parsedIdx) === targetName) {
6045
- const archiveLogs = listLogs();
6046
- const archive = archiveLogs.archives[parsedIdx - 1];
6047
- if (!archive) {
6048
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`);
6049
- console.log('\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868');
5334
+ target = subs[0];
5335
+ }
5336
+ const cached = getSubscriptionsWithCache().find((s) => s.name === target.name);
5337
+ let webPageUrl = cached?.web_page_url;
5338
+ if (!webPageUrl) {
5339
+ console.log("\u8BA2\u9605\u4FE1\u606F\u4E2D\u7F3A\u5C11\u9875\u9762\u5730\u5740\uFF0C\u6B63\u5728\u66F4\u65B0\u8BA2\u9605...");
5340
+ try {
5341
+ await downloadSubscription(target.url, target.name);
5342
+ const cache = readSubscriptionCache();
5343
+ if (cache[target.name]?.web_page_url) {
5344
+ webPageUrl = cache[target.name].web_page_url;
5345
+ } else {
5346
+ console.error("\u9519\u8BEF: \u8BE5\u8BA2\u9605\u6CA1\u6709\u63D0\u4F9B\u9875\u9762\u5730\u5740");
6050
5347
  process.exit(1);
6051
5348
  }
6052
- logPath = archive.path;
6053
- } else {
6054
- logPath = getLogPathByName(targetName);
5349
+ } catch (e) {
5350
+ console.error(`\u66F4\u65B0\u5931\u8D25: ${e.message}`);
5351
+ process.exit(1);
6055
5352
  }
6056
5353
  }
6057
- if (!logPath) {
6058
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`);
6059
- console.log('\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868');
5354
+ console.log(`\u6253\u5F00\u8BA2\u9605\u9875\u9762: ${webPageUrl}`);
5355
+ const opened = openUrl(webPageUrl);
5356
+ if (!opened) {
5357
+ console.log("\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740");
5358
+ }
5359
+ return;
5360
+ }
5361
+ if (action === "remove" || action === "rm" || action === "delete") {
5362
+ const name = args[2];
5363
+ const subs = getSubscriptions();
5364
+ if (!name) {
5365
+ console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8981\u5220\u9664\u7684\u8BA2\u9605\u540D\u79F0");
5366
+ if (subs.length > 0) {
5367
+ console.log("\n\u53EF\u7528\u8BA2\u9605:");
5368
+ for (const s of subs) console.log(` ${s.name}`);
5369
+ }
6060
5370
  process.exit(1);
6061
5371
  }
6062
- if (openInViewer) {
6063
- openLogFile(logPath);
6064
- return;
5372
+ const matches = findSubscriptionFuzzy(subs, name);
5373
+ const target = pickSingleSubscription(matches, name);
5374
+ const switchedTo = removeSubscription(target.name);
5375
+ console.log(`\u5DF2\u5220\u9664\u8BA2\u9605 "${target.name}"`);
5376
+ if (switchedTo) {
5377
+ console.log(`\u5DF2\u81EA\u52A8\u5207\u6362\u5230 "${switchedTo}"`);
6065
5378
  }
6066
- viewLogWithTail(logPath, { follow: false, lines });
5379
+ console.log("");
5380
+ await printSubscriptionList({ autoUpdate: false });
6067
5381
  return;
6068
5382
  }
6069
- const logs = listLogs();
6070
- const all = [];
6071
- if (logs.current) all.push(logs.current);
6072
- all.push(...logs.archives);
6073
- if (all.length === 0) {
6074
- console.log("\u6682\u65E0\u65E5\u5FD7");
5383
+ if (action === "clean") {
5384
+ const { target, timeout, concurrency } = resolveTestTarget(args);
5385
+ console.log(`\u6E05\u7406\u8BA2\u9605 "${target.name}"...`);
5386
+ console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
5387
+ console.log("");
5388
+ const progress = createProgressPrinter();
5389
+ const result = await withTestInstance(target.name, async (apiBase) => {
5390
+ return autoCleanSubscription(target.name, {
5391
+ timeout,
5392
+ concurrency,
5393
+ apiBase,
5394
+ onResult: progress.onResult,
5395
+ onRetryRound: progress.onRetryRound
5396
+ });
5397
+ });
5398
+ progress.finish();
5399
+ console.log(formatTestSummary(result.summary));
5400
+ if (result.skipped) {
5401
+ console.log("");
5402
+ 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"));
5403
+ } else if (result.removedProxies > 0) {
5404
+ console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
5405
+ const status = getStatus();
5406
+ if (status.running) {
5407
+ console.log("");
5408
+ console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
5409
+ }
5410
+ }
6075
5411
  return;
6076
5412
  }
6077
- console.log("");
6078
- console.log("\u65E5\u5FD7\u5217\u8868:");
6079
- console.log("");
6080
- let archiveCounter = 0;
6081
- for (const log of all) {
6082
- let num;
6083
- if (log.isCurrent) {
6084
- num = " 0";
6085
- } else {
6086
- archiveCounter++;
6087
- num = archiveCounter < 10 ? ` ${archiveCounter}` : `${archiveCounter}`;
6088
- }
6089
- const time = formatDate(log.mtime);
6090
- const size = formatBytes(log.size);
6091
- const name = log.isCurrent ? "mihomo.log (\u5F53\u524D\u8FD0\u884C\u4E2D)" : log.name;
6092
- console.log(` ${num}. ${name}`);
6093
- console.log(` \u65F6\u95F4: ${time} \u5927\u5C0F: ${size}`);
6094
- if (!log.isCurrent) {
6095
- console.log(` \u67E5\u770B: mihomo logs ${archiveCounter} \u6216 mihomo logs ${archiveCounter} -o`);
5413
+ if (action === "test") {
5414
+ const { target, timeout, concurrency } = resolveTestTarget(args);
5415
+ console.log(`\u6D4B\u8BD5\u8BA2\u9605 "${target.name}" \u7684\u8282\u70B9\u8FDE\u901A\u6027...`);
5416
+ console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
5417
+ console.log("");
5418
+ const progress = createProgressPrinter();
5419
+ const summary = await withTestInstance(target.name, async (apiBase) => {
5420
+ return testSubscriptionProxies(target.name, {
5421
+ timeout,
5422
+ concurrency,
5423
+ apiBase,
5424
+ onResult: progress.onResult
5425
+ });
5426
+ });
5427
+ progress.finish();
5428
+ console.log(formatTestSummary(summary));
5429
+ return;
5430
+ }
5431
+ console.error("\u9519\u8BEF: \u672A\u77E5\u7684\u8BA2\u9605\u547D\u4EE4");
5432
+ console.log("\u7528\u6CD5: mihomo sub [list|use|add|update|remove|web|test|clean]");
5433
+ process.exit(1);
5434
+ }
5435
+
5436
+ // src/commands/start.ts
5437
+ var AUTO_CLEAN_THRESHOLD = 50;
5438
+ function handleStopResult(result) {
5439
+ if (result.remaining && result.remaining.length > 0) {
5440
+ console.error(`${colors.red("\u90E8\u5206\u8FDB\u7A0B\u672A\u7EC8\u6B62:")} ${result.remaining.join(", ")}`);
5441
+ console.error("\u8BF7\u624B\u52A8\u8FD0\u884C: sudo pkill -9 mihomo");
5442
+ process.exit(1);
5443
+ }
5444
+ }
5445
+ async function cmdStart(args) {
5446
+ if (!hasKernel()) {
5447
+ console.error('\u9519\u8BEF: \u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
5448
+ process.exit(1);
5449
+ }
5450
+ const targetMode = args[1] === "tun" ? "tun" : "mixed";
5451
+ const sub = getActiveSubscription();
5452
+ if (!sub) {
5453
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
5454
+ process.exit(1);
5455
+ }
5456
+ await autoUpdateStaleSubscription();
5457
+ const status = getStatus();
5458
+ const hasProcess = status.running || status.allProcesses.length > 0;
5459
+ if (hasProcess) {
5460
+ const count = status.allProcesses.length > 0 ? status.allProcesses.length : 1;
5461
+ console.log(`\u505C\u6B62 ${count} \u4E2A\u8FDB\u7A0B...`);
5462
+ }
5463
+ handleStopResult(stop());
5464
+ if (hasProcess) {
5465
+ console.log(`${colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B")}
5466
+ `);
5467
+ }
5468
+ let configInfo;
5469
+ try {
5470
+ configInfo = prepareConfigForStart(targetMode, sub.name);
5471
+ } catch (e) {
5472
+ console.error(`${colors.red("\u914D\u7F6E\u9519\u8BEF:")} ${e.message}`);
5473
+ process.exit(1);
5474
+ }
5475
+ const modeLabel = targetMode === "tun" ? "TUN" : "Mixed";
5476
+ console.log([colors.cyan(modeLabel), sub.name, formatProxySummary(configInfo)].join(" \xB7 "));
5477
+ try {
5478
+ const result = await start(targetMode);
5479
+ console.log(`${colors.green("\u5DF2\u542F\u52A8")} (PID ${result.pid})`);
5480
+ } catch (e) {
5481
+ const msg = e.message;
5482
+ const lines = msg.split("\n");
5483
+ console.error(`${colors.red("\u542F\u52A8\u5931\u8D25:")} ${lines[0]}`);
5484
+ if (lines.length > 1) {
5485
+ for (const line of lines.slice(1)) console.error(line);
6096
5486
  }
5487
+ process.exit(1);
5488
+ }
5489
+ if (configInfo.proxies > AUTO_CLEAN_THRESHOLD) {
5490
+ console.log("");
5491
+ console.log(`\u8282\u70B9\u6570 ${configInfo.proxies} \u8D85\u8FC7 ${AUTO_CLEAN_THRESHOLD}\uFF0C\u81EA\u52A8\u6E05\u7406...`);
6097
5492
  console.log("");
5493
+ await sleep(1e3);
5494
+ const progress = createProgressPrinter();
5495
+ const cleanResult = await autoCleanSubscription(sub.name, {
5496
+ onResult: progress.onResult,
5497
+ onRetryRound: progress.onRetryRound
5498
+ });
5499
+ progress.finish();
5500
+ console.log(formatTestSummary(cleanResult.summary));
5501
+ if (cleanResult.skipped) {
5502
+ 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"));
5503
+ } else if (cleanResult.removedProxies > 0) {
5504
+ console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(cleanResult)}`);
5505
+ console.log("");
5506
+ console.log("\u91CD\u65B0\u52A0\u8F7D\u914D\u7F6E...");
5507
+ handleStopResult(stop());
5508
+ try {
5509
+ configInfo = prepareConfigForStart(targetMode, sub.name);
5510
+ const result = await start(targetMode);
5511
+ console.log(`${colors.green("\u5DF2\u91CD\u542F")} (PID ${result.pid}) \xB7 ${formatProxySummary(configInfo)}`);
5512
+ } catch (e) {
5513
+ console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
5514
+ process.exit(1);
5515
+ }
5516
+ }
6098
5517
  }
6099
- console.log("\u7528\u6CD5:");
6100
- console.log(" mihomo logs 0 # \u67E5\u770B\u5F53\u524D\u65E5\u5FD7 (\u6700\u540E 100 \u884C)");
6101
- console.log(" mihomo logs 1 # \u67E5\u770B\u7B2C 1 \u4E2A\u5F52\u6863\u65E5\u5FD7\uFF08\u6700\u65B0\uFF09");
6102
- console.log(" mihomo logs 1 -n 200 # \u67E5\u770B 200 \u884C");
6103
- console.log(" mihomo logs 1 -o # \u7528\u7CFB\u7EDF\u9ED8\u8BA4\u7A0B\u5E8F\u6253\u5F00");
6104
- console.log("");
5518
+ printStatus();
6105
5519
  }
6106
5520
 
6107
5521
  // src/commands/overwrite.ts
6108
- import path7 from "path";
6109
5522
  function printOverwriteList() {
6110
5523
  const info = listOverwriteFile();
6111
5524
  const statusText = info.enabled ? colors.green("\u5DF2\u542F\u7528") : colors.yellow("\u5DF2\u7981\u7528");
@@ -6115,8 +5528,8 @@ function printOverwriteList() {
6115
5528
  if (info.files.length === 0) {
6116
5529
  console.log("\u6682\u65E0\u8986\u5199\u6587\u4EF6");
6117
5530
  console.log("");
6118
- console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path7.join(info.dir, "overwrite.yaml")}`);
6119
- console.log(` \u6216 ${path7.join(info.dir, "overwrite.dns.yaml")}`);
5531
+ console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path6.join(info.dir, "overwrite.yaml")}`);
5532
+ console.log(` \u6216 ${path6.join(info.dir, "overwrite.dns.yaml")}`);
6120
5533
  console.log("");
6121
5534
  } else {
6122
5535
  console.log(`${colors.cyan("\u8986\u5199\u6587\u4EF6")} (${info.files.length} \u4E2A\uFF0C\u6309\u987A\u5E8F\u52A0\u8F7D):`);
@@ -6180,7 +5593,7 @@ async function cmdOverwrite(args) {
6180
5593
  }
6181
5594
 
6182
5595
  // src/commands/reset.ts
6183
- import fs9 from "fs";
5596
+ import fs8 from "fs";
6184
5597
  import readline from "readline";
6185
5598
  var RESET_TARGETS = [
6186
5599
  {
@@ -6235,8 +5648,8 @@ var RESET_TARGETS = [
6235
5648
  label: "\u8986\u5199",
6236
5649
  paths: () => {
6237
5650
  const dir = USER_DATA_DIR;
6238
- if (!fs9.existsSync(dir)) return [];
6239
- return fs9.readdirSync(dir).filter((f) => f === "overwrite.yaml" || /^overwrite\..+\.ya?ml$/.test(f)).map((f) => `${dir}/${f}`);
5651
+ if (!fs8.existsSync(dir)) return [];
5652
+ return fs8.readdirSync(dir).filter((f) => f === "overwrite.yaml" || /^overwrite\..+\.ya?ml$/.test(f)).map((f) => `${dir}/${f}`);
6240
5653
  },
6241
5654
  needsStop: false
6242
5655
  }
@@ -6436,7 +5849,7 @@ function cmdUI(args) {
6436
5849
  }
6437
5850
 
6438
5851
  // src/commands/update.ts
6439
- import { exec, spawn as spawn4 } from "child_process";
5852
+ import { exec, spawn as spawn3 } from "child_process";
6440
5853
  import { promisify } from "util";
6441
5854
  var execAsync = promisify(exec);
6442
5855
  async function cmdUpdate() {
@@ -6445,7 +5858,7 @@ async function cmdUpdate() {
6445
5858
  console.log("\u6B63\u5728\u66F4\u65B0 mihomo-cli...");
6446
5859
  console.log("");
6447
5860
  await new Promise((resolve) => {
6448
- const npm = spawn4("npm", ["install", "-g", "mihomo-cli"], { stdio: "inherit" });
5861
+ const npm = spawn3("npm", ["install", "-g", "mihomo-cli"], { stdio: "inherit" });
6449
5862
  npm.on("close", (code) => {
6450
5863
  if (code === 0) {
6451
5864
  resolve();
@@ -6590,9 +6003,6 @@ async function main() {
6590
6003
  case "overwrite":
6591
6004
  await cmdOverwrite(args);
6592
6005
  break;
6593
- case "bench":
6594
- await cmdBench(args);
6595
- break;
6596
6006
  case "test":
6597
6007
  await cmdTest(args);
6598
6008
  break;